diff --git a/allowedsymbols/symbols_builtins.go b/allowedsymbols/symbols_builtins.go index ab21830f..78d7e807 100644 --- a/allowedsymbols/symbols_builtins.go +++ b/allowedsymbols/symbols_builtins.go @@ -125,6 +125,7 @@ var builtinPerCommandSymbols = map[string][]string{ "strings.Split", // 🟢 splits a string by separator into a slice; pure function, no I/O. }, "help": { + "bytes.Buffer", // 🟢 in-memory buffer to capture --help output from commands; no I/O side effects. "context.Context", // 🟢 deadline/cancellation plumbing; pure interface, no side effects. }, "head": { @@ -388,6 +389,7 @@ var builtinAllowedSymbols = []string{ "bufio.NewScanner", // 🟢 line-by-line input reading (e.g. head, cat); no write or exec capability. "bufio.Scanner", // 🟢 scanner type for buffered input reading; no write or exec capability. "bufio.SplitFunc", // 🟢 type for custom scanner split functions; pure type, no I/O. + "bytes.Buffer", // 🟢 in-memory buffer to capture command output; no I/O side effects. "bytes.Equal", // 🟢 compares two byte slices for equality; pure function, no I/O. "bytes.IndexByte", // 🟢 finds a byte in a byte slice; pure function, no I/O. "bytes.NewReader", // 🟢 wraps a byte slice as an io.Reader; pure in-memory, no I/O. diff --git a/builtins/break/break.go b/builtins/break/break.go index 22f13f3e..1fdecfb9 100644 --- a/builtins/break/break.go +++ b/builtins/break/break.go @@ -25,9 +25,26 @@ import ( "github.com/DataDog/rshell/builtins/internal/loopctl" ) +const helpText = "break: break [n]\n" + + " Exit for, while, or until loops.\n" + + " \n" + + " Exit a FOR, WHILE or UNTIL loop. If N is specified, break N enclosing\n" + + " loops.\n" + + " \n" + + " Exit Status:\n" + + " The exit status is 0 unless N is not greater than or equal to 1." + // Cmd is the break builtin command descriptor. -var Cmd = builtins.Command{Name: "break", Description: "exit from a loop", MakeFlags: builtins.NoFlags(run)} +var Cmd = builtins.Command{ + Name: "break", + Description: "exit from a loop", + MakeFlags: builtins.NoFlags(run), +} func run(_ context.Context, callCtx *builtins.CallContext, args []string) builtins.Result { + if len(args) > 0 && args[0] == "--help" { + callCtx.Outf("%s\n", helpText) + return builtins.Result{Code: 2} + } return loopctl.LoopControl(callCtx, "break", args) } diff --git a/builtins/builtins.go b/builtins/builtins.go index 586d0ff5..4106e151 100644 --- a/builtins/builtins.go +++ b/builtins/builtins.go @@ -36,6 +36,7 @@ type HandlerFunc func(ctx context.Context, callCtx *CallContext, args []string) type Command struct { Name string Description string + Help string MakeFlags func(*FlagSet) HandlerFunc } @@ -56,7 +57,7 @@ func NoFlags(fn HandlerFunc) func(*FlagSet) HandlerFunc { func (c Command) Register() { name := c.Name factory := c.MakeFlags - metaRegistry[name] = CommandMeta{Name: name, Description: c.Description} + metaRegistry[name] = CommandMeta{Name: name, Description: c.Description, Help: c.Help} addToRegistry(name, func(ctx context.Context, callCtx *CallContext, args []string) Result { fs := pflag.NewFlagSet(name, pflag.ContinueOnError) fs.SetOutput(io.Discard) // handler formats errors itself @@ -198,6 +199,7 @@ var registry = map[string]HandlerFunc{} type CommandMeta struct { Name string Description string + Help string } var metaRegistry = map[string]CommandMeta{} diff --git a/builtins/cat/cat.go b/builtins/cat/cat.go index 91d74c85..9b86473b 100644 --- a/builtins/cat/cat.go +++ b/builtins/cat/cat.go @@ -76,7 +76,11 @@ import ( ) // Cmd is the cat builtin command descriptor. -var Cmd = builtins.Command{Name: "cat", Description: "concatenate and print files", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "cat", + Description: "concatenate and print files", + MakeFlags: registerFlags, +} // MaxLineBytes is the per-line buffer cap for the line scanner. Lines // longer than this are reported as an error instead of being buffered. diff --git a/builtins/continue/continue.go b/builtins/continue/continue.go index 7574431b..ca107a79 100644 --- a/builtins/continue/continue.go +++ b/builtins/continue/continue.go @@ -25,9 +25,26 @@ import ( "github.com/DataDog/rshell/builtins/internal/loopctl" ) +const helpText = "continue: continue [n]\n" + + " Resume for, while, or until loops.\n" + + " \n" + + " Resumes the next iteration of the enclosing FOR, WHILE or UNTIL loop.\n" + + " If N is specified, resumes the Nth enclosing loop.\n" + + " \n" + + " Exit Status:\n" + + " The exit status is 0 unless N is not greater than or equal to 1." + // Cmd is the continue builtin command descriptor. -var Cmd = builtins.Command{Name: "continue", Description: "continue a loop iteration", MakeFlags: builtins.NoFlags(run)} +var Cmd = builtins.Command{ + Name: "continue", + Description: "continue a loop iteration", + MakeFlags: builtins.NoFlags(run), +} func run(_ context.Context, callCtx *builtins.CallContext, args []string) builtins.Result { + if len(args) > 0 && args[0] == "--help" { + callCtx.Outf("%s\n", helpText) + return builtins.Result{Code: 2} + } return loopctl.LoopControl(callCtx, "continue", args) } diff --git a/builtins/cut/cut.go b/builtins/cut/cut.go index c195e236..08902904 100644 --- a/builtins/cut/cut.go +++ b/builtins/cut/cut.go @@ -75,7 +75,11 @@ import ( ) // Cmd is the cut builtin command descriptor. -var Cmd = builtins.Command{Name: "cut", Description: "remove sections from each line", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "cut", + Description: "remove sections from each line", + MakeFlags: registerFlags, +} // MaxLineBytes is the per-line buffer cap for the line scanner. const MaxLineBytes = 1 << 20 // 1 MiB diff --git a/builtins/echo/echo.go b/builtins/echo/echo.go index 08108183..f4c6766f 100644 --- a/builtins/echo/echo.go +++ b/builtins/echo/echo.go @@ -54,7 +54,24 @@ import ( ) // Cmd is the echo builtin command descriptor. -var Cmd = builtins.Command{Name: "echo", Description: "write arguments to stdout", MakeFlags: builtins.NoFlags(run)} +var Cmd = builtins.Command{ + Name: "echo", + Description: "write arguments to stdout", + Help: `echo: echo [-neE] [arg ...] + Write arguments to the standard output. + + Display the ARGs, separated by a single space character and followed by a + newline, on the standard output. + + Options: + -n do not output the trailing newline + -e enable interpretation of backslash escapes + -E disable interpretation of backslash escapes (default) + + Exit Status: + Returns success unless a write error occurs.`, + MakeFlags: builtins.NoFlags(run), +} func run(_ context.Context, callCtx *builtins.CallContext, args []string) builtins.Result { // Parse flags: bash treats leading args matching -[neE]+ as flags. diff --git a/builtins/exit/exit.go b/builtins/exit/exit.go index 9acbe923..09aa44bf 100644 --- a/builtins/exit/exit.go +++ b/builtins/exit/exit.go @@ -27,10 +27,24 @@ import ( "github.com/DataDog/rshell/builtins" ) +const helpText = "exit: exit [n]\n" + + " Exit the shell.\n" + + " \n" + + " Exits the shell with a status of N. If N is omitted, the exit status\n" + + " is that of the last command executed." + // Cmd is the exit builtin command descriptor. -var Cmd = builtins.Command{Name: "exit", Description: "exit the shell", MakeFlags: builtins.NoFlags(run)} +var Cmd = builtins.Command{ + Name: "exit", + Description: "exit the shell", + MakeFlags: builtins.NoFlags(run), +} func run(_ context.Context, callCtx *builtins.CallContext, args []string) builtins.Result { + if len(args) > 0 && args[0] == "--help" { + callCtx.Outf("%s\n", helpText) + return builtins.Result{Code: 2} + } var r builtins.Result if len(args) > 0 && args[0] == "--" { args = args[1:] diff --git a/builtins/false/false.go b/builtins/false/false.go index 08ba6004..52779fea 100644 --- a/builtins/false/false.go +++ b/builtins/false/false.go @@ -23,7 +23,15 @@ import ( ) // Cmd is the false builtin command descriptor. -var Cmd = builtins.Command{Name: "false", Description: "return unsuccessful exit status", MakeFlags: builtins.NoFlags(run)} +var Cmd = builtins.Command{ + Name: "false", + Description: "return unsuccessful exit status", + Help: `false: false + Return an unsuccessful result. + + Exit with a status code indicating failure.`, + MakeFlags: builtins.NoFlags(run), +} func run(_ context.Context, _ *builtins.CallContext, _ []string) builtins.Result { return builtins.Result{Code: 1} diff --git a/builtins/find/find.go b/builtins/find/find.go index 1e48eaf1..8e5248d8 100644 --- a/builtins/find/find.go +++ b/builtins/find/find.go @@ -97,7 +97,11 @@ func isNotExist(err error) bool { const maxTraversalDepth = 256 // Cmd is the find builtin command descriptor. -var Cmd = builtins.Command{Name: "find", Description: "search for files in a directory hierarchy", MakeFlags: builtins.NoFlags(run)} +var Cmd = builtins.Command{ + Name: "find", + Description: "search for files in a directory hierarchy", + MakeFlags: builtins.NoFlags(run), +} func run(ctx context.Context, callCtx *builtins.CallContext, args []string) builtins.Result { // Parse global options (-L) and separate paths from expression. diff --git a/builtins/grep/grep.go b/builtins/grep/grep.go index 59ec59aa..d03f73d7 100644 --- a/builtins/grep/grep.go +++ b/builtins/grep/grep.go @@ -125,7 +125,11 @@ import ( ) // Cmd is the grep builtin command descriptor. -var Cmd = builtins.Command{Name: "grep", Description: "print lines that match patterns", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "grep", + Description: "print lines that match patterns", + MakeFlags: registerFlags, +} // MaxLineBytes is the per-line buffer cap for the line scanner. Lines // longer than this are reported as an error instead of being buffered. diff --git a/builtins/head/head.go b/builtins/head/head.go index 177494ee..6d0fc330 100644 --- a/builtins/head/head.go +++ b/builtins/head/head.go @@ -60,7 +60,11 @@ import ( ) // Cmd is the head builtin command descriptor. -var Cmd = builtins.Command{Name: "head", Description: "output the first part of files", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "head", + Description: "output the first part of files", + MakeFlags: registerFlags, +} // MaxCount is the maximum accepted line or byte count. Values above this // are clamped. This prevents huge theoretical allocations while remaining diff --git a/builtins/help/help.go b/builtins/help/help.go index c9f5492d..2e6641df 100644 --- a/builtins/help/help.go +++ b/builtins/help/help.go @@ -5,44 +5,90 @@ // Package help implements the help builtin command. // -// help — display available commands +// help — display help for commands // -// Usage: help +// Usage: help [command] // -// List all available builtin commands with a brief description. -// For detailed information on a specific command, run ' --help'. +// With no arguments, list all available builtin commands with a brief +// description. When a command name is given, display detailed help for +// that command. // // Exit codes: // // 0 Success. -// 1 Arguments were provided or --help was requested. +// 1 Unknown command or --help was requested. package help import ( + "bytes" "context" "github.com/DataDog/rshell/builtins" ) // Cmd is the help builtin command descriptor. -var Cmd = builtins.Command{Name: "help", Description: "display available commands", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "help", + Description: "display help for commands", + MakeFlags: registerFlags, +} func printUsage(callCtx *builtins.CallContext) { - callCtx.Out("Usage: help\n") - callCtx.Out("List all available builtin commands with a brief description.\n") - callCtx.Out("Takes no arguments.\n") + callCtx.Out("Usage: help [command]\n") + callCtx.Out("Display help for builtin commands.\n") } func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc { helpFlag := fs.Bool("help", false, "print usage and exit") - return func(_ context.Context, callCtx *builtins.CallContext, args []string) builtins.Result { - if *helpFlag || len(args) > 0 { + return func(ctx context.Context, callCtx *builtins.CallContext, args []string) builtins.Result { + if *helpFlag { printUsage(callCtx) return builtins.Result{Code: 1} } - // Filter to only commands allowed under the current policy. + // help — show detailed help for a specific command. + if len(args) > 1 { + printUsage(callCtx) + return builtins.Result{Code: 1} + } + if len(args) > 0 { + name := args[0] + if callCtx.CommandAllowed != nil && !callCtx.CommandAllowed(name) { + callCtx.Errf("help: no help topics match '%s'\n", name) + return builtins.Result{Code: 1} + } + meta, ok := builtins.Meta(name) + if !ok { + callCtx.Errf("help: no help topics match '%s'\n", name) + return builtins.Result{Code: 1} + } + + // Use static Help text if available (for commands that don't + // handle --help, like echo, true, false). + if meta.Help != "" { + callCtx.Outf("%s\n", meta.Help) + return builtins.Result{} + } + + // Otherwise, invoke the command with --help and capture the output. + if handler, ok := builtins.Lookup(name); ok && handler != nil { + var buf bytes.Buffer + captureCtx := *callCtx + captureCtx.Stdout = &buf + captureCtx.Stderr = &buf + handler(ctx, &captureCtx, []string{"--help"}) + if buf.Len() > 0 { + callCtx.Outf("%s", buf.String()) + return builtins.Result{} + } + } + + callCtx.Outf("%s - %s\n", meta.Name, meta.Description) + return builtins.Result{} + } + + // No arguments — list all allowed commands. allNames := builtins.Names() var names []string for _, name := range allNames { @@ -65,7 +111,7 @@ func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc { callCtx.Outf("%-*s %s\n", maxLen, name, meta.Description) } - callCtx.Out("\nRun ' --help' for more information on a specific command.\n") + callCtx.Out("\nRun 'help ' for more information on a specific command.\n") return builtins.Result{} } } diff --git a/builtins/ip/ip.go b/builtins/ip/ip.go index 31815f05..80d192dc 100644 --- a/builtins/ip/ip.go +++ b/builtins/ip/ip.go @@ -88,7 +88,11 @@ import ( ) // Cmd is the ip builtin command descriptor. -var Cmd = builtins.Command{Name: "ip", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "ip", + Description: "show network interface information", + MakeFlags: registerFlags, +} // displayOpts holds the resolved global display options. type displayOpts struct { diff --git a/builtins/ls/ls.go b/builtins/ls/ls.go index 27c8509f..5d8d1cd0 100644 --- a/builtins/ls/ls.go +++ b/builtins/ls/ls.go @@ -99,7 +99,11 @@ const MaxDirEntries = 1_000 var errFailed = errors.New("ls: one or more errors occurred") // Cmd is the ls builtin command descriptor. -var Cmd = builtins.Command{Name: "ls", Description: "list directory contents", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "ls", + Description: "list directory contents", + MakeFlags: registerFlags, +} func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc { // Preserve parse order so Visit() returns flags in the order set. diff --git a/builtins/printf/printf.go b/builtins/printf/printf.go index 7f6043da..82bc00f9 100644 --- a/builtins/printf/printf.go +++ b/builtins/printf/printf.go @@ -118,7 +118,12 @@ func isRangeErr(err error) bool { // Cmd is the printf builtin command descriptor. // printf uses NoFlags because its arguments (format string and data) can look // like flags (e.g. printf "%d" -42). Manual pre-parsing handles --help and -v. -var Cmd = builtins.Command{Name: "printf", Description: "format and print data", MakeFlags: builtins.NoFlags(run)} +var Cmd = builtins.Command{ + Name: "printf", + Description: "format and print data", + Help: `printf: usage: printf [-v var] format [arguments]`, + MakeFlags: builtins.NoFlags(run), +} // maxFormatIterations bounds the format-reuse loop to prevent runaway output. const maxFormatIterations = 10_000 diff --git a/builtins/ps/ps.go b/builtins/ps/ps.go index 169ebfe1..051c62f2 100644 --- a/builtins/ps/ps.go +++ b/builtins/ps/ps.go @@ -51,7 +51,11 @@ import ( ) // Cmd is the ps builtin command descriptor. -var Cmd = builtins.Command{Name: "ps", Description: "report process status", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "ps", + Description: "report process status", + MakeFlags: registerFlags, +} func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc { // Both -e/--all and -A/--All write to the same bool so that diff --git a/builtins/sed/sed.go b/builtins/sed/sed.go index 6c83ae18..d8153238 100644 --- a/builtins/sed/sed.go +++ b/builtins/sed/sed.go @@ -116,7 +116,11 @@ import ( ) // Cmd is the sed builtin command descriptor. -var Cmd = builtins.Command{Name: "sed", Description: "stream editor for filtering and transforming text", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "sed", + Description: "stream editor for filtering and transforming text", + MakeFlags: registerFlags, +} // MaxLineBytes is the per-line buffer cap for the line scanner. const MaxLineBytes = 1 << 20 // 1 MiB diff --git a/builtins/sort/sort.go b/builtins/sort/sort.go index 02790d5c..fe8e6da3 100644 --- a/builtins/sort/sort.go +++ b/builtins/sort/sort.go @@ -84,7 +84,11 @@ import ( ) // Cmd is the sort builtin command descriptor. -var Cmd = builtins.Command{Name: "sort", Description: "sort lines of text files", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "sort", + Description: "sort lines of text files", + MakeFlags: registerFlags, +} // checkTracker is a pflag.Value that tracks all --check/-c modes set // during argument parsing so conflicting modes (diagnose vs silent) can diff --git a/builtins/ss/ss.go b/builtins/ss/ss.go index 47e4dbc6..23355a09 100644 --- a/builtins/ss/ss.go +++ b/builtins/ss/ss.go @@ -104,7 +104,11 @@ import ( ) // Cmd is the ss builtin command descriptor. -var Cmd = builtins.Command{Name: "ss", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "ss", + Description: "display socket statistics", + MakeFlags: registerFlags, +} // MaxLineBytes is the per-line buffer cap for the Linux /proc/net/ scanner. const MaxLineBytes = 1 << 20 // 1 MiB diff --git a/builtins/strings_cmd/strings.go b/builtins/strings_cmd/strings.go index 036ef4a5..802dfd16 100644 --- a/builtins/strings_cmd/strings.go +++ b/builtins/strings_cmd/strings.go @@ -72,7 +72,11 @@ import ( ) // Cmd is the strings builtin command descriptor. -var Cmd = builtins.Command{Name: "strings", Description: "print printable character sequences", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "strings", + Description: "print printable character sequences", + MakeFlags: registerFlags, +} const ( defaultMinLen = 4 diff --git a/builtins/tail/tail.go b/builtins/tail/tail.go index f623401d..4eeaff5b 100644 --- a/builtins/tail/tail.go +++ b/builtins/tail/tail.go @@ -85,7 +85,11 @@ import ( ) // Cmd is the tail builtin command descriptor. -var Cmd = builtins.Command{Name: "tail", Description: "output the last part of files", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "tail", + Description: "output the last part of files", + MakeFlags: registerFlags, +} // MaxCount is the maximum accepted line or byte count. Values above this // are clamped to prevent huge theoretical allocations. diff --git a/builtins/testcmd/testcmd.go b/builtins/testcmd/testcmd.go index 9712eb59..909c6751 100644 --- a/builtins/testcmd/testcmd.go +++ b/builtins/testcmd/testcmd.go @@ -81,10 +81,18 @@ import ( ) // Cmd is the "test" builtin command registration. -var Cmd = builtins.Command{Name: "test", Description: "evaluate conditional expression", MakeFlags: builtins.NoFlags(runTest)} +var Cmd = builtins.Command{ + Name: "test", + Description: "evaluate conditional expression", + MakeFlags: builtins.NoFlags(runTest), +} // BracketCmd is the "[" builtin command registration. -var BracketCmd = builtins.Command{Name: "[", Description: "evaluate conditional expression", MakeFlags: builtins.NoFlags(runBracket)} +var BracketCmd = builtins.Command{ + Name: "[", + Description: "evaluate conditional expression", + MakeFlags: builtins.NoFlags(runBracket), +} const helpText = `Usage: test EXPRESSION or: [ EXPRESSION ] diff --git a/builtins/tests/help/help_test.go b/builtins/tests/help/help_test.go index afa5bbd4..9ff0b31b 100644 --- a/builtins/tests/help/help_test.go +++ b/builtins/tests/help/help_test.go @@ -114,14 +114,14 @@ func TestHelpIncludesDescriptions(t *testing.T) { // Spot-check a few descriptions. assert.Contains(t, stdout, "concatenate and print files") assert.Contains(t, stdout, "write arguments to stdout") - assert.Contains(t, stdout, "display available commands") + assert.Contains(t, stdout, "display help for commands") assert.Contains(t, stdout, "list directory contents") } func TestHelpIncludesFooterHint(t *testing.T) { stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands()) assert.Equal(t, 0, code) - assert.Contains(t, stdout, "Run ' --help' for more information on a specific command.") + assert.Contains(t, stdout, "Run 'help ' for more information on a specific command.") } func TestHelpColumnsAligned(t *testing.T) { @@ -234,23 +234,23 @@ func TestHelpAlwaysAvailableNoCommands(t *testing.T) { // --- Error handling --- -func TestHelpRejectsArguments(t *testing.T) { - stdout, _, code := runScript(t, "help foo", "", interp.AllowAllCommands()) +func TestHelpUnknownCommandShowsError(t *testing.T) { + _, stderr, code := runScript(t, "help foo", "", interp.AllowAllCommands()) assert.Equal(t, 1, code) - assert.Contains(t, stdout, "Usage: help") + assert.Contains(t, stderr, "no help topics match 'foo'") } -func TestHelpRejectsMultipleArguments(t *testing.T) { - stdout, _, code := runScript(t, "help foo bar baz", "", interp.AllowAllCommands()) - assert.Equal(t, 1, code) - assert.Contains(t, stdout, "Usage: help") +func TestHelpShowsCommandHelp(t *testing.T) { + stdout, _, code := runScript(t, "help echo", "", interp.AllowAllCommands()) + assert.Equal(t, 0, code) + assert.Contains(t, stdout, "echo: echo [-neE]") } func TestHelpFlagPrintsUsage(t *testing.T) { stdout, _, code := runScript(t, "help --help", "", interp.AllowAllCommands()) assert.Equal(t, 1, code) assert.Contains(t, stdout, "Usage: help") - assert.Contains(t, stdout, "Takes no arguments.") + assert.Contains(t, stdout, "Display help for builtin commands.") } func TestHelpUnknownFlagRejected(t *testing.T) { @@ -287,7 +287,7 @@ func TestHelpListsItself(t *testing.T) { stdout, _, code := runScript(t, "help", "", interp.AllowAllCommands()) assert.Equal(t, 0, code) assert.Contains(t, stdout, "help") - assert.Contains(t, stdout, "display available commands") + assert.Contains(t, stdout, "display help for commands") } // --- Empty stderr on success --- diff --git a/builtins/tr/tr.go b/builtins/tr/tr.go index 1db00d8d..43133d75 100644 --- a/builtins/tr/tr.go +++ b/builtins/tr/tr.go @@ -65,7 +65,11 @@ import ( ) // Cmd is the tr builtin command descriptor. -var Cmd = builtins.Command{Name: "tr", Description: "translate or delete characters", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "tr", + Description: "translate or delete characters", + MakeFlags: registerFlags, +} const readBufSize = 32 * 1024 diff --git a/builtins/true/true.go b/builtins/true/true.go index 36c6155a..d77e299e 100644 --- a/builtins/true/true.go +++ b/builtins/true/true.go @@ -23,7 +23,15 @@ import ( ) // Cmd is the true builtin command descriptor. -var Cmd = builtins.Command{Name: "true", Description: "return successful exit status", MakeFlags: builtins.NoFlags(run)} +var Cmd = builtins.Command{ + Name: "true", + Description: "return successful exit status", + Help: `true: true + Return a successful result. + + Exit with a status code indicating success.`, + MakeFlags: builtins.NoFlags(run), +} func run(_ context.Context, _ *builtins.CallContext, _ []string) builtins.Result { return builtins.Result{} diff --git a/builtins/uniq/uniq.go b/builtins/uniq/uniq.go index 4af72a27..0bb3c399 100644 --- a/builtins/uniq/uniq.go +++ b/builtins/uniq/uniq.go @@ -85,7 +85,11 @@ import ( ) // Cmd is the uniq builtin command descriptor. -var Cmd = builtins.Command{Name: "uniq", Description: "report or omit repeated lines", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "uniq", + Description: "report or omit repeated lines", + MakeFlags: registerFlags, +} // MaxLineBytes is the per-line buffer cap for the line scanner. const MaxLineBytes = 1 << 20 // 1 MiB diff --git a/builtins/wc/wc.go b/builtins/wc/wc.go index c9839bce..a80059ad 100644 --- a/builtins/wc/wc.go +++ b/builtins/wc/wc.go @@ -68,7 +68,11 @@ import ( ) // Cmd is the wc builtin command descriptor. -var Cmd = builtins.Command{Name: "wc", Description: "print newline, word, and byte counts", MakeFlags: registerFlags} +var Cmd = builtins.Command{ + Name: "wc", + Description: "print newline, word, and byte counts", + MakeFlags: registerFlags, +} const chunkSize = 32 * 1024 // 32 KiB read buffer const nonRegularMinWidth = 7 // GNU wc minimum column width for non-regular files diff --git a/tests/scenarios/cmd/exit/help.yaml b/tests/scenarios/cmd/exit/help.yaml new file mode 100644 index 00000000..b4fc4296 --- /dev/null +++ b/tests/scenarios/cmd/exit/help.yaml @@ -0,0 +1,9 @@ +description: Exit --help displays usage information without exiting the shell. +input: + script: |+ + exit --help + echo "still running" +expect: + stdout: "exit: exit [n]\n Exit the shell.\n \n Exits the shell with a status of N. If N is omitted, the exit status\n is that of the last command executed.\nstill running\n" + stderr: "" + exit_code: 0 diff --git a/tests/scenarios/cmd/help/restricted_commands.yaml b/tests/scenarios/cmd/help/restricted_commands.yaml index cf781169..16883e20 100644 --- a/tests/scenarios/cmd/help/restricted_commands.yaml +++ b/tests/scenarios/cmd/help/restricted_commands.yaml @@ -7,8 +7,8 @@ input: expect: stdout: | echo write arguments to stdout - help display available commands + help display help for commands - Run ' --help' for more information on a specific command. + Run 'help ' for more information on a specific command. stderr: "" exit_code: 0 diff --git a/tests/scenarios/cmd/help/too_many_args.yaml b/tests/scenarios/cmd/help/too_many_args.yaml index c71343d5..c6e6a8e3 100644 --- a/tests/scenarios/cmd/help/too_many_args.yaml +++ b/tests/scenarios/cmd/help/too_many_args.yaml @@ -1,9 +1,9 @@ -description: Help prints usage when arguments are provided. +description: Help prints error for unknown command. skip_assert_against_bash: true input: script: |+ help foo expect: - stdout_contains: ["Usage: help"] - stderr: "" + stdout: "" + stderr: "help: no help topics match 'foo'\n" exit_code: 1 diff --git a/tests/scenarios/shell/for_clause/break_cont/break_help.yaml b/tests/scenarios/shell/for_clause/break_cont/break_help.yaml new file mode 100644 index 00000000..c3fbd987 --- /dev/null +++ b/tests/scenarios/shell/for_clause/break_cont/break_help.yaml @@ -0,0 +1,8 @@ +description: Break --help displays usage information. +input: + script: |+ + for i in 1; do break --help; done +expect: + stdout: "break: break [n]\n Exit for, while, or until loops.\n \n Exit a FOR, WHILE or UNTIL loop. If N is specified, break N enclosing\n loops.\n \n Exit Status:\n The exit status is 0 unless N is not greater than or equal to 1.\n" + stderr: "" + exit_code: 2 diff --git a/tests/scenarios/shell/for_clause/break_cont/continue_help.yaml b/tests/scenarios/shell/for_clause/break_cont/continue_help.yaml new file mode 100644 index 00000000..8692ed5d --- /dev/null +++ b/tests/scenarios/shell/for_clause/break_cont/continue_help.yaml @@ -0,0 +1,8 @@ +description: Continue --help displays usage information. +input: + script: |+ + for i in 1; do continue --help; done +expect: + stdout: "continue: continue [n]\n Resume for, while, or until loops.\n \n Resumes the next iteration of the enclosing FOR, WHILE or UNTIL loop.\n If N is specified, resumes the Nth enclosing loop.\n \n Exit Status:\n The exit status is 0 unless N is not greater than or equal to 1.\n" + stderr: "" + exit_code: 2