Scripting rules

Scripts: commands in files

A program or command is executed by typing the name, with any inputs after it. A file with commands in it may be executed the same way, if it has execute permission (chmod u+x file or chmod 700 file.) These are called scripts. Lines in the file are run as if they were typed at the keyboard. There are a few extra commands that are only useful in scripts:
scriptsample runs
% cat abc
ls
# First line:
head -n1 abc
echo done   boss
% abc
aardvark.dat abc stuff.txt
ls
done boss

The echo UNIX command prints all of it's inputs to the screen, with a space between them. A # at the start of any line is a comment -- the line is ignored (but #! on the first line is special.) As usual with inputs, extra spaces are ignored. ""'s can save spaces (they really turn it all into one space-containing input):

scriptsample run
echo  a   b    c
echo "a   b    c"    d
% abc
a b c
a   b   c d

The second echo has two inputs: "a   b   c" and d.

To keep echo on the same line, use the -n option:

scriptsample run
% cat abc
echo -n echo   test
echo     is here
echo   again
% abc
echo test is here
again

Input/Parameters

Inputs are "numbered" $1, $2, $3, ... . $0 is the command name. $# is the number of parameters, and is made automatically when you run a script. Parameters not entered can be used, and are "" (no letters.)

scriptsample runs
echo "Running:" $0
echo $2 $4 $6 $8
echo "Total inputs: " $#
% xyz a b c d e f g h i j k l
Running: xyz
b d f h
Total inputs: 12

% xyz baby doggy
Running: xyz
doggy
Total inputs: 2

$@ is a short-cut for $1 $2 $3 ... . It is all of the inputs, with spaces between them. It is mostly useful to have a for loop (later) look at all inputs. The shift command moves all input parameters (not $0) "left." It removes $1, moves $2 to $1, and so on. It can also be useful in loops:

scriptsample runs
echo All: $@
shift
echo "New first:" $1
shift
echo "New first:" $1
echo "All:" $@
%xyz a b c d e f
All: a b c d e f
New first: b
New first: c
All: c d e f

Variables

You can make your own variables just by picking a name. Give it a value with = (there cannot be any spaces on either side of the =.) To look up the value, put a $ in front of the name:

scriptsample runs
a=grumpy
b="sleepy sneezy"
echo a X b
echo $a X $b
ls=dopey
echo ls X $ls
ls
$ls
a=X$1${a}Y
echo "-->" $a
%xyz happy
a X b
grumpy X sleepy sneezy
ls X dopey
xyz stuff.txt scripts.html
dopey: command not found
--> XhappygrumpyY

This is different from many computer languages, when it is obvious you are using a variable (examples: =, for a in, read a) leave out the $. When you want to look in a variable, use the $. It is the only way the script can tell the letter a from the contents of box a.

In the line a=X$1${a}Y, the {}'s are to let bash know you want the variable a. Without it, it would look up (the nonexistant) $aY. {}'s are optional when they are not needed (${1} is the same as $1.)

Interpreting variables and commands

Variables, like $abc or $1, are substituted before most other things happen. This can make new words, that are checked in the normal way. You can do some strange tricks with this, such as putting two things together to make a command, or a pathname:

scriptsample runs
% cat abc
$1$2
$1 $2/stuff.txt
% abc l s
aardvark.dat abc stuff.txt
l: command not found

%abc cat xxx
catxxx: no such command
cat: xxx/stuff.txt: file not found

