Guide for adding and modifying shell builtin commands in Crush's embedded shell (internal/shell/). Use when creating a new builtin command, editing an existing one, or understanding how commands are intercepted in the mvdan/sh interpreter.
Crush's shell (internal/shell/) uses mvdan.cc/sh/v3 for POSIX shell
emulation. Commands can be intercepted before they reach the OS by adding
builtins — functions handled in-process.
Builtins live in Shell.builtinHandler() in internal/shell/shell.go.
This is an interp.ExecHandlerFunc middleware registered in
execHandlers() before the block handler, so builtins run even for
commands that would otherwise be blocked.
The handler is a switch on args[0]. Each case either handles the command
inline or delegates to a helper function.
builtinHandler() in shell.go.os.Stdin/os.Stdout.
This ensures the builtin works with pipes and redirections:
case "mycommand":
hc := interp.HandlerCtx(ctx)
return handleMyCommand(args, hc.Stdin, hc.Stdout, hc.Stderr)
internal/shell/mycommand.go). The function signature should accept
args, stdin, stdout, and stderr:
func handleMyCommand(args []string, stdin io.Reader, stdout, stderr io.Writer) error {
// args[0] is the command name ("mycommand"), args[1:] are arguments.
// Write output to stdout, errors to stderr.
// Return nil on success, or interp.ExitStatus(n) for non-zero exit codes.
}
nil for success, interp.ExitStatus(n) for
non-zero exit codes. Write error messages to stderr before returning.builtinHandler() is already registered
in execHandlers().| Command | File | Description |
|---|---|---|
jq | jq.go | JSON processor using github.com/itchyny/gojq |