Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

In Bash, the script name is $0, the arguments are $1, $2, and so on, and $# tells you how many arguments were supplied. Quote "$@" when forwarding or looping over arguments, use shift as you consume them, and use the POSIX getopts builtin for conventional short options such as -v and -o file.

How Linux passes arguments to a shell script

When you run ./report.sh weekly.csv email@example.com, the shell starts the interpreter named by the script’s shebang and assigns the command-line words to positional parameters. In a normal Bash script, $0 is the script name and the first user-supplied word is $1. The next words become $2, $3, and later parameters. If no argument is supplied, the corresponding positional parameter expands to an empty value.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The assignment is based on shell words, not on visible spaces. For example, ./report.sh 'Q4 sales.csv' supplies one argument containing a space. The quotes used at the prompt tell the invoking shell to keep that text together; they are not part of the value received by the script.

Special parameters you use most often

Parameter Meaning Typical use
$0 Name of the script in a normal Bash script invocation Usage and error messages
$1, $2, and later parameters Positional arguments in the order supplied Reading fixed operands
$# Number of positional parameters Checking required or maximum argument counts
"$@" One quoted word for each positional argument Forwarding or iterating without merging arguments
"$*" One quoted word containing all arguments joined with the first character of IFS Deliberately creating one combined string
OPTARG Argument associated with the option currently processed by getopts Reading the value after an option such as -o
OPTIND Index of the next argument examined by getopts Shifting past options to reach operands

Read a required argument safely

For a script with a fixed operand list, direct positional access is the clearest approach. Validate the count before assigning or using a required value. The following script requires one file name and prints a usage message to standard error when it is missing:

#!/usr/bin/env bash

