Command Prompt: Shell Scripting and Configuration
Reference skill for writing commands, scripts, and configuration across Unix shells. Detects the target shell from context and routes to the appropriate reference.
Target versions (May 2026):
- Zsh: 5.10
- Bash: 5.3
- Fish: 4.6
- Nushell: 0.111
- Tcsh: 6.24
- Dash: 0.5.13
When to use
- Writing shell commands, scripts, or one-liners
- Configuring dotfiles (
.zshrc,.bashrc,.profile,config.fish) - Writing completions, shell functions, or aliases
- Porting scripts between shells
- Debugging shell-specific behavior (globbing, arrays, expansion, quoting)
- Setting up oh-my-zsh, starship, p10k, or other shell frameworks
- Choosing which shell to target for a new script
- Writing interactive commands on the user's local machine (zsh)
When NOT to use
- Remote FreeBSD/OPNsense/pfSense commands - use firewall-appliance (handles tcsh/csh in the BSD context)
- Ansible shell/command modules - use ansible (module gotchas differ from raw shell)
- CI/CD pipeline shell blocks - use ci-cd (restricted environments, no interactive features)
- General Linux sysadmin that isn't shell-specific - just do the task directly
AI Self-Check
Before returning any generated shell script or command, verify:
- Shebang matches the detected target shell (not assumed bash)
-
set -euo pipefail(bash/zsh) orset -eu(POSIX sh) present in scripts - All variables double-quoted (
"$var") unless word splitting is intentional - No shell-isms from the wrong shell (no
[[ ]]in#!/bin/sh, noBASH_SOURCEin zsh) - Array indexing correct for the target shell (bash: 0-indexed, zsh: 1-indexed)
-
printfused overechofor non-trivial output - Glob safety guards in place (empty-glob case handled)
- No hardcoded paths for tools (
/usr/bin/git) - usecommand -vor bare command names - Temp files use
mktempwith cleanup traps, not hardcoded/tmp/foo - No secrets in command history (use
read -sor environment variables) - Current source checked: dated versions, CLI flags, API names, and support windows are verified against primary docs before repeating them
- Hidden state identified: local config, credentials, caches, contexts, branches, cluster targets, or previous runs are made explicit before acting
- Verification is real: final checks exercise the actual runtime, parser, service, or integration point instead of only linting prose or happy paths
- Routing overlap checked: overlapping skills, trigger terms, and "When NOT to use" boundaries are checked before returning guidance
- Spec claims verified: claims about tool behavior, output contracts, or repo conventions are checked against current docs, scripts, or skill files
- Shell identified: examples match POSIX sh, Bash, Zsh, or Fish semantics intentionally
- Quoting tested: paths with spaces, empty variables, and glob characters behave safely
Performance
- Use builtins and stream processing for large inputs; avoid command substitution that buffers entire files.
- Prefer
rg,fd, and targeted file lists when available, with portable fallbacks noted. - Avoid spawning subshells inside tight loops when
xargs, arrays, or shell builtins fit.
Best Practices
- Default to
set -euo pipefailonly when the script is written to handle those semantics. - Use
--before user-controlled paths for commands that support it. - Preview destructive expansions before
rm,mv,chmod,chown, or recursive edits.
Workflow
Step 1: Detect the target shell
Before writing any shell code, determine the target shell. Check these signals in order:
| Signal | How to check | Routes to |
|---|---|---|
| Shebang | First line of existing script | #!/usr/bin/env zsh -> zsh, #!/usr/bin/env bash -> bash, #!/bin/sh -> posix-sh |
| File name/extension | .zsh, .zshrc, .zprofile, .zshenv -> zsh; .bash, .bashrc, .bash_profile -> bash; .fish, config.fish -> fish | |
| User's shell | Conversation context, $SHELL | User's local machine = zsh |
| Task type | What the script does | See routing below |
Task-based routing
| Task | Target shell | Why |
|---|---|---|
| Interactive commands on user's machine | zsh | User's default shell |
| Portable scripts (new) | bash | Widest deployment, good feature set |
| Docker/CI containers | bash or sh | Containers often lack zsh |
| Minimal Alpine/BusyBox scripts | POSIX sh | Only ash/dash available |
| BSD system administration | tcsh | FreeBSD default (but see firewall-appliance skill) |
| Cross-shell startup (env vars, PATH) | POSIX sh | .profile sourced by all POSIX shells |
| Maximum portability requirement | POSIX sh | Only standard guaranteed on all Unixes |
Step 2: Load the right reference
| Target shell | Reference file |
|---|---|
| Zsh | references/zsh.md (~680 lines, 14 sections) |
| Bash | references/bash.md (~710 lines, 13 sections) |
| POSIX sh | references/posix-sh.md (~490 lines, 10 sections) |
| Fish, tcsh, nushell, others | references/alt-shells.md (~420 lines, 4 shells) |
Don't load all references. Pick the one that matches. If porting between two shells, load both.
Step 3: Write code, then verify
Use the cross-shell comparison below for quick lookups. After writing, run through the Verification Checklist at the bottom of this section.
Quick Cross-Shell Comparison
| Feature | POSIX sh | Bash | Zsh | Fish |
|---|---|---|---|---|
| Arrays | no (use $@) | 0-indexed | 1-indexed | lists (1-indexed) |
| Assoc arrays | no | declare -A (4.0+) | typeset -A | no |
Glob **/ | no | shopt -s globstar | built-in | built-in |
| Failed glob | passes literal | passes literal | error | no match |
[[ ]] | no | yes | yes | no (use test) |
Process sub <() | no | yes | yes + =() | (command | psub) |
| Word splitting | on unquoted $var | on unquoted $var | no | no |
| Arithmetic | $(( )) only | $(( )), (( )), let | $(( )), (( )) | math |
| String lowercase | - | ${var,,} | ${var:l} | string lower |
| Completions | none | basic (bash-completion) | powerful (compsys) | powerful (built-in) |
| Config file | .profile | .bashrc | .zshrc | config.fish |
| Shebang | #!/bin/sh | #!/usr/bin/env bash | #!/usr/bin/env zsh | #!/usr/bin/env fish |
| Script safety | set -eu | set -euo pipefail | set -euo pipefail | N/A (strict by default) |
| Non-forking cmd sub | no | ${ cmd; } (5.3+) | ${ cmd } (5.10+) | no |
Universal Patterns (All POSIX Shells)
These work in sh, bash, and zsh. Fish has different syntax for most of these - see the alt-shells reference.
Piping and redirection
| Pattern | Effect |
|---|---|
cmd1 | cmd2 | Pipe stdout of cmd1 to stdin of cmd2 |
cmd > file | Redirect stdout to file (overwrite) |
cmd >> file | Redirect stdout to file (append) |
cmd 2> file | Redirect stderr to file |
cmd &> file | Redirect both stdout and stderr (bash/zsh, not POSIX) |
cmd 2>&1 | Redirect stderr to stdout |
cmd > /dev/null 2>&1 | Silence all output (POSIX-portable) |
cmd < file | Feed file as stdin |
cmd <<'EOF' | Here document (single-quoted delimiter = no expansion) |
cmd <<< "string" | Here string (bash/zsh, not POSIX) |
cmd1 | tee file | cmd2 | Send stdout to both file and cmd2 |
Chaining
| Pattern | Behavior |
|---|---|
cmd1 ; cmd2 | Run sequentially, ignore exit codes |
cmd1 && cmd2 | Run cmd2 only if cmd1 succeeds (exit 0) |
cmd1 || cmd2 | Run cmd2 only if cmd1 fails (exit non-0) |
cmd & | Run in background |
cmd1 && cmd2 || cmd3 | Poor man's if/else (not reliable - cmd3 runs if cmd2 fails too) |