Bash Check Number of Arguments: A Complete Guide With Examples
Learn how to bash check number of arguments in your shell scripts using $#, if statements, and best practices to make your scripts reliable and user-friendly.
One of the first things any solid shell script needs is argument validation. If you’re writing a script that expects input and someone runs it without passing anything, it should tell them what went wrong, not silently fail or throw a cryptic error three steps later. Knowing how to bash check number of arguments is a foundational skill, and this post covers every approach you’ll need, from the simplest one-liner to more robust patterns used in production scripts.
The Variable That Makes It Possible: $#
In bash, $# holds the number of arguments passed to a script or function. It’s a special variable, always available, and always reflects the count of positional parameters the current script or function received.
#!/bin/bash
echo "You passed $# argument(s)"
Run it:
$ ./myscript.sh hello world
You passed 2 argument(s)
$ ./myscript.sh
You passed 0 argument(s)
That’s the core of it. Everything else is just using $# inside a conditional to decide what to do.
The Basic Pattern: Check if Arguments Were Passed
The most common check is confirming that the user passed at least one argument. Here’s the standard pattern:
#!/bin/bash
if [ $# -eq 0 ]; then
echo "Usage: $0 <filename>"
exit 1
fi
echo "Processing: $1"
Breaking this down:
$#is the argument count-eq 0means “equal to zero”echo "Usage: $0 <filename>"prints a helpful message;$0is the script name itselfexit 1exits with a non-zero status, signaling failure to any calling process
This is the pattern you’ll write most often. It’s clear, it’s short, and it tells the user what the script expects.
Checking for an Exact Number of Arguments
Sometimes your script needs a precise number of arguments, not just “at least one.” Use -ne (not equal) for that:
#!/bin/bash
if [ $# -ne 2 ]; then
echo "Usage: $0 <source> <destination>"
exit 1
fi
echo "Copying $1 to $2"
If someone passes one argument or three, the check catches it. Only exactly two arguments will get through.
For a minimum number of arguments, use -lt (less than):
if [ $# -lt 3 ]; then
echo "Error: at least 3 arguments required"
exit 1
fi
For a maximum, use -gt (greater than):
if [ $# -gt 5 ]; then
echo "Error: too many arguments (max 5)"
exit 1
fi
Combining Minimum and Maximum Checks
When your script accepts a range of arguments, combine both checks:
#!/bin/bash
if [ $# -lt 1 ] || [ $# -gt 3 ]; then
echo "Usage: $0 <required> [optional1] [optional2]"
exit 1
fi
echo "Got $# argument(s)"
The || operator means “or”. If the count is less than 1 OR greater than 3, the check fails. This handles scripts with required arguments and optional ones cleanly.
Using [[ ]] Instead of [ ]
You’ll see both [ ] and [[ ]] used for conditionals in bash scripts. The double bracket [[ ]] is a bash extension that’s less strict and generally safer:
if [[ $# -eq 0 ]]; then
echo "No arguments provided"
exit 1
fi
With [ ], you need to be careful about quoting variables to avoid word splitting issues. With [[ ]], bash handles most of that for you. For arithmetic comparisons like checking $#, both work fine, but [[ ]] is the more modern convention in bash-specific scripts.
Checking Arguments Inside Functions
$# works inside functions too, but it reflects the number of arguments passed to the function, not the script. This is useful for writing reusable validation logic:
#!/bin/bash
validate_args() {
if [[ $# -lt 2 ]]; then
echo "Function requires at least 2 arguments"
return 1
fi
echo "Function got $# arguments"
}
validate_args "one" # Fails
validate_args "one" "two" # Passes
validate_args "one" "two" "three" # Passes
Note the use of return 1 instead of exit 1 inside functions. exit would terminate the entire script. return exits just the function with a status code.
A Practical Script With Full Argument Validation
Here’s a real-world example that puts everything together. This script backs up a file and takes an optional destination directory:
#!/bin/bash
SCRIPT_NAME=$(basename "$0")
usage() {
echo "Usage: $SCRIPT_NAME <file> [destination_dir]"
echo ""
echo " file The file to back up (required)"
echo " destination_dir Where to store the backup (optional, defaults to ./backups)"
exit 1
}
# Check argument count
if [[ $# -lt 1 ]] || [[ $# -gt 2 ]]; then
usage
fi
SOURCE_FILE="$1"
DEST_DIR="${2:-./backups}"
# Check the file exists
if [[ ! -f "$SOURCE_FILE" ]]; then
echo "Error: '$SOURCE_FILE' is not a valid file"
exit 1
fi
mkdir -p "$DEST_DIR"
cp "$SOURCE_FILE" "$DEST_DIR/"
echo "Backed up '$SOURCE_FILE' to '$DEST_DIR/'"
A few things worth noting in this script:
- The
usage()function centralizes the help text so it’s easy to update ${2:-./backups}uses parameter expansion to set a default value if$2is not provided- Argument validation happens before any file operations, so bad input fails fast and cleanly
Using getopts for Flag-Based Arguments
If your script uses flags like -f filename or -v for verbose mode, the positional $# check gets more complex. That’s where getopts comes in:
#!/bin/bash
VERBOSE=false
OUTPUT_FILE=""
while getopts "vo:" opt; do
case $opt in
v) VERBOSE=true ;;
o) OUTPUT_FILE="$OPTARG" ;;
?) echo "Usage: $0 [-v] [-o output_file] <input>"; exit 1 ;;
esac
done
# Shift past the options to check remaining positional arguments
shift $((OPTIND - 1))
if [[ $# -lt 1 ]]; then
echo "Error: input file required"
exit 1
fi
echo "Input: $1"
[[ "$VERBOSE" == true ]] && echo "Verbose mode on"
[[ -n "$OUTPUT_FILE" ]] && echo "Output: $OUTPUT_FILE"
getopts handles the flag parsing. After the while loop, shift $((OPTIND - 1)) removes the processed flags from the argument list, leaving only positional arguments. Then $# reflects just those remaining arguments.
This pattern is standard for scripts that mix flags and positional arguments. It scales cleanly as the script grows.
Common Mistakes When Checking Arguments in Bash
Not quoting $# in comparisons. For $#, this rarely causes issues because it’s always a number, but quoting variables is a good habit:
if [[ "$#" -eq 0 ]]; then # Fine
Using exit inside a function. As mentioned, exit kills the whole script. Use return inside functions.
Checking arguments too late. Validate at the top of the script, before any work starts. There’s nothing worse than a script that processes for 30 seconds before telling you it needed a second argument.
Unhelpful error messages. "Error" alone is useless. Always include what the script expected, ideally with an example invocation.
Argument handling quality directly reflects script reliability. Scripts that validate inputs properly behave predictably, which matters in automated environments and CI pipelines. This connects to the same principles behind good software testing practices where catching bad inputs early prevents downstream failures.
Quick Reference: Bash Comparison Operators for $#
| Operator | Meaning | Example |
|---|---|---|
-eq |
Equal to | $# -eq 2 |
-ne |
Not equal to | $# -ne 0 |
-lt |
Less than | $# -lt 3 |
-le |
Less than or equal | $# -le 5 |
-gt |
Greater than | $# -gt 1 |
-ge |
Greater than or equal | $# -ge 2 |
These operators work inside both [ ] and [[ ]]. For string comparisons you use == and !=, but for counting arguments, always use these arithmetic operators.
Bash scripting is a core skill for anyone working in DevOps, system administration, or data engineering. Argument validation is one of those habits that separates scripts written for personal use from scripts that can be handed off and run by others. If you’re building tools that interact with data pipelines or automation workflows, pairing solid bash fundamentals with knowledge of programming tools and performance testing practices gives you a well-rounded foundation for writing reliable automation.
Key Takeaways
$#is the bash variable that holds the number of arguments passed to a script or function- Use
-eq 0to check for no arguments,-nefor exact counts,-ltand-gtfor ranges - Always print a usage message and
exit 1when argument validation fails - Use
returninstead ofexitinside functions getoptshandles flag-style arguments; after parsing flags,shift $((OPTIND - 1))resets$#to reflect remaining positional arguments- Validate arguments at the top of the script, before any other logic runs
Once you make argument checking a habit in every script you write, your scripts become more robust and far easier for others to use without needing to read the source code first.