Commands are defined using comment-based annotations in .sh files under the commands/ directory.
Marks a file as containing a command. Required for command discovery.
# @cmd
cmd_hello() {
echo "Hello!"
}Command description shown in help output.
# @cmd
# @desc Greet the user with a friendly messageDefine a positional argument.
# @arg name Optional argument
# @arg name! Required argument (!)
# @arg items~ Variadic argument (multiple values)Example:
# @cmd
# @desc Copy files to destination
# @arg src! Source file (required)
# @arg dest! Destination path (required)
# @arg extras~ Additional files to copy
cmd_copy() {
local src="$1"
local dest="$2"
shift 2
local -a extras=("$@")
cp "$src" "$dest"
for f in "${extras[@]}"; do
cp "$f" "$dest"
done
}Provide a static list of completion values for a named argument. Unlike @complete which calls a function at runtime,
@arg-values embeds the values directly in the annotation.
# @arg-values <name> <value1> <value2> ...name: Argument name (must match a preceding@argdeclaration)value1 value2 ...: Space-separated list of valid values
Example:
# @cmd
# @desc Upgrade project components
# @arg components~ Components to upgrade
# @arg-values components entry ide gitignore version workflows packaging globals all
cmd_upgrade() {
local -a components=("$@")
# ...
}When the user presses TAB:
myapp upgrade <TAB>→ shows:entry ide gitignore version workflows packaging globals all
Note: For dynamic completion values that require runtime computation, use @complete instead.
Define boolean flags (switches with no value). When present, the variable is set to "true"; when absent, the variable is unset.
# @flag -v, --verbose Enable verbose output
# @flag -q, --quiet Suppress output
# @flag --dry-run Show what would be done without executingFlags are available as opt_<name> variables:
# @cmd
# @desc Run with flags
# @flag -v, --verbose Enable verbose mode
# @flag -n, --dry-run Show what would be done
cmd_run() {
if [[ "${opt_verbose:-}" == "true" ]]; then
echo "Verbose mode enabled"
fi
if [[ "${opt_dry_run:-}" == "true" ]]; then
echo "[DRY RUN] Would execute..."
return 0
fi
}Define command-line options that take a value.
# @option -c, --config <file> Configuration file
# @option -n, --count <num> Number of iterations
# @option -e, --env <name> Environment name [default: local]Options are available as opt_<name> variables:
# @cmd
# @desc Run with options
# @option -c, --config <file> Config file path
# @option -e, --env <name> Environment name [default: development]
cmd_run() {
if [[ -n "${opt_config:-}" ]]; then
echo "Using config: $opt_config"
fi
echo "Environment: ${opt_env}"
}Define dynamic shell completion for arguments or options. The completion function is called at runtime to provide completion values.
# @complete <name> <function>name: Argument name or option long name (without--)function: Shell function that outputs completion values (one per line)
Example:
# @cmd
# @desc Install a package
# @arg name! Package name
# @complete name _my_complete_packages
# @option -c, --category <name> Filter by category
# @complete category _my_complete_categories
cmd_install() {
local name="$1"
local category="${opt_category:-}"
# ...
}
# Completion function for packages
_my_complete_packages() {
echo "fzf"
echo "bat"
echo "jq"
}
# Completion function for categories
_my_complete_categories() {
echo "cli-tools"
echo "languages"
echo "editors"
}When the user presses TAB:
myapp install <TAB>→ shows:fzf bat jqmyapp install -c <TAB>→ shows:cli-tools languages editors
Note: The completion function must be available when the shell completion script runs. For built-in commands, define completion functions in your project's libs. For user-facing features, ensure the functions are sourced in the entry script.
Enable passthrough mode for commands that need to forward all arguments to an external tool. When set, the framework skips argument parsing (getopt) and passes all arguments directly to the command function.
This is useful for wrapper commands that call other CLI tools (like vagrant, docker, kubectl) where you don't want the framework to interpret the external tool's options.
# @cmd
# @desc Run vagrant commands (passthrough to vagrant)
# @meta passthrough
# @example vg up
# @example vg provision myvm --provision-with shell
cmd_vg() {
# All arguments ($@) are passed directly without parsing
# Use environment variables for wrapper-specific options
exec vagrant "$@"
}Behavior in passthrough mode:
- No
@flag,@option, or@argparsing — all arguments go to the command -h/--helpis not intercepted (use<app> help <cmd>instead)- The command function must handle its own argument parsing
@flag/@option/@argannotations still appear in<app> help <cmd>output
When to use:
- Wrapper commands for external tools (vagrant, docker, kubectl, etc.)
- Commands where options conflict with external tool options
- Commands that need to pass through
--and options transparently
Show usage examples in help output.
# @cmd
# @desc Deploy application
# @arg env! Target environment
# @flag -f, --force Force deployment
# @example deploy staging
# @example deploy production --force
cmd_deploy() {
...
}#!/usr/bin/env bash
# src/main/shell/commands/db/migrate.sh
# @cmd
# @desc Run database migrations
# @arg version Target version (optional, defaults to latest)
# @flag -n, --dry-run Show what would be done without executing
# @flag -v, --verbose Show detailed output
# @option --env <name> Target environment (default: development)
# @example db migrate
# @example db migrate 20240101
# @example db migrate --dry-run --verbose
cmd_db_migrate() {
local version="${1:-latest}"
local dry_run="${opt_dry_run:-false}"
local verbose="${opt_verbose:-false}"
local env="${opt_env:-development}"
if [[ "$dry_run" == "true" ]]; then
echo "[DRY RUN] Would migrate to version: $version"
return 0
fi
if [[ "$verbose" == "true" ]]; then
echo "Environment: $env"
echo "Target version: $version"
fi
echo "Running migrations..."
}Subcommands are created by organizing command files into directories. The directory hierarchy maps directly to the command hierarchy.
commands/
├── hello.sh # myapp hello
├── db/
│ ├── migrate.sh # myapp db migrate
│ ├── seed.sh # myapp db seed
│ └── reset.sh # myapp db reset
├── config/
│ ├── get.sh # myapp config get
│ └── set.sh # myapp config set
└── vf/
├── init.sh # myapp vf init
├── list.sh # myapp vf list
└── template/
├── list.sh # myapp vf template list
└── show.sh # myapp vf template show
Function names follow the file path, replacing / with _ and prefixed by cmd_:
| File Path | Function Name | Invocation |
|---|---|---|
commands/hello.sh |
cmd_hello() |
myapp hello |
commands/db/migrate.sh |
cmd_db_migrate() |
myapp db migrate |
commands/vf/template/list.sh |
cmd_vf_template_list() |
myapp vf template list |
Each subcommand file is a standalone .sh file with # @cmd marker and a cmd_*() function:
# src/main/shell/commands/db/migrate.sh
# @cmd
# @desc Run database migrations
# @arg version Target version (optional, defaults to latest)
# @flag -n, --dry-run Show what would be done without executing
# @flag -v, --verbose Show detailed output
# @option --env <name> Target environment [default: development]
# @example db migrate
# @example db migrate 20240101
# @example db migrate --dry-run --verbose
cmd_db_migrate() {
local version="${1:-latest}"
local dry_run="${opt_dry_run:-false}"
local verbose="${opt_verbose:-false}"
local env="${opt_env:-development}"
if [[ "$dry_run" == "true" ]]; then
echo "[DRY RUN] Would migrate to version: $version (env=$env)"
return 0
fi
[[ "$verbose" == "true" ]] && echo "Environment: $env"
echo "Migrating to version: $version..."
}A directory under commands/ automatically becomes a command group (parent command). Command groups do not
need their own .sh file — the framework auto-generates group help from their subcommands.
$ myapp db
Error: Missing subcommand for: db
db - Available subcommands:
Usage:
myapp db <command >[options]
Commands:
migrate Run database migrations
seed Seed database with test data
reset Reset database to initial stateRequesting help explicitly also works:
$ myapp db --helpSubcommands can be nested to arbitrary depth. Each level is a subdirectory:
commands/vf/template/show.sh → myapp vf template show
The framework uses longest-match dispatch: given myapp vf template show --verbose, it matches
vf template show as the command and passes --verbose as the command argument.
- Files must contain
# @cmdto be discovered — files without this marker are ignored - Files starting with
_are ignored (reserved for internal helpers, e.g.,_utils.sh) - Discovery is recursive and automatic via
radp_cli_discover()
Use _-prefixed files for shared logic within a command group. These files are not discovered as commands
but can be sourced by sibling commands:
commands/db/
├── _common.sh # Shared DB utilities (not a command)
├── migrate.sh # Sources _common.sh if needed
└── seed.sh
# commands/db/migrate.sh
# @cmd
# @desc Run database migrations
# shellcheck source=./_common.sh
source "${BASH_SOURCE[0]%/*}/_common.sh"
cmd_db_migrate() {
db_connect # function from _common.sh
# ...
}myapp db migrate --dry-run 20240101
│
├─ Parse args: ["db", "migrate", "--dry-run", "20240101"]
│
├─ Longest match: "db" → group, "db migrate" → command (match!)
│
├─ Remaining args: ["--dry-run", "20240101"]
│
├─ Parse options/args from metadata:
│ opt_dry_run="true"
│ positional_args=("20240101")
│
├─ Source: commands/db/migrate.sh
│
└─ Execute: cmd_db_migrate "20240101"
Help is auto-generated at every level:
# Top-level help (all commands)
$ myapp --help
# Command group help (subcommands of db)
$ myapp db --help
# Specific command help (options, args, examples)
$ myapp db migrate --help