Script Commands
When you think of powerful CLI tools - git, docker, kubectl - they all share a common pattern: they're organized
around commands. You don't just run git with flags; you run git commit, git push, git branch - each a distinct
operation with its own arguments.
Rad lets you build tools like this through first-class command support. You can define multiple commands in a single script, each with their own arguments and implementation.
Basic Syntax¶
Let's start with a very simple example:
1 2 3 4 5 6 7 8 | |
This script defines a single command called greet that takes a name argument.
Invoke it by specifying the command name followed by its arguments:
> ./script.rad greet Alice
Hello, Alice!
Let's break down the syntax:
command greet:- Defines a command namedgreetname str- The command takes one required string argument callednamecalls greet_user- Specifies which function to execute when this command runsfn greet_user():- Defines the function that implements the command logic (defined after commands)
Command arguments (like name) become script-wide variables, accessible throughout your script.
Underscores become hyphens
Command names with underscores are automatically converted to hyphens for CLI invocation.
This matches standard CLI conventions (like kubectl, docker-compose).
For example, command deploy_staging: is invoked as deploy-staging:
> ./script.rad deploy-staging
This is the same convention used for argument names.
Multiple Commands¶
The power of commands emerges when you define several in one script. Let's create a simple deployment tool:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Now you can invoke either command:
> ./tool.rad deploy staging
Deploying to staging...
Deployment complete!
> ./tool.rad status production
Checking status of production...
Environment production is healthy
Each command has its own arguments and implementation, but they live in the same script and can share code.
Adding Descriptions¶
Commands should include descriptions to make your tool self-documenting. Use the familiar --- ... --- header syntax:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
These descriptions appear in the help output:
> ./tool.rad -h
Usage:
tool.rad [command] [OPTIONS]
Commands:
deploy Deploy the application to an environment
status Check the health of an environment
Notice how Rad automatically generates a usage string listing all available commands.
Multi-line descriptions
Just like script headers, command descriptions can span multiple lines:
command deploy:
---
Deploy the application to an environment.
This will build, test, and deploy your application.
---
Important: The first line appears in the script's overall help output, so keep it concise. Additional lines
only appear when you request help for that specific command (./tool.rad deploy -h).
Command Arguments¶
Each command can define its own arguments using the same syntax you learned in Args. Let's expand our deployment tool:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
The arguments work exactly as they do in the args: block - you can use defaults, optional types, constraints, and
comments for help text.
Invoke with positional arguments:
> ./tool.rad deploy staging feature-branch
Running tests...
Deploying feature-branch to staging...
✅ Deployment complete!
Or use flags (especially for booleans):
> ./tool.rad deploy --env=production --skip-tests
⚠️ Skipping tests
Deploying main to production...
✅ Deployment complete!
Shared Args¶
Often you want arguments that apply to all commands - like a --verbose flag or a --config path. Define these in
an args: block to share them across commands:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
Shared args are available to all commands:
> ./tool.rad deploy staging --verbose
Config: ~/.config/tool.yaml
Deploying to staging...
✅ Deployed!
> ./tool.rad status production --verbose
Config: ~/.config/tool.yaml
Checking production...
Environment healthy
Shared args are flag-only
When commands exist, shared args can only be passed as flags (like --verbose, -v, or --config=value), not
positionally. That keeps the first positional argument free to name a command - or, if you declare a
default command, to be that command's first argument.
Both long form (--verbose) and short form (-v) work for shared args.
Command-specific args can be positional or flags, just like regular script args.
Nested Commands¶
Larger tools group related commands under a namespace, like git remote add. A command that contains other commands becomes one: it has no calls of its own, and routes to its sub-commands instead.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
Name the full path to reach the command you want:
> ./tool.rad remote add origin https://github.com/me/repo.git
> ./tool.rad remote remove origin
Args declared directly on a namespace are shared with every descendant. They behave like the script-level args: block, just scoped to that sub-tree - flag-only, and accepted at any point along the path:
> ./tool.rad remote add origin https://... --timeout 5
> ./tool.rad remote --timeout 5 add origin https://...
Namespaces nest as deep as you like. Only the terminal command's callback runs; an intermediate namespace exists to group and to share args, not to execute.
Default Commands¶
Most tools have one thing they mostly do. Marking a command default means the user doesn't have to type its name:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
> ./remind.rad 15m standup
Adding: [ "15m", "standup" ]
./remind.rad add 15m standup does exactly the same thing. The default keeps its own name, and that matters more than it looks.
When A Value Looks Like A Command¶
A named command always wins. So if you want to add a reminder that says "list", the short form won't do it - list is a command:
> ./remind.rad list # runs do_list, no reminder added
Two ways to say you meant the value. Name the default explicitly, or use --:
> ./remind.rad add list # adds a reminder saying "list"
> ./remind.rad -- list # same
Which command runs, exactly
Routing happens at the first token the level can't consume itself. A command name appearing after one of those goes to the default as data, not as a command. If a flag is needed by the default but rejected elsewhere, declare it as a shared arg so it's consumed before routing.
Defaults And Namespaces¶
The rule is the same at every level, so a namespace's default is what runs when you name the namespace and stop:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
./tool.rad remote lists remotes, exactly as ./tool.rad remote list would.
Only leaf commands can be marked. A namespace routes rather than runs, so marking one would leave the question unanswered - mark one of its sub-commands instead.
Command Callbacks¶
We've been using function references (calls on_deploy), which is the recommended approach for most commands.
However, for very short implementations, you can also use inline lambdas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
Shared Logic¶
You can write code after all command blocks that runs before any callback is invoked. This is useful for setup logic that all commands need:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
When you run ./script.rad deploy staging, the flow is:
- Parse arguments
- Run shared logic (lines 12-14)
- Run the callback (
on_deploy)
This pattern is useful for loading configuration files, setting up connections, or validating preconditions that apply to all commands.
Getting Help¶
Rad automatically generates help documentation for your commands. There are two levels of help:
Script-level help shows all available commands:
> ./tool.rad -h
Usage:
tool.rad [command] [OPTIONS]
Commands:
deploy Deploy the application
rollback Rollback a deployment
status Check environment status
Command-level help shows arguments for a specific command:
> ./tool.rad deploy -h
Deploy the application
Usage:
deploy <env> [branch] [OPTIONS]
Command args:
--env str Environment to deploy to
--branch str Branch to deploy from (default "main")
--skip-tests Skip running tests before deploy
-v, --verbose Enable verbose output
--config str (default "~/.config/tool.yaml")
Notice how the help includes:
- The command description
- Required and optional arguments
- Default values
- Shared args (like
--verboseand--config) - Help text from
#comments
Practical Example¶
Here's a concise, realistic example that demonstrates the "dev script" pattern - a common use case for replacing messy Makefiles or complex package.json script sections with a single, readable CLI entry point.
Dev Script¶
Instead of remembering different commands for building, testing, and running your project, you can wrap them in a
single dev.rad script. This demonstrates shared arguments, boolean flags, and how to pass arguments down to underlying tools:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
Usage:
> ./dev.rad start
🚀 Starting server on http://localhost:3000...
> ./dev.rad start --port 8080 --detach
🚀 Starting server on http://localhost:8080...
Server started in background
> ./dev.rad test --grep "login_flow" --watch
🧪 Running tests...
> ./dev.rad build
📦 Building for production...
✅ Build complete in ./dist
Notice how this example uses:
- Shared args (
--verbose) available to all commands - Command-specific arguments with defaults (
port,detach,grep,watch) - Shared logic that runs before any callback
- Function references for callbacks (
calls on_start, etc.) - Integration with shell commands to wrap existing tools
- Clear, self-documenting help text
Summary¶
- Script commands partition scripts into operations using
command name:blocks - Each command has:
- Its own arguments (using standard
argssyntax) - A description block (
--- ... ---) - A callback implementation (function reference or inline lambda)
- Its own arguments (using standard
- Shared args (from
args:block) are available to all commands- Must be passed as flags when commands exist
- Shared logic runs before any callback. Write code after command blocks for setup that all commands need.
- Help is automatic:
./script -hlists available commands./script command -hshows command-specific help
- Callbacks:
- Function references:
calls function_name(recommended) - Inline lambdas:
calls fn():(for short implementations)
- Function references:
- Use script commands to build CLI tools, not just scripts
Next¶
Rad provides a powerful system for looking up values from predefined resource files, which is particularly useful for building interactive tools.
We'll explore this in the next section: Resources.