KornShell (ksh)

Arguments, Lists & Loops

A Practical Introduction

12 Command Line Arguments

When you run a script you can pass values to it on the command line, just as you pass values to commands like ls or grep. These values are called arguments, and inside the script the shell makes them available automatically in a set of numbered variables.

12.1 $1, $2, $3 …

The first argument on the command line is stored in $1, the second in $2, the third in $3, and so on. You can use their value in the script exactly like any other variable.

Create a script called greet2 and make it executable with chmod +x greet2:

# greet2 – greet a user by name
echo “Hello $1”

Then run it with an argument:

./greet2 Alice
Hello Alice

You can use as many arguments as you need. Here is a script that takes a first name and a last name:

# greet3 – greet with full name
echo “Hello $1 $2”
./greet3 Alice Smith
Hello Alice Smith

If a user passes fewer arguments than the script expects, the missing variables are simply empty. If they pass more, the extras are ignored — unless you use them.

There’s a problem with arguments beyond nine. For example it can’t tell whether $10 is $1 followed by 0 or really the tenth argument. To avoid the ambiguity you should put the number in {}, ${10}, ${11} and so on. This is rarely needed in practice.

12.2 $0 — The Script Name

$0 is a special case. It contains the name of the script itself, exactly as it was typed on the command line. This is most useful for printing error messages, because it lets the message tell the user which script the error came from.

# checkfile – check that a file exists
if test -f $1
then
echo “$1 exists”
else
echo “$0: cannot find $1”
fi
./checkfile /etc/passwd
/etc/passwd exists

./checkfile /etc/nosuchfile
./checkfile: cannot find /etc/nosuchfile

When the file is not found, the error message includes the script name from $0. This is a common Unix convention and you will see it in the error messages from standard commands too.

13 Lists

A list in shell scripting is simply a sequence of values separated by whitespace. There is no special syntax for creating one. Any sequence of words is already a list. This is a fundamental idea in shell scripting, and once you understand it, a lot of other things fall into place.

13.1 The Arguments as a List: $@

All the arguments passed to a script can be referred to as a list using $@. You can echo them all in one go:

# showargs – display all arguments
echo “You passed: $@”
./showargs tom dick harry
You passed: tom dick harry

$@ expands to all the arguments, in order, as separate words. Think of it as shorthand for $1 $2 $3 … however many there are.

13.2 Counting the Arguments: $#

The shell also keeps a count of how many arguments were passed, in the variable $#. This is useful when your script needs a specific number of arguments and you want to check before proceeding:

# needsone – requires exactly one argument
if test $# -ne 1
then
echo “$0: expected one argument”
fi
Note: $# counts the arguments, not including $0. If the user runs ./needsone alice bob, $# is 2.

14 for Loops

A for loop processes a list of values one at a time. For each value in the list, the shell runs the body of the loop with a variable set to that value. When the list is exhausted, the loop ends.

14.1 A Simple Example

The clearest way to see how a for loop works is with a literal list of values:

for name in tom dick harry
do
echo “Hello $name”
done
Hello tom
Hello dick
Hello harry

The for loop repeats everything between do and done once for every item in the list. Each time around the loop the name variable is loaded with the next item in the list. Anything following “in” is considered a list, with each item separated from the next with whitespace.

In this example, name is called the “loop variable”. You can give it any name you like, and it helps if you pick something meaningful!

for day in Monday Tuesday Wednesday Thursday Friday
do
echo “$day is a working day”
done
Monday is a working day
Tuesday is a working day
Wednesday is a working day
Thursday is a working day
Friday is a working day

You can store a list in a variable, and this is very common.

fruit=”Apple Orange Banana Pear Tomato”
for myfruit in $fruit
do
echo “$myfruit is a type of fruit”
done
Note: When you specify the loop variable it doesn’t have a $ prefix but when you use it inside the loop it does. This is a common mistake to make.



14.2 Looping Over the Script’s Arguments

Rather than a literal list, you can loop over the arguments passed to the script by using $@ as the list. This is one of the most common uses of a for loop:

# checkfiles – check whether each named file exists

for filename in $@
do
if test -f $filename
then
echo “$filename exists”
else
echo “$filename not found”
fi
done
./checkfiles /etc/passwd /etc/group /etc/nosuchfile
/etc/passwd exists
/etc/group exists
/etc/nosuchfile not found

Each argument becomes the value of filename in turn. The if block inside the loop runs separately for each one.

