Check number of arguments passed to a Bash script
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
In Bash, the special variable $# holds the count of positional arguments passed to a script or function. Checking this count near the top of your script is one of the simplest ways to fail fast, give users a clear usage message, and prevent downstream errors caused by missing input.
That single guard is enough to turn a confusing runtime failure into an immediate, actionable error message. The rest of this article covers exact counts, minimum/maximum ranges, usage functions, the transition to getopts, and the quoting rules that keep your argument handling correct.
Checking for an Exact Number of Arguments
The most straightforward check is an exact match. If a script requires exactly three arguments, compare $# against 3 with -ne (not equal).
The -ne operator compares integers. Because $# is always an integer, this comparison is safe and idiomatic.
Checking Minimum and Maximum Counts
Scripts with optional arguments need range checks instead of exact matches.
Use -lt (less than) for a minimum.
Combine -lt and -gt for a full range.
This pattern makes it clear which arguments are required and which are optional, directly in the validation logic.
Comparison Operators Reference
| Operator | Meaning | Example |
-eq | Equal to | [[ "$#" -eq 3 ]] |
-ne | Not equal to | [[ "$#" -ne 3 ]] |
-lt | Less than | [[ "$#" -lt 2 ]] |
-le | Less than or equal | [[ "$#" -le 5 ]] |
-gt | Greater than | [[ "$#" -gt 4 ]] |
-ge | Greater than or equal | [[ "$#" -ge 1 ]] |
All of these work inside both [[ ]] (Bash-specific) and [ ] (POSIX-compatible) test brackets.
Writing a Usage Function
For scripts beyond a one-liner, extracting the validation message into a function keeps the top of the script clean and makes it easy to show the same message from multiple places.
The ${3:-fast} syntax assigns a default value when the third argument is not provided. This pairs naturally with a minimum-count check that allows the argument to be absent.
Default Values for Optional Arguments
Bash provides two forms of default assignment.
The :- form is almost always what you want, because an empty string passed as an argument is rarely intentional.
Running ./greet.sh Alice prints Hello, Alice! while ./greet.sh Alice Hi prints Hi, Alice!.
Transitioning to getopts for Flags and Options
$# handles positional arguments well. Once a script needs named flags like -v, -o FILE, or --help, raw argument counting is no longer sufficient. The getopts built-in parses flags and their values.
The important insight is that argument counting and argument parsing are complementary tasks. For pure positional scripts, $# is sufficient. For flag-based scripts, getopts handles the structure while you validate required options separately after parsing.
Checking Arguments Inside Functions
$# works inside functions too, where it counts the arguments passed to that function, not the script.
Using "$@" to forward all script arguments to the function preserves quoting and spacing. This pattern keeps validation close to the code that uses the arguments.
Why Quoting Always Matters
Even in scripts that only check argument count, always quote positional parameters when you use them. Unquoted parameters are subject to word splitting and pathname expansion, which can change the meaning of input.
A filename like my report.pdf passed as a single argument will be split into two words without quotes, causing subtle and hard-to-trace bugs.
Exit Codes and Automation
Returning a nonzero exit code on incorrect usage is not just convention. It lets calling scripts, CI pipelines, and orchestration tools detect that the command failed for a predictable reason.
| Exit Code | Common Meaning |
0 | Success |
1 | General error or bad usage |
2 | Misuse of shell built-in (Bash convention) |
126 | Command found but not executable |
127 | Command not found |
Using exit 1 for bad usage is the standard practice. Some tools use exit 2 to distinguish usage errors from runtime errors, but the key requirement is consistency within your project.
Common Pitfalls
Using $* or $@ when you only need the count. These expand to the argument values. $# gives the count directly without any expansion or word splitting risk.
Sending usage messages to stdout instead of stderr. Error and usage messages should go to stderr with >&2. This keeps stdout clean for downstream piping and makes automation logs easier to interpret.
Relying only on $# when the script uses flags. A script called with -v -o output.txt input.txt has $# equal to 4, but the meaningful structure is two flags and one positional argument. Use getopts for flag-based interfaces.
Forgetting to quote $# inside [[ ]]. While [[ ]] does not perform word splitting, quoting $# is still good practice for consistency and to avoid surprises if the code is later moved to [ ].
Not using set -euo pipefail at the top of scripts. This combination catches unset variables (-u), pipeline failures (-o pipefail), and general errors (-e). Pair it with argument validation for robust scripts.
Summary
- Use
$#to check how many positional arguments were passed to a Bash script or function. - Validate exact, minimum, or range counts near the top of the script with
-ne,-lt, and-gt. - Extract usage messages into a function for anything beyond a trivial script.
- Use
${N:-default}to assign default values for optional positional arguments. - Transition to
getoptswhen the script accepts flags instead of purely positional arguments. - Always quote positional parameters and send error messages to stderr.