if (( $# < 1 )); then
  printf 'usage: %s FILEn' "$0" >&2
  exit 2
fi

file=$1
printf 'file=%sn' "$file"

$# is evaluated numerically in the arithmetic condition. Exit status 2 conventionally signals incorrect command-line usage. The assignment file=$1 is safe as an assignment, but quote the variable whenever you pass its value to another command:

cat -- "$file"

The -- tells programs that support it to stop interpreting subsequent text as options. It is useful when a user supplies a file whose name begins with a hyphen. Whether a particular utility supports -- is utility-specific, so check that command’s documentation when needed.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Require exactly one argument

Use an exact count when extra operands would indicate a mistake:

if (( $# != 1 )); then
  printf 'usage: %s FILEn' "$0" >&2
  exit 2
fi
file=$1

Require a minimum while allowing the rest

For a command that needs at least two operands, test the lower bound and then process all remaining values with "$@" or a controlled loop:

if (( $# < 2 )); then
  printf 'usage: %s SOURCE... DESTn' "$0" >&2
  exit 2
fi

Do not test a required value only after shifting it away. Check the count first, then consume parameters in a known order.

Why quoting matters: "$@", "$*", and unquoted values

Unquoted parameter expansion can undergo word splitting and pathname expansion. That can turn one argument containing spaces into several words, or replace wildcard characters with matching file names in the current directory. Treat command-line values as data and quote them when expanding them.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Forward every original argument

Use a quoted "$@" when calling another command or script:

#!/usr/bin/env bash

/usr/local/bin/process-files "$@"

If the caller supplied three arguments, the called program receives three arguments, including any spaces or wildcard characters inside each original value. A loop has the same property:

for arg in "$@"; do
  printf 'arg=%sn' "$arg"
done

With an invocation such as ./show.sh 'annual report.pdf' '*.csv' '', the loop runs three times. The first value contains a space, the second contains a literal asterisk, and the third is an empty argument.

What quoted "$*" does

Quoted "$*" deliberately produces one word. The positional parameters are joined using the first character of IFS (normally a space). Thus, arguments one, two words, and three become one combined string. This is appropriate only when a single display or log string is what you want; it must not replace "$@" when forwarding arguments.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Expansion Result when arguments are alpha, two words, and gamma Use it for
"$@" Three separate words Passing or iterating over original arguments
"$*" One word joined with the first IFS character Intentional aggregation into one string
$@ or $* without quotes Subject to splitting and pathname expansion Avoid for data values

Quote individual parameters too: use "$1" rather than $1 when passing the first argument to a command, and use "$OPTARG" for an option’s value.

Access the tenth and later arguments

A positional parameter is a parameter denoted by one or more digits, except for the special parameter zero. Because a bare $10 is parsed as $1 followed by the literal character 0, use braces for two-digit and longer parameters:

printf 'tenth=%sn' "${10}"
printf 'twelfth=%sn' "${12}"

Braces also make boundaries explicit in ordinary expansions, for example "${1}.bak". In most scripts, looping over "$@" is easier to maintain than naming a large number of individual parameters.

Consume arguments with shift

shift removes the leading positional parameters and renumbers the rest. After shift, the old $2 becomes the new $1, the old $3 becomes $2, and so forth. The count in $# decreases by the number shifted.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A simple command that accepts a mode followed by any number of files can process one value at a time:

#!/usr/bin/env bash

if (( $# < 1 )); then
  printf 'usage: %s MODE [FILE...]n' "$0" >&2
  exit 2
fi

mode=$1
shift

printf 'mode=%sn' "$mode"
for file in "$@"; do
  printf 'file=%sn' "$file"
done

Validate before shifting. Calling shift when there are not enough positional parameters is an error in many shells and can make the remaining logic misleading.

Use a loop for custom syntax

A manual while/case parser is useful when a project intentionally supports long options or syntax that getopts does not describe. Each branch must consume exactly the parameters it recognizes and reject malformed input:

#!/usr/bin/env bash

verbose=false
output=

while (( $# > 0 )); do
  case $1 in
    -v|--verbose)
      verbose=true
      shift
      ;;
    -o|--output)
      if (( $# < 2 )); then
        printf 'option %s requires an argumentn' "$1" >&2
        exit 2
      fi
      output=$2
      shift 2
      ;;
    --)
      shift
      break
      ;;
    -* )
      printf 'unknown option: %sn' "$1" >&2
      exit 2
      ;;
    * )
      break
      ;;
  esac
done

for operand in "$@"; do
  printf 'operand=%sn' "$operand"
done

The -- case gives callers an explicit way to mark the end of options. The parser leaves the first non-option and everything after it in "$@", so the final loop handles operands without losing their boundaries.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Replace the positional-parameter list with set --

set -- one two replaces the current positional parameters with one and two. This is the supported way to rebuild an argument list after filtering or transforming values. Do not try to assign to a positional parameter with syntax such as $2=value; positional parameters are not ordinary assignable variables.

set -- 'first value' second
printf 'count=%sn' "$#"
printf 'first=%sn' "$1"
printf 'second=%sn' "$2"

Because set -- changes the same parameters that your parser may still need, save any values you must retain before replacing the list.

Parse short options with POSIX getopts

getopts is the portable shell interface for short options and their option-arguments. Put the option letters in an optstring: a letter followed by a colon requires a value. In ':vo:', -v is a flag and -o requires an argument. The leading colon requests explicit error handling for unknown options and missing option arguments.

#!/bin/sh

verbose=false
output=
while getopts ':vo:' opt; do
  case $opt in
    v) verbose=true ;;
    o) output=$OPTARG ;;
    :) printf 'option -%s requires an argumentn' "$OPTARG" >&2; exit 2 ;;
    ?) printf 'unknown option: -%sn' "$OPTARG" >&2; exit 2 ;;
  esac
done
shift $((OPTIND - 1))

for operand do
  printf 'operand=%sn' "$operand"
done

How the parser advances

During each iteration, getopts places the option letter in opt. For an option that takes a value, its value is in OPTARG. OPTIND records the index of the next argument to inspect. Once the loop ends, shift $((OPTIND - 1)) removes the options and their values, leaving the remaining operands in "$@".

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For example, an invocation such as ./copy.sh -v -o 'nightly backup' source.txt destination.txt sets verbose to true, stores nightly backup in output, and leaves source.txt and destination.txt as operands after the shift.

Handle errors explicitly

With a leading colon in the optstring, a missing option argument produces : in the case statement and the option letter in OPTARG. An unrecognized option produces ? and its letter in OPTARG. This lets the script print a precise message and exit with a usage error instead of continuing with an empty setting.

Always quote "$OPTARG" when assigning or passing an option value. A value such as reports/final copy.txt must remain one shell word.

Choosing between direct access, a manual loop, and getopts

Approach Portability Short options Option arguments Long options Error handling and clarity
Direct $1, $2 access Works in Bash and POSIX shells Must be implemented by your script Must be validated by your script Must be implemented by your script Clearest for a fixed operand list
Manual while/case with shift Can remain POSIX when written with POSIX syntax Supports any syntax you code Requires explicit count checks and consumption Supports long options and custom forms Flexible, but every malformed case needs validation
getopts Portable POSIX interface Designed for short options Built in through the optstring and OPTARG Not a general long-option parser Consistent option scanning; a leading colon enables explicit missing/unknown-option messages

Use direct access when the interface is simply “one input file” or “source and destination.” Choose getopts for conventional short flags and option values. Choose a manual loop when long options or a project-specific grammar are requirements, and budget code for validation, the -- terminator, and every shift.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The shebang determines which shell reads the script

The first line selects the interpreter when the file is executed directly. #!/usr/bin/env bash requests Bash; #!/bin/sh requests the system’s POSIX shell. A script advertised as /bin/sh should use POSIX syntax and utilities, including the POSIX form of getopts. Bash-only arithmetic syntax such as (( $# < 1 )), arrays, or other Bash extensions should be kept in a Bash script.

The way you invoke the file also matters. Running ./script.sh after making it executable uses the shebang. Running bash script.sh explicitly asks Bash to interpret it, while running sh script.sh asks the system’s sh even if the file contains a Bash shebang. Keep the shebang, the syntax, and the documented invocation consistent.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Common mistakes and their fixes

Using $10 for the tenth argument

Symptom: The script prints the first argument followed by a zero. Cause: The shell parsed $10 as $1 plus the character 0. Fix: Use "${10}", or iterate over "$@".

Forwarding $@ without quotes

Symptom: A filename containing spaces arrives as multiple arguments, or a wildcard expands unexpectedly. Cause: Unquoted expansion permits splitting and pathname expansion. Fix: Use "$@" when forwarding and "$arg" inside a loop.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Using "$*" when separate arguments are required

Symptom: A called command receives one long argument instead of the original list. Cause: Quoted "$*" joins all positional parameters using the first IFS character. Fix: Replace it with "$@" unless one combined string is intentional.

Reading $1 before checking $#

Symptom: The script proceeds with an empty path or emits a confusing downstream error. Cause: The required operand was never validated. Fix: Check the count first and print a usage line that includes "$0".

Shifting too far

Symptom: A parser reports a shift error or silently loses the value it needed. Cause: The code shifted without confirming that the required value followed an option. Fix: Check $# before shift 2, and handle a missing option argument as an error.

Expecting getopts to parse long options

Symptom: An argument such as --output is rejected or split into unexpected option letters. Cause: getopts is intended for short options. Fix: Use a manual case loop for a deliberately supported long-option interface, or redesign the interface around short options.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Mixing Bash syntax with a /bin/sh shebang

Symptom: The script works when run with Bash but fails when run as sh script.sh. Cause: The invoking shell does not implement the Bash-specific syntax. Fix: Either use POSIX syntax throughout or change the shebang and documented command to Bash.

A complete pattern for options followed by operands

The following POSIX-style script combines validation, option parsing, safe quoting, and operand processing. It accepts -v, requires a value for -o, reports malformed options, and then prints every remaining operand separately:

#!/bin/sh

verbose=false
output=

while getopts ':vo:' opt; do
  case $opt in
    v)
      verbose=true
      ;;
    o)
      output=$OPTARG
      ;;
    :)
      printf '%s: option -%s requires an argumentn' "$0" "$OPTARG" >&2
      exit 2
      ;;
    ?)
      printf '%s: unknown option -%sn' "$0" "$OPTARG" >&2
      exit 2
      ;;
  esac