15 Building Lists in Practice

Literal lists of values are useful for understanding how loops work, but in real scripts the list usually comes from somewhere: the output of a command, or the lines in a file.

15.1 A List from a Command

When you use command substitution as the list in a for loop, the shell runs the command and splits its output on whitespace to produce the list. Each word of output becomes one value.

A simple example — loop over the users currently logged in, as reported by who:

for user in $(who | cut -d’ ‘ -f1)
do
echo “$user is logged in”
done
alice is logged in
bob is logged in
root is logged in

The who command lists logged-in users, one per line. cut -d’ ‘ -f1 extracts just the username from each line. The shell splits the resulting list of names on whitespace and the loop processes them one by one.

Another common example — process every file in a directory:

for filename in $(ls /etc)
do
echo “Found: $filename”
done
Note: Using $(ls …) to build a list works well for straightforward cases, but breaks if filenames contain spaces. For serious scripts, there are safer approaches — but for learning, and for files with normal names, this is fine.

15.2 A List from a File

You can also build a list by reading the lines of a file. The standard way to do this in ksh is to redirect the file into a while read loop, but a simple approach that works well for straightforward cases is to use cat inside command substitution:

# process_servers – ping every server in a list file
for server in $(cat serverlist)
do
echo “Checking $server…”
ping -c 1 $server
done

Where the file serverlist might contain:

web1
web2
dbserver
backupserver

Each line of the file becomes one value in the list, and the loop body runs once for each server. This pattern — keeping a list of names in a plain text file and looping over it — is extremely common in Unix system administration scripts.

Tip: If the file has blank lines or comment lines beginning with #, they will be included in the loop. You can filter them out by piping through grep: $(grep -v ‘^#’ serverlist | grep -v ‘^$’)

15.3 Combining It All

Here is a more complete script that brings together arguments, loops, and a command-generated list. It takes a directory as an argument and reports the line count of every file in it:

# countlines – report line count for each file in a directory
#
if test $# -ne 1
then
echo “$0: usage: $0 directory”
fi

dir=$1
for filename in $(ls $dir)
do
lines=$(wc -l < $dir/$filename)
echo “$lines $dir/$filename”
done
./countlines /etc
3 /etc/group
47 /etc/passwd
12 /etc/hosts

16 Exercises

Work through the following exercises. Write each one as a script file, make it executable with chmod +x, and test it with a variety of inputs.

Section I — Arguments

Exercise 27: Write a script called hello that takes one argument and prints “Hello” followed by the argument. Run it as ./hello world and check the output. Hint: ./hello world → Hello world
Exercise 28: Write a script called full_name that takes a first name as $1 and a last name as $2 and prints them in the form: Last, First. Hint: echo “$2, $1”
Exercise 29: Write a script called fileinfo that takes a filename as $1. If the file exists, print its name and line count. If it does not exist, print an error message that includes $0 and $1. Hint: echo “$0: cannot find $1”

Section J — Lists and Loops

Exercise 30: Write a script called greetall that takes any number of names as arguments and prints “Hello” followed by each name in turn. Run it as: ./greetall alice bob carol Hint: for name in $@
Exercise 31: Write a script that uses a for loop with a literal list to print the numbers one through five, one per line. No arguments needed. Hint: for n in 1 2 3 4 5
Exercise 32: Write a script called checkusers that loops over the currently logged-in users (use who | cut -d’ ‘ -f1) and prints each username followed by the word ‘online’. Hint: for user in $(who | cut -d’ ‘ -f1)
Exercise 33: Create a plain text file called names containing three or four names, one per line. Write a script that reads the file using $(cat names) and prints a greeting for each name. Hint: for name in $(cat names)
Exercise 34: Write a script called argcount that checks whether it was called with exactly two arguments. If not, print an error message using $0 and $#. If so, echo both arguments back. Hint: if test $# -ne 2

End of exercises — good luck!

17 The Shebang Line

You might have noticed the first line of shell scripts often look something like this:

#!/bin/sh

This is known as a shebang, a sort of portmanteau as hash-bang. Why bang? That’s what some Americans call an exclamation mark, at least informally!!!

If the shell reads what it thinks is a program (because the X bit is set) it looks at the first two bytes, and if they’re a “#!” it knows that it’s some kind of interpreted language, and the remainder of the line is the language interpreter need to run the rest of it.

