gbash

Custom Commands

gbash ships with a broad default registry of file, text, archive, and system commands. You can extend that registry with your own commands to expose application-specific functionality to scripts running inside the sandbox.

The Command Interface

Every command implements the commands.Command interface:

type Command interface {
	Name() string
	Run(ctx context.Context, inv *Invocation) error
}

The Invocation struct provides everything a command needs at runtime: Args, Env, Cwd, Stdin, Stdout, Stderr, FS (the sandbox filesystem), and Limits.

Defining a Command

The simplest way to create a command is commands.DefineCommand:

import "github.com/ewhauser/gbash/commands"
 
cmd := commands.DefineCommand("greet", func(ctx context.Context, inv *commands.Invocation) error {
	name := "world"
	if len(inv.Args) > 0 {
		name = inv.Args[0]
	}
	_, err := fmt.Fprintf(inv.Stdout, "hello, %s\n", name)
	return err
})

Registering Commands

Start with the default registry and add your commands to it:

registry := gbash.DefaultRegistry()
registry.Register(cmd)

Then pass the registry to the runtime:

gb, err := gbash.New(gbash.WithRegistry(registry))

Scripts can now call greet:

result, _ := gb.Run(ctx, &gbash.ExecutionRequest{
	Script: "greet Alice",
})
// result.Stdout => "hello, Alice\n"

Lazy Registration

For commands that are expensive to initialize, use RegisterLazy to defer construction until first use:

registry.RegisterLazy("expensive-cmd", func() (commands.Command, error) {
	return commands.DefineCommand("expensive-cmd", handler), nil
})

Exit Codes

Return a commands.ExitError to set a non-zero exit code:

return &commands.ExitError{Code: 1, Err: fmt.Errorf("something went wrong")}

Or use the helper to write to stderr and exit in one call:

return commands.Exitf(inv, 1, "file not found: %s", path)

Full Example

The examples/custom-zstd directory shows a complete custom command that adds zstd compression and decompression to gbash. The key pattern:

package main
 
import (
	"context"
	"github.com/ewhauser/gbash"
	"github.com/ewhauser/gbash/commands"
)
 
func main() {
	registry := gbash.DefaultRegistry()
	registry.Register(commands.DefineCommand("zstd", func(ctx context.Context, inv *commands.Invocation) error {
		// compress/decompress logic using inv.Stdin, inv.Stdout, inv.FS
		return nil
	}))
 
	gb, err := gbash.New(gbash.WithRegistry(registry))
	if err != nil {
		panic(err)
	}
 
	result, _ := gb.Run(ctx, &gbash.ExecutionRequest{
		Script: "echo data | zstd -o /tmp/data.zst && zstd -d /tmp/data.zst",
	})
	fmt.Print(result.Stdout)
}

See examples/custom-zstd/main.go for the full implementation.

Building a CLI Wrapper

The cli package provides a shared frontend for building custom gbash binaries with your commands baked in:

package main
 
import (
	"context"
	"os"
 
	"github.com/ewhauser/gbash"
	"github.com/ewhauser/gbash/cli"
)
 
func main() {
	cfg := cli.Config{
		Name: "myshell",
		BaseOptions: []gbash.Option{
			gbash.WithRegistry(buildRegistry()),
		},
	}
	code, err := cli.Run(context.Background(), cfg, os.Args[0], os.Args[1:], os.Stdin, os.Stdout, os.Stderr)
	if err != nil {
		os.Stderr.WriteString(err.Error() + "\n")
	}
	os.Exit(code)
}

This gives you a fully functional CLI with -c command strings, file execution, stdin piping, and interactive mode -- all backed by your custom command registry.