Bash Tips and Pitfalls
Reference
Local page:
External links:
Tips for Robust Scripts
Use set -u
This will detect uninitialized variable, the king of all evils!
#! /bin/bash
set -o nounset # Or "set -u"
chroot=$1
rm -r $chroot/etc # Will delete /etc if $1 is not given!!!
Use set -e
Script will exit if any command fails.
#! /bin/bash
set -o errexit # Or "set -e"
# Don't do
command # Will fail and exit!
if [ "$?"-ne 0]; then echo "command failed"; exit 1; fi
# But do instead:
command || { echo "command failed"; exit 1; } # Ok
# Temporarily disable the check for some code section
set +e
command1
command2
set -e
Expect space in filenames
if [ $filename = "foo" ]; # WRONG
if [ "$filename" = "foo" ]; # Correct
for i in $@; do echo $i; done } # WRONG
{ for i in "$@"; do echo $i; done } # Correct
find | xargs ls # WRONG
find -print0 | xargs -0 ls # Correct
for i in $(locate .pdf); do basename $i; done # WRONG
locate .pdf | xargs -d '\n' -n 1 basemane # Correct
Use signals to fail cleanly
if [ ! -e $lockfile ]; then
trap "rm -f $lockfile; exit" INT TERM EXIT
touch $lockfile
critical-section
rm $lockfile
trap - INT TERM EXIT
else
echo "critical-section is already running"
fi
Beware of Race conditions
There is race condition between the test of file and its creation. If 2 processes run simultaneously, they might both pass the test successfully and think that they are running alone. To solve it, we need an operation that tests & create the file in an atomic way. An example in [4] and [5], is to use IO redirection and bash's noclobber mode, which won't redirect to an existing file:
if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null;
then
trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT
critical-section
rm -f "$lockfile"
trap - INT TERM EXIT
else
echo "Failed to acquire lockfile: $lockfile."
echo "Held by $(cat $lockfile)"
fi
Another solution is to use mkdir (see [6]). mkdir is atomic, it will fail if directory already exists, or create it otherwise, both atomically.
LOCKDIR="~/.$(basename $0).lock"
if (mkdir “$LOCKDIR”); then echo “Could not lock…”; exit 1; fi
# “locking” succesful
do_stuff()
rmdir -f “$LOCKDIR”
A more thorough example below from [7]:
{{{content}}}
Tips
Parsing Command-Line Option Parameters
- To ease parsing, pre-parse with executable getopt (see here for more information and examples).
#!/bin/bash
# (old version)
args=`getopt abc: $*`
if test $? != 0
then
echo 'Usage: -a -b -c file'
exit 1
fi
set -- $args
for i
do
case "$i" in
-c) shift;echo "flag c set to $1";shift;;
-a) shift;echo "flag a set";;
-b) shift;echo "flag b set";;
esac
done
$ ./g -abc "foo"
flag a set
flag b set
flag c set to foo
- Better yet, parse using Bash/sh built-in getopts (see here for more information and examples).
#!/bin/bash
while getopts "abc:" flag
do
echo "$flag" $OPTIND $OPTARG
done
shift $((OPTIND-1))
echo $@
$ ./g -abc "foo" "bar"
a 1
b 1
c 3 foo
bar
- To parse option like --value=name ([8])
until [[ ! "$*" ]]; do
if [[ ${1:0:2} = '--' ]]; then
PAIR=${1:2}
PARAMETER=$(echo ${PAIR%=*} | tr [:lower:]- [:upper:]_)
eval P_$PARAMETER=${PAIR##*=}
fi
shift
done
Empty a file keeping permissions
Empty a file named filename, keeping the same permission and user/group:
>filename
Print multi-lines with echo
Print multi-lines text with echo:
$ echo -e "Some text\n...on 2 lines..." # Enable interpretation of backslash escapes (must be quoted!)
Some text
...on 2 lines...
Print multi-line variables with echo
One can save in a variable the multi-line output of a command. Later this variable can echoed while preserving the linefeeds if the variable is enclosed in quotes "...":
$ mymultilinevar=$(<myfile.txt sed -e'/first line/,/last line/')
$ echo "$mymultilinevar"
first line
second line
...
last line
Echo with colors
The command echo can display colors thanks to escape sequence commands [9]:
echo -e "\033[35;1m Shocking \033[0m" #Display "shocking" in bright purple
The first character is the escape character 27 (033 in octal). One can also type directly ^[ (i.e. Ctrl-AltGr-[). The syntax is (where spaces were added for clarity)
\033 [ <command> m \033 [ <command> ; <command> m
Note that commands can be chained. The set of commands is given in the color table below:
code | style | code | foreground | code | foreground | code | background | code | background |
---|---|---|---|---|---|---|---|---|---|
0 | default colour | 90 | dark grey | 40 | black | 100 | dark grey | ||
1 | bold | 31 | red | 91 | light red | 41 | red | 101 | light red |
4 | underlined | 32 | green | 92 | light green | 42 | green | 102 | light green |
5 | flashing text | 33 | orange | 93 | yellow | 43 | orange | 103 | yellow |
7 | reverse field | 34 | blue | 94 | light blue | 44 | blue | 104 | light blue |
35 | purple | 95 | light purple | 45 | purple | 105 | light purple | ||
36 | cyan | 96 | turquoise | 46 | cyan | 106 | turquoise | ||
37 | grey | 47 | grey |
Get file size
The different ways to extract file size in a Bash script:
SIZE=$(stat -c%s "$FILENAME") # Using stat
SIZE=$(ls -l $FILENAME | awk -F" "'{ print $5 }') # Using ls / awk
SIZE=$(du -b $FILENAME | sed 's/\([0-9]*\)\(.*\)/\1/') # Using du
SIZE=$(cat $FILENAME | wc -c) # Using cat / wc
SIZE=$(ls -l $FILENAME | cut -d " " -f 6) # Using ls / cut
Read file content into env variable
Read the content of a file into an environment variable:
PID=`cat $PIDFILE`
read PID < $PIDFILE
Get the PID of a new process
Getting the pid of a new process (when other processes with same name are already running)
oldPID=`pidofproc /usr/bin/ssh`
/usr/bin/ssh -f -N -n -q -D 1080 noekeon
RETVAL=$?
newPID=`pidofproc /usr/bin/ssh`
uniqPID=`echo $oldPID $newPID|sed -e 's/ /\n/g'|sort|uniq -u`
echo $uniqPID
Get the PID of a running process
Getting the pid of a running process
pid=$(pidof -o $$ -o $PPID - o %PPID -x /bin/ssh)
Detect if a given process is running
This is actually a tricky one. Some good solutions, all giving answer in $?:
[ -e /proc/$pid ] # PID - nice, but is it portable?
ps -p $pid >/dev/null # PID - need redirect, otherwise ps will print the process found
pgrep "^$name$" # NAME - probably the best using command-name
pkill -0 $name # NAME - ... similar & less robust (fail if process can't accept signal)
/bin/kill -0 $pid 2>/dev/null # PID - need redirect, otherwise kill will complain if no process found
# ... also works with bash built-in kill
Some wrong / bad solutions:
ps -aef | grep $pid # --== FAIL ==-- Will match grep process itself + $pid as ppid
ps -aef | grep $name # --== FAIL ==-- Will match grep process itself
ps -aef | grep -v grep | grep $pid # --== UGLY ==-- ... and slow. Better use ps -fp $(pgrep $pid)
ps -p $pid | grep $pid # --== SLOW ==-- better test $? immediately
Don't use this method for locking in startup scripts. Be careful with race condition. The best solution is to use a mutex, or use an atomic command (like mkdir). See for example:
- http://flabdablet.nfshost.com/linux-scripts/test-locking.sh
- http://www.davidpashley.com/articles/writing-robust-shell-scripts.html#id2326620
Launch a process in the background
Different ways to launch process in the background (unordered - might be useful one day...). The double ampersand trick comes from here.
myprocess.exe &
exec myprocess.exe
exec myprocess.exe &
( ( exec myprocess.exe & ) & )
nohup myprocess.exe &
( ( nohup myprocess.exe & ) & )
Display the name / body of functions
To list the functions declared in the current environment, or to list the body of a function:
declare -f # List all defined functions and their bodies
declare -f name # List the body of function "name"
declare -F # List name of all defined functions
Return the subnet address
Solution from [10].
/sbin/ifconfig eth0 |
grep 'inet addr' | tr .: ' ' |
(read inet addr a b c d Bcast e f g h Mask i j k l;
echo $(( $a & $i )).$(( $b & $j )).$(( $c & $k )).$(( $d & $l )) )
Remove file name extensions
FILENAME="myfile.pdf"
echo ${FILENAME%%.pdf} # only matches '.pdf', not '.PDF'
echo ${FILENAME%%.???} # only matches 3-letter extension
Formatted output / printing using printf
printf
is a Bash built-in function that allows printing formatted output much like the standard C printf
instructions.
printf "%02d" 1 # outputs '01'
Delete files with special characters
find . -inum [inode] -exec rm -i {} \; # Use inode
rm -- -foo # Special case for name with a heading dash
rm ./-foo
Remove useless invocation of 'cat'
There are basically only 3 valid uses of cat:
- Show the content of a file in a terminal
- Write a "here" document or standard input to a file in a terminal
- Concatenating several files together (hence the name of cat)
However cat is frequently used for other purposes like piping a file in a process. This is a bad habit. It is slow and add an unnecessary process. A better alternative is to use the file redirection feature of the shell:
|
|
Using Process Substitution
The process substitution feature of Bash takes the form <(list)
or >(list)
. The process list is run with its input or output connected to a FIFO (named pipe) or a file in /dev/fd. The name of this file is then passed as an argument to the current command (as a result of the expansion). We can see this explicitly with the following examples:
echo >(true)
# /dev/fd/63
echo <(true)
# /dev/fd/63
This feature can be used to build some very advanced redirection [11]:
diff <(ls dir1) <(ls dir2) # Compare the content of 2 directories
sort -k 9 <(ls -l /bin) <(ls -l /usr/bin) <(ls -l /usr/X11R6/bin) # Sort content of 3 directories
tar cf >(gzip -c > file.tar.gz) $directory # Equivalent of tar czf file.tar.gz $directory
It can also be used to use variables that would otherwise be limited to some subprocess, like:
: | ((x++)) # This actually starts a subprocess
: | ( ((x++)) ) # ... like this.
echo x # ... so 'x' is undefined here
((x++)) < <(:) # now variable 'x' remains in the main process
echo $x # x is defined
Redirecting stdout and stderr with tee and a pipe
Using tee and the standard piping mechanism, it is easy to redirect the content of stdout to a file and stdout:
command | tee stdout.log # Keep a copy of 'command' output in file 'stdout.log'
What if we also want to do the same with stderr? In other words, can we also pipe stderr?
Yes, in Bash this is easy! We only need to use the process substitution feature (reference [12])!
command 2> >(tee stderr.log) >&2 # Keep a copy of 'command' stderr in file 'stderr.log'
command > >(tee stdout.log) 2> >(tee stderr.log >&2) # Keep both a copy of stdout and stderr in separate files
Note that tee always print the content of stdin to stdout. That's why we need the redirection >&2 to redirect it back to stderr.
Forcing program to read from standard input instead of file
See /proc filesystem
Pits
A list of frequent gotcha's !
Description | Example |
---|---|
Space! - Don't forget to add spaces whenever necessary, in particular around brace in function definition, or in test conditions for ifs. |
if -space- [ -space- -f /etc/foo -space- ]; then ... |
Quote - Always quote parameters, variables passed to test in if ... then ... else: |
if [ "$name" -eq 5 ]; then ... |
For loops with file - Use simply * to list files in for loops, not `ls *`: |
for file in *; cat "$file"; done # SUCCEEDS, even if white space
for file in `ls *`; cat "$file"; done # FAILS miserably
|
Incorrect variable definition
So it is MYVAR=value and not |
srcDir = $1 # WRONG - spaces around = sign
$srcDir=$1 # WRONG - $ prefix
maxW= $(sed -rn '/$^/Q' myfile.txt) # WRONG - SPACE!
srcDir=$1 # CORRECT
srcDir="$1" # BEST
|
Semi-colon in find - Semi-colon in find commands must be escaped ! |
find . -exec echo {} ; # WRONG - semi-colon not escaped
find . -exec echo {} \; # CORRECT
|
Using a bash built-in instead of external program Bash built-in commands override external commands with same name (eg. kill and echo) |
$ type kill # kill is a shell builtin
$ type /bin/kill # /bin/kill is /bin/kill
$ /bin/kill -v # kill (cygwin) 1.14
|
Wrong redirection order |
read pid < $PID_FILE 2> /dev/null # WRONG - error msg if $PID_FILE
# doesn't exist
read pid 2> /dev/null < $PID_FILE # CORRECT
|
Variable not exported outside parens |
( read pid < $PID_FILE ) 2> /dev/null # WRONG - var pid not kept
read pid 2> /dev/null < $PID_FILE # CORRECT
|
Read and piping
|
echo "1 2 3" | read a b c; echo $a $b $c # WRONG - subshell
echo "1 2 3" | (read a b c; echo $a $b $c) # CORRECT - same subshell
set -- $(echo "1 2 3"); echo $1, $2, $3 # BETTER
|
Don't quote tilde in if test block | if [ -a ~/bin/"my file" ]; then echo found; fi # CORRECT
if [ -a "~/bin/my file" ]; then echo found; fi # WRONG
|
Need quoting when echoing a variable with embedded newlines. This is because echo takes newlines (like any blanks) as parameter separator |
HEADER=$(sed -rn '/$^/Q' myfile.txt)
echo "$HEADER" # CORRECT
echo $HEADER # WRONG - newline are removed
|