This document covers only the sh/bash shells, although others are somewhat similar. I had only Linux bash (Mandrake 7.0) available for testing, but my memory of HP UX tells me that it's pretty applicable across Unices.
Note: Some nice sed one liners can be had at http://www-h.eng.cam.ac.uk/help/tpl/unix/sed.html.
Special thanks go out to Tony Becker, whose incredible bash presentation illuminated the fact that the left bracket character is really a symbolic link to /usr/bin/test command. After seeing Tony's presentation, I decided to start using more shellscripts and to write this web page. Mark Alexander and Nickolai Zeldovich helped me with shellscript file I/O.
And of course, thanks to the help and support of Troubleshooters.Com's visitors.
echo "Hello World" |
chmod a+x hello.shFinally, run it as follows:
./hello.shIn the tradition of hello world programs since K&R, it prints out the words "Hello World". This is the simplest possible shellscript.
currdir=`pwd` |
RIGHT |
currdir = `pwd` |
WRONG |
if test "$currdir" = "/home/john"; then |
RIGHT |
if test "$currdir"="/home/john"; then |
WRONG |
if test "$currdir" = "/home/john"; thenIf that sounds unbelievable, try doing an ls command on an opening square bracket in the /usr/bin directory:
[slitt@mydesk bin]$ ls -l [That's the proof. [ is a symbolic link to the test executable. So use the test command whenever possible, and when you must maintain code with the bracket construct, remember that [ is a link to test , and you would certainly put a space after test, so put a space after the opening square bracket.
lrwxrwxrwx 1 root root 4 Apr 25 17:01 [ -> test*
[slitt@mydesk bin]$
aname="Florida" |
The preceding code prints the following:
[slitt@mydesk slitt]$ ./myscript |
As you can see, fields are properly padded. This is very similar to the C printf command.
echo "Hello World" |
Running this script you'll see both lines. But if you do the following:
./hello.sh | lessand then refresh with Ctrl-L, you'll see only the "Hello World".
You can observe stderr in isolation with the following command, which diverts stdout from the screen:
./hello.sh > /dev/nullThe preceding command prints only "This is an error" on the screen.
$0 | The name of the script file (like argv[0] in C) |
$1, $2, ... | The positional arguments (like argv[1], argv[2]... in C) |
$@ | Expands to a string containing all positional arguments (but not $0), separated by spaces. |
$# | The number of arguments (not counting $0). Similar to argc in C. |
$$ | The process ID of the running script (not of the calling process). |
$? | The exit status (return value) of the last foreground process. This is used to test the return value of programs run from the script. |
$(command) | Expands to the stdout output by the command in the parens. Note that the same effect can be produced by surrounding the command with backticks, but that syntax is becoming depreciated. |
There is other info that can be accessed from inside the script. For complete info, search the string Special Parameters in the bash man page. For another interesting topic, search Brace Expansion .
Any running program appears in a ps ax command, and you can certainly grep for the executable. The problem is you don't know which process is the one spawned by a particular invocation of your shellscript. But if you can use the shellscript's process id (PID) as an argument to the binary executable, you can uniquely "brand" the executable, thus grep for the one and only one copy run from the particular shellscript.
Check out this code:
open less jj $$ #run job with script pid as arg |
The preceding prints the binary executable's PID to stdout. It could also be redirected to a file for safe keeping. The first stsep is to run the command with the shellscript's PID ($$) as an argument. This is safe if the command ignores extraneous arguments. The next step is to find the job in a ps ax listing. Assuming $$ expands to 43210, you can grep for "less jj 43210 to find the executable. Except the grep will also return the grep command itself. So you filter out any commands containing the word "grep" with grep -v grep. This yields the single line containing the spawned binary executable's process. The number at the start of the line is the spawned executable's PID, and everything right of that point is extraneous. So you blow it off with the ${jobline%% *} construct (explained in the section on string manipulation). What's left is the PID of the spawned binary executable. I like it.
echo $PWD |
You use the dollar sign to *read* an environment variable, but not to write one. The dollar sign can be though of as a "what is the value of" operation.
To write an environment variable, simply use the name of the
variable on the left side of an equal sign (no spaces please). For
instance, the
following script creates an environment variable called SHELLSCRIPTS
and
sets it to the string "are cool"
echo \>$@\< |
Once again, use the dollar sign only when you want to *use* the value of the variable -- you do not use the dollar sign when you want to *set* the value of the variable.
You'll notice that the new environment variable is in scope only within the shellscript. This can be proven by running the following command at the shell prompt:
set | grep SHELLSCRIPTSA shellscript *cannot* change the environment of the caller unless the caller calls the shellscript in one of two special ways. Assuming the preceding script is file hello.sh, either of the following would apply any environment changes to the caller (typically the shell prompt):
. ./hello.sh
source ./hello.shEven more interesting, the new value of the variable is not passed even to the script's children. Assuming the following parent.sh and child.sh scripts:
parent.sh |
SHELLSCRIPTS="set by parent" |
child.sh |
echo "In child, value is >$SHELLSCRIPTS<" |
Look what happens when you run it:
[slitt@mydesk slitt]$ ./parent.sh |
The setting didn't survive the call to child.sh. If you want to pass
the
parent value to the child, use the export command in parent.sh, as
shown below:
parent.sh |
SHELLSCRIPTS="set by parent" |
Now the value is passed to the child:
[slitt@mydesk slitt]$ ./parent.sh |
Can the child change the variable and send it back to the parent?
Let's modify each to find out:
parent.sh |
SHELLSCRIPTS="set by parent" |
child.sh |
echo "In child, value is >$SHELLSCRIPTS<" |
[slitt@mydesk slitt]$ ./parent.sh |
Oops! The child can't pass back its changes. In fact, there is *nothing* the *child* can do to pass back the change. Only the parent has the power to request the passback. That is done via the dot command or the source command. To use it, in parent.sh the call to child.sh would be changed to one of these two:
. ./child.sh
source ./child.shThe following session shows what happens after changing the parent's call to the child as in the first of the preceding two lines:
[slitt@mydesk slitt]$ ./parent.sh |
The following script, called showreturn.sh, can be used to
show the return status of a command:
$@ |
Running it on hello.sh confirms that the default return is 0:
[slitt@mydesk slitt]$ ./showreturn.sh ./hello.sh |
To return a specific return value, use the exit command as
added to hello.sh below:
echo "Hello World" |
[slitt@mydesk slitt]$ ./showreturn.sh ./hello.sh |
NOTE: The $? special parameter accesses only the last process
run, not processes run before that. To save the value of the return
value, save it to a new environment variable like this:
IFCONFIG_RETURN=$? |
$0 | The name of the script file (like argv[0] in C) |
$1, $2, ... | The positional arguments (like argv[1], argv[2]... in C) |
$@ | Expands to a string containing all positional arguments (but not $0), separated by spaces. |
$# | The number of arguments (not counting $0). Similar to argc in C. |
$$ | The process ID of the running script (not of the calling process). |
$? | The exit status (return value) of the last foreground process. This is used to test the return value of programs run from the script. |
$(command) | Expands to the stdout output by the command in the parens. Note that the same effect can be produced by surrounding the command with backticks, but that syntax is becoming depreciated. |
You can deduce the PID of a spawned executable by using the present shellscript's PID as an argument to the executable, and grepping for it in a ps ax command.
There is other info that can be accessed from inside the script. For complete info, search the string Special Parameters in the bash man page. For another interesting topic, search Brace Expansion.
The value of an environment variable is read by preceding the variable name with a dollar sign. However, the dollar sign is not used when setting the value of the variable. Instead, the variable name is immediately followed by an equal sign and the desired value. Setting an environment variable within a script only effects the value within that script, not in the calling parent or any called children. To have it affect child processes, you must use the export command before issuing the call to the child. A script *cannot* affect environment variable values of its calling parent without the cooperation of the parent. That parent cooperation is in the form of calling the script with a preceding dot and space, or with the source keyword.
By default, scripts return a value of 0. To return a different value, use the exit command with the desired numerical return value. Within a script you can access the returned value of the last process run from the script with the $? special parameter. Because that special parameter changes with each new process, be sure to save it if it will be needed later.
Here is the syntax for shellscript "if" statements:
if command; thenNotice the following:
statement 1
statement 2
...
fi
if command |
The following is a very simple if construct that tells you whether
or not the first argument was the letter s.
if test "$1" = "s"; then |
In the preceding, you can see how the test executable evaluated the condition "$1" = ""s" and returns 0 if that statement is true.
Shellscripts give you full "if, else if, else" capabilities without
obnoxious nesting. The following example tests arg1 for either s, t, or
something else:
if test "$1" = "s"; then |
In the preceding, note that there is only one fi keyword. That fi keyword ends the entire if elif else statement. There's no necessity for nesting in an if elif else construct. However, note that the elif statement requires a command, semicolon, and then keyword just like if. It does not require its own fi terminator.
Although nesting isn't necessary for if elif else constructs, in some cases it's desirable. That's not a problem -- it is done with fi keywords. The following evaluates arg2 if arg1 is "s", otherwise not. It nests an if elif else inside the first if statement:
if test "$1" = "s"; then |
In the preceding, notice the fi keyword at the end of the nested if elif else, just before the unindented elif . It's that fi that does the nesting.
if test "$1" = "mystring"; thenImportant note: All relational operators must be surrounded on both sides with whitespace.
mycommand
fi
Relation |
Arithmetic |
Text |
Equal |
-eq |
= |
Not equal |
-ne |
!= |
Less than |
-lt |
|
Greater than |
-gt | |
Less than or equal |
-le |
|
Greater than or equal |
-ge | |
Zero length string |
-z STRING |
|
Non-sero length string |
[-n] STRING |
if test "$1" = "one" -a "$2" = "two"; thenImportant Note: All boolean operators must be surrounded on both sides with whitespace. That includes the Not operator.
echo "TRUE"
fi
Boolean |
Operator |
Example |
Not |
! |
! EXPRESSION |
And |
-a |
EXPRESSION1 -a EXPRESSION2 |
Or |
-o |
EXPRESSION1 -o EXPRESSION2 |
if test -d /home/slitt/umenu; thenEntries with leading asterisks are most common.
echo DIRECTORY
fi
Operator |
Explanation |
|
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 |
-b FILE |
FILE exists and is block special |
|
-c FILE |
FILE exists and is character special |
|
* |
-d FILE |
FILE exists and is a directory |
* |
-e FILE |
FILE exists |
-f FILE | FILE exists and is a regular file |
|
-g FILE |
FILE exists and is set-group-ID |
|
-G FILE |
FILE exists and is owned by the effective group ID | |
-k FILE |
FILE exists and has its sticky bit set |
|
* |
-L FILE |
FILE exists and is a symbolic link |
-O FILE |
FILE exists and is owned by the effective user ID |
|
-p FILE |
FILE exists and is a named pipe |
|
* |
-r FILE |
FILE exists and is readable |
* |
-s FILE |
FILE exists and has a size greater than zero |
-S FILE |
FILE exists and is a socket | |
-t [FD] | file descriptor FD (stdout by default) is opened
on a terminal |
|
-u FILE |
FILE exists and its set-user-ID bit is set |
|
* |
-w FILE |
FILE exists and is writable |
* |
-x FILE | FILE exists and is executable |
case "$1" in |
The variable or expression to be matched appears immediately to the right of the case keyword. Each list of matches is in parentheses. Between the closing paren and a line with a double semicolon are all the statements to be executed if there's a match between the expression on the case line and any of the items in the parentheses. The equivalent of "default" in a C switch statement is achieved by making the last choice be an asterisk, which matches anything. The entire case structure is bottom delineated by keyword esac, which is case spelled backwards.
Note that case statements are often used with just one choice. That's because they directly evaluate a match without using the test command or other commands, and because they can match against a list of possibilities. If you've ever wished if statements could be tested against a list of strings, use case.
While maintaining code, you may find that the opening parentheses are omitted. That works, but the bash man page says to use opening and closing parentheses. Also, I think it looks more satisfying than having unmatched parens.
test $# -eq 0 || echo 1>&2 Information: Command takes no arguments |
The preceding is an example of short circuit logic. If the first part is true, there's no need to "evaluate" the second part, so it's not done. If the first part is false, the truth of the entire statement depends on the truth of the second part, so it's "evaluated", which in this case prints an informational message. Note that the same effect can be had with negative logic:
test $# -ne 0 && echo 1>&2 Information: Command takes no arguments |
In the preceding, the first clause produces a true, but to make the entire statement true requires truth in both clauses, so the second clause is "evaluated", printing the message.
Short circuit logic is a great shorthand notation when there's
exactly one statement to be done upon satisfaction of the condition.
Typically,
that single statement is a function call (shellscript subroutines are
discussed later in this document). However, the ease and conciseness of
this form
breaks down rapidly when more than one statements must be done if the
condition
is satisfied. For instance, suppose that you want to print a message
AND
exit if there are command line arguments. Here's the syntax:
test $# -eq 0 || { echo 1>&2 "Error: Command takes no arguments";exit; } |
Notice that braces are used to make both the echo command and the exit command into a single command that is the second clause of the or statement. Without the braces, the exit command would be done regardless of the condition. Notice that the braces must be separated from the commands by a space. That non-obvious shellscriptism is truly ugly, and very hard to troubleshoot when neglected. For all these reasons, short circuit logic is best employed only when there's a single statement to be executed on satisfaction. Otherwise, it's best to use the standard if elif else, or possibly the case statements.
Bracket notation is often used on the condition in short circuit logic. Always remember that the brackets are shorthand for the test executable, and therefore must be separated from the text by space. I recommend that on new code you write, you simply use the test executable.
echo -n "Please type your name==>" |
This has no input validation. Validation can be had by placing the preceding in a loop that exits on valid input, and re-enquires on bad input. Also, the preceding snippet can be a security hazzard. The most obvious case is if the user is allowed to type in a command (typically in a CGI shellscript).
To restrict the user to a set of choices, use the select
keyword, which constructs a sort of poor man's menu. The following is
an incredibly simple example which gives the user three choices -- a
directory listing of *.sh, a directory listing of *.c, or terminate the
select statement by executing a break statement:
select choice in "ls *.sh" "ls *.c" "break";do |
The test for a blank $choice variable prevents execution of an invalid user choice. Invalid choices assign a NULL to the select variable ($choice in this case).
The preceding throws up a menu that looks like this:
[slitt@mydesk slitt]$ ./jj |
Each number corresponds in to a list element after the in keyword. Note that the $choice variable is filled by the string corresponding to the choice, not the number the user typed. Note also that if the user types anything not corresponding to one of the choices, the $choice variable is NULL.
Most real world menus with error messages and the like include a case statement inside the select construct. This is also vital if the choices are not commands.
for i in *.sh; do |
The preceding code lists all files in the current directory ending in .sh, and assigns variable i to each one sequentially. Have you ever wondered how to do a wildcard rename in UNIX? You do it with a script like the preceding. Of course the script would have a mv statement instead of an echo.
This gets really handy when you need a multiple file "rename"
command. UNIX has no "rename" command, because any "rename" command at
best guess what you meant. The following is a one liner to rename files
starting with a yymmdd date format in the 90's, and prepending 19 onto
the front to make them accurate yyyymmdd:
$ for i in 9*.html; do mv $i 19$i; done |
QUIRK ALERT!
By default, if there are no matching files, the loop still executes once, with the wildcard string as $i, which is incredibly bizarre. You can stop such bizarre behavior by placing the following statement before all file finding loops: shopt -s nullglob |
Lists needn't be just filenames. Any space delimited sequence of
strings will do. The following makes a list of all the vowels, and
echos them:
for i in a e i o u; do |
Whole words can be used as a list and looped through, as shown in
the presidential lister that follows:
for i in Reagan Bush Clinton; do |
Any space delimited string can be used as a list. Not only that, the
list
can be modified by changing the space delimited string. The following
code
snippet places Carter, Reagan, Bush and Clinton in a list of
presidents, and then belatedly adds Ford to the front of the list. Then
a for statement loops through the presidents:
presidents="Carter Reagan Bush Clinton" |
The following is a recursive directory lister that loops through all
the
file objects in a directory, and calls other invocations of itself for
subdirectories.
The result is a list of all regular files in a tree:
directory=$1 |
Life's not always that simple. Sometimes there's no list to
conveniently loop through. In such a case you use a while
loop. The following while loop prints numbers from 1 to 4:
let repeats=4 |
Note that once again, the while loop operates on a commmand, not a condition. Therefore you must use the test executable to convert the condition into a return value.
Generally speaking, you use the let keyword when you want to make the variable a number rather than a string, and/or when you want to do arithmetic on the number.
There's also an until keyword that's identical to while
except that it reverses the sense of the condition. In other words, it
repeats UNTIL the command being tested returns 0. The following
produces
output identical to the preceding code snippet, but it uses until
instead of while:
let repeats=4 |
Shellscript loops do not yield good performance when looping numerous times. This is because each iteration requires a call to the test executable to test the condition. Additionally, many other actions must spawn programs, including echo. Replace tight loops with C, Perl or Python, and watch your performance skyrocket, possibly 100 fold or more. Use shellscripts as the glue, and do the heavy lifting with languages designed to do the work from within a single session. |
function fibonacci() |
In the preceding example, notice that the function is declared with the word function, the name, and a set of *empty* parentheses. Unlike C and the new Perl syntax, subroutine arguments are accessed as $1, $2... and the number of arguments as $#. The statements of the function are between braces. Because braces in shellscripts are space dependent, I believe the best syntax is to have each brace on its own line. Other syntaxes work -- you can experiment. Notice the keyword local. That declares variable newnumber as local to function fibonacci in scope. Otherwise it would be a global variable, even though it's declared inside the function. Global variables are the kiss of death for modularity.
Notice that the function uses its return statement to return a value. If you do not have a return statement, the return value is the return value of the last executable statement executed in the function. That being somewhat arbitrary, it's a good idea to explicitly specify a return value in any function whose return value is important.
Unlike C, Perl and the like, the return value cannot immediately be assigned to a variable. In other words, the following is a no-no:
temp=fibonacci $n1 $n2Instead the function must be run without assignment, and then the return value deduced on the next line from the $? special parameter. You can see how that's done in the main routine of the code snippet above. The inability to directly assign function returns is unfortunate, because if there were a way to make a direct assignment, the call to the subroutine could have been moved to the while loop test, and the entire routine would have been much simpler and more readable. If you run across a way to execute, test and assign a subroutine from within the test of a while loop, please let me know.
echo $mystring > myfile.txtYou should use that syntax for programs with trivial file output. Unfortunately, that syntax requires the opening and closing of the file for each write -- mucho inefficient if file output is the bottleneck of the process.
You can get better performance out of the echo statement by collecting large amounts of text in the variable (including newline characters), and then writing the huge text collection to the bottom of the file. Unfortunately, that takes lots of memory, probably coming off the stack.
Of course, you can simply echo to stdout (echo statement without
redirection), and then have the calling process redirect your script's
stdout to a file. That's a great alternative if practical. But
sometimes you just have to
byte the bullet and have your script write directly to a file. Here's
what
you do:
exec 44>test.txt |
The first exec statement opens a file on file descriptor 44. The echo statement in the loop writes to that continously open file descriptor. The exec at the end closes the file descriptor.
The problem is that often you need to use a variable for the file
descriptor number, and doing so totally messes up the preceding syntax
(trust me on this, I tried everything). So to use variables instead of
hard coded numbers and strings, you use the eval command as follows:
filename=test.txt |
Even this does not yield good performance, because every call to echo and every call to test must spawn a program, a very slow task. As a result, the same algorithm in C is over 100 times faster.
Shellscripts have filename type wildcards * and ?, which expand the same as in filenames. It is possible to use those as a "poor man's regular expression".
The following shows a code snippit to indent relative to arg1. The second statement creates the longest possible indent. The third line prints a substring of $indents*2 spaces, without printing a newline (the -n suppresses the newline).
indents=$1 |
The syntax ${stringvar:offset:length} is a "substring" type function
that
works very well. To prepend a string to a string variable, simply put
both
inside a single set of doublequotes as follows:
mystring=xyz |
However, if you append literal "abc" to the variable, the shell will
interpret
it as a variable called $mystringabc, clearly not what you
want.
So you place braces around the variable name (but not the dollar sign),
to isolate the variable name:
mystring=xyz |
You can get the length of a string variable with the ${#parameter}
syntax:
mystring="1234" |
A string variable can be subjected to search and replace with the
following syntax:
mystring="I say replace me before it's too late to replace me!" |
A couple facts about the preceding syntax. The ${parameter//pattern/replacement} syntax does not, in and of itself, change the parameter string. The reason $mystring changed is because I assigned the value of ${mystring//replace me/replaced} back to $mystring. It could have just as easily been assigned to something else, leaving $mystring untouched. Also, note that the preceding syntax replaces all instances of the pattern text. To replace just the first instance, use a single slash instead of the double slash.
Sometimes you need to walk through a string finding occurrences of a
word.
Check out this script, which uses the word "delimit" to delimit parts
of
the string:
mystring="ReagandelimitBush SeniordelimitClinton" |
If this script is called walkthrough.sh, here is the result:
[slitt@mydesk slitt]$ ./walkthrough.sh |
Here's a statement by statement description:
"*delimit" means any string followed by string "delimit". The fact that there is only a single pound sign (hash mark) means expand the search text to the shortest such possible string. If there had been two pound signs it would have expanded the search text to include everything up to the final "delimit" string, which is not what we want. The pound sign operator used in this way returns a substring, to variable $xstring, of the contents of $mystring *after* the end of the first "delimit" string. In other words, on the first loop through, it removes "Reagandelimit" and returns the rest. Because we will later be assigning $xstring to $mystring , we will iteratively walk through the string.
Notice that this basically returns *everything but* what we really want, which is the name at the front of the string. The next few statements take care of that.
So after the execution of this statement, $name contains the name we want with a trailing "delimit" string, which must be gotten rid of. Note, however, that if no more "delimit" strings exist, this construct returns an empty string (or NULL, or whatever) to the $name variable. That is how the loop is exited.
The preceding example is pretty ugly, and one excellent example why Perl and Python (and C and Java) are often chosen over shellscripts. Nevertheless, it goes to show that if you're willing to get down and dirty, shellscripts often have the power you need.
The next sample is the walkthrough.sh shellscript rewritten. It
directly grabs the first name on the list using %% to delete all before
the first delimit, thus eliminating the extra step to remove delimit
from the end
of the name. It also detects the last record directly by comparing the
length of $mystring to its length on the previous iteration, thereby
removing the need for the if statement.
mystring="ReagandelimitBush SeniordelimitClinton" |
#!/bin/bash |
#!/bin/bash |