Most special characters (like *) can be put in double-quotes to treat them as letters. But, variables or parameters (or anything with a $) are still looked up. To treat $ as a normal character, use single quotes (' next to ENTER, not the upper-right -- that is a back-quote):
scriptsample runs
echo "abc$1de$2f"
a=lazy
echo "a$a"    'a$a'
echo $a"te"
% abc tiger puma
abctigerdepumaf
alazy a$a
lazyte

Besides "start of line=run this command", the script can't tell the difference between the letter 5, the number 5 (seems silly, but the letter 5 plus the letter 0 is 50; sometimes we want that,) the command 5 (you can name a script 5) or the 5th input ($5), unless you tell it. You can force something not at the start of a line to be a command with $(word). This is completely different from the $ to look up a variable. It can be useful for assigning variables, and, later, is useful in loops:

scriptsample runs
a=$( echo $1 | cut -c1-2)
echo First two letters are $a
ls=xyz
echo ls
echo $ls
echo $(ls)
xyz=ls
echo $($xyz)
% abc panther
First two letters are pa
ls
xyz
aardvark.dat abc stuff.txt
aardvark.dat abc stuff.txt

To do math, use $((math)). Again, this use of $ has nothing to do with variables at all:

scriptsample runs
abc=10
echo 1+4 $1+4 $1+$abc
echo $((1+4)) $(($1+4)) $(($1+$abc))
echo $((1+1))+$((2+2))
echo $(1+1)
% abc 6
1+4 6+4 6+10
5 10 16
2+4
1+1 : command not found

Ifs and tests

It is possible to check parameters and variables to see if they are a certain word; or are less than, greater than, ... ; or check if they are filenames, directories, ... . This is done using an if with the test command (%info test for all of the rules):

scriptsample runs
if test $1 -lt 10
then
  echo $1 is less than 10
fi
if test -f $1
then
  echo $1 is in this dir
else
  echo $1 isnt a file!
fi
% abc 6
6 is less than 10
6 isnt a file!

% abc stuff.txt
line 3: not a math expression
stuff.txt is in this dir

then and fi are required. The else is optional. Indentation is just to make it look nice. Errors (if test stuff.txt -lt 10) will produce a message, but the script keeps running.

Brackets are a shorthand for the test command (but they need spaces around them.) if test $1 -eq 2 is the same thing as if [ $1 -eq 2 ].

Many people prefer to combine the if and then (using a semicolon.) Oddly, then may not have a semi colon after it: if test $1 -le 10; then echo "input is 10 or less"; fi.

An if may be nested inside another if, following the same rules. This checks whether both inputs are files:

scriptsample runs
# swap two files (if they exist):
if test -f $1
then
  if test -f $2
  then
    mv $1 tempfile.temp
    mv $2 $1
    mv tempfile.temp $2
  else
    echo "$2 is not a file. Quitting"
  fi
else
  echo "$1 isn't a file"
fi
%swap xhgj abc.dat
xhgj is not a file. Quitting

%swap abc.dat hkjs
hkjs isn't a file

%swap abc.dat def.dat

The middle if is indented, with its else and fi lined up, to hopefully make it a little easier to read.

An alternate would be to combine into one big if with nots and ors (these can get tricky) to say "if $1 or $2 isn't a file": if [ ! -f $1 -o ! -f $2 ].

An if does not need a test command after it. The if really just runs the next command and checks the "exit status." An exit status of 0 means true. For examples, test 5 -gt 8 has an exit status of 1 (which means false,) test -d "WWW" might have an exit status of 0 (there is a WWW dir,) cat a.dat has exit status 0 and ls notafile has exit status 1. The man pages describe the exit status's. In general, "normal" unix commands use: 0=no errors, not-0=type of error. Even after an if, they will also produce output (which you should redirect to /dev/null.) For example, if cat abc.dat &> dev/null is a (bad) way to check if abc.dat exists (recall &> is a short cut for > and 2> combined.)

test was specially written to produce a useful exit status for use with if. But, for example, grep fox a.dat prints lines with the word "fox" and returns 0 if there were any. This is a way to say "if file $1 has the word 'fox' in it": if grep fox "$1" > /dev/null

Loops

The most useful type of loop looks like:
for a in LIST OF STUFF
do
# thing to do goes here
  echo -n "X${a}X"
done
It expects a list, separated by spaces, after the for a in. a is a variable (probably one you haven't used before.) It assigns each item in the list to a and performs the lines between do and done for each. In this case, a will be LIST, then OF, then STUFF, and the output will be XLISTX XOFX XSTUFFX.

This is useful when you use a command to generate the list:

Misc problems

text file busy: Using cat > abc to write to abc works fine, but legally ends with ^D (the end-of-file character.) Typing ^Z suspends the command. This means it is still running, just not going anywhere. It can be restarted later. If you try to run abc the computer may think you are still writing to it (technically, you are) and tell you the text file is busy right now.

bash: abc not found: When a bash script is run, it first runs .bashrc. Our system guys added a line at the end of .bashrc, cd ~/, that fixes some problem somewhere else and makes bash not really work. They assumed everyone uses the older sh instead of bash.
What happens is you type % abc, the computer sees is a script (not a compiled program) and starts a new bash program to run it. It runs .bashrc, goes home, and then can't find abc. Comment out that line to fix this problem. For fun, add a line echo "Running, boss" to see it really does run .bashrc.

Typing % sh abc runs it under the old sh shell. Typing %bash abc runs it under bash. Typing just % abc runs it under your current shell (which is probably bash (%finger username to check.) A "real" shell always starts with a line like: #! /bin/sh which tells it which shell to use for running the script (yes, # is a comment, but #! as the first thing is special.)

cd in a script: In technical-speak, a script forks off a new process that gets a copy of the current state. That means it is like opening a new window that starts in the same directory as when you ran it. As usual, if you give a long path to run a script from a different directory, it still starts in the current dir. A script can use cd in the normal way, and will change the directory for the script, but as soon as the script ends, the imaginary window closes and you are back where you started, in the same dir.

Index of commands

scriptsample run
for a in 4 8 3 7 13 4
do
  echo -n $a"  "
  if test $a -ge 5
  then
    echo "big"
  else
    echo "small"
  fi
done
%forexample
4  small
8  big
3  small
7  big
13  big
4  small

The lines between do and done are run six times, with a equal to each number. -ge means >=. Indentation is just to make it easier to read.

scriptsample run
if [ $# -ne 1 -o ! -f $1 ]
then
  echo "Usage: $0 FILENAME"
  exit 1
fi

words=0
for a in $(cat $1)
do
#  echo "["$a"]"
words=$((1+$words))
done

echo "Num of words is" $words
%wordcount abc.dat
Num of words is 32

$(cat $1) simply "pastes" that file after for a in, which then looks at every word. Note the output of cat is automatically redirected (it does not go to the screen.) words is a standard counter, with one added for each word, in the awkward script manner. If you have a lot of math, use a more formal programming language.

The first part is a standard check: if there is not one input, or it is not a file (-o (letter O) is or, ! is not -- see %info test)