Skip to content

Latest commit

 

History

History
471 lines (352 loc) · 11.4 KB

File metadata and controls

471 lines (352 loc) · 11.4 KB

Command Annotations Reference

Commands are defined using comment-based annotations in .sh files under the commands/ directory.

Basic Annotations

@cmd

Marks a file as containing a command. Required for command discovery.

# @cmd
cmd_hello() {
  echo "Hello!"
}

@desc

Command description shown in help output.

# @cmd
# @desc Greet the user with a friendly message

Arguments

@arg <name>

Define 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
}

@arg-values

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 @arg declaration)
  • 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.

Flags

@flag

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 executing

Flags 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
}

Options

@option

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}"
}

Dynamic Completion

@complete

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 jq
  • myapp 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.

Meta Annotations

@meta passthrough

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 @arg parsing — all arguments go to the command
  • -h / --help is not intercepted (use <app> help <cmd> instead)
  • The command function must handle its own argument parsing
  • @flag/@option/@arg annotations 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

Examples

@example

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() {
  ...
}

Complete Example

#!/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

Subcommands are created by organizing command files into directories. The directory hierarchy maps directly to the command hierarchy.

Directory Structure

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 Naming

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

Writing a Subcommand

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..."
}

Command Groups

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 state

Requesting help explicitly also works:

$ myapp db --help

Nesting Depth

Subcommands 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.

Discovery Rules

  • Files must contain # @cmd to 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()

Internal Helper Files

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
  # ...
}

Dispatch Flow

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 Generation

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