Skip to content
Merged
2 changes: 2 additions & 0 deletions allowedsymbols/symbols_builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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.
Expand Down
19 changes: 18 additions & 1 deletion builtins/break/break.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
4 changes: 3 additions & 1 deletion builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
Expand Down Expand Up @@ -198,6 +199,7 @@ var registry = map[string]HandlerFunc{}
type CommandMeta struct {
Name string
Description string
Help string
}

var metaRegistry = map[string]CommandMeta{}
Expand Down
6 changes: 5 additions & 1 deletion builtins/cat/cat.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 18 additions & 1 deletion builtins/continue/continue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
6 changes: 5 additions & 1 deletion builtins/cut/cut.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion builtins/echo/echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 15 additions & 1 deletion builtins/exit/exit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:]
Expand Down
10 changes: 9 additions & 1 deletion builtins/false/false.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
6 changes: 5 additions & 1 deletion builtins/find/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion builtins/grep/grep.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion builtins/head/head.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 59 additions & 13 deletions builtins/help/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<command> --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 <command> — 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) {
Comment thread
matt-dz marked this conversation as resolved.
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 {
Expand All @@ -65,7 +111,7 @@ func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc {
callCtx.Outf("%-*s %s\n", maxLen, name, meta.Description)
}

callCtx.Out("\nRun '<command> --help' for more information on a specific command.\n")
callCtx.Out("\nRun 'help <command>' for more information on a specific command.\n")
return builtins.Result{}
}
}
6 changes: 5 additions & 1 deletion builtins/ip/ip.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion builtins/ls/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 6 additions & 1 deletion builtins/printf/printf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion builtins/ps/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading