{"id":45984270,"url":"https://github.com/bruno-de-queiroz/oosh","last_synced_at":"2026-04-04T23:07:58.277Z","repository":{"id":341294835,"uuid":"1167957948","full_name":"bruno-de-queiroz/oosh","owner":"bruno-de-queiroz","description":"Annotation-driven bash CLI framework — flags, help, and tab completion for free","archived":false,"fork":false,"pushed_at":"2026-03-04T00:52:46.000Z","size":1840,"stargazers_count":20,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-05T06:54:44.691Z","etag":null,"topics":["autocompletion","bash","bash-framework","cli","cli-framework","code-generation","developer-tools","shell"],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bruno-de-queiroz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-26T21:40:30.000Z","updated_at":"2026-03-05T05:58:32.000Z","dependencies_parsed_at":"2026-03-05T03:02:21.185Z","dependency_job_id":null,"html_url":"https://github.com/bruno-de-queiroz/oosh","commit_stats":null,"previous_names":["bruno-de-queiroz/oosh"],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/bruno-de-queiroz/oosh","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bruno-de-queiroz%2Foosh","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bruno-de-queiroz%2Foosh/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bruno-de-queiroz%2Foosh/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bruno-de-queiroz%2Foosh/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bruno-de-queiroz","download_url":"https://codeload.github.com/bruno-de-queiroz/oosh/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bruno-de-queiroz%2Foosh/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31418288,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T20:09:54.854Z","status":"ssl_error","status_checked_at":"2026-04-04T20:09:44.350Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["autocompletion","bash","bash-framework","cli","cli-framework","code-generation","developer-tools","shell"],"created_at":"2026-02-28T18:36:36.548Z","updated_at":"2026-04-04T23:07:58.261Z","avatar_url":"https://github.com/bruno-de-queiroz.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cvideo src=\"https://github.com/user-attachments/assets/216d82ae-2bc2-418c-bb75-d2b2e8614cc7\" width=\"100%\" autoplay loop muted playsinline\u003e\u003c/video\u003e\n\u003c/p\u003e\n\nAn annotation-driven bash CLI framework with a built-in generator. ✨\n\nSprinkle some annotations on your functions and get help text, autocompletion, and flag parsing **for free**. Supports bash 3.2+ (macOS \u0026 Linux) -- no exotic dependencies, just good ol' bash. 🐚\n\n**~45 lines of annotations replace ~130 lines of manual flag parsing, case statements, help text, and completion logic — for 7-12ms overhead on macOS, 2-4ms on Linux.**\n\n## 🚀 Quick start\n\n```bash\n# Install oosh\ncurl -fsSL https://raw.githubusercontent.com/bruno-de-queiroz/oosh/main/install.sh | bash\n\n# Spin up a brand-new CLI called \"mytool\"\noosh mytool\n\n# The generator offers to install right away -- or do it later:\nmytool install\n```\n\nDrop more `.sh` files into `modules/` and they're auto-discovered. That's it. Go build something cool. 🔧\n\n[Annotations](#-annotations-reference) · [Flags](#-flag-syntax) · [Modules](#-module-structure) · [Customizing](#-customizing) · [Autocompletion](#-autocompletion) · [Colors](#-colors) · [Generator](#-generator) · [Agent-friendly](#-agent-friendly) · [Claude Code](#-claude-code-skill) · [Limitations](#-known-limitations) · [Cheatsheet](#-cheatsheet) · [Performance](#-performance)\n\n### 🎬 What the generator looks like\n\n```\n    ___    ___    ___   _\n   / _ \\  / _ \\  / __| | |__\n  | (_) || (_) | \\__ \\ | '_ \\\n   \\___/  \\___/  |___/ |_| |_|\n\n  mytool -\u003e ~/.mytool\n\n  oo.sh                  framework engine\n  mytool.sh              entry point\n  mytool.comp.sh         bash completion\n  mytool.zcomp.sh        zsh completion\n  modules/hello.sh       sample module\n  modules/install.sh     CLI installer\n  modules/uninstall.sh   CLI uninstaller\n\n  Enable colored output? (Y/n) y\n  Install mytool to PATH? (Y/n) y\n  Installed\n\n  Done! Get started:\n\n    mytool help\n    mytool hello greet\n    mytool hello greet --name World\n```\n\n### 📖 Help output\n\nYour CLI comes with auto-generated help -- zero effort required:\n\n```\n  Usage: mytool [ hello install uninstall help ]\n\n  Commands:\n  help                 show options and flags available\n\n  Modules:\n  hello                Hello - sample module demonstrating oosh annotations\n  install              Install - install and configure the mytool CLI\n  uninstall            Uninstall - remove mytool from the system\n```\n\nAnd each module gets its own help too:\n\n```\n  Usage: hello [ greet farewell help ] [ -n ]\n\n  Flags:\n  -n|--name            who to greet\n\n  Commands:\n  greet                say hello\n  farewell             say goodbye\n  help                 show options and flags available\n```\n\nMethod-scoped flags appear indented under their command:\n\n```\n  Usage: mytool [ deploy status help ] [ -v ]\n\n  Flags:\n  -v|--verbose         enable verbose output\n\n  Commands:\n  deploy               deploy the app\n    -e|--env           target environment\n    -f|--file          config file path\n  status               check status\n  help                 show options and flags available\n```\n\n## 🏷️ Annotations reference\n\n| Annotation | Where | What it does |\n|---|---|---|\n| `#@module Description` | Top of module file | Module description shown in help |\n| `#@public [~ description]` | Before a function | Expose as a user-facing command |\n| `#@protected [~ description]` | Before a function | Hide from help/shortlist -- still callable internally |\n| `#@default` | Before a function | Mark this function to run when no arguments provided (instead of showing help) |\n| `#@flag -s\\|--long VAR \"default\" [type] [~ description]` | Before a function or top-level | Declare a flag with short/long form, env var, default, and optional type |\n| `#@description text` | After `#@flag` or `#@public`/`#@protected` | Legacy alternative to inline `~` descriptions |\n| `#@version x.y.z` | Top of module file | Module version shown with `--version` |\n\nDescriptions can be written inline using `~` on the same line, or on a separate `#@description` line (backward compatible). Flags declared after `#@public`/`#@protected` are scoped to that method and shown indented under it in help output.\n\nBuilt-in commands: `help` / `--help` / `-h` show help, `version` / `--version` / `-V` show version info.\n\n## 🚩 Flag syntax\n\n```bash\n#@flag -e|--env DEPLOY_ENV \"default\" ~ target environment\n\n#@flag -f|--file DEPLOY_FILEPATH \"\" file ~ path to config file (triggers file completion)\n\n#@flag -d|--dir DEPLOY_DIRPATH \"\" dir ~ output directory (triggers dir completion)\n\n#@flag -v|--verbose DEPLOY_VERBOSE \"false\" boolean ~ toggle flag (doesn't consume the next arg)\n\n#@flag -p|--port DEPLOY_PORT \"8080\" number ~ validated as numeric\n\n#@flag -e|--env DEPLOY_ENVIRONMENT \"prod\" enum(dev,staging,prod) ~ validated against allowed values\n\n#@flag -b|--branch DEPLOY_BRANCH \"\" enum(${_get_branches}) ~ dynamic enum resolved by calling a function\n\n#@flag -k|--key DEPLOY_API_KEY \"\" required ~ must be provided (errors before dispatch if missing)\n\n#@flag -p|--port DEPLOY_PORT \"\" required:number ~ required + type validation combined\n\n#@flag -t|--token DEPLOY_TOKEN \"${DEPLOY_TOKEN}\" ~ uses env var as fallback, shown in help as [env: DEPLOY_TOKEN]\n\n#@flag -l|--lang DEPLOY_LANG \"${DEPLOY_LANG:-en}\" ~ env var with inline fallback (uses \"en\" if env var is unset)\n\n#@flag -s|--secret DEPLOY_SECRET \"${DEPLOY_SECRET}\" required ~ required with env var fallback (satisfied if either is provided)\n```\n\n| Type | Completion | Validation |\n|---|---|---|\n| *(none)* | -- | -- |\n| `file` | file completion | -- |\n| `dir` | directory completion | -- |\n| `boolean` | -- | toggle: `--flag` sets `true`, only consumes `true`/`false` as next arg |\n| `number` | -- | must be numeric (integers or decimals) |\n| `enum(a,b,c)` | completes with listed values | must match one of the listed values |\n| `enum(${func})` | calls `func` at completion time | calls `func` at parse time for validation |\n| `required` | -- | errors before dispatch if not provided and value is empty |\n| `required:type` | inherits from type | required + type validation (e.g. `required:number`, `required:enum(a,b)`) |\n\n- Short and long forms separated by `|`\n- Variable name must be `UPPER_SNAKE_CASE`, prefixed with the module name (e.g. `deploy.sh` → `DEPLOY_`)\n- Default value in double quotes (empty string = no default). Escaped quotes supported: `\"say \\\"hello\\\"\"`\n- Optional description after `~` separator (or use `#@description` on the next line)\n- Function declarations work with or without the `function` keyword (`deploy() {` and `function deploy()` are both discovered)\n- Unknown flags are reported on stderr (e.g. `ignored unknown flag '--vrebose'`) and execution continues — flags destined for a child module are not warned about in the parent\n- A bare `--` stops flag parsing: everything after it is passed through as positional args (e.g. `mytool deploy -- --literal-arg`)\n- Unknown commands show an error with help text and exit 2 (e.g. `unknown command 'foo'`)\n\nFlags are parsed from `$@` and set as shell variables. If a flag isn't provided and the variable is unset, the default kicks in.\n\n**Required flags**: Add `required` as the type (or prefix with `required:` for compound types like `required:number`). If the flag isn't provided and the value is empty after defaults, the CLI errors before dispatch. Help/shortlist/version commands always work without required flags. Help output shows `(required)` next to the flag.\n\n**Env var fallback**: Use `\"${VAR_NAME}\"` as the default value to read from an environment variable. If the env var is set, its value becomes the default; if not, the default is empty. Use `\"${VAR_NAME:-fallback}\"` to provide an inline fallback when the env var is unset. Help output shows `[env: VAR_NAME]` so users know the fallback exists.\n\n```bash\n#@flag -k|--key API_KEY \"${API_KEY}\" required ~ must provide via flag or env\n#@flag -l|--lang LANG \"${LANG:-en}\" ~ reads $LANG, falls back to \"en\"\n```\n\nHelp output: `-k|--key             must provide via flag or env (required) [env: API_KEY]`\n\nPriority: explicit flag \u003e env var \u003e inline fallback \u003e empty\n\n**Method-scoped flags**: Flags declared after `#@public`/`#@protected` (but before the `function` line) belong to that method. They're shown indented under the command in help output and only appear in tab-completion when that command is selected.\n\n## 🧱 Module structure\n\n```bash\n#!/bin/bash\n#@module MyModule - does useful things\n\n#import oo.sh\n. ${MODULES_DIR}/../oo.sh\n\n#@flag -v|--verbose MYMODULE_VERBOSE \"false\" boolean ~ enable verbose output\n\n#@public ~ deploy the app\n#@flag -e|--env MYMODULE_ENVIRONMENT \"prod\" enum(dev,staging,prod) ~ target environment\nfunction deploy() {\n  echo \"Deploying to ${MYMODULE_ENVIRONMENT}...\"\n}\n\n#@protected ~ internal helper\nfunction _validate() {\n  # hidden from help and shortlist -- your little secret 🤫\n  echo \"validating...\"\n}\n\n# Bootstraps the parser\nmain $0 \"$@\"\n```\n\nIn the example above, `-v|--verbose` is a **module-level flag** (available to all commands), while `-e|--env` is **scoped to deploy** (shown only under the deploy command in help).\n\nEvery module **must** end with `main $0 \"$@\"` to bootstrap the annotation parser. Don't forget this or nothing works! ⚠️\n\n## 🎛️ Customizing\n\nThe entry point (`\u003cname\u003e.sh`) overrides three functions to route commands to modules:\n\n- **`_shortlist`** -- returns completable words for the current context\n- **`_help`** -- prints help text\n- **`_call`** -- dispatches the command\n\nThese delegate to `_default_shortlist`, `_default_help`, and `_default_call` for non-module commands. Override them further to add global commands or custom routing.\n\n## ⌨️ Autocompletion\n\nTab-completion works out of the box in **bash** and **zsh**! Both completion scripts call `\u003cname\u003e shortlist \u003cwords...\u003e` to figure out what to suggest at the current cursor position.\n\nSpecial markers:\n- `__file__` -- triggers file completion\n- `__dir__` -- triggers directory completion\n\nThese are returned automatically when a flag is declared with the `file` or `dir` type. Enum flags return their allowed values directly (static values or the output of a dynamic function). No extra wiring needed. 🪄\n\n**bash** uses `\u003cname\u003e.comp.sh` — registered via `complete -F` and sourced from `~/.bashrc`.\n\n**zsh** uses `\u003cname\u003e.zcomp.sh` — a native zsh completion function using `compdef` and `compadd`, sourced from `~/.zshrc` after `compinit`. No `bashcompinit` required.\n\n### 📦 Installation\n\nThe generator prompts to install after scaffolding. You can also run it manually:\n\n```bash\n\u003cname\u003e install\n```\n\nThis will:\n- Find a writable bin dir (`/opt/homebrew/bin`, `/usr/local/bin`, `~/.local/bin`)\n- Find a bash completion dir (`/opt/homebrew/etc/bash_completion.d`, `/usr/local/etc/bash_completion.d`, `/etc/bash_completion.d`, `/usr/share/bash-completion/completions`) -- falls back to `~/.bash_completion.d/` with profile sourcing\n- Symlink `\u003cname\u003e.sh` and `\u003cname\u003e.comp.sh` into those directories\n- Add `\u003cNAME\u003e_DIR`, `\u003cNAME\u003e_PATH` exports and completion sourcing to `~/.bashrc` and `~/.zshrc`\n- Sources `\u003cname\u003e.comp.sh` (bash) or `\u003cname\u003e.zcomp.sh` (zsh) from the appropriate profile — zsh gets native completion via `compdef`, no `bashcompinit` needed\n\nAnd when you're done? `\u003cname\u003e uninstall` cleans everything up. No leftovers. 🧹\n\n### 🔄 Updating\n\nTo update the oosh framework in an existing CLI, just run the generator again with the same name:\n\n```bash\noosh mytool\n```\n\nIf the directory already exists and contains `oo.sh`, the generator will offer to update it in place -- your modules and configuration are left untouched.\n\n### 🔍 Trace\n\nProfile tab-completion paths to find slow resolvers (e.g. dynamic enums calling `kubectl`):\n\n```bash\noosh trace mytool                    # trace everything\noosh trace mytool kube               # trace only the kube module\noosh trace mytool kube use           # trace only \"use\" command in kube\noosh trace ./modules/kube.sh         # trace a specific module file\noosh trace mytool -t 50              # custom threshold (default 150ms)\noosh trace mytool -r 10              # custom runs (default 5)\n```\n\nSample output:\n\n```\n    ___    ___    ___   _\n   / _ \\  / _ \\  / __| | |__\n  | (_) || (_) | \\__ \\ | '_ \\\n   \\___/  \\___/  |___/ |_| |_|\n\n  oosh trace — mytool (5 runs, threshold: 150ms)\n\n  Shortlist\n  ✔  shortlist                                  15ms\n  ✔  shortlist hello                            14ms\n  ✔  shortlist hello greet                      18ms\n  ✔  shortlist hello greet --name               12ms\n  ✘  shortlist kube deploy --namespace        5204ms\n\n  Help\n  ✔  help                                       45ms\n\n  1 warning — slowest: shortlist kube deploy --namespace (5204ms)\n```\n\nColors: green \u003c100ms, yellow 100–150ms, red \u003e150ms. Exit code: 0 if no warnings, 1 if any — CI-friendly.\n\n### 🔎 Lint\n\nCatch annotation mistakes before they bite you at runtime:\n\n```bash\noosh lint mytool                     # lint all files\noosh lint mytool hello               # lint only the hello module\noosh lint ./modules/deploy.sh        # lint a specific file\noosh lint mytool --fix               # auto-fix: add prefixes + placeholder descriptions\noosh lint mytool --no-color          # no ANSI codes (CI-friendly)\n```\n\nSample output:\n\n```\n    ___    ___    ___   _\n   / _ \\  / _ \\  / __| | |__\n  | (_) || (_) | \\__ \\ | '_ \\\n   \\___/  \\___/  |___/ |_| |_|\n\n  oosh lint — mytool\n\n  ►  mytool.sh\n  ►  hello.sh\n  ►  deploy.sh\n  ✘  error deploy.sh:5 — malformed #@flag — expected: #@flag -x|--name VAR \"default\" [type] [~ desc]\n  ✘  error deploy.sh:8 — invalid type 'string' for -t|--target\n  ⚠  warn  deploy.sh:12 — PATH shadows environment variable\n  ⚠  warn  deploy.sh:15 — flag TIMEOUT has no description\n\n  3 command(s), 8 flag(s) across 3 file(s)\n  2 errors, 2 warnings\n```\n\n**Errors** (exit 1): malformed `#@flag`, invalid type, orphaned `#@public`/`#@protected`, duplicate flag names in the same scope.\n\n**Warnings** (exit 0): missing module prefix (variable should start with `HELLO_` in `hello.sh`), variable name collisions across scopes, env var shadows (`PATH`, `HOME`, etc.), oosh internal shadows (`MODULES_DIR`, etc.), missing descriptions, cross-module variable collisions.\n\n**`--fix`** auto-fixes what it can: renames unprefixed variables to use the module prefix (both in annotations and `$VAR` / `${VAR}` usages) and appends `~ TODO` descriptions to flags missing them. Run `oosh lint` again after fixing to verify.\n\n## 🎨 Colors\n\nOutput is colored by default because life's too short for monochrome terminals. Three ways to tame it:\n\n- **`OO_COLOR=0`** -- edit `oo.sh` to permanently disable (the generator asks during setup)\n- **`NO_COLOR=1`** -- environment variable ([no-color.org](https://no-color.org)) to disable per-session\n- **`--no-color`** -- pass to `generate.sh` to run the generator itself without colors\n\n## ⚙️ Generator\n\n```bash\n./generate.sh \u003cname\u003e [output-dir]\n./generate.sh --no-color \u003cname\u003e [output-dir]\n```\n\n- `\u003cname\u003e` -- CLI tool name (e.g., `sc`, `devops`, `mytool`)\n- `[output-dir]` -- parent directory (defaults to `~/.\u003cname\u003e`), creates `\u003coutput-dir\u003e/\u003cname\u003e/`\n- `--no-color` -- disable colored generator output\n\nGenerated structure:\n\n```\n\u003cname\u003e/\n├── \u003cname\u003e.sh        # Entry point\n├── \u003cname\u003e.comp.sh   # Bash completion\n├── \u003cname\u003e.zcomp.sh  # Zsh completion (native, no bashcompinit)\n├── oo.sh            # Framework engine\n└── modules/\n    ├── hello.sh       # Sample module\n    ├── install.sh     # CLI installer (symlinks + profile setup)\n    └── uninstall.sh   # CLI uninstaller (self-deletes)\n```\n\n## 🤖 Agent-friendly\n\noosh is designed to be easy for AI agents to work with. To add functionality to an oosh-generated CLI, an agent just needs to drop a `.sh` file into `modules/` following this template:\n\n```bash\n#!/bin/bash\n#@module Name - short description\n\n. ${MODULES_DIR}/../oo.sh\n\n#@flag -x|--example NAME_VAR \"default\" ~ what this flag does\n# types: file, dir, boolean, number, enum(a,b,c), enum(${func})\n\n#@public ~ what this command does\nfunction mycommand() {\n  echo \"doing things with ${NAME_VAR}\"\n}\n\nmain $0 \"$@\"\n```\n\nThat's it -- no config files, no registration, no build step. The module is auto-discovered and immediately available with help text, flag parsing, and tab completion. Agents can scaffold entire CLIs by generating one module per concern. 🧩\n\n## 🧠 Claude Code Skill\n\nThe installer optionally installs skills for [Claude Code](https://claude.ai/code), giving you slash commands to work with oosh from within your editor:\n\n| Command | What it does |\n|---|---|\n| `/oosh \u003cname\u003e` | Scaffold a new CLI |\n| `/oosh-module \u003ccli\u003e \u003cmodule\u003e` | Create a new module with proper annotations |\n| `/oosh-lint \u003ccli\u003e [module]` | Lint annotations and offer auto-fixes |\n| `/oosh-trace \u003ccli\u003e [module]` | Profile tab-completion and flag slow resolvers |\n\nSkills are installed to `~/.claude/skills/` during `install.sh` (only prompted if Claude Code is detected). To install manually:\n\n```bash\nREPO=\"https://raw.githubusercontent.com/bruno-de-queiroz/oosh/main\"\nfor skill in oosh oosh-module oosh-lint oosh-trace; do\n  mkdir -p ~/.claude/skills/$skill\n  curl -fsSL \"$REPO/skill/$skill/SKILL.md\" -o ~/.claude/skills/$skill/SKILL.md\ndone\nmkdir -p ~/.claude/skills/oosh/references\ncurl -fsSL \"$REPO/skill/references/annotations.md\" -o ~/.claude/skills/oosh/references/annotations.md\n```\n\n## 📋 Known limitations\n\n- **Two-level command nesting** — oosh supports `cli module command` (entry point dispatches to a module, module dispatches to a function). Arbitrary depth (`cli foo bar baz`) is not supported. Each module is a self-contained script with its own `main` call.\n- **Control characters in values** — flag values are internally delimited with `\\x1F` (unit separator) and arrays use `\\x1E` (record separator). Values containing these characters will be split incorrectly. This is a non-issue in practice but worth knowing if you're piping binary data through flags.\n- **Global variables** — the framework uses `GLOBAL_SCRIPT`, `GLOBAL_METHODS`, `GLOBAL_FLAGS`, `GLOBAL_PREFIX`, `GLOBAL_VERSION`, and `_SL_*` variables at the top level. These are reset on each `main()` call and isolated per-process in the module system, but avoid naming your own variables with these names.\n\n## 💡 Cheatsheet\n\nPatterns for things oosh doesn't handle natively.\n\n### Mutually exclusive flags\n\nValidate in your function body — oosh parses both, you enforce the constraint:\n\n```bash\n#@public ~ deploy the app\n#@flag -s|--staging DEPLOY_STAGING \"false\" boolean ~ deploy to staging\n#@flag -p|--production DEPLOY_PRODUCTION \"false\" boolean ~ deploy to production\nfunction deploy() {\n  if [[ \"$DEPLOY_STAGING\" == true \u0026\u0026 \"$DEPLOY_PRODUCTION\" == true ]]; then\n    _error \"--staging and --production are mutually exclusive\"; exit 2\n  fi\n}\n```\n\nAlternatively, use an enum to make it a single choice:\n\n```bash\n#@flag -t|--target DEPLOY_TARGET \"staging\" enum(staging,production) ~ deployment target\n```\n\n### Dynamic enum from a slow source\n\nWrap the call in a function that caches to a temp file:\n\n```bash\n_get_namespaces() {\n  local cache=\"/tmp/_ns_cache_$$\"\n  if [[ ! -f \"$cache\" ]]; then\n    kubectl get ns -o name | sed 's|namespace/||' \u003e \"$cache\"\n  fi\n  cat \"$cache\"\n}\n#@flag -n|--namespace DEPLOY_NS \"\" enum(${_get_namespaces}) ~ k8s namespace\n```\n\noosh resolves dynamic enums lazily (only when the flag is actually used), so help and shortlist won't trigger the slow call. But tab-completing the flag will — the cache helps there.\n\n### Multiline or computed enum values\n\nDynamic enums can return any list — build it however you want:\n\n```bash\n_get_regions() {\n  # hardcoded but maintainable in one place\n  printf \"us-east-1\\nus-west-2\\neu-west-1\\nap-southeast-1\\n\"\n}\n\n_get_envs() {\n  # computed from directory contents\n  ls environments/ | sed 's/\\.yaml$//'\n}\n#@flag -r|--region DEPLOY_REGION \"\" enum(${_get_regions}) ~ AWS region\n#@flag -e|--env DEPLOY_ENV \"\" enum(${_get_envs}) ~ environment config\n```\n\n### Dependent flags (flag B only makes sense with flag A)\n\nValidate the dependency in your function:\n\n```bash\n#@flag -f|--format REPORT_FORMAT \"text\" enum(text,json,csv) ~ output format\n#@flag -o|--output REPORT_OUTPUT \"\" file ~ write to file (requires --format json or csv)\nfunction report() {\n  if [[ -n \"$REPORT_OUTPUT\" \u0026\u0026 \"$REPORT_FORMAT\" == \"text\" ]]; then\n    _error \"--output requires --format json or csv\"; exit 2\n  fi\n}\n```\n\n### Flag value with spaces\n\nUsers can quote values or use `=` syntax:\n\n```bash\nmytool --name \"John Doe\"\nmytool --name=\"John Doe\"\n```\n\n### Passing flag-like values as positional args\n\nUse `--` to stop flag parsing. Everything after `--` is passed through to the function as `$@`:\n\n```bash\nmytool run -- --not-a-flag -x something\n# $@ in run() receives: --not-a-flag -x something\n```\n\nThe `--` itself is consumed and not passed through.\n\n### Default that references another variable\n\nCompute it in the function body — annotation defaults are static strings or env var lookups:\n\n```bash\n#@flag -o|--output DEPLOY_OUTPUT \"\" ~ output path\nfunction deploy() {\n  : \"${DEPLOY_OUTPUT:=\"${DEPLOY_TARGET}/build\"}\"  # set default from another flag\n}\n```\n\n### Global pre-flight checks\n\nOverride `_call` in your entry point or module to run validation before dispatch:\n\n```bash\n_call() {\n  # skip checks for help/completion\n  case \"$1\" in help|shortlist|--help|-h|--version|-V) _default_call \"$@\"; return ;; esac\n\n  # pre-flight\n  command -v docker \u003e/dev/null || { _error \"docker is required\"; exit 1; }\n\n  _default_call \"$@\"\n}\n```\n\n### Custom help sections\n\nOverride `_help` to append or replace the default output:\n\n```bash\n_help() {\n  _default_help\n  printf \"  ${_B}Examples:${_RST}\\n\"\n  printf \"  ${_DIM}%s${_RST}\\n\" \"mytool deploy --env staging\"\n  printf \"  ${_DIM}%s${_RST}\\n\" \"mytool deploy --env production --dry-run\"\n  echo \"\"\n}\n```\n\n### Deeper command nesting\n\noosh natively supports two levels (`cli module command`). For deeper nesting like `mytool db migrate up`, override `_shortlist` and `_call` in your module to add a sub-group:\n\n```bash\n#!/bin/bash\n#@module DB - database operations\n\n. ${MODULES_DIR}/../oo.sh\n\n# --- sub-group: migrate ---\n_migrate_commands=\"up down status\"\n\nmigrate-up()     { echo \"running migrations...\"; }\nmigrate-down()   { echo \"rolling back...\"; }\nmigrate-status() { echo \"pending: 3\"; }\n\n# --- top-level commands ---\n#@public ~ seed the database\nfunction seed() { echo \"seeding...\"; }\n\n_shortlist() {\n  case \"$1\" in\n    migrate)\n      # third level: mytool db migrate \u003ctab\u003e\n      if [[ -n \"$2\" ]]; then\n        _default_shortlist migrate \"$2\"  # handle flags on migrate sub-commands\n      else\n        echo $_migrate_commands\n      fi ;;\n    *)\n      _default_shortlist \"$@\"\n      echo migrate  # add migrate as a completable word alongside seed, help\n      ;;\n  esac\n}\n\n_call() {\n  case \"$1\" in\n    migrate)\n      local sub=\"${2:-help}\"; shift 2 2\u003e/dev/null || shift\n      case \"$sub\" in\n        up|down|status) \"migrate-${sub}\" \"$@\" ;;\n        help) printf \"\\n  Usage: db migrate [ ${_migrate_commands} ]\\n\\n\" ;;\n        *)    _error \"unknown migrate command '${sub}'\"; exit 2 ;;\n      esac ;;\n    *) _default_call \"$@\" ;;\n  esac\n}\n\nmain $0 \"$@\"\n```\n\nThis gives you `mytool db migrate up`, `mytool db migrate status`, etc. with working tab completion at every level. The pattern scales — add more sub-groups by extending the `case` statements in `_shortlist` and `_call`.\n\n## ⏱️ Performance\n\noosh parses annotations at runtime — no compilation, no caching. The display and dispatch layer (`help`, `shortlist`, `_call`) uses zero external process forks — all string operations are pure bash builtins (`${var%%pattern}`, `${var//old/new}`, glob matching). Here's the overhead vs a hand-rolled pure bash CLI (~130 lines of manual flag parsing, case statements, help text, and completion) doing the same job that oosh does in ~45 lines of annotations.\n\n**macOS (bash 3.2, Apple Silicon, 20 runs)**\n\n| Operation | bash | oosh | Overhead |\n|---|---|---|---|\n| Tab-complete: top-level | 6ms | 13ms | +7ms |\n| Tab-complete: command flags | 6ms | 14ms | +8ms |\n| Tab-complete: enum values | 6ms | 14ms | +8ms |\n| Help | 6ms | 18ms | +12ms |\n| Dispatch: simple command | 6ms | 13ms | +7ms |\n| Dispatch: enum + number flags | 6ms | 14ms | +8ms |\n| Dispatch: all flags combined | 6ms | 15ms | +9ms |\n\n**Linux (bash 5.1, Docker ubuntu:22.04, 20 runs)**\n\n| Operation | bash | oosh | Overhead |\n|---|---|---|---|\n| Tab-complete: top-level | 4ms | 7ms | +3ms |\n| Tab-complete: command flags | 4ms | 7ms | +3ms |\n| Tab-complete: enum values | 4ms | 7ms | +3ms |\n| Help | 4ms | 8ms | +4ms |\n| Dispatch: simple command | 4ms | 6ms | +2ms |\n| Dispatch: enum + number flags | 4ms | 6ms | +2ms |\n| Dispatch: all flags combined | 4ms | 6ms | +2ms |\n\nWorth noting: the pure bash baseline is ~130 lines of tedious boilerplate (manual `case` flag parsing, `_parse_global_flags`, per-command `while/case` loops, hand-written help text, hand-written completion function). The oosh module is ~45 lines of annotations. That's the trade-off — 2-12ms overhead for 3x less code and zero manual flag parsing. All times include bash startup (~6ms on macOS, ~4ms on Linux).\n\nUsage errors (invalid flags, missing required flags, unknown commands) exit with code 2 following POSIX convention. Runtime errors exit with code 1.\n\nHappy hacking! 🎉\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbruno-de-queiroz%2Foosh","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbruno-de-queiroz%2Foosh","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbruno-de-queiroz%2Foosh/lists"}