shell-auto_complete
Version, currently 0.9.121 versions
- 2.5.0latestJul 26, 2026
- 2.4.1not indexedJul 26, 2026
- 2.4.0not indexedJul 26, 2026
- 2.3.0not indexedJul 26, 2026
- 2.2.1not indexedJul 26, 2026
- 2.2.0not indexedJul 26, 2026
- 2.1.0not indexedJul 26, 2026
- 2.0.1not indexedJul 26, 2026
- 2.0.0not indexedJul 26, 2026
- 1.2.0not indexedJul 26, 2026
- 1.1.1not indexedJul 26, 2026
- 1.1.0not indexedJul 26, 2026
- 1.0.3not indexedJul 26, 2026
- 1.0.2not indexedJul 26, 2026
- 1.0.1not indexedJul 26, 2026
- 1.0.0not indexedJul 26, 2026
- 0.9.3not indexedJul 26, 2026
- 0.9.2not indexedJul 26, 2026
- 0.9.1not indexedJul 26, 2026
- 0.9.0not indexedJul 26, 2026
- 0.1.0not indexedJul 26, 2026
github.com/plambert/shell-auto_complete.cr
Opinionated CLI option parsing and shell autocompletion for Crystal
Nothing has been indexed for 0.9.1 yet. The tag is recorded, its shard.yml has not been read, so the manifest and dependency list below are empty because they are unknown rather than because they are absent.
Installation
# Add this to your shard.yml
dependencies:
shell-auto_complete:
github: plambert/shell-auto_complete.cr
version: ~> 0.9.1Then run:
shards installshard.yml
No shard.yml has been indexed for 0.9.1. You can read it on the repository.
Dependencies
Unknown: the shard.yml for this version has not been read yet.
README
This README is the one indexed from the repository at its latest ref, not from the tag for this version.
shell-auto_complete
A Crystal shard for building command-line applications. You declare your commands, flags, and
positional arguments once with a small macro DSL, and the shard generates the argument parser,
--help text, and shell completion scripts (bash, zsh, fish) from that single definition.
Introduction
Most CLI frameworks make you write argument parsing in one place and shell completions in another,
by hand, and keep them in sync forever. This shard generates both from the same source — and the
completions are smart: they call back into your binary, so dynamic candidates, alias filtering,
@[Flags] enum trailing-comma completion, and filesystem completion all stay correct as your CLI
changes.
A program is a tree of command classes. Each declares flags and positionals, optionally
subcommands, and a run method. dispatch(ARGV) parses the line, intercepts --help and
--shell-completion, routes to the right subcommand, and calls its run.
Installation
Add the dependency to your shard.yml:
dependencies:
shell-auto_complete:
github: plambert/shell-auto_complete.cr
version: ~> 2.0
Run shards install.
Basic use
A single command with a few flags and a variadic positional:
require "shell-auto_complete"
enum LogLevel
Debug
Info
Warn
Error
end
Shell::AutoComplete.command Build, name: "build", description: "Build the project" do
flag message : String?, "--message", "--msg", "-m", "Build message"
flag color : Bool = true, "--color", "-c", "Colorize output"
flag log_level : LogLevel = LogLevel::Info, "--log-level", "Log verbosity", shortcut_flags: true
flag jobs : Int32 = 1, "--jobs", "-j", "Parallel jobs", range: 1..64
# Use Array(Path) to automatically enable file path completion in shells!
positionals files : Array(Path), "Source files", min: 1
def run
puts "building #{files.size} file(s) with #{jobs} job(s), log=#{log_level}"
end
end
Build.dispatch(ARGV)
Build it and the binary already supports the full surface:
build src/main.cr src/lib.cr --jobs 8 --debug # --debug is a generated shortcut for --log-level debug
build --no-color src/main.cr # --no-color is generated from the Bool flag
build --help
build --shell-completion bash > /etc/bash_completion.d/build
--help is generated from the same declarations, with a placeholder showing what each flag expects:
Usage: build [options] <files...>
Build the project
Options:
--message, --msg, -m TEXT Build message
--color, -c Colorize output
--log-level debug|info|warn|error Log verbosity
--jobs, -j NUMBER Parallel jobs
Positional arguments:
<files...> Source files
Adding subcommands is a second command class plus subcommand:
Shell::AutoComplete.command Tool, name: "tool", description: "Project tool" do
flag verbose : Bool = false, "--verbose", "-V", "Verbose output"
subcommand Build
end
Tool.dispatch(ARGV)
Now tool build ..., tool --verbose build ... (a parent flag before the subcommand), and tool build --help all work, and completion descends into subcommands.
For step-by-step recipes ("accept one or more files", "validate only even integers above 5", "accept
--include/--exclude keeping their order"), see cookbook.md. For complete
runnable programs, see examples/.
Reference
Defining a command
Shell::AutoComplete.command ClassName, name: "cmd", description: "...", **options do
# flags, positionals, subcommands, before_run, version config
def run
# parsed values are available as methods (port) or ivars (@port)
end
end
ClassName.dispatch(ARGV)
command options:
name:— the program/subcommand name (defaults to the basename ofPROGRAM_NAME).aliases:— alternate names this command answers to when routed as a subcommand (see Subcommands and routing).description:— one-line description shown in help.parent:— inherit another command's flags (see Inheriting flags).header:/footer:— prose before theUsage:line and after the body.usage:— override the generatedUsage:line.help_sections:— order/omit the middle help sections, any subset of[:description, :options, :subcommands, :positionals].
dispatch(argv, rescue_errors: true, stdout: STDOUT, stderr: STDERR) parses and runs. With
rescue_errors: true (default) a ParseError prints cmd: <message> to stderr and exits 1; with
false it raises.
Flags
flag declaration : Type, "--long", "-s", "--alias", "Description", **options
The first non-dash, non-placeholder string literal is the description; --x strings are the
canonical spelling and long aliases; a single -x is the short form. A Bool/Bool? flag is a
switch (no value, auto --no- negation); every other type takes a value. Give a value flag a
default with =; a nilable type without one defaults to nil.
Flag options:
| Option | Effect |
|---|---|
choices: | restrict to a set (%w[a b c]); drives validation, completion, placeholder |
range: | numeric range validation (1..65535) |
matches: | regex validation for strings |
transform_with: | class method String -> value (may raise ArgumentError) |
validate_with: | class method value -> Bool | String (String = error message) |
complete_with: | class method CompletionContext -> Array(String) for dynamic completion |
negatable: false | suppress the generated --no- for a switch |
hidden: true | omit from help (still parses) |
delimiter: | required on Array/Set: "," splits each value, nil is one element per occurrence |
set_operations: true | Set flag treats +x add, -x remove, bare add |
hash_operations: false | Hash flag rejects the bare -key delete form |
shortcut_flags: | enum flag: true, or {only:/except:/aliases:} (see Enums) |
placeholder: | help metavar for the value |
group: | render under a named help heading |
immediate: | switch flag: run a handler and exit as soon as it appears |
override: true | replace an inherited or imported flag of the same spelling |
description: | the description as a named option (needed before a bare constant) |
Per-element transform_with:/validate_with: apply to Array/Set/Hash element values too.
Value types
Out of the box, value flags and positionals accept: every Int*/UInt*/Float*, String, Char,
Bool/Bool?, Path, File, Dir, URI, Time, Regex, Log::Severity, and
Socket::IPAddress. Collections: Array(T), Set(T), Hash(String, T). Synthetic constrained
types ship under Shell::AutoComplete::Types: PositiveInt, NonNegativeInt, Percentage,
EpochTime, Date, EnvVar, DirPath, and the SetDelta positional type. A union type, or an
element type with no built-in parser (Tuple(String, String)), needs an explicit transform_with:.
Placeholders
A value flag's help shows a placeholder derived from its type (NUMBER, TEXT, FILE, URL,
KEY=VALUE, pipe-joined values for small enums/choices:). Override it three ways: an ALL-CAPS
string before the description ("--port", "PORT", "..."), embedded in the flag string
("--after TIME"), or placeholder: "HOST[:port]". A leftover string literal the shard can't place
is a compile error. Switches take no placeholder.
Positionals
positional name : String, "Description" # one required (nilable type = optional)
positionals files : Array(Path), "Description", min: 1, max: 10 # variadic, at most one per command
Path/File/Dir complete against the filesystem; File/Dir also check existence, and
Shell::AutoComplete::Types::DirPath completes directories without checking. A
Shell::AutoComplete::Types::SetDelta variadic positional binds +name/-name/name tokens into
a Hash(String, Bool). The placeholder in the usage line is derived from the property name
(<files...>), not an ALL-CAPS string.
Subcommands and routing
subcommand ChildClass (in the parent's block, or by reopening the parent class) registers a child.
Routing walks past the parent's own flags to find the subcommand word, so a shared flag may sit
before or after it. When a subcommand declares a flag the parent doesn't, the parent still routes
past it (consulting the subcommand's flag arity) and the subcommand accepts or rejects it — so
tool --format json list works while tool --format json other is rejected at other. Subcommands
disagreeing on whether a shared spelling takes a value is a compile error.
A subcommand may answer to more than one name with aliases: on its command macro:
Shell::AutoComplete.command Move,
name: "move",
aliases: ["mv", "rename"],
description: "Move or rename a file",
parent: Files do
# ...
end
Each alias routes to the command exactly as its canonical name does, is offered in completion, and
is listed beside the name in the parent's help (move, mv, rename). A canonical name match on any
subcommand wins over an alias, so an alias can never shadow another command's real name.
external_subcommands (on a root command's block) adds git-style external dispatch: a subcommand
word matching no declared subcommand is looked up on PATH as <command_name>-<word>, and if found
the process is replaced (exec) with it, passing every argument after the word. Declared
subcommands always win; a word containing a path separator is never looked up; and when nothing is
found the usual unknown subcommand error is raised. It works with declared subcommands or on its
own (a pure PATH-dispatch tool), and discovered external subcommands are offered in completion
alongside declared ones. Declaring it on a parent:-derived command is a compile error.
search_path: restricts the lookup to a fixed, colon-separated directory list instead of PATH. A
relative entry resolves against the directory holding the running binary, so
external_subcommands search_path: "commands:../lib/commands:/etc/tool/commands" for a binary at
/opt/tool/bin/tool searches /opt/tool/bin/commands, /opt/tool/lib/commands, and
/etc/tool/commands, in order, regardless of the caller's PATH. Both dispatch and completion use
it.
Inheriting flags
parent: OtherCommand on the command macro makes a command inherit every flag the parent declares
— real properties, parsed and completed, shown under an Inherited options: heading. Inheritance
chains through levels and is independent of subcommand routing. A leaf flag colliding with an
inherited one is a compile error unless it uses override: true.
Sharing flags across commands
Shell::AutoComplete.common_flag :format, format : Format = Format::Table, "--format", "Output format"
# inside a command:
import_flags :format, :quiet
common_flag defines a reusable flag once, outside any command; import_flags replays a chosen
subset inside a command, where each behaves exactly as a directly declared flag (registry, help,
completion, duplicate detection). Different commands import different subsets.
Call common_flag at the top level, not inside a module. It defines its replay macro in the
scope where it expands, while import_flags resolves that macro from the command class — so a
catalog nested in a module is invisible even to commands in that same module
(undefined local variable '__sac_common_flag_<name>').
Ordered flag groups
ordered_flag_group "Filter rules (in command-line order)",
{"--include" => "PATTERN: include", "--exclude" => "PATTERN: exclude"} do |key, value|
@rules << {key, value} # key has "--" stripped; runs at parse time, in argv order
end
For the rsync/tar shape where the interleaving between flags is the semantics. The block runs once
per occurrence in command-line order; raising ArgumentError becomes a parse error. Value-taking
members only.
Delimited flags
delimited_flag command : Array(String), "--command", "-c", "Command to run"
Captures a run of raw argv tokens into a collection, ending at a delimiter (default --, which is
discarded). Every token in the run is appended verbatim, so flag-looking tokens are taken literally
— the env/xargs/time shape where a whole sub-command is embedded:
tool --command echo hello -- --json path
# @command => ["echo", "hello"] (built with .new then <<(String) per token)
# @json => true (parsing resumes after the delimiter)
The declared type only has to answer .new and <<(String) (Array(String), Set(String), or a
custom type). If the delimiter never appears, capture runs to the end of argv; when the flag is
absent the property is an empty .new (or nil, if the type is nilable). The delimiter is
configurable with delimiter:. Only the space-separated form is captured — --command=x is not a
delimited invocation. Composes with subcommands through parent: inheritance: the routing walk
skips the capture to find the subcommand word.
external_command: true marks the captured value as a command line for completion: inside the
capture the shell completes the first word as a command name and the rest with that command's own
completion (falling back to file completion) — via _command_offset (bash), _normal (zsh), and
complete -C (fish). It changes completion only, not parsing.
before_run hooks
before_run do
# runs on the parsed instance after parsing, before run
end
For once-before-run setup that mutates shared state or can fail (open a connection, configure a
global, resolve an inherited flag, cross-flag validation). Raising ArgumentError becomes a clean
parse error. Hooks collect down a parent: hierarchy and run parent-first, so subcommands inherit
base setup without super. They run only for the command whose run executes.
Version
tool_version "1.2.3" and tool_name "name" set the strings (plain strings; both inherit via
parent:). --version with no subcommand prints <name> <version> and exits;
enable_version_subcommand adds a version subcommand printing the same line. Without
tool_version, the version is the nearest visible VERSION constant, falling back to
shards version at build time. Declaring your own --version flag, or disable_version_flag,
turns the intercept off.
Introspection
On a parsed instance: flag_given?(:name) reports whether a flag was set under any spelling
(distinguishing an explicit value from absence); parsed_occurrences : Array({String, String?}) is
an ordered log of every flag occurrence (spelling as typed, raw value or nil).
Shell completion
Every command gets --shell-completion <bash|zsh|fish>, which prints a script that calls back into
your binary for candidates:
eval "$(mytool --shell-completion bash)" # try it in the current shell
mytool --shell-completion zsh > ~/.zsh/completions/_mytool
mytool --shell-completion fish > ~/.config/fish/completions/mytool.fish
By default the generated script's callback invokes the command by name, so it resolves through
PATH — the right behavior for an installed script. For testing a dev build, add --absolute (or
-a): the callback then invokes this exact binary by its resolved absolute path, so
eval "$(bin/mytool --shell-completion bash --absolute)" completes against bin/mytool even when a
different mytool is on PATH. The command name still registers the completion, so re-eval the
installed script when you're done. Don't bake --absolute into an installed script — the path goes
stale when the binary moves.
Completion covers subcommands (declared and external), flag names (with alias filtering),
choices:/enum values, @[Flags] trailing commas, dynamic complete_with: candidates, embedded
commands (delimited_flag ..., external_command: true), and native filesystem completion for
Path/File/Dir.
Using String, the shell completion won't complete anything. If you are expecting one of a fixed
list of values, either use an Enum, or set choices: %w(a b c) on the flag.
Examples
Runnable programs under examples/, each with its own README:
- cat — BSD/GNU
catclone: multipleBoolflags, a variadicArray(Path)positional,-as a stdin marker. - deploy — custom
transform_with:,validate_with:, andcomplete_with:on one command, including all three on a single flag. - multitool — every bundled value type and synthetic type, across a subcommand tree.
- containers — a docker-style CLI:
parent:inheritance,before_run, acommon_flagcatalog withimport_flags, the routing union,--version, andshortcut_flagsconfiguration. - sync — an rsync-style CLI:
ordered_flag_group,parsed_occurrences,choices:/range:/matches:,set_operations:, animmediate:flag, and per-element transforms. - toggles — a feature-flag CLI: a
SetDeltapositional, dotted/colonHashkeys,Bool?tri-state withflag_given?, andoverride:.
Documentation
- Cookbook — task-oriented recipes.
- Design spec
- CHANGELOG
Contributing
Bug reports and PRs welcome.
License
MIT (see LICENSE).
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
0.9.1- Tagged
- Jul 26, 2026
- Commit
614ffe658292- Indexed
- not yet
Dependents
Repository
github.com/plambert/shell-auto_complete.cr
Metadata
- Created
- Aug 12, 2026
- Updated
- Aug 17, 2026
- Synced
- Aug 17, 2026
- Versions
- 21