Skip to content
Linux Basics: Consume Bash Positional Arguments with shift

Linux Basics: Consume Bash Positional Arguments with shift

October 22, 2026

Some small commands need ordered operands rather than flags: a list of filenames, a subcommand followed by names, or a sequence of name/value pairs. Use shift to consume those inputs while preserving spaces, empty arguments, and literal wildcard text.

These examples were tested on Linux with GNU Bash 5.2.37. Use Bash rather than sh. These scripts only preview their arguments. They do not open, copy, or modify the filenames used in the examples.

Prepare a scratch directory

You need Bash, a text editor, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file in this directory before running it. The guards stop setup if it fails; invoking bash explicitly avoids changing executable permissions. No administrator access is needed.

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

1. Consume one operand at a time

Save this as shift-items.sh:

#!/usr/bin/env bash
printf 'Starting count: %s\n' "$#"
while (( $# > 0 )); do
  printf 'Next: <%s>\n' "$1"
  shift || exit 1
done
printf 'Remaining: %s\n' "$#"

Run it:

bash shift-items.sh 'draft notes.txt' '' '*.csv' '-archive.csv'

Output:

Starting count: 4
Next: <draft notes.txt>
Next: <>
Next: <*.csv>
Next: <-archive.csv>
Remaining: 0

$# counts arguments, including an explicitly supplied empty string. Each successful shift discards the current $1 and renumbers the remaining arguments. The loop checks the count before reading $1, so a zero-argument call safely performs no iterations.

The invocation’s quotes keep the wildcard literal and the spaced filename intact. The script quotes "$1" again when printing it. A leading hyphen has no special meaning here because this script does not parse options. The angle brackets make the empty argument visible rather than silently losing it.

2. Remove a subcommand and forward the remaining operands

Save this as shift-dispatch.sh:

#!/usr/bin/env bash
if (( $# == 0 )); then
  printf 'Usage: shift-dispatch.sh preview [name ...]\n' >&2
  exit 2
fi
action=$1
shift || exit 1
if [[ $action != preview ]]; then
  printf 'Unknown action: %s\n' "$action" >&2
  exit 2
fi
show_names() {
  printf 'Preview count: %s\n' "$#"
  for name in "$@"; do
    printf 'Preview: <%s>\n' "$name"
  done
}
show_names "$@"

Run it:

bash shift-dispatch.sh preview 'weekly report.csv' '-notes.txt'

Output:

Preview count: 2
Preview: <weekly report.csv>
Preview: <-notes.txt>

The first operand chooses the action; after one shift, only that action’s operands remain. Quoted "$@" forwards each remaining argument separately to the helper, including any empty ones. It also passes zero arguments when there are none, so preview alone prints a count of zero.

This is subcommand dispatch, not an option parser. The explicit action check rejects misspellings instead of treating them as filenames. If you need short flags such as -v or -o, use an option parser rather than extending this example into ad hoc flag handling.

3. Validate complete pairs before shifting twice

Save this as shift-pairs.sh:

#!/usr/bin/env bash
if (( $# % 2 != 0 )); then
  printf 'Expected name/value pairs\n' >&2
  exit 2
fi
while (( $# > 0 )); do
  printf '%s = <%s>\n' "$1" "$2"
  shift 2 || exit 1
done

Run it:

bash shift-pairs.sh owner 'Ada Lovelace' channel ''
bash shift-pairs.sh owner
printf 'Status: %s\n' "$?"

Output:

owner = <Ada Lovelace>
channel = <>
Expected name/value pairs
Status: 2

The first call contains two complete pairs, even though the last value is empty. The second call is intentionally invalid. Checking parity before the loop prevents partial processing when a value is missing. The diagnostic goes to standard error; the combined terminal output is shown above.

shift 2 is safe after this count check. Asking shift to discard more arguments than remain fails and leaves the argument list unchanged, so do not use an unchecked shift as an end-of-input detector. Print or save $? immediately when you need the failure status.

Keep in mind

Shifting does not change $0, and a separately launched script cannot consume its caller’s arguments. Inside a function, the function has its own positional parameters. These examples deliberately avoid reconstructing a command line as a string: forwarding quoted arguments is clearer and preserves the original boundaries.

Clean up

In the same terminal, remove only the named practice files, return to your original directory, and remove the empty scratch directory:

rm -- shift-items.sh shift-dispatch.sh shift-pairs.sh
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

References

Acknowledgements

This post was created with assistance from AI.

Last updated on