How do I write a shell script?
I was asked:
"I'm trying to write a shell script to [snip]. I kind of know what I want
to do, but don't know how the various programming type commands (ie
for...in...do and if...then) work. I'd be grateful for any hints/tips, or a
sample script using those commands..."
Below is an edited version of my rather verbose reply. There is of course much
more that could or should have been covered.
I probably should have referred to other guides on the web instead:
sh scripting
Shell scripts are very convenient for short tasks involving lots of files and little logic. However, programming anything complicated in shell can be slow and painful. Consider using Perl, Python (or Ruby?) instead - these let you manage collections of files too. Otherwise, here are some things to get your started.
In Unix you can make any file 'executable' using "chmod +x filename". If the first line of that file begins with #! then that line gives the name of the program that should be used to interpret the file.
To specify that you wish to use the standard Bourne Shell for your
script, put this on the first line.
#!/bin/sh
Then put each command that you would type at the command line on a new line or separated by semicolons.
Aside: other interpreters you may see are bash, csh, tcsh, ksh, zsh, perl, python, etc. You are basically guaranteed to have a /bin/sh executable on any Unix system you use (although it may just be a link to bash). Note that the existence or location of other shells may vary from system to system. Some people hate csh for scripts. I don't know how to use csh, but I don't see the point as every Unix system has sh and most GNU systems use the sh-compatible bash by default.
for loops
for loops go over a list of items separated by spaces:
for <var> in <list>
do <cmd> ; [<cmd>; [<cmd>; ...] ] done
for example:
iam23:~% for a in 1 2 5
for> do echo $a
for> echo $(($a*10))
for> done
1
10
2
20
5
50
You are looping using the variable a and it takes on values 1, 2, 5 in succession. You can give the contents of this variable to commands by using $a. As you can see, arithmetic is done by putting it inside $((...))
Note that the shell has very few abilities of its own. Most functionality is obtained by calling other programs. There is no print statement. Instead it is executing a program "echo" and passing it an argument on the command line. This can lead to gross inefficiency as many programs are loaded to do the simplest thing (which is why in some shells, like zsh which I use, echo is built into the shell). However, it does mean you can bolt together lots of programs easily.
If you write IMG*.jpg the shell interprets this as if you had typed all the files that match the expression. eg:
for a in IMG*.jpg ; do echo ${a%.jpg}
echo ${a#IMG} ; done
if you had files IMG_01.jpg IMG_02.jpg IMG_03.jpg this would result in:
IMG_01
IMG_02
IMG_03
_01.jpg
_02.jpg
_03.jpg
${a%.jpg} knocks the .jpg off the end of the variable's contents ${a#IMG} knocks the IMG off the beginning.
Note that IMG*.jpg does not match *.JPG as Unix is case sensitive. You could do: for a in *.jpg *.Jpg *.JPG ; do ... But that is a bit naff; you don't want to have to write out every combination of case you can imagine. I'm not sure of the best way of sorting out this issue. One way is this:
for a in `ls | grep -i \\.jpg$` ; do ...
The expression within the backtics (note that I used backtics "`" not normal quotes "'") is evaluated first:
- ls lists all the files in the current directory
- this output is sent to the command grep
- -i means case insensitive
- \.jpg$ is a regular expression that matches .jpg on the end of a line. An additional slash is required to escape the first one.
- see "man grep" for more info
- The output of grep is then placed between "in" and the semicolon,
replacing the backtic section. It is just as though you had typed:
for a in IMG_01.jpg IMG_02.jpg IMG_03.jpg ; do ...
if statements
if <cmd> ; then <cmd>
[<cmd>; [<cmd>; ...] ] [else <cmd>
[<cmd>; ...] ] fi
The return value of the first command determines whether the stuff after then is executed. If the return value is non-zero (indicating an 'error') then the stuff after else is executed (if else is used).
eg:
iam23:~% if test 1 -eq 1 ; then echo foo ; fi
foo
iam23:~% if [ 1 -gt 5 ] ; then echo foo
then> else echo bar; fi
bar
test is a command that is designed for use in shell scripts. Do "man test" for more info.
MAJOR ASIDE: The square bracket notation (equivalent to test) is neat - but I want to explain why it works as it will prevent you from making mistakes like using "[1 -gt 5]". In some shells the square bracket notation is built in. However, it doesn't have to be:
iam23:~% which [
[: shell built-in command
iam23:~% sh
$ which [
/bin/[
which is a command that tells you what is being run when you type a command. Here in zsh we see "[" is a built-in command. However, in sh "[" is actually a program (yes a file can be called "["). So we pass "[" the arguments "1", "-gt", "5" and "]" separated by spaces. Then it thinks: "aha! I want to know whether 1 is greater than 5 and I know that's the end of the question as I've seen ']'". The important thing to realise is that [1 -gt 5] will not work, as there is no program called "[1". We need to separate our program "[" and its arguments using spaces or confusion will result.
while loops
while <cmd>; do <cmd>; [<cmd>; [<cmd>; ...] done
eg:
iam23:~% b=0 ; while [ b -lt 5 ]
while> do echo _"$b"_
while> b=$(($b+1))
while> done
_0_
_1_
_2_
_3_
_4_
