Skip to content

Variables and Arguments

The hello.sh script in Bash Script Basics always printed the same, hardcoded message. In a real task, though, a script needs to work with different data every time — a different filename, a different user, a different date. Two mechanisms solve this: variables, which store data inside the script, and arguments, which pass data into the script from outside, from the command line. This article covers both.

What You Will Learn

By the end of this article you will be able to:

  • follow the rules for declaring and using a variable;
  • explain when the ${VAR} syntax is required;
  • capture a command's output into a variable (command substitution);
  • pass arguments to a script through positional parameters ($1, $2, $@, $#);
  • accept interactive input from a user with read;
  • explain why quoting matters.

Declaring and Using a Variable

A variable is a storage location made of a name and a value. In Bash, the syntax for assigning a value to a variable is:

MY_SHELL="bash"

Two rules are strict here: there must be no space before or after the equals sign. MY_SHELL = "bash" (with spaces) fails, because Bash then tries to interpret MY_SHELL as a command name, with = and "bash" as its arguments.

To use a variable, put a $ in front of the name:

echo $MY_SHELL
bash

By convention, variable names are written in uppercase, but this isn't mandatory — a lowercase variable works exactly the same way. What matters is staying consistent within your own scripts.

A variable name can contain letters, digits, and underscores (_), but it cannot start with a digit:

# Correct
user_1="ali"
_TEMP="/tmp/data"

# Incorrect — starts with a digit
1user="ali"

# Incorrect — disallowed characters (-, @)
user-name="ali"
user@host="srv"

Curly Brace Syntax: ${VAR}

Consider the following situation:

MY_SHELL="bash"
echo "$MY_SHELLing"

You might expect bashing here, but the result is empty — because Bash tries to read $MY_SHELLing as one whole variable name, and no variable with that name exists. Curly braces solve the problem:

echo "${MY_SHELL}ing"
bashing

${ } marks the exact boundary of the variable name. It can always be used, but it's only required when other text immediately follows or precedes the variable. In every other case, $VAR and ${VAR} work identically.

Capturing a Command's Output into a Variable

To store a command's output in a variable, command substitution is used:

SERVER_NAME=$(hostname)
echo "Server name: $SERVER_NAME"
Server name: linuxsvr

The command inside $( ) runs first, and its standard output (stdout) becomes the variable's value. Older scripts sometimes use backticks for the same purpose:

SERVER_NAME=`hostname`

Both forms give the same result, but the backtick syntax is considered outdated: nesting it is awkward and harder to read. New scripts should use only the $( ) form.

Expanding a Variable Inside Quotes

A variable expands even inside double quotes ("..."):

NAME="World"
echo "Hello, $NAME!"
Hello, World!

Inside single quotes ('...'), nothing expands$NAME is printed literally, character for character:

echo 'Hello, $NAME!'
Hello, $NAME!

Tip

Practical rule: always use a variable inside double quotes ("$VAR"), especially when it's a file path or user-entered text. Without quotes, if $VAR's value contains a space, Bash can split it into several separate words — for example, in a command like rm $FILE, this can misinterpret a filename containing a space and delete unexpected files. rm "$FILE" protects against exactly this mistake.

Positional Parameters: Passing Arguments to a Script

Values can be passed from the command line when a script is run — these are called positional parameters, stored in special variables $0 through $9:

  • $0 — the script itself (the path it was invoked with);
  • $1 — the first argument;
  • $2 — the second argument, and so on.
./script.sh parameter1 parameter2 parameter3

In this invocation, $0 is ./script.sh, $1 is parameter1, $2 is parameter2, and $3 is parameter3.

A practical example — an archive_user.sh script that takes a username as an argument:

#!/bin/bash

# Archives a user's home directory
# Usage: ./archive_user.sh <username>

user="$1"
tar -czf "/backup/${user}.tar.gz" "/home/${user}"
echo "Archived: /backup/${user}.tar.gz"

Notice: as soon as $1's value is read, it's stored in a meaningfully named variable (user) — this makes the rest of the script considerably easier to read, since later lines use $user, which shows immediately what it means, instead of $1.

Working with All Arguments: $@ and $#

If a script doesn't know in advance how many arguments it will receive, it needs to iterate through all of them one by one. Two special variables exist for this:

  • $@ — gives every positional parameter (starting from $1) as a separate word;
  • $# — gives the number of arguments.
#!/bin/bash

echo "Total number of arguments: $#"

for user in "$@"; do
    echo "Archiving: $user"
    tar -czf "/backup/${user}.tar.gz" "/home/${user}"
done
./archive_user.sh chet joe
Total number of arguments: 2
Archiving: chet
Archiving: joe

The for loop is covered in depth in Conditions and Loops, but the key rule here is that "$@" is always used inside double quotes ("$@", not $@) — only then does an argument containing a space (for example, "ali vali") get preserved as a single, whole element, instead of being split into two.

Note

If arguments are missing (for example, using $1 when it was never given), the script continues with an empty value — this often leads to unexpected errors. Checking the argument count is covered in Exit Codes and Error Handling.

"$@" vs "$*": the Difference That Matters

Bash has a second all-arguments variable, $*, and the difference between the two — especially once quoted — is one of the most common Bash interview questions, because getting it wrong silently corrupts argument handling.

  • "$@" expands to each argument as a separate quoted word: "$1" "$2" "$3".
  • "$*" expands to all arguments joined into a single word, separated by the first character of IFS (a space by default): "$1 $2 $3".
#!/bin/bash
# called as: ./demo.sh "ali vali" bob

printf 'with @: [%s]\n' "$@"
printf 'with *: [%s]\n' "$*"
with @: [ali vali]
with @: [bob]
with *: [ali vali bob]

"$@" preserved the two arguments (and kept the space inside "ali vali" intact), so printf ran twice; "$*" collapsed everything into one string, so printf ran once. The practical rule: use "$@" almost always — it's what correctly forwards a script's arguments to another command (some_tool "$@"). Reach for "$*" only when you deliberately want the arguments as one joined string (for example, building a log message). Unquoted $@ and $* behave identically and re-split on whitespace — which is exactly the word-splitting bug quoting exists to prevent.

shift: Consuming Arguments One at a Time

shift discards $1 and renumbers the rest down ($2 becomes $1, and so on), decrementing $#. It's the standard way to process a variable-length argument list, especially when the first argument is a subcommand or an option and the rest are its targets:

#!/bin/bash
action="$1"     # e.g. "delete"
shift           # drop the action; "$@" is now just the remaining files

for file in "$@"; do
    echo "$action -> $file"
done
./run.sh delete a.txt b.txt
delete -> a.txt
delete -> b.txt

shift N drops the first N arguments at once. Calling shift when no arguments remain is a no-op in Bash (it returns non-zero but doesn't error), which is worth knowing under set -e.

Accepting Interactive Input from a User: read

Sometimes a script needs to ask the user directly for input while running — for example, when an argument wasn't provided. The read command is used for this:

#!/bin/bash

read -p "Enter the username: " user
tar -czf "/backup/${user}.tar.gz" "/home/${user}"
echo "Archived: /backup/${user}.tar.gz"
Enter the username: mitch
Archived: /backup/mitch.tar.gz

The -p flag shows a prompt message, and the word after it is the name of the variable the entered value is written to. read reads from standard input (stdin) — usually the keyboard, but it can also be another command's output through a pipeline, for example echo "mitch" | read -p "..." user (this case needs extra care because of how a subshell works — when read runs inside a pipeline, the variable doesn't carry over to the main shell).

Practical Scenario: Asking via read When No Argument Is Given

Task: make archive_user.sh work both with and without an argument — if $1 isn't given, ask the user for it.

#!/bin/bash

# Archives a user's home directory.
# Uses the argument if given, otherwise asks interactively.

if [ -z "$1" ]; then
    read -p "Enter the username: " user
else
    user="$1"
fi

tar -czf "/backup/${user}.tar.gz" "/home/${user}"
echo "Archived: /backup/${user}.tar.gz"

The [ -z "$1" ] test returns true if $1 is empty (or not given at all). This test and if syntax are explained fully in Conditions and Loops; what matters here is seeing that a single script can support both an argument and interactive input.

Common Mistakes

Leaving a Space Around the Equals Sign

NAME = "ali"
NAME: command not found

Bash interprets this as calling a command named NAME with arguments = and "ali". The correct form is: NAME="ali".

Using a Variable Without Quotes

FILE="report 2026.txt"
rm $FILE

This is interpreted as two separate arguments (report and 2026.txt), and rm tries to delete both of them (if they exist) — not the single intended file. Correct: rm "$FILE".

Using $VAR Instead of ${VAR} and Merging Text into the Variable Name

The $MY_SHELLing example above — this mistake is especially common when a file extension or prefix is appended, for example writing $name_backup.tar.gz and having Bash look for a variable named name_backup.

Relying on $1 Without Checking the Argument Count

If a script relies on the $1 argument and it isn't given, the variable becomes an empty string, and later commands (like tar or rm) can run with an empty or incorrect path. Checking for this ahead of time is covered in Exit Codes and Error Handling.

Exercises

  1. Write a greet.sh script that takes a name via $1 and prints "Hello, <name>!". Cover the case where no argument is given, with read -p.
  2. Write a sysinfo.sh script that stores the output of hostname, whoami, and date into three separate variables via command substitution, and prints them on one line.
  3. Try an argument containing a space (for example, ./greet.sh "Ali Vali") — first using $1 without quotes, then with quotes, and explain the difference in the result.
  4. Write a list_args.sh script that prints the argument count via $#, then shows each one on a separate line via "$@".

Summary

Variables store data inside a script, while positional parameters ($1, $@, $#) let you accept data from outside; read asks the user for input directly. Following quoting rules — especially when working with file paths — is a necessary part of writing a safe script. The next topic, Conditions and Loops, covers making decisions (if) based on these variables and repeating actions (for, while).

References