Course outline

Bash / Advanced: channels, concurrency, and maintainable CLI design / Lesson 18

Publish a maintainable command-line contract

Choose a supported shell language, parse options predictably, document compatibility, and keep modules and tests aligned with the CLI.

About 4 min readContent reviewed
On this page

Lesson goals

  • Separate POSIX portability from Bash-version compatibility.
  • Use getopts with deliberate error and operand handling.
  • Design a stable CLI with tests for unsupported and malformed input.

Portability is a declared target set

A portable script names the shells and platforms it intends to support. POSIX sh is a specification target rather than a synonym for every installed shell. Bash adds arrays, [[, process substitution, coprocesses, and other facilities that should not appear in a script advertised as generic sh without qualification. Conversely, avoiding every Bash feature can make a Bash-only program needlessly obscure. Choose the language from deployment needs and maintenance cost. Bash POSIX mode adjusts documented behavior but does not prove the script works on another implementation; test the actual target set.

A recent manual can describe unavailable features

Record a supported minimum or feature set and verify it on deployment. New substitution forms, wait options, array details, and builtin flags can differ from the tested Bash baseline. A feature probe or explicit startup check can produce a clear diagnostic instead of a later syntax surprise, but code requiring unsupported syntax may fail parsing before a runtime check executes. Keep the entry strategy consistent with that limitation. Use matching documentation and tests rather than claiming all current online examples run on older distribution shells. The course records exactly which interpreter validated its runnable examples.

Parse short options with explicit errors

getopts processes an option vocabulary and tracks progress in OPTIND. A colon after an option letter requires an argument; a leading colon selects controlled diagnostic handling. After parsing, shift by OPTIND minus one and validate the remaining operands. Reset OPTIND when reusing a parser in the same shell with a new argument list. getopts does not automatically provide a universal long-option parser or validate the meaning of an option value. Keep help, invalid-option, missing-argument, and extra-operand behavior explicit and return documented statuses without mixing diagnostics into data.

bash Parse one short option and one exact operand
parse() {
  local OPTIND=1 option label=default
  while getopts ':n:' option; do
    case $option in
      n) label=$OPTARG ;;
      :) return 2 ;;
      \?) return 2 ;;
    esac
  done
  shift "$((OPTIND - 1))"
  [[ $# == 1 ]] || return 2
  printf 'label=<%s> operand=<%s>\n' "$label" "$1"
}
parse -n 'review mode' -- 'red apple'
Expected output
label=<review mode> operand=<red apple>

The parser uses Bash local state and a deliberate one-operand contract. -- terminates options, while quoted values preserve spaces; the example performs no external work.

Design outputs and evolution before adding flags

A stable CLI documents what stdout contains, where human diagnostics go, what nonzero statuses mean, and whether output ordering is guaranteed. Do not add progress banners to a machine-readable stream in a maintenance release. New flags should have clear defaults and conflict rules, and invalid combinations should fail before effects begin. Separate argument parsing from the operation so each can be tested with synthetic inputs. A reusable library should avoid implicit execution at source time. Keep version notes truthful: compatibility claims should identify tests actually run, not merely an intended future environment.

Keep a portable core simple when it is needed

For a small portable transformation, use positional parameters, case, quoted expansions, and documented POSIX utilities. Avoid relying on Bash local, arrays, or [[ in that core. Test under another available target shell as well as Bash, recognizing that one additional shell is evidence for that target only. When requirements genuinely need Bash, declare it and retain its clearer data structures rather than emulating arrays with unsafe strings. Maintain a compact regression matrix covering values, statuses, parsing, and execution boundaries. A predictable interface is more valuable than a clever script that no maintainer can safely change.

bash Preserve a positional list with a POSIX-style core
set -- 'red apple' ''
for item do
  printf '<%s>\n' "$item"
done
printf 'count=%s\n' "$#"
Expected output
<red apple>
<>
count=2

The implicit positional for list preserves both arguments. This complete example is checked under Bash and the locally available sh; that is not a claim about every shell version.

Common errors and fixes

A Bash extension appears under a generic sh claim

Why it happens
Language requirements were not documented or tested.
How to fix it
Declare Bash or replace the extension and test targets.
Prevent it next time
Maintain an explicit compatibility matrix.

Repeated getopts calls skip options

Why it happens
OPTIND retained parser progress from an earlier call.
How to fix it
Reset parser state for the new argument list.
Prevent it next time
Test repeated parser invocation.

A new banner breaks an automated consumer

Why it happens
Diagnostic text was added to stable data output.
How to fix it
Move human messages to the documented diagnostic channel.
Prevent it next time
Test stdout as a public interface.

Check your understanding

1. Why can a runtime version check fail to protect unsupported syntax later in the same parsed file?

Show worked solution

The shell may need to parse that syntax before it executes the check. Choose an entry structure that remains parseable on the rejected interpreter or use a launcher with an explicit supported interpreter. Test the rejection path, not just the supported path.

2. What does passing a portable example under local sh establish?

Show worked solution

It establishes observed behavior for that specific implementation and version under the test environment. It supports, but does not replace, a standards-based review and tests on the remaining declared targets. Report exactly which shells were exercised.

Key takeaways

  • Declare shell and version support as part of the CLI.
  • Keep parsing, effects, outputs, and regression contracts separate.

Official references