This page describes shell completion support in Repl Toolkit, including setup modes and install commands.
Completion resolution is done by a bridge command:
completion __complete --shell <bash|powershell|zsh|fish|nu> --line <input> --cursor <position>
The shell passes current line + cursor, and Repl returns candidates on stdout (one per line).
completion __complete is mapped in the regular command graph through the shell-completion module (CLI channel only).
The bridge route is marked as protocol passthrough, so repl suppresses banners and routes framework diagnostics to stderr.
The bridge handler writes candidates through IReplIoContext.Output, which remains bound to the protocol stream (stdout) in local CLI passthrough.
IReplIoContext is optional in general protocol commands:
- optional for local CLI commands that already use
Console.*directly - recommended when handlers need explicit stream injection, deterministic tests, or hosted-session compatibility
The module exposes a real completion context scope:
completion installcompletion uninstallcompletion statuscompletion detect-shellcompletion __complete(protocol bridge; hidden)
Shell completion behavior is configured through ReplOptions.ShellCompletion:
Enabled(default:true)SetupMode(default:Manual)PreferredShell(optional override)PromptOnce(default:true)StateFilePath(optional)BashProfilePath/PowerShellProfilePath/ZshProfilePath/FishProfilePath/NuProfilePath(optional overrides)
SetupMode values:
Manual: no automatic profile mutation. User runs install/uninstall commands.Prompt: interactive startup can propose installation once.Auto: interactive startup installs automatically when a supported shell is confidently detected.
Prompt and Auto apply only when entering interactive mode. Terminal one-shot commands never auto-install.
The management surface is:
completion install [--shell bash|powershell|zsh|fish|nu] [--force] [--silent]
completion uninstall [--shell bash|powershell|zsh|fish|nu] [--silent]
completion status
completion detect-shell
These commands are CLI-only (they are not available in interactive mode or hosted session mode). They also require invoking the app through its own executable command head (the running process must match the app binary).
Structured output is supported via global output flags:
completion status --jsoncompletion detect-shell --output:jsoncompletion install --jsoncompletion uninstall --json
Notes:
completion installwrites a managed block in the shell profile.completion uninstallremoves only the managed block.completion statusprints mode, detection, profile paths, profile existence, and install status.completion detect-shellprints detected shell and detection reason.- for Nushell, a shared global dispatcher block is managed in addition to per-app blocks. If another app already manages it, use
--forceto merge.
Human output:
Enabled : True
Setup mode : Manual
Detected shell : powershell (env suggests PowerShell; parent process chain: <process-a> -> <process-b>)
Bash profile : <home>/.bashrc
Bash profile exists : True
Bash installed : False
PowerShell profile : <documents>/PowerShell/Microsoft.PowerShell_profile.ps1
PowerShell profile exists: True
PowerShell installed : False
Zsh profile : <home>/.zshrc
Zsh profile exists : False
Zsh installed : False
Fish profile : <config>/fish/config.fish
Fish profile exists : False
Fish installed : False
Nushell profile : <config>/nushell/config.nu
Nushell profile exists : False
Nushell installed : False
JSON output includes these per-shell fields:
bashProfilePath,bashProfileExists,bashInstalledpowerShellProfilePath,powerShellProfileExists,powerShellInstalledzshProfilePath,zshProfileExists,zshInstalledfishProfilePath,fishProfileExists,fishInstallednuProfilePath,nuProfileExists,nuInstalled
Shell detection is best-effort and uses weighted signals:
PreferredShelloverride (highest priority).- Environment variables (for example
BASH_VERSION,SHELL,PSModulePath). - Parent/grand-parent process names as validation (
bash,zsh,fish,nu,nushell,pwsh,powershell).
If signals are conflicting or weak, result is unknown (no auto-install in Auto mode).
- Command literals from the mapped graph.
- Static command options from handler parameters (resolved terminal routes).
- Static global options (
--help,--interactive,--no-interactive,--no-logo, output aliases,--output:<format>,--answer:<name>, and the result-flow flags--result:page-size,--result:cursor,--result:pager,--result:all). - Enum member names for a pending enum-typed route option.
WithCompletion(...)provider values — opt-in per provider (see below).
Not included:
- Dynamic data values for contexts.
Every shell Tab spawns a new process and blocks the user's shell until the bridge answers, so a slow completion provider (network, database) must never run there implicitly. A provider only serves shell completion when its registration opts in:
app.Map("contact inspect {clientId}", (string clientId) => Inspect(clientId))
.WithCompletion(
"clientId",
(ctx, input, ct) => LookupClientIdsAsync(input, ct),
CompletionProviderScope.InteractiveAndShell);The default scope (CompletionProviderScope.Interactive) keeps the provider on in-process
surfaces only: the interactive Tab menu and the complete ambient command. Opt in only
when the provider is fast enough for a blocking shell Tab, such as in-memory or local
lookups. Opted-in providers complete both the positional value being typed and a pending
route option's value, with the same binding rules as the interactive menu.
Bridge-side guarantees for provider values:
- Each provider invocation is bounded by
ShellCompletionOptions.ProviderTimeout(default: 1 second) — a stalled provider is abandoned and completion degrades to the static candidates instead of blocking the user's shell. - The bridge protocol is line-delimited plain text, so values containing control characters (newlines, ANSI/OSC sequences) are rejected whole rather than forwarded to the shell's completion UI.
- Values are emitted as literal shell data — a value needing quoting is single-quoted
(
New York→'New York',$(cmd)→'$(cmd)', never a form the shell would interpolate). A value containing an apostrophe is dropped (no single-quote literal can hold one without a shell-specific escape the bridge can't re-parse on the next Tab); a backslash is fine except on fish, whose single quotes escape it. The provider itself receives the decoded value prefix. - Completion requested from inside an already-open quote (e.g.
contact "Ne) yields no provider values, since the bridge cannot safely reshape the user's opening quote.
Install/uninstall is idempotent through namespaced markers:
# >>> repl completion [appId=<app-id>;shell=<bash|powershell|zsh|fish|nu>] >>># <<< repl completion [appId=<app-id>;shell=<bash|powershell|zsh|fish|nu>] <<<
Update/remove targets only the block matching the current app and shell.
The installed profile block is a thin shim: it forwards the command line to
<app> completion __complete and renders the result, so all completion logic lives in
the app binary and is picked up automatically the next time you run the upgraded binary.
The block itself changes only rarely (e.g. a shell adapter fix). When it does, re-run
completion install — it idempotently rewrites the managed block between the markers.
Auto/Prompt setup does not rewrite an already-installed block (it only checks for the
markers, which carry no version), so a block-level fix requires this manual re-install.
The bridge protocol and marker format are stable, so an old block keeps working with a
new binary and vice versa.
Bash:
_myapp_complete() {
local line cursor
local candidate
line="$COMP_LINE"
cursor="$COMP_POINT"
COMPREPLY=()
while IFS= read -r candidate; do
COMPREPLY[${#COMPREPLY[@]}]="$candidate"
done < <(myapp completion __complete --shell bash --line "$line" --cursor "$cursor" --no-interactive --no-logo)
}
complete -F _myapp_complete myappPowerShell:
$__replCompletionCommandNames = @('myapp')
$__replCompleter = {
param($wordToComplete, $commandAst, $cursorPosition)
$invokedCommand = if ($commandAst.CommandElements.Count -gt 0 -and $commandAst.CommandElements[0] -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
$commandAst.CommandElements[0].Value
} else {
'myapp'
}
# Rebuild the line and pad it to the cursor: $commandAst drops trailing
# whitespace, so an empty value position ('myapp deploy ') would otherwise be
# analyzed as the previous token.
$replLine = $commandAst.Extent.Text
$replCursor = $cursorPosition - $commandAst.Extent.StartOffset
if ($replCursor -lt 0) { $replCursor = 0 }
if ($replLine.Length -lt $replCursor) { $replLine = $replLine.PadRight($replCursor) }
& $invokedCommand completion __complete --shell powershell --line $replLine --cursor $replCursor --no-interactive --no-logo |
ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}
if ((Get-Command Register-ArgumentCompleter).Parameters.ContainsKey('Native')) {
Register-ArgumentCompleter -Native -CommandName $__replCompletionCommandNames -ScriptBlock $__replCompleter
} else {
Register-ArgumentCompleter -CommandName $__replCompletionCommandNames -ScriptBlock $__replCompleter
}Zsh:
_myapp_complete() {
local line cursor
local candidate
local -a reply
line="$BUFFER"
cursor=$((CURSOR > 0 ? CURSOR - 1 : 0))
reply=()
while IFS= read -r candidate; do
reply+=("$candidate")
done < <(myapp completion __complete --shell zsh --line "$line" --cursor "$cursor" --no-interactive --no-logo)
if (( ${#reply[@]} > 0 )); then
# -Q suppresses zsh's own quoting: the bridge already returns shell-literal syntax.
compadd -Q -- "${reply[@]}"
fi
}
compdef _myapp_complete myappFish:
function _myapp_complete
set -l line (commandline -p)
set -l cursor (commandline -C)
myapp completion __complete --shell fish --line "$line" --cursor "$cursor" --no-interactive --no-logo
end
complete -c myapp -f -a "(_myapp_complete)"Nushell:
const __repl_completion_entries = [
{ appId: "myapp", command: "myapp" }
]
def _repl_nu_dispatch_completion [spans: list<string>] {
if (($spans | length) == 0) {
return []
}
let head = ($spans | get 0)
let matches = ($__repl_completion_entries | where { |item| $item.command == $head })
if (($matches | length) == 0) {
return []
}
let entry = ($matches | get 0)
let line = ($spans | str join ' ')
let cursor = ($line | str length)
(
^$entry.command completion __complete --shell nu --line $line --cursor $cursor --no-interactive --no-logo
| lines
| each { |line| { value: $line, description: "" } }
)
}
$env.config = (
$env.config
| upsert completions.external.enable true
)
$env.config.completions.external.completer = { |spans| _repl_nu_dispatch_completion $spans }- PowerShell 7+ is recommended. In Windows PowerShell 5.1, native completion registration for external executables is limited and may not trigger reliably.
- Nushell uses one global
completions.external.completer; Repl manages a shared dispatcher block to route completions per app command head.
Shell completion and the interactive REPL autocomplete draw option-name candidates from the
same source, normalize prior tokens through the same parser profile (option values consumed,
POSIX -- honored, response files never expanded), and use the same option-prefix gate — a
single dash already surfaces short aliases such as -f, while signed numeric literals
(-42) stay positional. Both also complete enum values for a pending option (its member
names, under the parameter's effective case sensitivity), and both run WithCompletion
value providers — while the value is being typed for a positional parameter, and for a
pending option's value (provider first, enum fallback second). Both stop offering provider
values once the position can no longer bind at execution (a bound value, or the route's
trailing option region). Two deliberate differences remain:
- On an empty token after a complete command, shell completion lists option names (a
dump-style list is cheap there), while the interactive menu shows parameter placeholders —
options appear from the first typed
-. - The interactive menu runs every registered provider; the shell bridge only runs
providers registered with
CompletionProviderScope.InteractiveAndShell, because each shell Tab spawns a blocking process.