Tests: [[ ]]

  • [[ expression ]] returns 0=true/success or 1=false/failure depending on the evaluation of the conditional expression.

  • [[ expression ]] is a new upgraded variation on test (also known as [ ... ]), all the earlier examples with single brackets that one can find online will also work with double

  • Inside the double brackets it performs tilde expansion, parameter and variable expansion, arithmetic expansion, command substitution, process substitution, and quote removal

  • Conditional expressions can be used to test file attributes and perform string and arithmetic comparisons

Selected examples file attributes and variables testing:
  • -f file true if is a file

  • -r file true if file exists and readable

  • -d dir true if is a directory

  • -e file true if file/dir/etc exists in any form

  • -z string true if the length of string is zero (always used to check that var is not empty)

  • -n string true if the length of string is non-zero

  • file1 -nt file2 true if file1 is newer (modification time)

  • many more others

# check that directory does not exist before creating one
# where $dir is a variable
[[ -d $dir ]] || mkdir $dir

# checks that file exists
[[ -r $file ]] && mv $file ${file}.$(date +%Y-%m-%d) || echo $file is either non-readable or does not exist
# in the script
[[ -r $file ]] && mv $file ${file}.$(date +%Y-%m-%d) || { echo $file does not exist; exit 1; }

# Check if script/function is given an argument
[[ -z $1 ]] && { echo no argument; exit 1; }

# Often used alternative to ${var:-a_value} or ${var:?not defined}
[[ -n $var ]] || echo var is not defined

Note that integers have their own construction (( expression )) (we come back to this), though [[ ]] will work for them too. The following are more tests:

  • == strings or integers are equal (= also works)

  • != strings or integers are not equal

  • string1 < string2 true if string1 sorts before string2 lexicographically

  • > vice versa, for integers greater/less than

  • string =~ pattern matches the pattern against the string

  • && logical AND, conditions can be combined

  • || logical OR

  • ! negate the result of the evaluation

  • () group conditional expressions

# If 'var' is ... then
[[ $var == 'some_value' ]] && ...

# If path is ... then
[[ $(pwd) == /some/path ]] && ...

# grouping () and booleans && || within the [[ ]]
[[ $(hostname -s) == kosh && ($(pwd) == $WORK || $(pwd) == $SCRATCH) ]] ...

# note that [[ ]] always require spaces before and after brackets (!)

In addition (old school), double brackets inherit several operands to work with integers mainly:

  • -eq, -ne, -lt, -le, -gt, -ge equal to, not equal to, less than, less than or equal to, greater than, or greater than or equal

# Some use cases for [[ ]]

