How to Pass Arguments to a Bash Script

By 

Updated on

8 min read

Bash script arguments represented as connected command-line parameters

Arguments let a Bash script behave differently each time you run it. Instead of hard-coding a file path, username, environment name, or port number inside the script, you pass those values on the command line and read them from inside the script.

This guide explains how to pass arguments to Bash scripts, read positional parameters such as $1 and $2, work with all arguments through "$@", set defaults, validate required input, and parse named flags with getopts.

Quick Reference

For a printable quick reference, see the Bash cheatsheet .

SyntaxDescription
bash script.sh arg1 arg2Run a script with two arguments
$0Script name or path
$1, $2First and second positional arguments
${10}Tenth positional argument
$#Number of arguments passed
"$@"All arguments as separate quoted words
"$*"All arguments joined into a single string
"${1:-default}"Use a default value when the first argument is missing
shiftRemove the first positional argument and move the rest down
getoptsParse short named options such as -p 3000 -v
--name valueNamed argument read with a while and case loop

Positional Parameters

When you run a script, Bash assigns each argument to a numbered variable:

~/greet.shsh
#!/usr/bin/env bash

printf 'Hello, %s!\n' "$1"
printf 'Argument count: %s\n' "$#"

Run the script with one argument:

Terminal
bash greet.sh World
output
Hello, World!
Argument count: 1

The special variables are:

VariableValue
$0The script name or path
$1, $2, …First, second, … argument
${10}, ${11}, …Tenth argument and beyond
$#Number of arguments passed
$@All arguments as separate words
$*All arguments as a single string

Note that $10 does not give you the tenth argument. Bash reads it as $1 followed by the character 0, which is almost never what you want. Wrap the number in braces and write ${10} once the position reaches double digits.

Bash does not provide an argv array by that name. The positional parameters fill the same role, and you can copy them into an array with args=("$@") when you need array indexing or slicing. In that array, ${args[0]} is the first argument, while $0 still holds the script name.

For more detail about each special parameter, see the Bash positional parameters guide.

Access All Arguments

Use $@ to iterate over every argument regardless of how many were passed:

~/list-args.shsh
#!/usr/bin/env bash

for arg in "$@"; do
    printf 'Argument: %s\n' "$arg"
done

Run it with three arguments, one of which contains a space:

Terminal
bash list-args.sh alpha beta "hello world"
output
Argument: alpha
Argument: beta
Argument: hello world

Always quote "$@" so arguments with spaces stay as single values. Unquoted $@ allows word splitting, which turns "hello world" into two separate words.

The $* variable looks interchangeable with $@, but the two behave differently once they are quoted. "$@" expands to one word per argument, while "$*" joins every argument into a single string separated by the first character of IFS, which is normally a space:

~/expand.shsh
#!/usr/bin/env bash

printf 'With "$@":\n'
for arg in "$@"; do
    printf '  [%s]\n' "$arg"
done

printf 'With "$*":\n'
for arg in "$*"; do
    printf '  [%s]\n' "$arg"
done
Terminal
bash expand.sh one "two three"
output
With "$@":
  [one]
  [two three]
With "$*":
  [one two three]

Notice that the first loop runs twice and the second loop runs only once. Use "$@" whenever you pass arguments on to something else, and reserve "$*" for the rare case where you want the arguments as one printable string.

Forward Arguments to Another Command

Wrapper scripts often need to hand everything they received to another program. Putting "$@" in the command position does that, and each argument arrives on the other side as its own word:

~/run-logged.shsh
#!/usr/bin/env bash

printf 'Running: %s\n' "$*"
"$@"
Terminal
bash run-logged.sh echo "hello world"
output
Running: echo hello world
hello world

The script uses both forms for their respective strengths. "$*" produces a readable one-line summary for the log message, and "$@" runs the command with hello world intact as a single argument. Drop the quotes around $@ here and echo would receive two arguments instead of one.

Set Default Values

When an argument is optional, assign a default with ${N:-default}:

~/deploy.shsh
#!/usr/bin/env bash

ENV=${1:-production}
BRANCH=${2:-main}

printf 'Deploying %s to %s\n' "$BRANCH" "$ENV"

If the caller passes no arguments, ENV becomes production and BRANCH becomes main. If they pass one argument, ENV uses that value and BRANCH falls back to main.

Validate Required Arguments

Exit early with a usage message when a required argument is missing:

~/backup.shsh
#!/usr/bin/env bash

