Learn BASH Scripting - Command Line Arguments & Special Parameters
Episode 5 of 27

Learn BASH Scripting - Command Line Arguments & Special Parameters

Mastering how to pass data from the outside into a script through positional parameters ($1, $2, ${10}), understanding special parameters ($0, $#, $@, $*, $$, $?), and using shift to consume arguments sequentially along with the traps that most often make scripts fail silently.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Introduction

In episode 4 we covered quoting, escaping & word splitting — how single quotes, double quotes, and the backslash control the way bash splits strings into words, and why "$@" is safer than $@ — so in this episode we step up a level: passing data from the outside into a script.

A script that only works with values typed directly into its code is like a cash register that can only ever calculate the amount "10000". Useless, right? To fix that, we need to learn how a script receives command line arguments and takes advantage of bash's built-in special parameters. This is the foundation that lets your scripts be reused over and over with different data — without changing a single line of code.

Main Discussion

Why Does a Script Need to Receive Arguments?

Think about the commands you already use all the time: cp file.txt backup/, rm -rf cache/, systemctl restart nginx. None of those commands asks for data through dialogs or menus — they accept arguments directly on the command line. That's the Unix paradigm: small, versatile commands configured through arguments, not giant commands that ask for everything one by one.

Imagine you're writing a backup.sh script to back up a directory. There are two ways to write it:

#!/usr/bin/env bash
tar -czf /home/arman/backup.tar.gz /home/arman/Projects
echo "Backup selesai."

The first version can only back up one folder, forever. The second version can be used for any folder: ./backup.sh /tmp/backup.tar.gz /home/arman/Projects. That's why we learn arguments — so scripts become reusable materials, not single-use items.

Positional Parameters: $1, $2, $3, ..., ${10}

Bash marks the first argument as $1, the second $2, the third $3, and so on. They're called positional parameters because their position in the command determines their value. Run the following script with a few arguments:

skrip_cerita.sh — using $1 to $3
#!/usr/bin/env bash
echo "Aksi     : $1"
echo "Objek    : $2"
echo "Lokasi   : $3"
Running with 3 arguments
./skrip_cerita.sh install nginx server-01
Output
Aksi     : install
Objek    : nginx
Lokasi   : server-01

Each argument is separated by whitespace as you type. If an argument contains spaces (e.g. My Documents), you must wrap it in quotes: ./skrip_cerita.sh install "My App" server-01 — so "My App" counts as one argument, not two. Remember the quoting lesson from episode 4: quotes are how we "glue" several words into one.

The 10th Argument Trap: ${10}, not $10

This is one of the most classic and hardest-to-trace traps. For the 10th argument and beyond, you must use braces: ${10}, ${11}, and so on. Why? Because $10 is read by bash as $1 followed by the literal character 0. Take a look:

skrip_dengan_banyak_argumen.sh
#!/usr/bin/env bash
echo "Argumen ke-1  : $1"
echo "Argumen ke-10 : ${10}"
Run with 10 arguments
./skrip_dengan_banyak_argumen.sh a b c d e f g h i J
Output
Argumen ke-1  : a
Argumen ke-10 : J

Now imagine if we mistakenly wrote $10:

echo "Argumen ke-10 : $10"
echo "Argumen ke-11 : $11"
Wrong output (BEFORE)
Argumen ke-10 : a0

Bash displays the value of $1 (which is a) and then appends the character 0 — with no error at all. That's the most dangerous kind of bug: silent failure. You see the output "a0", there's no error message, and you might waste hours hunting for it. Always use ${10} and up, and get used to using braces for all positional parameters — safe and consistent.

Special Parameters: $0, $#, $@, $*, $$, $?

Besides positional parameters, bash provides a set of special parameters marked by a single symbol. They aren't arguments, but the "metadata" bash gives to every script:

ParameterMeaningExample usage
$0Name of the script currently runningShowing the script name in error/usage messages
$#Number of arguments (not their values)if [ $# -eq 0 ]; then → check for no arguments
$@All arguments as separate wordsfor arg in "$@"; do → loop per argument
$*All arguments as one string"$*" → join all arguments with spaces
$$PID (Process ID) of this scriptCreating a unique temp file name: /tmp/script_$$.log
$?Exit status of the last commandcommand; if [ $? -eq 0 ]; then ...

$0 — The Script Name

$0 stores the name of the script currently running. It's useful for two things: (1) showing the correct usage message when arguments are wrong, and (2) making clear error messages. Notice this pattern in almost every professional script:

Showing usage with $0
#!/usr/bin/env bash
if [ $# -lt 2 ]; then
    echo "Usage: $0 <nama> <npm>" >&2
    exit 1
fi

$# — The Number of Arguments

$# counts how many arguments were given, not their values. If ./script.sh a b c, then $# is 3. This is the number one validation tool: before using $1, $2, first check that $# is sufficient. Why? Because reading a non-existent $1 produces an empty string — the script keeps running without error, but with empty data.

$? — Exit Status of the Last Command

Every command in bash leaves an exit status: 0 means success, anything other than 0 (usually 1-255) means failure. $? always holds the exit status of the command that ran last. This is the language Linux uses to communicate with scripts: did your job succeed?

Checking a command's success via $?
ls /tmp/siap-produksi
echo "Exit status: $?"
Output
ls: cannot access '/tmp/siap-produksi': No such file or directory
Exit status: 2

Notice: because ls failed, its exit status isn't 0. In episode 9 we'll cover how this pattern becomes the heart of all if logic in bash. But one thing to remember from now on: everything in bash is about exit status.

$$ — The Script's PID

$$ holds the Process ID of the process running the script. A PID is the unique identity of every process in Linux (remember the /proc concept from the Linux series). What's it useful for? Creating a unique temporary file name:

Unique temp file per process
log_tmp="/tmp/install_$$.log"
echo "Logging ke $log_tmp"
Output
Logging ke /tmp/install_12345.log

Because every process has a different PID, two scripts running at the same time won't overwrite each other's temp files — like two hospital patients having different medical record numbers, so their files are never mixed up.

$@ vs $*: A Life-or-Death Difference

Both of these parameters represent "all arguments", but they behave drastically differently — and this is one of the most common sources of bugs in bash. Remember the golden rule:

  • "$@" — each argument stays a separate word. Spaces within one argument are preserved.
  • "$*" — all arguments are joined into one string, separated by spaces (the first character of IFS).

Let's see the difference with an experiment. Create a script that prints each argument in brackets, so we can see the boundaries between arguments:

show_args.sh — comparing
#!/usr/bin/env bash
echo "Dengan \"\$@\":"
for arg in "$@"; do
    echo "  [\$arg]"
done
 
echo "Dengan \"\$*\":"
for arg in "$*"; do
    echo "  [\$arg]"
done
Run with two arguments that contain spaces
./show_args.sh "file penting.txt" "laporan 2026"
Output
Dengan "$@":
  [file penting.txt]
  [laporan 2026]
Dengan "$*":
  [file penting.txt laporan 2026]

Note the last line: "$*" merges both arguments into one word. For the for loop, that's just one iteration — even though you wanted two files processed. Practical conclusion: always use "$@" to "forward all arguments". "$*" is rarely needed — only when you truly want to join arguments into a single string (for example, for a log message).

Tip

A quick way to remember the difference: "$@" preserves the boundaries between arguments, while "$*" removes all boundaries. Think of "$@" as five separate shopping bags carried home — their contents don't mix — while "$*" pours everything into one big basket. When forwarding arguments between scripts or into a for loop, you almost always need the separate bags: "$@".

What Happens Without Quotes?

There's an even more dangerous combination: $@ and $* without quotes. Without quotes, word splitting applies (remember episode 4): each argument gets split again by spaces. The intact argument "file penting.txt" becomes file, penting.txt, and txt. This is why for f in $@; do is considered a code smell — use for f in "$@"; do.

Important

A non-negotiable rule: always write "$@" — with quotes. When you forward arguments to another command (e.g. script_kedua.sh "$@"), this is the way to keep every argument intact no matter what it contains, including spaces and weird characters. A script that writes $@ without quotes will break the moment it receives a file name with spaces — and in the real world, spaced file names are very common.

shift: Consuming Arguments Sequentially

Sometimes you don't know how many arguments are coming, or you want to process them one by one from the front. That's where shift comes in: shift discards $1, then shifts all arguments one position to the left. $2 becomes $1, $3 becomes $2, and $# decreases by one.

Consuming arguments one by one with shift
#!/usr/bin/env bash
while [ $# -gt 0 ]; do
    echo "Memproses: $1"
    shift
done
echo "Semua argumen selesai diproses."
Run
./proses.sh api server web
Output
Memproses: api
Memproses: server
Memproses: web
Semua argumen selesai diproses.

Why is this pattern so important? Because it lets a script process an unlimited number of arguments without writing out $1 through $100. The analogy is a cashier serving a queue of customers: as long as the queue exists ($# -gt 0), they serve the front customer ($1), then the next customer steps forward (shift). In the episode covering getopts later, this pattern becomes the foundation for building professional CLIs.

Also note the loop condition above: [ $# -gt 0 ]. That's a test — we'll only study test operators in depth in episode 9, but for now it's enough to understand that the statement reads "as long as the number of arguments is still greater than zero, continue".

Practice: A Greet Script with Argument Validation

Time to tie it all together. We'll build a small script that greets a user, validates the number and format of arguments, and uses $0 for a clear usage message:

greet.sh — complete argument validation
#!/usr/bin/env bash
set -euo pipefail
 
if [ $# -ne 2 ]; then
    echo "Usage: $0 <nama> <npm>" >&2
    exit 1
fi
 
nama="$1"
npm="$2"
 
echo "Halo, $nama!"
echo "Selamat datang di sesi latihan NPM $npm."
Script with shebang, validation, and clean output

Let's break down each line:

LineWhat HappensWhy
#!/usr/bin/env bashShebang — points to the bash interpreterThe script runs with bash, not another shell
set -euo pipefailStrict mode: stop on error, empty variable, failed pipelinePrevents the script from "continuing" once something's wrong — covered in depth in the error handling episode
[ $# -ne 2 ]Test: number of arguments not equal to 2If the user forgets arguments, don't proceed — tell them how to use it
$0 <nama> <npm>Usage message using the actual script nameThe user knows exactly what command to type
>&2Redirect the message to stderrError messages must go to stderr, not stdout — so they can be separated when piped
exit 1Stop the script with a failed exit statusThe caller (e.g. CI) knows the script failed
Test various cases
./greet.sh
./greet.sh Arman
./greet.sh Arman 202601001
Output
Usage: ./greet.sh <nama> <npm>
Usage: ./greet.sh <nama> <npm>
Halo, Arman!
Selamat datang di sesi latihan NPM 202601001.

Notice that ./greet.sh and ./greet.sh Arman both produce the usage message — because neither provides the two promised arguments. A good script refuses to work with incomplete data and gives clear guidance. This is a habit that will be highly appreciated when your scripts are used by others (including "yourself six months from now").

Common Pitfalls: The Most Frequent Argument Traps

1. Reading $10 instead of ${10}

Already covered above, but worth repeating as the number one warning: $10 is $1 + 0. The script doesn't error, but the output is totally wrong. Always use ${10} and beyond.

2. $@ without quotes

for f in $@ will break spaced arguments into pieces. On a production machine full of file names like hasil_rekap 2026.xlsx, that's a disaster. Always write "$@".

3. Off-by-one in loops with shift

The pattern while [ $# -gt 0 ]; do ...; shift; done is correct because the condition is checked before each iteration. A common mistake is moving shift to the end without the right condition — or writing while [ $# -ge 0 ], which makes the loop run one more time when $# is already 0, so $1 becomes empty. Remember: use -gt 0, not -ge 0.

4. Ignoring $# before reading $1

Reading $1 when there are no arguments produces an empty string — the script runs "normally" with empty data, and the error only appears deep in the process (e.g. when creating an empty file). Always validate $# first. Prevention is easier than detection.

5. Forgetting exit 1 after the usage message

If a script shows the usage message and then continues working with missing arguments, that's worse than not validating at all. Always end the failed validation path with exit 1 (or an appropriate non-zero status).

Conclusion

In episode 5, we learned how a script receives commands from the outside world: positional parameters $1, $2, up to ${10}, which distinguish the position of arguments; special parameters $0 (the script name), $# (the number of arguments), $? (the last exit status), $$ (the PID), plus the crucial "$@" vs "$*" difference; and shift for consuming arguments sequentially. We tied it all together into a greet.sh script that validates its own arguments.

The takeaways:

  • Arguments are how you make scripts reusable — data comes from outside, not hardcoded.
  • ${10} and beyond must use braces; $10 is a hidden bug.
  • "$@" keeps each argument intact; "$*" merges them — always choose "$@".
  • $? is the communication language between commands and scripts: 0 = success.
  • Validate $# before reading arguments, and end failures with exit 1.

However, a script that receives commands from the command line is only one data entry point. There's another equally important one: interactive input from the user. In episode 6, we'll cover Interactive User Input & Menus with read — mastering the read command with its -p, -s, -t, -n, -a flags, building interactive menus with select, and input validation patterns that are immune to empty and wrongly formatted input. See you in the next episode!

Learn BASH Scripting - Command Line Arguments & Special Parameters | Learn BASH Scripting