# a popular way to check input arguments, if no input, exit (in functions
# 'return 1').  Remember, $# is special variable for number of arguments.
[[ $# -eq 0 ]] && { echo Usage: $0 arguments; exit 1; }

# if dir exists and is not empty, then archive it
d=path/to/dir; [[ -d $d && $(ls -A $d) ]] && tar caf $(basename $d).$(date +%Y-%m-%d).tar.gz $d

# append PATH function
# as an exercise, we will re-implement this with the matching operator =~, see below
appendPATH() {
  local dpath=${1:?directory is missing} && \
  [[ -d $dpath && ! $(echo $PATH|grep $dpath) ]] && export PATH+=:$dpath

The matching operator =~ brings more opportunities, because regular expressions come in play. Matched strings in parentheses assigned to ${BASH_REMATCH[]} array elements.

# change shell on the Linux server if it is not BASH
[[ ! $SHELL =~ bash ]] && chsh -s /bin/bash
  • Regular expressions (regexs) are basically a mini-language for searching within, matching, and replacing text in strings.

  • They are extremely powerful and basically required knowledge in any type of text processing.

  • Yet there is a famous quote by Jamie Zawinski: “Some people, when confronted with a problem, think ‘I know, I’ll use regular expressions.’ Now they have two problems.” This doesn’t mean regular expressions shouldn’t be used, but used carefully. When writing regexs, start with a small pattern and slowly build it up, testing the matching at each phase, or else you will end up with a giant thing that doesn’t work and you don’t know why and can’t debug it. There are also online regex testers which help build them.

  • While the basics (below) are the same, there are different forms of regexs! For example, the grep program has regular regexs, but grep -E has extended. The difference is mainly in the special characters and quoting. Basically, check the docs for each language (Perl, Python, etc) you want to use regexs in.

Selected operators:

  • . matches any single character

  • ? the preceding item is optional and will be matched, at most, once

  • * the preceding item will be matched zero or more times

  • + the preceding item will be matched one or more times

  • {N} the preceding item is matched exactly N times

  • {N,} the preceding item is matched N or more times

  • {N,M} the preceding item is matched at least N times, but not more than M times

  • [abd], [a-z] a character or a range of characters/integers

  • ^ beginning of a line

  • $ the end of a line

  • () grouping items, this what comes to ${BASH_REMATCH[@]}

# match an email
email=''; regex='(.*)@(.*)'; [[ "$email" =~ $regex ]]; echo ${BASH_REMATCH[*]}

# extract a number out of the text
txt='A text with #1278 in it'; regex='#([0-9]+ )'; [[ "$txt" =~ $regex ]] && echo ${BASH_REMATCH[1]} || echo do not match

# case insensitive matching
var1=ABCD, var2=abcd; [[ ${var1,,} =~ ${var2,,} ]] && ...

For case insesitive matching, alternatively, in general, set shopt -s nocasematch (to disable it back shopt -u nocasematch)

Conditionals: if/elif/else

Yes, we have [[ ]] && ... || ... but scripting style is more logical with if/else construction:

if condition; then
elif condition; then

At the condition place can be anything what returns an exit code, i.e. [[ ]], command/function, an arithmetic expression $(( )), or a command substitution.

# to compare two input strings/integers
if [[ "$1" == "$2" ]]
  echo The strings are the same
  echo The strings are different

# checking command output
if ping -c 1 &> /dev/null; then
  echo Online
elif ping -c 1 &> /dev/null; then
  echo Local interface is down
  echo No external connection

# check input parameters
if [[ $# == 0 ]]; then
  echo Usage: $0 input_arg
  exit 1
... the rest of the code

Expanding to a script


# usage: <dirname>


# if directory name is given as an argument
if [[ -d $dir ]]; then
  archive=$(basename $dir).$(date +%Y-%m-%d).tar.gz

# if no argument, then the current directory
elif [[ -z $dir ]]; then
  archive=$(basename $(pwd)).$(date +%Y-%m-%d).tar.gz

# otherwise error and exit
  echo $dir does not exist or empty
  exit 1

# run tar
tar caf $archive $dir


Another option to handle flow, instead of nested ifs, is case.

read -p "Do you want to create a directory (y/n)? " yesno   # expects user input
case $yesno in
    echo Creating a new directory $dir
    mkdir $dir
    cd $dir
    echo Proceeding in the current dir $(pwd)
    echo Invalid response
    exit 1
# $yesno can be replaced with ${yesno,,} to convert to a lower case on the fly

In the example above, we introduce read, a built-in command that reads one line from the standard input or file descriptor.

case tries to match the variable against each pattern in turn. Understands patterns rules like *, ?, [], |.

Here is the case that could be used as an idea for your ~/.bashrc

case $host in
    export PRINTER=mynearbyprinter
    # making your promt smiling when exit code is 0 :)
    PS1='$(if [[ $? == 0 ]]; then echo "\[\e[32m\]:)"; else echo "\[\e[31m\]:("; fi)\[\e[0m\] \u@\h \w $ '
    [[ -n $WRKDIR ]] && alias cwd="cd $WRKDIR" && cwd
    export IGNOREEOF=0
    echo 'Where are you?'

;; is important, if replaced with ;&, execution will continue with the command associated with the next pattern, without testing it. ;;& causes the shell to test next pattern. The default behaviour with ;; is to stop matches after first pattern has been found.

# create a file 'cx'
case "$0" in
 *cx) chmod +x "$@" ;&
 *cw) chmod +w "$@" ;;
 *c-w) chmod -w "$@" ;;
 *) echo "$0: seems that file name is somewhat different"; exit 1 ;;

# chmod +x cx
# ln cx cw
# ln cx c-w
# to make a file executable 'cx filename'

The following example is useful for Triton users: array jobs, where one handles array subtasks based on its index.

Exercise 2.3


  • Re-implement the above mentioned example ... [[ -d $d && ! $(echo $PATH|grep $d) ]] ... with the matching operator =~

  • Improve the script we developed recently:

    • add check for the number of the given arguments. Hint: $# must be zero or one.

    • validate the given path like path/to/file. Hint: [[ $d =~ regexpr ]], the path may have only alphanumeric symbols, dots, underscore and slashes as a directory delimiter.

  • Expand cx script:

    • check that $@ not empty

    • add option for cr that would add read rights for all. Hint: chmod a+r ...

  • (*) Write a function (add to bin/functions) that validates an IPv4 using =~ matching operator. The function should fail incorrect IPs like or The problem should be solved with the regular expression only. Use return command to exit with the right exit code.