A shell script is a program. Therefore, it deserves all of the care that any other program should get, including, as appropriate:
Only an idiot would justify sloppy work with, “It’s only a shell script, so I didn’t bother doing …”.
Sure, if it’s a short-lived (you hope) program (whether script or not), then it may not require the full treatment. However, that decision is not determined by whether or not the program is a shell script, a Perl script, a Python script, or a C++ program.
Shell scripts are programs that:
There are many shells, including:
sh
bash
ksh
csh
zsh
From now on, we will focus on bash
.
echo
"string" — send string to stdout
read
variable — read variable from stdin
pathname="/usr/lib"
$
to get its value
$ name=Bozo $ let amount=2+7 $ echo "name amount" name amount $ echo "$name $amount" Bozo 9 $ echo $UID 1038
$0
— same as C’s argv[0]
$1
— same as C’s argv[1]
$2
— same as C’s argv[2]
$@
— same as "$1" "$2" "$3" …
$*
— same as "$1 $2 $3 …"
$#
— same as C’s argc-1
#! /bin/bash
# Make sure to remove all gophers here
usage() { echo "Usage: $0 <param>" >&2; }
$ echo -e '#! /bin/bash\nlet sum=$1+$2\necho "Sum is $sum"' >s $ cat s #! /bin/bash let sum=$1+$2 echo "Sum is $sum" $ ./s 34 5 12 /tmp/pmwiki-bash-scriptIfsxBL: line 14: ./s: Permission denied $ ls -l s -rw------- 1 ct320 class 46 Sep 6 07:17 s $ chmod +x s $ ls -l s -rwx------ 1 ct320 class 46 Sep 6 07:17 s $ ./s 34 5 12 Sum is 39
((
expression ))
allows a C-like arithmetic expression
as a condition:
cd /etc let amount=$(ls | wc -l) if (( amount == 0 )) then echo "$PWD is empty." elif ((amount==1)) then echo "$PWD contains one file." else echo "$PWD contains $amount files." fi
/etc contains 148 files.
Of course, as in most programming languages, there’s also a
case
construct:
cd /etc let amount=$(ls | wc -l) case $amount in 0) echo "$PWD is empty.";; 1) echo "$PWD contains one file.";; *) echo "$PWD contains $amount files.";; esac
/etc contains 148 files.
To compare strings, use [[
… ]]
.
Unlike ((
… ))
, spaces are required.
if [[ $HOSTNAME = boston || $HOSTNAME = denver ]] then echo "You are in the CSB 120 lab." elif [[ $HOSTNAME = *[aeiouy]* ]] then echo "Well, at least $HOSTNAME contains a vowel." else echo "$HOSTNAME confuses me. ☹" fi
Well, at least beethoven contains a vowel.
[[
string =
string ]]
[[
string =
filename-pattern ]]
[[
string !=
string ]]
[[
string !=
filename-pattern ]]
[[
string <
string ]]
[[
string >
string ]]
[[
string =~
regular-expression ]]
==
may work the same as =
, but is non-standard.
[[
… ]]
:
[[ -e
path ]]
— Does this exist?
[[ -f
path ]]
— Is this a plain file?
[[ -d
path ]]
— Is this a directory?
[[ -s
path ]]
— Does this have a non-zero size?
[[ -r
path ]]
— Can I read this?
[[ -w
path ]]
— Can I write this?
[[ -x
path ]]
— Can I execute this?
Note that the operator is =
. This does not mean to test for
string equality, but, instead, to match the left side against the
filename expression that is the right side.
if [[ $OSTYPE = linux* ]] then echo "Great, we’re on something Linuxy: $OSTYPE" else echo "Sorry, but the OS $OSTYPE is not supported." fi
Great, we’re on something Linuxy: linux-gnu
The =~
operator uses a regular expression, like the grep
command. This is not a filename expression.
when=$(date) if [[ $when =~ (Tue|Thu).*Aug ]] then echo "Good." else echo "Sorry, this only works on August Tuesdays/Thursdays." echo "Current date: $when" fi
Sorry, this only works on August Tuesdays/Thursdays. Current date: Fri Sep 6 07:17:18 MDT 2024
$?
$ date Fri Sep 6 07:17:18 MDT 2024 $ echo $? 0 $ cp /etc/group xyz $ echo $? 0 $ cp /etc/superman xyz cp: cannot stat '/etc/superman': No such file or directory $ echo $? 1 $ grep 'beet' /etc/hosts 129.82.44.240 dung-beetle.cs.colostate.edu dung-beetle 129.82.45.48 beethoven.cs.colostate.edu beethoven $ echo $? 0 $ grep "cowabunga" /etc/hosts $ echo $? 1 $ echo $? 0
$?
is the exit code of the most recent program.
if
/then
/else
uses a program.
[[
… ]]
or ((
… ))
.
if grep -q "colostate.edu" /etc/resolv.conf then echo "operating at CSU" else echo "must be at home or out & about" fi
operating at CSU
Here’s the same code, written by someone who doesn’t understand
how if
/then
/else
works:
grep -q "colostate.edu" /etc/resolv.conf if (($? == 0)) # or, worse: [[ $? -eq 0 ]] then echo "operating at CSU" else echo "must be at home or out & about" fi
operating at CSU
You can combine all three syntaxes using &&
and ||
:
if ((a>4)) && [[ -r infile ]] && cmp -s infile outfile.$a then echo "Holy Toledo, that was a miracle!" fi
while
loops use the same argument as if
.
That is, you can have:
while
loop
while
loop
while
loop
let n=1 while ((n > 0)) do echo "n is $n" let n=n*32 done
n is 1 n is 32 n is 1024 n is 32768 n is 1048576 n is 33554432 n is 1073741824 n is 34359738368 n is 1099511627776 n is 35184372088832 n is 1125899906842624 n is 36028797018963968 n is 1152921504606846976
for beatle in John Paul George Ringo do echo "$beatle was one of the Beatles." done
John was one of the Beatles. Paul was one of the Beatles. George was one of the Beatles. Ringo was one of the Beatles.
for datafile in /bin/*a*e*i*o* do echo Processing $datafile done
Processing /bin/abrt-action-install-debuginfo Processing /bin/build-jar-repository Processing /bin/cinnamon-desktop-editor Processing /bin/cinnamon-file-dialog Processing /bin/cinnamon-menu-editor Processing /bin/cinnamon-screensaver-lock-dialog Processing /bin/cinnamon-session Processing /bin/cinnamon-session-cinnamon Processing /bin/cinnamon-session-cinnamon2d Processing /bin/cinnamon-session-quit Processing /bin/dbus-update-activation-environment Processing /bin/gtk-update-icon-cache Processing /bin/makeinfo Processing /bin/mariadb-service-convert Processing /bin/ocamlbyteinfo Processing /bin/opencv_interactive-calibration Processing /bin/pam-panel-icon Processing /bin/papi_mem_info Processing /bin/papi_version Processing /bin/papi_xml_event_info Processing /bin/pipewire-media-session Processing /bin/pkla-check-authorization Processing /bin/plasma-browser-integration-host Processing /bin/plasma-interactiveconsole Processing /bin/plasma_session Processing /bin/rebuild-jar-repository Processing /bin/start-pulseaudio-x11 Processing /bin/tpm2_startauthsession Processing /bin/UnicodeNameMappingGenerator Processing /bin/update-gtk-immodules Processing /bin/upload-system-info Processing /bin/vala-gen-introspect Processing /bin/vala-gen-introspect-0.40 Processing /bin/vala-gen-introspect-0.40-x86_64
A C-like arithmetic loop exists:
let max=5 for ((i=max; i>=0; i--)) do echo $i done echo "Blast off!"
5 4 3 2 1 0 Blast off!
Why not $max
?
Or:
for i in {5..1} do echo $i done echo "Blast off!"
5 4 3 2 1 Blast off!
Let’s try some straightforward code:
$ alpha=2+5 $ beta=alpha*3 $ echo beta beta
Of course, echo beta
simply yields “beta
”.
Perhaps a $
will help to extract the value of the variable:
$ alpha=2+5 $ beta=alpha*3 $ echo $beta alpha*3
We got the string “alpha” instead of its value. Let’s try $alpha
:
$ alpha=2+5 $ beta=$alpha*3 $ echo $beta 2+5*3
A sure sign of guesswork: “let’s try”. Don’t guess. Know.
Use let
for arithmetic assignment:
$ alpha=2+5 $ let beta=$alpha*3 $ echo $beta 17
The problem was that $alpha
literally returned the string
2+5
. What is 2+5*3
? Let’s avoid $
:
$ alpha=2+5 $ let beta=alpha*3 $ echo $beta 21
To be consistent, and to force arithmetic evaluation every step
of the way, use let
in the first assignment:
$ let alpha=2+5 $ let beta=alpha*3 $ echo $beta 21
Let’s boil this down into rules:
let
for every arithmetic assignment:
let alpha=beta*34+delta
let gamma=42
$
in arithmetic expressions.
let alpha='(beta+gamma)*3'
$ alpha=1 $ beta=2 # no spaces around = $ gamma=alpha+beta $ echo gamma gamma $ echo $gamma alpha+beta $ delta=$alpha+$beta $ echo $delta 1+2 $ let epsilon=alpha+beta $ echo $epsilon 3
Somewhat limited, and certainly inelegant, capabilities compared to C:
$ array=(one two three) $ echo ${array[1]} two $ echo ${array[*]} one two three $ echo ${#array[*]} 3
*
— matches anything of any length
[aeiou]
— matches any one lower-case vowel
[a-z]
— matches any one lower-case letter
a
doesn’t match bonehead
*a*
matches bonehead
$ cd ~/bin $ ls checkin curve imv p scores wikidiff checkin-checker demo-script l peek stats wikiedit checkin-file-checker domoss ll playpen tools wikigrep checkin_prog e lsf pwget u wikiupdate chit grade moss ruler unold wikiwhence cls grade-busy new run untar code grade-file-checker note runner vman cronedit grades old save wikicat $ ls ad /bin/ls: cannot access 'ad': No such file or directory $ ls *ad* grade grade-busy grade-file-checker grades $ ls [check]in* /bin/ls: cannot access '[check]in*': No such file or directory $ ls checkin* checkin checkin-checker checkin-file-checker checkin_prog
grep
and vi
commands
.
— matches any single character
[0-9a-fA-F]
— matches any one hexadecimal digit
[^a-zA-Z]
— matches any single character that is not alphabetic
*
— matches zero or more of what just came before
^
,$
— matches the beginning & end of a line
\d
is [0-9]
, \w
is [A-Za-z0-9_]
,
\s
is [ \f\t\n\r]
a
matches the line my dog has fleas
Beware of “levels” of regular expressions.
grep
— most basic (BRE, Basic Regular Expression)
egrep
— extended (ERE, Extended Regular Expression)
grep -P
— superior
(PCRE, Perl Compatible Regular Expression)
Use obsolete features only when dealing with obsolete computers.
if [ $PWD = / ] # ☠ avoid single [ ] if [[ $PWD = / ]] # good if [[ $n -eq 100 ]] # ☠ avoid [[ … ]] for numbers if (($n==100)) # better, but still poor if ((n==100)) # good now=`date` # ☠ `backquotes` resemble 'quotes' now=$(date) # good sum=`expr $sum + $n` # ☠ invoking an external program‽ sum=$(expr $sum + $n) # ☠ not quite as bad let sum=$sum+$n # poor let sum=sum+n # ok let sum+=n # good
#! /bin/bash # # Go into a temporary “playpen” directory; clean it up when done. # If we can’t execute a file in TMPDIR, then change it to somewhere executable. script=$(mktemp -t playpen-script-XXXXXX) chmod u=rx,go= "$script" "$script" 2>&- || TMPDIR=~/tmp rm -f "$script" cd "$(mktemp -d -t playpen-XXXXXX)" # Create temporary dir. cp -r ~/.playpen/* . 2>&- # Files to play with chmod -R u+rw . # Works even if no files got copied. ls -lhog | grep -v '^total ' # Show what’s here. $SHELL chmod -R u=rwx . # Make everything removable. cd /tmp # Get away from temporary directory. rm -rf $OLDPWD # Remove previous, temporary, directory.