Chapter 06Lesson 02~70 minutes

shift and Robust Argument Processing

As command lines grow, it is useful to consume already-processed arguments and leave the remainder in a predictable state. Bash's `shift` is the core mechanism for that style of parser.

BeginnerCLI designHands-on lab

Learning objectives

By the end of this lesson

  • Explain how shift renumbers parameters.
  • Consume options and values safely.
  • Support the -- end-of-options convention.
  • Route subcommands with remaining arguments.
  • Avoid eval and shell-string command construction.

1. shift removes the first positional parameter

set -- alpha beta gamma
printf 'before count=%d first=%s\n' "$#" "$1"
shift
printf 'after  count=%d first=%s\n' "$#" "$1"

$0 is not shifted; only the positional parameters are renumbered.

2. shift N consumes several parameters

set -- one two three four
shift 2
printf 'remaining=%d first=%s\n' "$#" "$1"

Do not shift past the available arguments. For options requiring values, validate the count before shift 2.

3. Parse one token at a time with case

while (( $# > 0 )); do
  case $1 in
    --verbose)
      verbose=true
      shift
      ;;
    --environment)
      (( $# >= 2 )) || { printf 'missing value\n' >&2; exit 64; }
      environment=$2
      shift 2
      ;;
    *)
      break
      ;;
  esac
done

4. -- conventionally ends option parsing

case $1 in
  --)
    shift
    break
    ;;
esac

Supporting -- lets users pass operands that begin with a dash without having them mistaken for options.

5. Unknown options should fail closed

case $1 in
  --verbose|--dry-run) ;;
  -*)
    printf 'unknown option: %s\n' "$1" >&2
    exit 64
    ;;
esac
Reject typos

Silently ignoring an option typo can change behavior in dangerous ways.

6. Manual parsing can support --name=value

case $1 in
  --environment=*)
    environment=${1#*=}
    [[ -n $environment ]] || {
      printf 'empty environment\n' >&2
      exit 64
    }
    shift
    ;;
esac

7. Consume the subcommand and delegate

subcommand=${1:-help}
shift || true

case $subcommand in
  deploy) deploy_command "$@" ;;
  status) status_command "$@" ;;
  help) usage ;;
  *) printf 'unknown command: %s\n' "$subcommand" >&2; exit 64 ;;
esac

Each subcommand can own a smaller argument grammar instead of sharing one oversized global parser.

8. Build downstream command vectors with arrays

curl_args=(--fail --silent --show-error)

[[ $verbose == true ]] && curl_args+=(--verbose)

curl "${curl_args[@]}" "$url"

Arrays preserve one option or operand per element and remain safe when values contain spaces or wildcard characters.

9. Do not parse by reconstructing a command string

# Avoid:
# command_line="$*"
# eval "$command_line"

# Preserve the vector instead:
target_command "$@"
Injection boundary

Using eval performs another shell parse and can turn data into syntax. Arrays and quoted "$@" avoid that class of error.

10. Treat leftovers as explicit operands

service=${1:-}
shift || true

if (( $# > 0 )); then
  printf 'unexpected extra operand: %s\n' "$1" >&2
  exit 64
fi

A robust parser knows whether zero, one, or many operands should remain after options are consumed.

11. Hands-on lab: long-option parser

mkdir -p "$HOME/devops-academy/bash/chapter06/lesson02"
cd "$HOME/devops-academy/bash/chapter06/lesson02"

cat > deploy-cli.sh <<'EOF'
#!/usr/bin/env bash
environment=staging
dry_run=false
replicas=2

while (( $# > 0 )); do
  case $1 in
    --environment)
      (( $# >= 2 )) || { printf 'missing environment\n' >&2; exit 64; }
      environment=$2
      shift 2
      ;;
    --replicas=*)
      replicas=${1#*=}
      shift
      ;;
    --dry-run)
      dry_run=true
      shift
      ;;
    --)
      shift
      break
      ;;
    -*)
      printf 'unknown option: %s\n' "$1" >&2
      exit 64
      ;;
    *)
      break
      ;;
  esac
done

service=${1:-}
[[ -n $service ]] || { printf 'SERVICE required\n' >&2; exit 64; }

case $environment in dev|staging|prod) ;; *) exit 65 ;; esac
[[ $replicas =~ ^[0-9]+$ ]] || exit 66

printf 'PLAN service=%s env=%s replicas=%s dry_run=%s\n' \
  "$service" "$environment" "$replicas" "$dry_run"
EOF

bash deploy-cli.sh --environment prod --replicas=3 --dry-run api
bash deploy-cli.sh -- --strange-service
bash deploy-cli.sh --unknown api || true

Verification checklist

12. Knowledge check

Question 1. What does shift do?

Question 2. Why support --?

Question 3. Why reject unknown options?

Question 4. Why prefer arrays to command strings?

13. Summary

shift enables clear left-to-right argument consumption. Combined with case, it supports long options, subcommands, and the -- convention. Keep argument vectors intact and never use eval as a general parser.

14. Further reading

  • GNU Bash Reference Manual — shift.
  • GNU Bash Reference Manual — Positional Parameters.
  • POSIX Utility Syntax Guidelines.
  • ShellCheck documentation — argument parsing and eval hazards.
Next lesson

Parsing Short Options with getopts

Continue Chapter 6 by refining the command-line interface and its validation contract.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.