Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: March 18, 2024
The test or [ builtin is a Bash-specific construct that can evaluate conditionals. It starts with test and ends after submitting the command or starts with [ and ends with ]. When working within the context of [], there are separate operators based on the different data types.
In this tutorial, we explore how Bash comparisons work depending on the compared values and context. First, we briefly refresh our knowledge about data types. Next, we understand shell number representation. After that, we thoroughly go over the test or [] command syntax. Finally, we briefly discuss Bash-specific features related to comparisons and conditionals.
We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15. It should work in most POSIX-compliant environments unless otherwise specified.
Data types exist in many languages. Their role is to introduce type safety as well as promote optimization.
For example, there can be a big difference between storing a number in its binary form and as a string:
So, we have 2 versus 3 bytes for such a relatively small and short number. Looking at it as a ratio, that’s around 33% more space taken up by the same data. Although encoding and compression can reduce these numbers, the raw data savings are still fairly obvious.
In other words, most often, it’s much more beneficial to use the [int]eger instead of a char or string data type when it comes to storing numbers.
Importantly, within Bash, all variables are basically strings by default:
$ var=666+1
$ echo $var
666+1
Still, there are cases when their interpretation matters.
Despite the fact that Bash variables are stored as strings, how they are interpreted depends on the context. Bash has very specific rules and syntax for shell arithmetic.
First off, we have the declare builtin and its [-i]nteger attribute:
$ declare -i var=555+111
$ echo $var
666
Here, we see the 555+111 calculation results in 666.
In fact, once a variable has been [declare]d this way, any assignment to that variable is interpreted as an arithmetic expression:
$ var=$var+1
$ echo $var
667
Moreover, incorrect expressions can result in errors:
$ var=$var+1.0
-bash: 667+1.0: syntax error: invalid arithmetic operator (error token is ".0")
$ var=string
$ echo $var
0
Since Bash doesn’t deal with floating-point numbers directly, it can’t evaluate .0. Meanwhile, a string, i.e., incorrect type, assignment results in a value of 0.
To drop the type of a variable after we declare it with -i, we either need to re[declare] or unset it.
The declare builtin and its functionality is one of the closest features to type safety that Bash has. Still, it’s only in the form of post-factum compensatory corrections.
The let builtin also evaluates arithmetic expressions when assigning values:
$ let var=666+1
$ echo $var
667
However, unlike declare, let performs arithmetic evaluation for that particular assignment:
$ var=666+1
$ echo $var
666+1
As we can see, future assignments are treated as usual without needing to unset or redeclare $var. Importantly, let returns an exit status.
To use arithmetic in any context, we can employ the $(()) arithmetic expansion syntax:
$ var=$((666+1))
$ echo $var
667
Similar to let, this doesn’t change the future behavior of the variable.
Both expr and bc attempt to evaluate their arguments as numbers:
$ var=$(bc <<< '666+1')
$ echo $var
667
$ var=$(expr $var + 1)
668
The bc command is a requirement to build the Linux kernel, while expr is in the coreutils package and part of POSIX.
At this point, we have a solid understanding of the numeric context in the shell.
After getting to know data types and their relation to the shell, let’s understand the test ([) command a bit better.
In general, we can just use test directly:
$ test -x script.sh
$ echo $?
1
Here, we check whether the given script.sh file is e[x]ecutable. The non-zero exit status tells us it isn’t.
An alternative way to write the same is by beginning with [ and ending with ]:
$ [ -x script.sh ]
$ echo $?
1
In practice, the builtin is rarely used like this directly in the shell.
Instead, we commonly see [] as part of an if statement:
$ if [ -x script.sh ]; then
echo 'script.sh is executable'
else
echo 'script.sh isn't executable'
fi
script.sh isn't executable
In fact, we can do the same with the test synonym just like any other command:
$if test -x script.sh; then echo 'script.sh is executable'; fi
script.sh is executable
In both cases, we decide on an action based on the exit status of test.
First, let’s check the common syntax via some basic tests:
+----------------------------+-------------------------------------------+
| Test | Meaning |
+----------------------------+-------------------------------------------+
| ( EXPRESSION ) | EXPRESSION is true |
| ! EXPRESSION | EXPRESSION is false |
| EXPRESSION1 -a EXPRESSION2 | both EXPRESSION1 and EXPRESSION2 are true |
| EXPRESSION1 -o EXPRESSION2 | either EXPRESSION1 or EXPRESSION2 is true |
+----------------------------+-------------------------------------------+
In this case, we see the logical AND and OR, as well as the negation operator !.
There are a number of options for file tests with [.
To begin with, we can compare modification dates, device, and inode numbers:
+-----------------+--------------------------------------------------------+
| Test | Meaning |
+-----------------+--------------------------------------------------------+
| FILE1 -ef FILE2 | FILE1 and FILE2 have the same device and inode numbers |
| FILE1 -nt FILE2 | FILE1 is newer (modification date) than FILE2 |
| FILE1 -ot FILE2 | FILE1 is older than FILE2 |
| -t FD | file descriptor FD is opened on a [t]erminal |
+-----------------+--------------------------------------------------------+
In addition, there are file type, permission, and attribute tests:
+---------+---------------------------------------+
| Test | Meaning |
+---------+---------------------------------------+
| -b FILE | [b]lock special |
| -c FILE | [c]haracter special |
| -d FILE | [d]irectory |
| -e FILE | [e]xists |
| -f FILE | regular [f]ile |
| -g FILE | set-[g]roup-ID |
| -G FILE | owned by the effective [G]roup ID |
| -h FILE | symbolic not [h]ard link (same as -L) |
| -k FILE | stic[k]y bit set |
| -L FILE | symbolic [L]ink (same as -h) |
| -N FILE | [N]ot same (modified since last read) |
| -O FILE | [O]wned by the effective user ID |
| -p FILE | named [p]ipe |
| -r FILE | user has [r]ead access |
| -s FILE | [s]ize greater than zero |
| -S FILE | [S]ocket |
| -u FILE | set-[u]ser-ID bit set |
| -w FILE | user has [w]rite access |
| -x FILE | user has e[x]ecute (or search) access |
+---------+---------------------------------------+
Notably, all switches in the last table check and verify that FILE exists, i.e., imply -e.
When it comes to string comparison, test uses the usual = and != operators. In addition, -z tests for the NULL string, but we can also check for [-n]on-NULL values.
On the other hand, integer comparisons have two-letter operators. Again, Bash doesn’t support floating-point operations.
Let’s summarize the comparisons of strings and integers within a single table:
+---------------------------+-----------------------+-----------------------+
| Comparsion | String | Integer |
+---------------------------+-----------------------+-----------------------+
| [eq]ual | STRING1 [=]= STRING2 | INTEGER1 -eq INTEGER2 |
| [n]ot [e]qual | STRING1 != STRING2 | INTEGER1 -ne INTEGER2 |
| [g]reater than or [e]qual | / | INTEGER1 -ge INTEGER2 |
| [g]reater [t]han | / | INTEGER1 -gt INTEGER2 |
| [l]ess than or [e]qual | / | INTEGER1 -le INTEGER2 |
| [l]ess [t]han | / | INTEGER1 -lt INTEGER2 |
| equal to NULL ([z]ero) | -z STRING | / |
| [n]ot equal to NULL | STRING [OR] -n STRING | / |
+---------------------------+-----------------------+-----------------------+
Importantly, even if we supply a literal number, it can be within quotes for both -eq and =:
$ if [ "666" -eq 666 ]; then echo 'same'; fi
same
$ if [ "666" = 666 ]; then echo 'same'; fi
same
Importantly, while test can diverge from the POSIX standard with comparisons like ==, we can use sh or a service like ShellCheck to ensure compliance, if necessary.
Considering this, let’s move on to some related Bash-specific features.
Unlike the test ([) command, [[ is a keyword in Bash. Also, although it bears some resemblance to [, [[ isn’t POSIX standard.
The same -xx integer equality and string equality operators work with both [[ and [.
However, there are a number of differences between the two constructs:
To clarify, let’s list the double quote preprocessing that Bash does:
In summary, we have only partial upgrades to the equivalence comparisons and added features in terms of logical operators and regex.
In this article, we talked about data types and the test or [] builtin of Bash.
In conclusion, depending on our needs for compliance with POSIX, we can use the basic or extended Bash feature set for matching and comparisons.