if [ $# -lt 2 ]; then
    echo "Usage: $0 SOURCE DESTINATION" >&2
    exit 1
fi

SOURCE=$1
DEST=$2

printf 'Ready to back up %s to %s\n' "$SOURCE" "$DEST"

$# -lt 2 checks whether fewer than two arguments were provided. The error message goes to stderr with >&2, and the script exits with code 1 to signal failure. After validation succeeds, you can safely use $SOURCE and $DEST in the real backup command, such as rsync.

Shift Through Arguments

shift removes the first argument and renumbers the rest. It is useful when a script accepts a variable number of items to process:

~/process.shsh
#!/usr/bin/env bash

printf 'Script: %s\n' "$0"

while [ $# -gt 0 ]; do
    printf 'Processing: %s\n' "$1"
    shift
done

Each shift call moves $2 into $1, $3 into $2, and so on, and decrements $# by one. The loop continues until no arguments remain.

Named Flags with getopts

For scripts with optional flags, getopts parses named options in the style of standard Unix commands:

~/server.shsh
#!/usr/bin/env bash

PORT=8080
VERBOSE=0

usage() {
    echo "Usage: $0 [-p PORT] [-v]" >&2
}

while getopts ":p:v" opt; do
    case $opt in
        p) PORT=$OPTARG ;;
        v) VERBOSE=1 ;;
        \?) usage; exit 1 ;;
        :) usage; exit 1 ;;
    esac
done

shift $((OPTIND - 1))

printf 'Starting server on port %s (verbose: %s)\n' "$PORT" "$VERBOSE"

The option string ":p:v" means -p takes a value, -v is a flag with no value, and the leading colon lets the script handle errors itself. $OPTARG holds the value for options that require one. shift $((OPTIND - 1)) removes all parsed flags so $1, $2, and the rest refer to any remaining positional arguments after the flags.

Run it:

Terminal
bash server.sh -p 3000 -v
output
Starting server on port 3000 (verbose: 1)

For a deeper look at option strings, OPTARG, OPTIND, and error handling, read the Bash getopts guide.

Named Arguments

The getopts built-in supports single-character options, not long option names. It treats an option such as --user as invalid instead of recognizing user as a name. When you want named arguments in the long form, walk the positional parameters yourself with a while loop and a case statement:

~/notify.shsh
#!/usr/bin/env bash

USER_NAME=""
MESSAGE="No message"

while [ $# -gt 0 ]; do
    case $1 in
        --user)
            if [ $# -lt 2 ] || [[ $2 == --* ]]; then
                echo "Error: --user requires a value" >&2
                exit 1
            fi
            USER_NAME=$2
            shift 2
            ;;
        --user=*)
            USER_NAME=${1#*=}
            shift
            ;;
        --message)
            if [ $# -lt 2 ] || [[ $2 == --* ]]; then
                echo "Error: --message requires a value" >&2
                exit 1
            fi
            MESSAGE=$2
            shift 2
            ;;
        --message=*)
            MESSAGE=${1#*=}
            shift
            ;;
        *)
            echo "Unknown option: $1" >&2
            exit 1
            ;;
    esac
done

if [ -z "$USER_NAME" ]; then
    echo "Usage: $0 --user NAME [--message TEXT]" >&2
    exit 1
fi

printf 'Sending to %s: %s\n' "$USER_NAME" "$MESSAGE"

Each option gets two branches so the script accepts both spellings that users expect. The --user and --message branches first check that another argument exists and that it is not a new long option. They then read the value from $2 and use shift 2 to move past both arguments. The equals branches strip everything up to the = sign with ${1#*=} and use one shift because the option and value occupy the same argument.

Both calling styles produce the same result:

Terminal
bash notify.sh --user alice --message "Build finished"
bash notify.sh --user=alice --message="Build finished"
output
Sending to alice: Build finished
Sending to alice: Build finished

The value checks stop a missing value from consuming the next option by mistake. The check after the loop makes --user required, while --message stays optional and keeps its default when you omit it.

Troubleshooting

Arguments with spaces arrive as separate values
This is almost always an unquoted $@ or $*. If the caller passes "hello world", the script receives it as one argument. Expanding $@ or $* without quotes later applies word splitting before the loop or forwarded command receives the value, turning it into hello and world. Quote the expansion as "$@" everywhere you loop over or forward arguments.

$10 prints the first argument followed by a zero
Bash parses $10 as $1 immediately followed by the literal character 0. Use ${10} for the tenth argument and beyond.

Flags still show up in $1 after the getopts loop
The getopts built-in does not remove what it parsed. Add shift $((OPTIND - 1)) right after the loop so the remaining positional arguments start at $1.

Long options such as --verbose cause an error
The getopts built-in does not recognize long option names. It treats --verbose as invalid input rather than as one option named verbose. Parse long options manually as shown above, or use the external GNU getopt command.

A named option is missing its value
Before reading $2, check that another argument exists and that it is not a new long option. Only then assign the value and run shift 2. Without these checks, the script can consume the next option as the value or fail to advance when the option is last.

Conclusion

Bash positional parameters handle the straightforward case of ordered arguments, while getopts adds named flag support for more complex scripts. Start with positional parameters, validate the count early, set defaults for optional arguments, and use getopts when your script grows a meaningful set of options. For more on script execution and permissions, see the guide on running Bash scripts .

Tags

Linuxize Weekly Newsletter

A quick weekly roundup of new tutorials, news, and tips.

About the authors

Dejan Panovski

Dejan Panovski

Dejan Panovski is the founder of Linuxize, an RHCSA-certified Linux system administrator and DevOps engineer based in Skopje, Macedonia. Author of 1000+ Linux tutorials with 20+ years of experience turning complex Linux tasks into clear, reliable guides.

View author page