Shell builtins · bash builtin
shift
Drop the first N positional parameters ($1, $2, ...) and renumber the rest down.
shift [N]
shift discards $1 and moves every other positional parameter down by one: $2 becomes
$1, $3 becomes $2, and $# decreases. shift N drops the first N at once. It works on
the script's arguments at the top level and on the function's own arguments inside a
function.
It is the engine of hand-written argument parsing: a while (( $# > 0 )) loop looks at
$1, handles it (taking a value from $2 and shifting twice when an option has an
argument), and shifts. Unlike getopts, that style handles long options such as
--output=FILE and options mixed with operands. It is also how you peel off a subcommand
(cmd=$1; shift) and pass the rest through with "$@".
When there are fewer than N parameters, shift N does nothing and returns 1. That is easy
to miss, and in a loop it can mean spinning forever, so check $# first.
Examples
Drop the first argument
set -- alpha beta gamma
echo "before: \$1=$1, $# args"
shift
echo "after: \$1=$1, $# args, all: $*"
before: $1=alpha, 3 args after: $1=beta, 2 args, all: beta gamma
set -- stands in for a script's arguments here. After shift, beta is $1 and the
count drops from 3 to 2.
Split off a subcommand and pass the rest through
#!/usr/bin/env bash
cmd=${1:?usage: tool.sh COMMAND [ARGS...]}
shift
case $cmd in
count) wc -l "$@" ;;
show) head -n 2 "$@" ;;
*) echo "unknown command: $cmd" >&2; exit 2 ;;
esac
bash tool.sh count todo.txt notes.txt
bash tool.sh show "file with spaces.txt"
5 todo.txt 6 notes.txt 11 total This file has spaces in its name.
The first argument picks the action, shift removes it, and "$@" forwards the remaining
arguments exactly as given, spaces included. This is how git-style command-line tools
are structured.
Parse options by hand, including long options
#!/usr/bin/env bash
env=staging dry_run=0
while (( $# > 0 )); do
case $1 in
-e|--env) env=$2; shift 2 ;;
--env=*) env=${1#*=}; shift ;;
-n|--dry-run) dry_run=1; shift ;;
--) shift; break ;;
-*) echo "unknown option: $1" >&2; exit 2 ;;
*) break ;;
esac
done
echo "env=$env dry_run=$dry_run targets=($*)"
bash deploy.sh --env=prod -n web01 web02
bash deploy.sh -e dev web01
bash deploy.sh --dry-run -- -odd-host
env=prod dry_run=1 targets=(web01 web02) env=dev dry_run=0 targets=(web01) env=staging dry_run=1 targets=(-odd-host)
Each branch shifts away what it consumed: two words for -e dev, one for --env=prod
or a flag. The loop stops at the first non-option (or after --), leaving the targets in
$@.
Drop several arguments at once
set -- 2026 03 14 backup.tar.gz notes.txt
date="$1-$2-$3"
shift 3
echo "date=$date files: $*"
date=2026-03-14 files: backup.tar.gz notes.txt
shift 3 removes the three date parts in one step, leaving only the file names.
Shifting past the end fails and does nothing
set -- one two
shift 3
echo "status $?, still have: $*"
status 1, still have: one two
With only two parameters, shift 3 returns 1 and leaves them all in place. It does not
shift as many as it can. Under set -e this failure would end the script.
An option missing its value
#!/usr/bin/env bash
env=staging
while (( $# > 0 )); do
case $1 in
-e) [[ $# -ge 2 ]] || { echo "-e needs a value" >&2; exit 2; }
env=$2; shift 2 ;;
*) break ;;
esac
done
echo "env=$env"
bash deploy.sh -e prod
bash deploy.sh -e
env=prod -e needs a value
Without the $# -ge 2 check, -e as the last argument would set env to an empty
string, and shift 2 would fail and leave -e in $1, looping forever. Always check
that the value is there before consuming it.
Walk through the arguments one by one
set -- web01 "db 01" cache01
while (( $# )); do
echo "processing [$1], $(( $# - 1 )) left"
shift
done
processing [web01], 2 left processing [db 01], 1 left processing [cache01], 0 left
A while (( $# )) loop with shift visits every argument in order. A for arg in "$@"
loop is simpler when you do not need to look ahead at $2.
shift inside a function affects only its own arguments
set -- script-arg1 script-arg2
log() { local level=$1; shift; echo "[$level] $*"; }
log WARN disk almost full
echo "script still has: $*"
[WARN] disk almost full script still has: script-arg1 script-arg2
In a function, the positional parameters are the function's arguments. shift there
leaves the script's own $@ untouched.
Common pitfalls
shift 2with only one argument left fails silently (status 1) and shifts nothing, which turns a parsing loop into an infinite loop. Check$#before shifting by more than one.After
shift, the old$1is gone. Save it first if you still need it (cmd=$1; shift).Using
$*or unquoted$@to pass the rest along breaks arguments with spaces. Use"$@".shiftcannot drop arguments from the end or the middle; use an array and slicing ("${@:1:$#-1}") for that.
GNU vs BSD and macOS
The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).
shiftandshift Nare POSIX. In dash, shifting past the end is a fatal error that ends the script instead of returning 1.
Related
getoptsParse short command-line options such as -v and -o FILE in a script, one option per call, inside a while loop.setTurn shell options such as -e, -u, -x and pipefail on or off, and replace the positional parameters.readRead one line from standard input and split it into shell variables.- Script argumentsPositional parameters $1 to ${10}, $# and $0, why "$@" is almost always right and "$*" rarely, shift, getopts, and ending options with --.
- FunctionsDefining and calling bash functions, arguments, local variables, return status versus output, namerefs, and sourcing a function library.
- Loopsfor over lists, files and ranges, C-style for, while and until, break and continue, retry loops, and redirecting a whole loop's input or output.
- The case statementMatching one value against glob patterns with case, alternatives with |, the default branch, fall-through with ;& and ;;&, and parsing options with case.