Shell Scripting Tips
(revisited. Again)
Nick Holland 11/21/2023
Target Audience
- Assuming you know what shell scripting is, you've done it, but
you could do it better.
- No claim of omnipotence
- Could be better ways
- Could be all wrong!
- Targeted at ksh, but much is applicable to bash & sh
Keep it neat and simple
- Comment! Comment! Comment!
- What the code does.
- Why you wrote it that way.
- Problems you ran into along the way.
- Code for readability, not to impress.
- One liners are hard to read.
- Hard to read is hard to maintain.
- If performance matters, shell script is probably not the right tool.
- --> It is for your own benefit!
Classes of scripts
- Quick & Dirty
- Run Once and Forget.
- Tempting to ignore error handling...DON'T!
- Administrative scripts
- Run by people who know what they are doing.
- Probably running in cron or other "controlled" environment.
- Tempting to minimize error handling...what could go wrong?
- User-facing applications
- Users can manage to enter the craziest things.
- They assume you controlled the damage they can do.
- Arrow keys. They found the damn arrow keys.
Deal with errors
- Test your assumptions:
cd $DIR
rm -r *
what if $DIR is blank? What if it fails?
- Validate input AND results
- Did that nifty bit of string manipulation and calculation
really give you a valid response under all circumstances?
- are you so sure you are betting your data on it?
Minimize errors
- avoid
rm -r
- avoid
rm -f
rm $DIR/*
rmdir $DIR
If $DIR is wrong, damage is minimized.
- Not always possible, but
the reflexive
rm -rf $DIRNAME
is bad.
-e : Trap all errors!
#!/bin/ksh -e
cd /some/dir
rm -r * # Could be bad if the 'cd' failed!
Big hammer. Often too big.
(though...IF you handle "ALL possible" errors, -e can work)
Header for Q&D scripts?
#!/bin/ksh
yell() { echo "$0: $*" >&2; }
die() { yell "$*" ; exit 111; }
try() { "$@" || die "cannot $*"; }
try cd /some/dir
rm -r * # Could be bad if the 'cd' failed!
$ ./test.ksh
./test.ksh[7]: cd: /some/dir - No such file or directory
./test.ksh: cannot cd /some/dir
A more selective hammer, but still...a hammer
but possibly what you need for Q&D.
Better Header for Q&D scripts?
#!/bin/ksh
MYTMP=$(mktmp -d /tmp/tmpdata.XXXXXXXX) || exit 254
yell() { echo "$0: $*" >&2; }
die() { yell "$*" ; exit 111; }
try() { "$@" || die "cannot $*"; }
function cleanexit () {
if [[ -n $MYTMP ]]; then
rm $MYTMP/*
rmdir $MYTMP
fi
exit
}
[ ... your code here ... ]
cleanexit
Footer line for all reports:
Build a shell script template!
try/yell/die
Site specific stuff
run-as user code
--> and comments about how to use it
/tmp file cleanup
footer line
Comment out the "irregular" stuff. Delete it if you are not going to use it in a script.
Validate all input
Don't try to remove undesired things. Only accept things you
expect.
"tr" is your friend -- "-d" (delete) and "-c" (complement).
tr -dc "valid-characters"
INPUT=$(echo "$INPUT"|tr -dc "a-zA-Z_0-9-")
definitely good for user-facing code
Never let users enter data directly into a data file!
What if they entered "$(reboot)"?
Make friends with set, typeset
- set -f -> Turns off globbing!
- set -- $A -> populate $1, $2, ... with $A
- typeset -l var -> lower case variable var
- typeset -u var -> upper case variable var
- There is much, much more to these commands.
- Good thing to comment, though.
set -f -- Turn off globbing!
$ A="*"
$ echo $A
Desktop Downloads ShellScriptTips.odp ShellScriptTips01.odp
[...]
$ set -f
$ A="*"
$ echo $A
*
$
don't forget if you are turning globbing off!
set -- $V -- put $V into $*
$ A=" A b c D e "
$ echo "$A"
A b c D e
$ echo "$*"
$ set -- $A
$ echo "$*"
A b c D e
$ echo "$3"
c
- (this is why normal people hate computer people)
- You may well want to save $0 (or $*) to $THISPROG or similar.
Case insensitivity: tr(1)
$ V="AbCdEf"
$ V=$(echo $V | tr "A-Z" "a-z")
$ echo $V
abcdef
$
Case insensitivity: the lazy way
$ typeset -l V
$ V="AbCdEf" ; echo $V
abcdef
$ typeset -u V
$ V="AbCdEf" ; echo $V
ABCDEF
tput(1)
BOLD=$(tput bold)
NORM=$(tput sgr0)
echo -n "Enter your input ->$BOLD"
read response
echo -n "$NORM"
Lots of other options, varying degrees of portability, however.
General Tips(using KSH!)
- Use [[ ... ]] instead of [ ... ]
- Use &&, || instead of -a, -o in [[ ... ]]
- Much less quoting needed in [[ ... ]]
- X"$var" no longer needed
- use $(program) instead of `program`
^^^ From Robert Peichaer (rpe@openbsd)
- Use $V instead of ${V} when possible. <- From Ken Westerback
Commenting complex pipelines
diff -U10000 $MYTMP/sftp.log $MYTMP/ftp.log |\
grep -v -e "^--- " -e "^+++ " -e "^@@ " |\
sed -e "s/^+/FTP /" -e "s/^ /SFTP /" -e "s/^-/SFTP /" |\
sed -e "s/ UNKNOWN / /" -e "s/ +.*/]/" |
awk -F' ' '{print $3, $2, $1}' |\
sort | uniq
# diff -U10000 $MYTMP/sftp.log $MYTMP/ftp.log |\
# -> the U1000 gives a lot of context, so all transfers should be
# here, assuming at least a few of each type of transfer. May
# have to examine process as we move people off FTP.
# grep -v -e "^--- " -e "^+++ " -e "^@@ " |\
# -> Eliminate the headers of the diff. Might be better to just
# lop off the first few lines of the output, and look for @@s later
# to indicate a problem.
# sed -e "s/^+/FTP /" -e "s/^ /SFTP /" -e "s/^-/SFTP /" |\
# -> explained above. Look for the first char in the line of the diff
# sed -e "s/ UNKNOWN / /" -e "s/ +.*/]/" |
# -> Remove excess data
# awk -F' ' '{print $3, $2, $1}' |\
# -> reorder output
# sort | uniq
# -> remove redundancies.
"Here documents"
Much less picky about formatting characters than many other ways to
store data.
## Classic:
cat <<__ENDOFTEXT
this text is to be output.
__ENDOFTEXT
really messes with indentation
# Starting your "here doc" with "<<-" will
# strip off leading tab characters.
cat <<-__ENDOFTEXT
this is text to be output.
__ENDOFTEXT
Running as root (or anyone)
# Check to see who we are running as, if not
# root, re-invoke with sudo.
if [[ $(whoami) != "root ]]; then
if ! sudo -n -u root $0 $* 2>/dev/null; then
echo "Sorry, sudo isn't configured properly
logger "$0 sudo is broke"
sleep 2
fi # sudo failed
exit
fi # not yet root
# We are now running as root
Hopefully, $0 returns 0.
Remote administration with SSH
- ssh user@$HOST "command to run"
- You will want to use keys
- You may want agent forwarding
- Script can run as non-root
- Careful with output redirection and think hard about what commands are run where.
- ssh $HOST "cmd opt|cmd2|cmd3 >output.file"
- ssh $HOST "cmd opt|cmd2|cmd3" >output.file
- ssh $HOST "cmd opt"|cmd2|cmd3 >output.file
- scp -3 : Copy between two machines through this machine
Optimizing admin with SSH
Auto-change control
- Two or more machines replicating the same data/config.
- Change one machine, test, run replication script that:
- Generate diff against $OTHER system
- Create file with user, date, time, diff
- Drop user into an editor to explain the change.
- Replicate change and diff/explaination file to $OTHER
- Great for firewalls, DNS servers.
ksh "select"
$ PS3="Pick a motorcycle ->"
$ select MOTORCYCLE in K100 K1200LT Buell Harley; do
> echo "Riding the $MOTORCYCLE today"
> done
1) K100
2) K1200LT
3) Buell
4) Harley
Pick a motorcycle ->2
Riding the K1200LT today
Pick a motorcycle ->^C
In modern bash now
Prompt maddness
Traditional Unix Prompt: "#" or "$" (or "%")
More info:
- /usr/src $
- nick@fluffy3 /usr/src/sys/arch/amd64/compile/GENERIC.MP $
Crazy info
- date time PWD user, machine+domain name, shell name and version, history number ...
Prompt maddness
- Who says a unix command prompt has to be one line?
- Blanks between prompts help space things out on the screen
- \n username @ machine name \n $PWD $"
- Who says a unix prompt has to lead the line at all?
- remember: a lot of micros used to just give you a blank line to type
commands into.
- lead info lines with a ":" so they can be copied/pasted
- export PS1="\n:"'\$\$\$ \u @ \h \w \$\n:\r'
- export PS2=":\r"
- Not quite advocating for this, it is weird. But proving to be useful
Shell Scripting Tips
Discussion?
`
/