If you have been running the scripts in this chapter, they have been working because the shell that is running them is already ksh. But that is not something you can rely on. When someone else runs your script, or when it is run automatically by the system, it may be handed to a different shell entirely — and different shells have small but significant differences in how they handle things.

These differences are sometimes called bashisms (or kshisms, or any other shell name followed by -isms). A bashism is a feature or syntax that works in bash but is not part of the POSIX standard and will not work in other shells. The same idea applies in reverse: a script written for ksh may use syntax that bash or sh does not understand. If your script relies on a specific shell, you should say so explicitly.

#!/bin/ksh

All AIX systems support ksh as the default shell, so to make sure your script runs on every AIX machine, write it for ksh.

A complete script with a shebang now looks like this:

#!/bin/ksh
#
# checkfiles – check whether each named file exists
#
for filename in $@
do
if test -f $filename
then
echo “$filename exists”
else
echo “$0: $filename not found”
fi
done

The shebang line begins with #, which is also the start of comment character, so the shell also treats it as a comment.

You can also use #!/bin/sh on pretty much all scripts. This is the path to the standard Bourne Shell, and on systems with newer shells there is nearly always a Bourne-compatible shell in this location. If you run the bash shell when it’s been renamed plain sh will normally run in compatible mode.

Note: The path after #! must be the exact location of the interpreter. On AIX, ksh lives at /bin/ksh. On other systems it may be at /usr/bin/ksh. If you are unsure, run: which ksh

18 Exit Values

Every script, like every command, exits with a numeric value that tells the caller whether it succeeded or failed. You have already used this idea when writing if statements. The exit value of the condition command is exactly what “if” responds to. Your own scripts are no different.

18.1 The Default Exit Value

If you do not do anything special, your script exits with the exit value of the last command it ran. If that command succeeded, the script exits 0. If it failed, the script exits with whatever non-zero value that command returned.

#!/bin/ksh
# lastval – exit value comes from the last command
grep root /etc/passwd > /dev/null 2>&1
./lastval echo $?
0

Here grep finds a match, exits 0, and so the script exits 0. If grep had found nothing, it would have exited 1, and so would the script.

18.2 Setting the Exit Value with exit

You can set the exit value explicitly using the exit command, followed by the number you want to return. This is the right thing to do when your script has finished its work and you want to be clear about what it is reporting back:

#!/bin/ksh
# checkroot – exit 0 if root exists in passwd, 1 if not
if grep root /etc/passwd > /dev/null 2>&1
then
echo “root found”
exit 0
else
echo “root not found”
exit 1
fi

The convention is always the same: 0 means success, anything else means failure. The specific non-zero value you choose can carry meaning. Many commands use different non-zero values to indicate different kinds of failure, and you can check the man page to find out what they mean. For your own scripts, 1 is a reasonable general-purpose failure value.

18.3 Exiting Early

You can place exit anywhere in the script, not just at the end. When the shell reaches an exit statement it stops immediately, regardless of how many lines are left. This is useful for bailing out early when something goes wrong:

#!/bin/ksh
# processfile – bail out early if the argument is missing or wrong
#
if test $# -ne 1
then
echo “$0: usage: $0 filename”
exit 1
fi

if test ! -f $1
then
echo “$0: cannot read $1”
exit 1
fi

# If we get here, $1 exists and is a regular file
wc -l $1
exit 0
./processfile
./processfile: usage: ./processfile filename

./processfile /etc/nosuchfile
./processfile: cannot read /etc/nosuchfile

./processfile /etc/passwd
47 /etc/passwd

Checking for problems at the top of the script and exiting early is a good habit. It means that by the time you reach the main work of the script, you already know the inputs are valid. Each guard clause checks one thing, reports a clear error, and exits with a non-zero value so that anything calling your script can detect the failure.

Tip: test ! -f $1 means the opposite of test -f $1 — the ! negates the condition. It is true when the file does not exist.

18.4 Checking a Script’s Exit Value

When you call one script from another, or from the command line, you can check its exit value with $? just as you would for any other command:

./processfile /etc/passwd
if test $? -eq 0
then
echo “Script succeeded”
else
echo “Script failed”
fi

Or more directly, use the script itself as the condition of an if, since if is already checking the exit value:

if ./processfile /etc/passwd
then
echo “Script succeeded”
else
echo “Script failed”
fi

This is exactly the same mechanism you have been using with test and grep. Any command, including your own scripts, can be used as the condition of an if.

Leave a Reply

Your email address will not be published. Required fields are marked *