Validate a script argument
$# is the number of arguments. Keep variables in double quotes inside a
check, so a file name with a space stays one argument.
test and [ ] check a file, a string or a number and answer with a code: 0 is yes, 1 is no; [[ ]] is the bash version.
[ -f /etc/app.conf ] && echo "file exists"
[ -d "$dir" ] || mkdir -p "$dir"
[ -z "$1" ] && { echo "Usage: $0 FILE" >&2; exit 2; }
[ "$answer" = yes ]
strings are equal
[ "$count" -gt 10 ]
number greater than 10
[ -r "$f" ] && [ -s "$f" ]
readable and not empty
[[ $name == *.log ]]
pattern, bash only
[[ $ver =~ ^[0-9]+\.[0-9]+$ ]]
regular expression, bash only
test checks a condition and prints nothing — it answers with an
exit code: 0 if the condition holds, 1 if not, 2 if the
expression itself is broken. [ is the same command under another name, and it
expects ] as its last argument. That is why the spaces around the brackets
matter: every part of the expression is a separate
argument.
[[ ]] is a bash keyword: the shell parses the expression itself, before
variables are expanded. Inside [[ ]] an unquoted variable does not split into
words, < and > are not redirections, and == matches a pattern. sh (dash)
has no [[ ]], so a #!/bin/sh script sticks to [ ].
[ ] and test |
[[ ]] |
|
|---|---|---|
| works in | any POSIX shell | bash and zsh |
unquoted $x |
splits into words — errors | safe |
pattern *.log |
no | ==, != |
| regular expression | no | =~ |
| two conditions | [ a ] && [ b ] |
[[ a && b ]] |
There are three kinds of checks:
-e exists, -f regular file, -d directory, -L symbolic link,
-r, -w, -x — the current user's permission, -s not empty,
a -nt b — a is newer than b;= (bash also accepts ==), !=, -z empty, -n not empty;-eq, -ne, -lt, -le, -gt, -ge.A ! before a check inverts it: [ ! -f "$f" ] means there is no such file.
< and > compare strings character by character: [[ 10 > 9 ]] is false
because "1" sorts before "9". Compare numbers with -gt, -lt or (( )).
The test checks are in the table below. The full list is in help test; for
[[ ]], see man bash, section CONDITIONAL EXPRESSIONS.
| Option | What it does | Example |
|---|---|---|
-e FILE |
The file exists (any type); a dangling link does not. | [ -e /etc/app.conf ] |
-f FILE |
A regular file — not a directory or a device. | [ -f "$1" ] || exit 1 |
-d DIR |
A directory. | [ -d /srv/backup ] || mkdir -p /srv/backup |
-r / -w / -x FILE |
The current user may read, write, execute it. | [ -w "$dir" ] |
-z / -n STRING |
The string is empty / not empty; quote the variable. | [ -z "$1" ] |
= / != A B |
Strings are equal / differ; bash also accepts ==. | [ "$answer" = yes ] |
-eq / -ne A B |
Integers are equal / differ. | [ $# -eq 1 ] |
-lt / -le / -gt / -ge A B |
Less than, at most, greater than, at least. | [ "$used" -ge 90 ] |
-s FILE |
The file exists and is not empty. | [ -s report.txt ] |
-L FILE |
A symbolic link — even if its target is missing. | [ -L /etc/localtime ] |
! EXPR |
Invert the check. | [ ! -f "$lock" ] |
[[ == ]] PATTERN |
The string matches a pattern with * and ?; leave it unquoted. | [[ $file == *.log ]] |
[[ =~ ]] REGEX |
The string matches a regular expression; groups go to BASH_REMATCH. | [[ $ver =~ ^[0-9]+\.[0-9]+$ ]] |
-nt / -ot A B |
File A is newer (older) than B by modification time. | [ app.conf -nt app.conf.bak ] |
-a / -o |
"And", "or" inside [ ]; obsolete — write [ a ] && [ b ]. | [ -d /tmp ] && [ -w /tmp ] |
Nothing found. Try a shorter query.
$# is the number of arguments. Keep variables in double quotes inside a
check, so a file name with a space stays one argument.
-ge wants a bare number. tr -dc '0-9' strips the spaces and the % sign;
without it the check fails with integer expression expected.
With day=2026-10-20 the second condition prints year 2026, month 10: the
groups in parentheses land in the BASH_REMATCH array. Leave the right-hand
side unquoted: [[ app.log == "*.log" ]] compares literally and returns 1.
The variable is empty and unquoted: [ $x = foo ] became [ = foo ]. Write
[ "$x" = foo ].
The value contains a space: with x="a b" the check [ $x = foo ] received
a, b, =, foo. Quote the variable or switch to [[ ]].
-eq, -gt and the rest got something that is not a number: an empty string,
85%, a word. Print the value (echo "[$used]") and strip it down to digits.
A space is missing after [ or before ]. The shell looks up [-f as a
command name, and [ -f file] never sees its closing ] argument.
This is how sh (dash) reacts to == inside [ ]. POSIX compares strings
with =. Either write =, or run the script with bash: #!/bin/bash on the
first line.
Unquoted, the empty value disappears and [ -n ] is left — a check that the
string -n is not empty, which is true. Quoted, [ -n "$x" ] returns 1 as you
expect.
[[ 08 -eq 8 ]] and (( 08 )) read a number with a leading zero as octal, and
octal has no 8 or 9. Months and days from date +%m break this way. Write
10#$m, or compare inside [ ], where 08 is plain decimal.
-e and -f look at the file the link points to. If the target is
missing, the check is false. -L checks the link itself.
Inside [ ] the > sign is a redirection. The shell creates an empty file
named 9 in the current directory, and the check shrinks to [ 10 ], which is
always true — even [ 1 > 9 ]:
Compare numbers with -gt.
Between [ -f "$lock" ] and creating the file, another copy of the script can
do the same. Guarding against a second run needs a lock — flock.
&&, || operators.if, loops and script arguments.* and ? patterns, the same ones [[ == ]] uses.=~.r, w, x mean for -r, -w, -x.[ ].Found an inaccuracy? Report an error in the article