done
shift $((OPTIND - 1))

if [ "$#" -lt 1 ]; then
  printf 'usage: %s [-v] [-o FILE] INPUT...n' "$0" >&2
  exit 2
fi

if [ "$verbose" = true ]; then
  printf 'output=%sn' "$output"
fi

for input in "$@"; do
  printf 'input=%sn' "$input"
done

The option loop runs before the required-operand check because getopts must remove the options first. The count test then applies to the remaining operands, not to the original complete command line.

A practical checklist before shipping a script

  • Choose and document the interpreter in the shebang.
  • Use $0 in usage and error messages so the displayed command name matches the invocation.
  • Check $# before reading required operands or shifting them away.
  • Use braces for positional parameters numbered 10 or higher.
  • Quote "$1", "$2", "$OPTARG", and loop variables when they are passed as data.
  • Use "$@" to preserve one word per original argument; use "$*" only for intentional joining.
  • Use shift only after confirming that the parameters you intend to consume exist.
  • Use set -- when replacing the positional-parameter list instead of attempting to assign to $1 or $2.
  • Use getopts for portable short-option parsing and shift by OPTIND-1 before processing operands.
  • If you implement long options manually, handle unknown options, missing values, and the -- end-of-options marker.
  • Test arguments containing spaces, wildcard characters, empty strings, leading hyphens, and no arguments at all.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.