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.

KornShell (ksh)

Reading Input, Conditionals & Cutting Fields

5 Writing a Script File

So far all the examples have been single commands typed at the prompt. Once you have more than one or two lines, or want to run the same sequence of commands again, you need a script file. A script is simply a plain text file containing shell commands, one per line. The shell reads them in order executes each one in turn unless it encounters instructions to change the order, exactly as if you had typed them yourself.

5.1 Creating a Script with vi

Use vi to create the file. Give it a meaningful name — there is no need for a file extension. To create a script called greet, run:

vi greet

vi opens in command mode. Press i to enter insert mode, then type your commands. When you are finished, press Esc to return to command mode, then type :x and press Enter to write the file and quit.

5.2 Making the Script Executable

A newly created file has no execute permission. Use chmod +x to add it:

chmod +x greet

You only need to do this once. After that, you can run the script by typing its name preceded by ./ to tell the shell to look in the current directory:

./greet
Note: The ./ prefix is necessary because the current directory is not normally in the shell’s search path. Without it, the shell looks for greet in the standard system directories and will not find your script.

5.3 A First Script

Here is a simple script that asks for a name, works out the day of the week, and prints a greeting. It uses only commands covered in earlier sections.

Note that if the shell encounters a # character it ignores the remainder of the line. This allows you to enter comments. If you start a line with a # the whole line is ignored. And blank lines do nothing. Using comments and blank lines will make your script easier to understand.

Create the file with vi greet, type the following, then save and quit:

name=$LOGNAME
dow=$(date +%A)
echo "Hello $username, happy $dow"
Hello root, happy Tuesday

LOGNAME is a system variable that holds the login name of the user. We’re copying it to a local variable here, just so it looks neater.

The date command accepts a format string beginning with +. The %A specifier expands to the full name of the current day of the week. The result is captured into the variable dow using command substitution, then passed to echo along with the name the user entered.

Tip: To see what other format specifiers date understands, run: man date

7 Reading User Input

The read command reads one line from standard input and stores it in a named variable. On its own, read gives no prompt. It simply waits silently until you type something and press enter, so we’re using the printf command to output a prompt.

7.1 Prompting with printf

printf "prompt text: "
read varname

Unlinke echo, which always prints a newline after it’s output, printf keeps the cursor on the same line. This looks much neater for the user. You might see some shells that support echo -n to suppress the newline, but this is not consistent across all Unix systems. printf is part of the POSIX standard and behaves identically everywhere, making it the more portable and reliable choice. Most modern shells also support a -p parameter for read, which allows you to specify a prompt. Ksh on AIX doesn’t.

printf "Enter your name: "
read username
echo "Hello, $username"
Enter your name: Alice
Hello, Alice

Once stored, the variable behaves identically to one you assigned directly. You can pass it to commands, embed it in strings, use it in comparisons, or store further processing results into it. Here’s a more complex example:

printf "Enter a filename: "
read fname
linecount=$(wc -l < $fname)
echo "$fname has $linecount lines"
Enter a filename: /etc/passwd
/etc/passwd has 47 lines

Here the value the user types is passed to wc, and the result is stored in a second variable. The < redirection feeds the file to wc, which then counts the lines and prints the total. This is then assigned to linecount.

7.2 Reading Multiple Variables

You can name more than one variable on a single read command. The shell splits the input on whitespace and assigns each word to the corresponding variable. Any leftover words all go into the last variable named.

printf "First name, last name: "
read first last echo "$last, $first"
First name, last name: Alice Smith
Smith, Alice

8 Conditional Execution: if…then…else…fi

The if statement in the shell works differently from most programming languages. It does not evaluate a boolean expression as in most languages, but it runs a command and responds to whether that command succeeded or failed.

Every command in the shell exits with a numeric return code when it finishes. Generally, 0 means success, and any non-zero value means failure; but you should always check the man page. The if statement treats 0 as true and non-zero as false. This is the same convention used everywhere in Unix, so any command can be used directly as the condition of an if.

8.1 Structure

if command
then
# runs if command exited 0 (success / true)
else

# runs if command exited non-zero (failure / false)
fi
Note: The else branch is optional. A plain if…then…fi is valid when you only need to act on success. Also note that you can have multiple lines between then…else and fi.

The if statement must be terminated with a fi, which is if reversed. Reversing the statement that started the block is a common way to end a block in Unix scripts.

8.2 The test Command

Before combining test with if, it is worth understanding what test does on its own. test is a regular command that evaluates a condition and sets its exit code accordingly: it exits 0 if the condition is true, and 1 if the condition is false. When a command completes it sets the exit value to a special variable called $?. You can see what directly by running test and then examining $?

One thing test can do is compare two numbers. The form is to give it the first number, then a comparison operator, then the second number. It’d be great if you could write “test 4 > 3” but unfortunately it would interpret the > as the output redirection character, so instead options like -gt, -lt, -eq, and -ne are used for Greater Than, Less Than, EQual or Not Equal.

test 5 -gt 3
echo $?
0
test 2 -gt 10
echo $?
1

The exit code 0 means true (the condition held) and 1 means false (it did not). This is the opposite of most programming languages, but it is consistent with how every Unix command signals success or failure.

test can also compare strings and test whether files exist using other options. For example, -f followed by a filename will return true if the file exists.

test -f /etc/passwd
echo $?
0 (file exists — true)
test -f /etc/nosuchfile
echo $?
1 (file does not exist — false)

The test command supports very many things you might want to check, and you should see the man page for a full list. Here are some common ones.

OperatorMeaning
n1 -eq n2equal (numeric)
n1 -ne n2not equal (numeric)
n1 -lt n2less than
n1 -gt n2greater than
n1 -le n2less than or equal
n1 -ge n2greater than or equal
s1 = s2equal (string)
s1 != s2not equal (string)
-z s1string is empty
-n s1string is not empty
-f filenamepath exists and is a regular file
-d filenamepath exists and is a directory

8.3 Using test with if

Because if runs a command and checks whether it exits 0, and because test exits 0 when its condition is true, the two fit together naturally. You simply put the test command where if expects a command:

printf “Enter your age: “
read age
if test $age -ge 18
then
echo “Access granted.”
else
echo “Access denied.”
fi
Enter your age: 21
Access granted.

A practical example using a file check before processing:

printf "File to process: "
read datafile

if test -f $datafile
then
echo "Found $datafile - processing..."

wc -l $datafile
else

echo "File not found: $datafile"
fi
File to process: /etc/passwd
Found /etc/passwd - processing...
47 /etc/passwd

8.4 elif for Multiple Branches

Use elif to add further conditions before the final else:

printf "Enter a number: "
read num
if test $num -gt 0
then
echo "Positive"
elif test $num -lt 0
then
echo "Negative"
else
echo "Zero"
fi
Enter a number: -4 Negative

8.5 Using Any Command as the Condition

Because if simply runs a command and checks its exit code, any command works as the condition — not just test. A very common pattern is to use grep directly:

printf "Username to look up: "
read searchuser
if grep $searchuser /etc/passwd > /dev/null 2>&1
then
echo "User exists."
else
echo "No such user."
fi
Username to look up: alice
User exists.

grep exits 0 when it finds a match and non-zero when it does not. The if responds to that exit code. The output is redirected to /dev/null so grep doesn’t print anything on the screen. Only the exit code matters here.

Tip: Redirect both standard output and standard error with > /dev/null 2>&1 when running a command purely for its exit code and you do not want any output to appear.

9 The [ Alias for test

Some people prefer their if statements to have an expression in brackets, as they might be familiar with from other languages. You can do this in ksh because the test command has an alias: [. You can find it at /usr/bin/[ . And yes, [ is a valid Unix filename!

It works exactly the same as test, except it expects a closing ] as its final argument. The ] is not special syntax — it is simply a required parameter that [ checks for and then ignores. The following lines are equivalent:

if test $age -ge 18
if [ $age -ge 18 ]

Notice that there must be a space before the ] — it is a separate argument to [, not part of the syntax. Forgetting the space is a common mistake that produces a confusing error message.

Here is a complete example using [ ] in place of test:

printf “Enter your age: “
read age
if [ $age -ge 18 ]
then
echo "Access granted."
else
echo "Access denied."
fi
Enter your age: 21
Access granted.
Note: The choice of test or [ is purely a matter of style and readability. They are exactly the same command. You will see both in scripts you encounter, so it is worth being comfortable with either form.

10 Cutting Fields from Command Output

Many commands produce output in columns: ps, df, who, and ls -l are common examples. The cut command extracts specific fields from each line, either by character position or by a named delimiter character. The -d flag sets the delimiter and -f selects which field or fields to extract, numbered from 1.

10.1 Clean Delimiters: /etc/passwd

The cleanest approach is to work with output that already uses a consistent single character as a separator. The /etc/passwd file is a good example — fields are always separated by a single colon, with no padding:

# Extract username (field 1) and home directory (field 6)
cut -d: -f1,6 /etc/passwd
root:/
daemon:/etc
...
alice:/home/alice

This is extracting fields one and six from the password file – i.e. the username and their home directory. Note the output is also delimited with the delimiter character, in this case :, if you are extracting multiple fields.

Combining cut with grep and command substitution lets you pull a specific value into a variable:

homedir=$(grep "^${LOGNAME}:" /etc/passwd | cut -d: -f6)
echo "Your home directory is: $homedir"
Your home directory is: /home/alice

The grep anchors on the start of a line (^) followed by your login name and a colon, so it matches exactly one line. The cut then extracts field 6 from that line. No space normalisation is needed because the delimiter is always a single colon.

Tip: Prefer commands whose output uses a fixed single-character delimiter. Files like /etc/passwd, /etc/group, and most configuration files in /etc use colons or commas, and are the most reliable source material for field cutting.

10.2 Space-Separated Output and tr

Commands like ps, df, and who separate fields with spaces, but typically use multiple spaces for visual alignment. This is a problem on AIX because cut does not support the -w flag found on BSD systems, which treats any run of whitespace as a single delimiter. When you pass -d’ ‘ to cut on such output, every individual space counts as its own delimiter, making field numbers unpredictable.

The solution is to normalise the whitespace first using the tr command, which “transforms” data. In this case we’re going to use tr with the -s, which squeezes any run of repeated characters down to a single one:

tr -s ‘ ‘

For example:

ls | wc
           38    38    270

This is no good as you don’t know how many spaces there are before the digits.

ls | wc | tr -s ' '
38 38 270

Here is a practical example extracting available disk space in /var from df output. You can give df a mount-point as a parameter if you don’t want everything listed.

df /home
Filesystem   512-blocks      Free     %Used  Iused  %Iused   Mounted on 
/dev/hd1 65536 60512 8% 55 1% /home

As you can see, we have a header line we don’t want, and the free space is field three. And there are a random number of space between fields. We’ll need to clean this up to get our number.

# df output has variable spacing between columns
# tail -1 skips the header line
# tr -s ' ' squeezes multiple spaces to one
# cut -d' ' -f3 extracts the third field

avail=$(df /home | tail -1 | tr -s ' ' | cut -d' ' -f3)
echo "Available blocks on /home: $avail"
Available blocks on /home: 2457600

Putting it together in a script that warns when disk space is low:

avail=$(df /home | tail -1 | tr -s ' ' | cut -d' ' -f3)
if test $avail -lt 100000
then
echo "WARNING: Low disk space on /home ($avail blocks remaining)"
fi
Note: Some df output on some versions of AIX has a leading space on data lines, which creates an empty first field after splitting on spaces. If your field numbers seem off by one, adjust the field number accordingly.

10.3 Extracting Specific Fields from ps

Another common use is extracting the process ID from ps output. The -ef flags give a full listing in a consistent format:

printf “Process name to find: ”
read procname
pid=$(ps -ef | grep “$procname” | grep -v grep | tr -s ‘ ‘ | cut -d’ ‘ -f2)
if test -n “$pid”
then
echo “$procname is running as PID $pid”
else
echo "$procname is not running"
fi
Process name to find: cron
cron is running as PID 1234
Tip: The grep -v grep step removes the grep command itself from the results — without it, grep would find its own process in the ps listing and include it in the output.

11 Exercises

Work through the following exercises at a ksh prompt. Type the commands as shown, observe the output, then experiment with variations.

Section F — Reading Input

Exercise 16: Write a short script that asks the user for a directory name and then reports how many files are in it. Use printf for the prompt, read to collect the input, ls to list the directory, and wc -l to count. Hint: dircount=$(ls $dirname | wc -l)
Exercise 17: Ask the user to enter two words on one line (e.g. a first name and a last name). Use printf for the prompt, then read to capture both words into two separate variables. Echo them back in reverse order. Hint: printf “First and last name: “; read first last
Exercise 18: Ask the user for a filename. Use printf for the prompt and read to collect it. Check whether the file exists using test -f. If it does, report the number of lines it contains using wc -l. If it does not exist, print a suitable error message. Hint: if test -f $fname

Section G — Conditionals

Exercise 19: At the command prompt (not in a script), run a test command to check whether 42 is greater than 100. Then check $? to see the exit code. Repeat with 100 -gt 42 and confirm the exit code changes. Hint: test 42 -gt 100; echo $?
Exercise 20: Write a script that asks the user to enter a number. Use test and if…elif…else to print whether the number is positive, negative, or zero. Hint: test $num -gt 0
Exercise 21: Ask the user for a username. Use grep against /etc/passwd (suppressing output with > /dev/null 2>&1) as the condition of an if. Print a message saying whether the user exists. Hint: if grep “^$user:” /etc/passwd > /dev/null 2>&1
Exercise 22: Write a script that asks the user for a path. Use test -d to check whether it is a directory, test -f to check if it might be a plain file, and print an appropriate message for each case. Hint: if test -d $path … elif test -f $path

Section H — Cutting Fields

Exercise 23: Use cut with the colon delimiter to extract just the login shell (field 7) for every user in /etc/passwd. Pipe the result through sort -u to list the unique shells in use. Hint: cut -d: -f7 /etc/passwd | sort -u
Exercise 24: Use grep to find your own entry in /etc/passwd (use $LOGNAME), then cut out your username and your home directory and print them on one line. Hint: grep “^${LOGNAME}:” /etc/passwd | cut -d: -f1,6
Exercise 25: Run df on the root filesystem (df /). Use tail, tr -s ‘ ‘, and cut to extract the percentage used figure. Store it in a variable and print it. Hint: df / | tail -1 | tr -s ‘ ‘ | cut -d’ ‘ -f5
Exercise 26: Use ps -ef to list all processes. Pipe through grep to find processes owned by root, then use tr -s ‘ ‘ and cut to extract just the process IDs (field 2). Pipe the result through head -5 to show the first five. Hint: ps -ef | grep ‘^root’ | tr -s ‘ ‘ | cut -d’ ‘ -f2 | head -5

End of exercises — good luck!

KornShell (ksh) Variables Tutorial

Variables & Macro Expansion

1 Variable Assignment

In ksh, a variable stores a value that can be reused later in commands or scripts. Assigning a value to a variable is straightforward: write the variable name, immediately followed by an equals sign and the value — with no spaces around the = sign.

Syntax

name=value

Some practical examples:

greeting=Hello
count=42
filename=report.txt
dir=/home/user/documents
Important: There must be no spaces on either side of the = sign. Writing name = value is a syntax error because ksh will interpret name as a command name.

Variable names may contain letters, digits, and underscores, and must begin with a letter or underscore. By convention, environment variables (those used by the system) are in UPPER_CASE, and variables used in scripts use lower_case, though ksh does not enforce this.

2 Macro Expansion

Macro expansion (also called parameter expansion or variable substitution) is the process by which ksh replaces a variable reference or expression with its current value before executing a command. The echo command is ideal for exploring expansion because it simply prints whatever arguments it receives. You may be familiar with echo from MS-DOS and Windows, where variables are bracketed with a % character. But Unix shell variables are a more mature system.

2.1 The $ Prefix for Variable Names

Prefix a variable name with $ to expand it. The shell sees a $ and knows that what follows is something it needs to substitute. It’s not just used for variables, but this is a common use case.

Basically the shell goes through a line it’s about to run and “fixes up” anything prefixed with a $, and certain other characters, before it runs it.

Command
city=Swansea
echo $city
Output
Swansea

You can embed variable expansions anywhere in a string or command line:

Command
user=Alice
echo “Hello, $user!”
Output

Hello, Alice!

If your variable expansion runs on into more alphanumeric characters you will need to wrap it in a { and } to avoid ambiguity. Otherwise the { and } are optional.

Command
ext=jpg
echo “photo.${ext}”

dir=/var/tmp
echo $dir123
echo ${dir}123
Output

photo.jpg



Problem! No output /var/tmp123
Tip: Using ${varname} to avoid ambiguity, but omitting it can sometimes improve readability

2.2 Glob Expansion

Glob expansion (also called pathname expansion or filename generation) lets you use wildcard characters in a command to match multiple file or directory names. ksh performs the expansion before passing the resulting list to the command.

Common wildcard characters

* matches any sequence of characters (including none) ? matches exactly one character [abc] matches any one of the listed characters [a-z] matches any character in the given range

Examples using echo to reveal what the shell expands:

Command
echo /etc/*.conf
Output
/etc/host.conf /etc/nsswitch.conf /etc/resolv.conf …
Command
echo file?.txt
Output
file1.txt file2.txt file9.txt
Command
echo report_[0-9][0-9].csv
Output
report_01.csv report_12.csv report_99.csv
Note: If no files match a glob pattern, ksh leaves the pattern unchanged and passes it literally to the command. This is a common source of confusion when a path does not yet exist.

2.3 Command Substitution: $() Syntax

Command substitution lets you capture the output of a command and use it as a value — either storing it in a variable or embedding it directly in another command.

result=$(command)

The shell runs command in a subshell, collects its standard output (with trailing newlines stripped), and substitutes the result in place of the $(…) expression. Note that the date command prints the current time and date, and the format of it’s output can be modified using a template prefixed with +. We’ll cover this later. The wc -l command simply counts the number of lines in the input.

Command
today=$(date +%Y-%m-%d)
echo $today
Output
2024-03-15
Command
echo “You have $(ls | wc -l) files here.”
Output
You have 23 files here.

Backtick alternative

An older syntax uses backtick characters ( ` ) instead of $(…). This is the character on the keyboard found on most keyboards to the left of the digit 1.It’s equivalent in basic use:

result=`command`

# These two lines produce identical results:
today=$(date +%Y-%m-%d)
today=`date +%Y-%m-%d`

The $() form is strongly preferred for several reasons:

  • It can be nested cleanly: $(outer $(inner))
  • Backticks require backslash-escaping inside nested calls, which quickly becomes unreadable.
  • $() syntax is visually clear and consistent with other shell constructs.
  • It’s easy to miss a back-tick or confuse it with an apostrophe.
Tip: Use $() in all new scripts. Backticks are found in older scripts and are worth recognising, but should not be written in new code, but are less typing if you’re running a one-off command.

3 Quoting to Prevent Expansion

Sometimes you want ksh to treat special characters — $ * ? and others — literally rather than expanding them. Quoting provides three ways to do this, each with different scope and purpose.

3.1 Backslash ( \ )

A backslash immediately before a character escapes that single character — the shell treats the following character literally instead of interpreting it.

Command
price=5
echo The cost is \$price
Output
The cost is $price
Command
echo show \* without glob
Output
show * without glob

Backslash escape is precise: it affects only the one character that immediately follows it.

3.2 Single Quotes ( ‘ )

Enclosing text in single quotes prevents all expansion within those quotes. Every character is taken literally — the shell performs no substitution, no glob expansion, and no command substitution.

Command
name=World
echo ‘Hello $name’
Output
Hello $name
Command
echo ‘*.txt files are $important’
Output
*.txt files are $important
Note: You cannot include a single-quote character inside a single-quoted string. To use a literal single quote, end the string, escape the quote with a backslash, and reopen the string: echo ‘it’\”s here’

3.3 Double Quotes ( ” )

Double quotes suppress glob expansion and word splitting, but still allow variable expansion ($var and ${var}) and command substitution ($(…)). They are the most commonly used form of quoting in scripts.

Command
name=Alice
echo “Hello, $name”
Output
Hello, Alice
Command
echo “Today is $(date +%A)”
Output
Today is Wednesday
Command
echo “No glob: *.txt”
Output
No glob: *.txt

Quick comparison

Quoting$var expansion$() substitutionGlob expansion
\backslashYes (next char only)Yes (next char only)Yes (next char only)
‘single quotes’NoNoNo
“double quotes”YesYesNo
(no quoting)YesYesYes

4 Using a Variable in a Command

A practical and very common use of variable expansion is building up path names or directory fragments that are then passed to commands such as ls.

Partial directory name in ls

Suppose you are frequently working inside a versioned project directory and do not want to retype the path each time. Store part of the path in a variable and let the shell expand it for you:

# Store a partial path
project=/home/alice/projects/webapp

# Use it with ls to list a subdirectory
ls ${project}/src

# Combine with a glob to find only Python files
ls ${project}/src/*.py

# Use in a longer path with another variable
subdir=tests
ls ${project}/${subdir}

Notice that braces are used around the variable name (i.e. ${project}) to avoid unexpected problems if there was something funny in $project.

You can also store just a partial directory segment — a prefix — and rely on glob expansion to complete it:

# List all log directories for a given year
year=2024
ls /var/log/app/${year}-*

# List any directory whose name starts with ‘web’
prefix=web
ls /srv/${prefix}*/html
Command
prefix=web
ls /srv/${prefix}*/html
Output
/srv/webapp/html /srv/webapi/html /srv/webstatic/html
Tip: Always quote the expanded variable in double quotes if the path might contain spaces: ls “${project}/My Documents”

5 Exercises

Work through the following exercises at a ksh prompt. Type the commands exactly as shown, observe the output, then experiment with variations to deepen your understanding.

Section A — Variable Assignment and $ Expansion

Exercise 1: Assign your first name to a variable called firstname and your surname to lastname. Use echo to print both on a single line in the form Firstname Lastname. Hint: Two separate echo arguments, or embed both variables in one double-quoted string.
Exercise 2: Create a variable ext with the value txt. Then use echo with brace syntax (${}) to print the string myfile.txt without writing .txt literally in the echo command. Hint: echo “myfile.${ext}”
Exercise 3: Assign any number to a variable n. Use echo to print the line: The value of n is: 42 (substituting your number). Then change n to a different number and run the same echo command again without retyping it — use the Up arrow to recall it. Hint: Re-run with !! or the Up arrow after changing n.

Section B — Glob Expansion

Exercise 4: Run echo /etc/p* and observe which files and directories are matched. Then try echo /etc/p*.conf — how does the output differ? Hint: The * wildcard matches any sequence of characters.
Exercise 5: In your home directory, create three files named note1.txt, note2.txt, and notex.txt using touch. Then use echo note?.txt and explain what the ? wildcard matched compared to echo note*.txt. Hint: touch note1.txt note2.txt notex.txt — then compare the two echo outputs.
Exercise 6: Use echo with a character-class glob to list only files in /etc whose names begin with a vowel (a, e, i, o, or u). Hint: echo /etc/[aeiou]*

Section C — Command Substitution

Exercise 7: Capture the current working directory into a variable called here using $() and the pwd command. Then echo the sentence: You are in: /your/path Hint: here=$(pwd)
Exercise 8: Store the number of lines in /etc/passwd into a variable usercount using $() and wc -l. Print a sentence such as: There are 42 users in /etc/passwd. Hint: usercount=$(wc -l < /etc/passwd)
Exercise 9: Rewrite your answer to exercise 7 using backtick syntax instead of $(). Confirm the output is identical, then consider: which form do you find easier to read? Hint: here=`pwd`

Section D — Quoting

Exercise 10: Assign the string don’t stop to a variable msg. Then echo $msg and verify the apostrophe is preserved. (Hint: use double quotes for the assignment: msg=”don’t stop”) Hint: Double quotes allow the apostrophe inside the string.
Exercise 11: Run these three commands and explain why each produces different output: name=World echo Hello $name echo ‘Hello $name’ echo “Hello $name” Hint: Unquoted and double-quoted expand $name; single-quoted does not.
Exercise 12: Use a backslash to print the literal string Cost: $10.00 without ksh expanding $10 as a variable. Try both the backslash approach and the single-quote approach and confirm they give the same result. Hint: echo Cost: \$10.00 vs echo ‘Cost: $10.00’

Section E — Variables in Commands

Exercise 13: Set a variable logdir to /var/log. Use it in an ls command to list all files in that directory ending in .log. Use brace syntax for the variable. Hint: ls ${logdir}/*.log
Exercise 14: Store a partial directory name — such as sys — in a variable called fragment. Then use ls with a glob combining ${fragment} and * to list any matching entries under /var/log. Hint: fragment=sys; ls /var/log/${fragment}*
Exercise 15: Create variables for the first part of a filename and its extension. Use echo (not ls) to demonstrate that combining them with ${} produces the expected filename string — for example report_2024.pdf. Hint: base=report_2024; ext=pdf; echo “${base}.${ext}”

Using the grep Command on AIX

What grep Does

The grep command searches one or more files (or the output of another command) for lines that match a pattern, and prints each matching line to standard output. If no file is specified, grep reads from standard input.

The name stands for globally search for a regular expression and print — a reference to the equivalent operation in the ed line editor.

The most common use is simple: give grep a word or phrase and it returns every line that contains it.

grep error /var/log/syslog

grep ‘disk full’ /var/adm/messages

When more than one file is given, grep prefixes each matching line with the filename so you know where the match came from.

Basic Syntax

grep [flags] pattern [file …]

The pattern is a regular expression (see the section below). Special shell characters such as $ * [ | ^ ( ) \ must be quoted to prevent the shell interpreting them before grep sees them. As a general rule, put the pattern in single quotes:

grep ‘out of memory’ /var/adm/messages

Essential Flags

-v: Invert the match

-v prints every line that does not match the pattern. This is useful for filtering out noise — comment lines, blank lines, or entries you already know about.

grep -v ‘^#’ /etc/services # skip comment lines

grep -v ‘^$’ report.txt # skip blank lines

grep -v DEBUG /var/log/app.log # hide debug entries

-i: Ignore case

-i makes the match case-insensitive. Error, ERROR, and error all match the pattern error when -i is given.

grep -i ‘failed’ /var/adm/messages

grep -i ‘root’ /etc/passwd

-c: Count matching lines

-c suppresses normal output and instead prints a count of how many lines matched. When multiple files are given, each filename is listed with its count.

grep -c ‘error’ /var/adm/messages

grep -c ‘FAILED’ /var/log/auth.log /var/log/syslog

This is often more useful than counting lines yourself with wc -l, because it counts lines, not occurrences — a line with two matches is still counted once.

-l: List filenames only

-l suppresses normal output and prints only the names of files that contain at least one match, one per line. This is ideal for finding which files in a tree contain a particular string without being swamped by the matching lines themselves.

grep -rl ‘PermitRootLogin’ /etc # find all config files mentioning this

grep -l ‘TODO’ *.c # find source files with TODO comments

Note: -l stops searching a file as soon as the first match is found, so it is faster than a normal grep over large files.

-n: Show line numbers

-n prefixes each matching line with its line number within the file. Useful when you need to go back and edit the file at the right location.

grep -n ‘MaxInstances’ /etc/ftpd.conf

-r + -R: Recursive search

-r searches all files under a directory, descending into subdirectories. -R does the same but also follows symbolic links to directories.

grep -r ‘connection refused’ /var/log

grep -rl ‘passwd’ /etc

-w: Whole word match

-w restricts matches to complete words. The pattern must be surrounded by non-word characters (spaces, punctuation, start/end of line). This prevents log matching login or syslog.

grep -w ‘log’ /var/adm/messages

-s: Suppress error messages

-s silences the errors grep would normally produce for files that don’t exist or can’t be read. This is useful in scripts where missing files are expected.

-h: Suppress filenames

When multiple files are given, grep normally prefixes each line with the filename. -h suppresses that prefix, giving clean output suitable for piping.

grep -h ‘error’ /var/log/*.log | sort | uniq -c

Context Lines: -A, -B, and -C

The -A, -B, and -C flags are found in BSD, GNU and other systems but not AIX. They are documented here because they are widely used systems and in cross-platform scripts. If you need this behaviour on AIX, the workaround is shown below.

On GNU/Linux systems, three flags control how many surrounding lines are shown alongside each match:

FlagEffect
-A nShow n lines After each match
-B nShow n lines Before each match
-C nShow n lines of Context (before and after)

These are invaluable when reading log files, because a single matching line rarely tells the whole story — you need to see what led up to an error and what followed it.

# Linux only — not available on AIX grep

grep -A 3 ‘FAILED LOGIN’ /var/log/auth.log

grep -B 2 -A 5 ‘panic’ /var/log/syslog

grep -C 4 ‘out of memory’ /var/adm/messages

AIX workaround

On AIX, the -p flag prints the whole paragraph (text block separated by blank lines) that contains the match, which sometimes gives enough context. For precise line-count context, pipe through awk instead:

# Print 3 lines after each match using awk

grep -n ‘panic’ /var/adm/messages | awk -F: ‘

{ n=$1; for(i=n; i<=n+3; i++) print i }’ | xargs -I{} sed -n ‘{}p’ /var/adm/messages

Regular Expression Matching

By default, grep treats its pattern as a Basic Regular Expression (BRE). You do not need to use regular expressions — a plain word or phrase works fine. But knowing the basics lets you write much more powerful searches.

Anchors

Anchors match a position in the line rather than a character.

PatternMeaning
^wordLine begins with word
word$Line ends with word
^word$Line contains exactly word and nothing else
^$Empty line

grep ‘^root’ /etc/passwd # lines starting with root

grep ‘sh$’ /etc/passwd # lines ending with sh

grep ‘^$’ report.txt # blank lines

The Dot: any character

. (dot) matches any single character except a newline.

grep ‘f.o’ file # matches foo, fao, f1o, f o, etc.

Character Classes

[…] matches any one of the characters listed inside the brackets.

PatternMeaning
[aeiou]Any vowel
[a-z]Any lowercase letter
[A-Z]Any uppercase letter
[0-9]Any digit
[^0-9]Any character that is not a digit (^ inside brackets negates)
[a-zA-Z]Any letter (upper or lower)

grep ‘^[a-zA-Z]’ pgm.s # lines beginning with a letter

grep ‘[0-9]’ report.txt # lines containing any digit

Repetition

SymbolMeaning
*Zero or more of the preceding character or group
\+One or more (BRE syntax — backslash required)
\?Zero or one (optional)

grep ‘err*or’ file # matches eror, error, errror …

grep ‘colou\?r’ file # matches color or colour

Escaping Special Characters

To search for a literal character that grep would otherwise treat as special (such as . or $), precede it with a backslash. Because the shell also interprets backslashes, you may need to double them or use single quotes:

grep ‘\$HOME’ script.sh # literal dollar sign

grep ‘192\.168’ hosts # literal dots in an IP address

Filtering Command Output

grep is very useful for filtering the output of a another command to find the lines you want.

Checking running processes

ps -ef | grep httpd # is the web server running?

ps -ef | grep -v grep # exclude the grep process itself

ps -ef | grep -i java # case-insensitive process search

The grep -v grep trick is necessary because ps -ef | grep httpd will always match its own grep process as well as the real target.

Checking network connections

netstat -an | grep LISTEN # show all listening ports

netstat -an | grep ‘:80 ‘ # is anything on port 80?

netstat -an | grep ESTABLISHED # active connections

Filtering filesystem and disk output

df -g | grep -v ‘Filesystem’ # disk usage, skip header line

lsdev -Cc disk | grep -i hdisk # list disk devices

mount | grep ‘/data’ # is /data mounted?

Inspecting installed filesets

lslpp -l | grep -i openssh # is openssh installed?

lslpp -l | grep COMMITTED # all committed filesets

User and group queries

grep ‘^frank’ /etc/passwd # frank’s passwd entry

grep -i ‘frank’ /etc/group # groups frank belongs to

who | grep frank # is frank logged in?

Searching Log Files

Log files on AIX are typically found under /var/adm/ and /var/log/ . At least on AIX systems that don’t store them in the ODM. The principles below apply equally to application logs wherever they are located, or indeed if you dump them from the ODM using the errpt command.

Finding errors and warnings

grep -i ‘error’ /var/adm/messages

grep -i ‘warn\|error\|fail’ /var/adm/messages

grep -c ‘error’ /var/adm/messages # how many error lines today?

Searching for a specific date or time

AIX syslog entries begin with a timestamp. You can anchor a search on the month and day:

grep ‘^Jun 8’ /var/adm/messages # all entries for 8 June

grep ‘^Jun 8 14:’ /var/adm/messages # entries for 14:xx on 8 June

Tracking a specific process or daemon

grep ‘sshd’ /var/adm/messages

grep ‘cron\[‘ /var/adm/messages # cron log entries (bracket is literal)

grep -i ‘lvmmon’ /var/adm/messages # LVM monitor messages

Finding login failures

grep -i ‘failed\|invalid\|illegal’ /var/adm/messages

grep ‘authentication failure’ /var/log/auth.log

Combining grep with other tools

Pipelines let you refine searches progressively or reformat the output:

# count errors per hour

grep ‘error’ /var/adm/messages | awk ‘{print $3}’ | cut -c1-2 | sort | uniq -c

# unique error messages, sorted by frequency

grep -i ‘error’ /var/adm/messages | sort | uniq -c | sort -rn | head -20

# errors from the last 100 lines of a log

tail -100 /var/adm/messages | grep -i error

# watch a log live and highlight errors

tail -f /var/adm/messages | grep –line-buffered -i error

Exit Status

grep’s exit status is useful in scripts for testing whether something exists:

Exit codeMeaning
0At least one matching line was found
1No matching lines were found
>1A syntax error occurred, or a file was inaccessible

# Test whether a user account exists

if grep -q ‘^frank:’ /etc/passwd; then

echo ‘Account exists’

fi

# -q suppresses all output — use purely for the exit code

grep -qs ‘error’ /var/adm/messages && echo ‘Errors found’

Quick Reference

grep [flags] pattern [file …]

Key flags:

-v Invert: print non-matching lines

-i Case-insensitive matching

-c Print count of matching lines

-l Print filenames only (not matching lines)

-n Prefix lines with line number

-r / -R Recursive (R also follows symlinks)

-w Whole-word match

-h Suppress filename prefix

-s Suppress file-not-found errors

-q Quiet: no output, use exit code only

-p Print whole paragraph containing match

-A n [BSD/GNU Only] Print n lines After match

-B n [BSD/GNU] Print n lines Before match

-C n [BSD/GNU] Print n lines Context around match

Pattern anchors:

^ Start of line

$ End of line

. Any single character

[abc] Any of a, b, or c

[^abc] Any character except a, b, or c

[a-z] Range of characters

* Zero or more of preceding

\+ One or more of preceding (BRE)

\? Zero or one of preceding (BRE)

\ Escape next character

Using the find Command on AIX

What find Does

The find command walks a directory tree and, for each file it encounters, evaluates a series of expressions you supply. Each expression returns either true or false for that file. By default, find prints the path of every file for which the overall result is true.

An expression takes the form -something, usually followed by an argument. The expression is the test, the argument is what you’re testing for.

When you write several expressions one after another, find ANDs them together. A file must satisfy all of them to be selected.

find /data -type f -name “*.log”

This finds only plain files (-type f is true) and whose name ends in .log (-name “*.log” is true). If either test fails, the file is skipped.

Note that there are quotes around *.log. If there weren’t the shell would expand the *.log into a list of files, which would confuse the find utility, which expecting exactly one file specification. You could also quote the * with a backslash (\*) if you want to save a keystroke.

Combining Expressions

AND (default)

Placing two expressions in a row implies AND. Both must be true. You can actually have as many as you want and they’ll all be ANDed together. Although AND is implied, you can also specify it with -a if you think it improves readability. Newer versions of find support -and too.

find /home -user frank -size +10M

Finds files owned by frank and larger than 10 MB.

OR — -o

Use -o between expressions when either condition should match.

find /var -name “*.log” -o -name “*.tmp”

Finds files ending in .log or .tmp.

When mixing AND and OR, AND binds more tightly, just like multiplication before addition in arithmetic. Newer versions of find allow -or as well as -o.

Use parentheses (escaped from the shell) to make grouping explicit:

find /var \( -name “*.log” -o -name “*.tmp” \) -mtime +7

This finds .log or .tmp files that are also older than 7 days.

NOT — !

Place ! (or -not) before an expression to negate it. -not is sometimes more readable.

find /data -type f ! -name “*.bak”

Finds plain files whose name does not end in .bak.

Choosing the Starting Point

The first argument to find is always the directory to search from. find descends into sub-directories automatically. You can actually specify as many starting points as you like. They don’t look like expressions because they don’t begin with a -.

find / # searches everything (use with caution)

find . # searches from the current directory

find /var /home # searches both /var and -home

Common Expressions

-name and -iname

-name matches the filename (not the full path) against a shell-style pattern. The pattern must be quoted to stop the shell expanding it.

find /etc -name “*.conf”

-iname matches the filename but is case insensitive.

find /home -iname “readme*”

This matches README.txt, Readme.md, readme, and so on.

-type

Filters by file type. The most useful values on AIX are:

ValueMeaning
fNormal file
dDirectory
lSymbolic link
bBlock device
cCharacter device
pNamed pipe (FIFO)

For example:

find /dev -type b

Find all block devices in /dev

Beware of not specifying a type, as you can get unexpected results with exec if you pass a directory to a utility when you thought it was getting just one file.

-mtime and -ctime

Both take a number of days.

-mtime n — the file’s modification time (data last written) is exactly n days ago.

-ctime n — the file’s change time (inode last changed — includes permission or ownership changes) is exactly n days ago. In other words, this is changes to the file’s metadata, not changes to its contents.

When using mtime or ctime you almost always use a + or – prefix:

PrefixMeaning
+nMore than n days ago
-nLess than n days ago (i.e. within the last n days)
nExactly n days ago (not often very useful)

find /logs -mtime +30 # not changed for over 30 days

find /tmp -ctime -1 # inode changed within the last day

Some versions of Unix, such as BSD, allow units other than days, as a suffix, such as s for seconds and h for hours. AIX may do so in the future, but not as of version 7.3.

-size

Matches on file size in blocks, and a block is 512 bytes.. As with the time expressions, other Unix systems allow a suffix, such as k for kilobytes, g for gigabytes, but as of AIX 7.3 this is not supported.

Again, + and – prefixes mean greater-than and less-than.

find /data -size +100 # files larger than 50K

find /tmp -size -2 # files smaller than 1K

-user and -group

Matches files owned by a specific user or group. You can use either the name or the numeric UID/GID.

find /home -user frank

find /projects -group dba

-perm

Matches on permission bits. The mode can be octal or symbolic.

find /bin -perm 4000 # files with the setuid bit set

find /tmp -perm 777 # files with exactly rwxrwxrwx

find /etc -perm 022 # files writable by group or other

Check the man page on the specific version of AIX you are using for symbolic representation as it has changed over time. It’s safer to stick with octal.

Finding Files with No Valid Owner: -nouser and -nogroup

find / -nouser

find / -nogroup

-nouser is true for any file whose numeric UID has no matching entry in /etc/passwd. -nogroup does the same against /etc/group.

These are useful after accounts are deleted — the files are left behind but owned by a UID that no longer maps to a name. On an AIX system using LDAP or NIS, the lookup is still done against the locally visible name database, so a file owned by a valid LDAP user will not be flagged unless that user is absent from the local resolution path.

What to Do With the Files Found

By default find simply prints each matching path. Several expressions change this behaviour.

-print

The default action — prints the full path, one per line. You rarely need to write it explicitly.

find /home -name “core” -print

-ls

Produces output in the format of ls -dils for each matched file: inode number, size in blocks, permissions, link count, owner, group, size in bytes, modification time, and name. Much more informative than -print for diagnostic work.

find /var/log -size +50M -ls

-exec

Runs an arbitrary command for each matched file. The string {} is replaced by the file’s path (this is the only time I’m aware of {} being used as a placeholder in Unix). The command must be terminated by \;. In fact, find is looking to end -exec with a semicolon, but this character has a special meaning to the shell so it’s necessary to use a \ or quotes to allow find to see it.

find /tmp -mtime +7 -exec rm {} \;

Deletes every file in /tmp not touched for more than 7 days.

find /home -name “*.log” -exec gzip {} \;

Compresses each .log file found using gzip.

The {} placeholder can appear more than once in the command if needed.

Running a command on all results at once with +

Replacing \; with + causes find to batch the paths and pass as many as possible to a single invocation of the command — much more efficient for commands like rm or chmod:

find /tmp -mtime +7 -exec rm {} +

Practical Examples

List all files larger than 100 MB modified within the last 30 days

find / -type f -size +200000 -mtime -30 -ls

This breaks down as:

  • -type f — regular files only (skip directories, devices, and links)
  • -size + 200000 — larger than 100 MB (e.g. 200,000 512-byte blocks)
  • -mtime -30 — data modified within the last 30 days
  • -ls — show full details rather than just the path

Move those files to /problems

find / -type f -size +100M -mtime -30 -exec mv {} /problems/ \;

Find and remove core dumps older than 14 days

find / -name “core” -type f -mtime +14 -exec rm {} \;

Find setuid files not owned by root

find / -type f -perm -4000 ! -user root -ls

Find files owned by deleted accounts

find /home -nouser -ls

Summary of Expression Syntax

find <path> [expression]

Expression operators:

expr1 expr2 AND (both must be true)

expr1 -o expr2 OR (either must be true)

! expr NOT (negates the result)

\( expr \) Grouping to override precedence

Common tests:

-name pattern Filename matches shell pattern (case-sensitive)

-iname pattern Filename matches shell pattern (case-insensitive)

-type [fdlbcp] File type

-mtime [+/-]n Modification time in days

-ctime [+/-]n Inode change time in days

-size [+/-]n[cMG] File size

-user name|uid Owned by user

-group name|gid Owned by group

-perm [-/]mode Permission bits

-nouser UID not in /etc/passwd

-nogroup GID not in /etc/group

Common actions:

-print Print path (default)

-ls Print detailed listing

-exec cmd {} \; Run command once per file

-exec cmd {} + Run command with batched file list

Swap Disks in the 21st Century

Although some of this is BSD specific, the principles apply to any Unix or Linux.

When you install your Unix like OS across several disks, either with a mirror or RAID system (particularly ZFS RAIDZ) you’ll be asked if you want to set up a swap partition, and if you want it mirrored.

The default (for FreeBSD) is to add a swap partition on every disk and not mirror it. This is actually the most efficient configuration apart from having dedicated swap drives, but is also a spectacularly bad idea. More on this later.

What is a swapfile/drive anyway?

The name is a hangover from early swapping multi tasking systems. Only a few programs could fit in main memory, so when their time allocation ran out they were swapped with others on a disk until it was their turn again.

These days we have “virtual memory”, where a Memory Management Unit (MMU) fixed it so blocks of memory known as pages are stored on disk when not in use and automatically loaded when needed again. This is much more effective than swapping out entire programs but needs MMU hardware, which was once complex, slow and expensive.

What an MMU does is remap the CPU address space so the running process believes it has a certain amount of RAM starting at address zero and going up as high as needed. It “thinks” it has the complete processor and all the RAM. However, the operating system is lying to the process, as not all the RAM the process believes it has is mapped to actual RAM. If the process tries to access an address that’s not mapped to anything the OS kernel takes over with a hardware interrupt called a “page fault”. The kernel than brings that page of RAM in from where it’s stored on the swap disk, gets the MMU to map it to the process address space, and restarts the process where it left off. The process doesn’t even know this has happened. If the kernel notices that some areas of RAM aren’t being used by the process it copies them to the swap disk and uses the physical RAM for some other purpose – until the next page fault.

So the swap partition should really be called the paging partition now, and Microsoft actually got the name right on Windows. But we still call it the swap partition.

What you need to remember is that parts of a running programs memory may be in the swap partition instead of RAM at any time, and that includes parts of the operating system.

Strategies

There are several ideas for swap partitions in the 2020s.

No swap partition

Given RAM is so cheap, you can decide not to bother with one, and this is a reasonable approach. Virtual memory is slow, and if you can, get RAM instead. It can still pay to have one though, as some pages of memory are rarely, if ever, used again once created. Parts of a large program that aren’t actually used, and so on. The OS can recognise this and page them out, using the RAM for something useful.

You may also encounter a situation where the physical RAM runs out, which will mean no further programs can be run and those already running won’t be able to allocate any more. This leads to two problems: Firstly “Developers” don’t often program for running out of memory and their software doesn’t handle the situation gracefully. Secondly, if the program your need to run is you login shell you’ll be locked out of your server.

For these reasons I find it better to have a swap partition, but install enough RAM that it’s barely used. As a rule of thumb, I go for having the same swap space as there is physical RAM.

Dedicated Swap Drive(s)

This is the classic gold standard. Use a small fast drive (and expensive), preferably short stroked, so your virtual memory goes as fast as possible. If you’re really using VM this is probably the way to go, and having multiple dedicated drives spreads the load and increases performance.

Swap partition on single drive

If you’ve got a single drive system, just create a swap partition. It’s what most installers do.

Use a swap file

You don’t need a drive or even a partition. Unix treats devices and files the same, so you can create a normal file and use that.

truncate -s 16G /var/swapfile
swapon /var/swapfile

You can swap on any number of files or drives, and use “swapoff” to stop using a particular one.

Unless you’re going for maximum performance, this has a lot going for it. You can allocate larger or smaller swap files as required and easily reconfigure a running system. Also, if your file system is redundant, your swap system is too.

Multiple swap partitions

This is what the FreeBSD installer will offer by default if you set up a ZFS mirror or RAIDZ. It spreads the load across all drives. The only problem is that the whole point of a redundant drive system is that it will keep going after a hardware failure. With a bit of swap space on every drive, the system will fail if any of the drives fails, even if the filing system carries on. Any process with RAM paged out to swap gets knocked out, including the operating system. It’s like pulling out RAM chips and hoping it’s not going to crash. SO DON’T DO IT.

If you are going to use a partition on a data drive, just use one. On an eight drive system the chances of a failure on one of eight drives is eight times higher than one one specific unit, so you reduce the probability of failure considerably by putting all your eggs in one basket. Counterintuitive? Consider that if one basket falls on a distributed swap, they all do anyway.

Mirrored swap drives/partitions

This is sensible. The FreeBSD installer will do this if you ask it, using geom mirror. I’ve explained gmirror in posts passem, and there is absolutely no problem mixing it with ZFS (although you might want to read earlier posts to avoid complications with GPT). But the installer will do it automatically, so just flip the option. It’s faster than a swap file, although this will only matter if your job mix actually uses virtual memory regularly. If you have enough RAM, it shouldn’t.

You might think that mirroring swap drives is slower – and to an extent it is. Everything has to be written twice, and the page-out operation will only complete when both drives have been updated. However, on a page-in the throughput is doubled, given the mirror can read either drive to satisfy the request. The chances are there will be about the same, or slightly more page-ins so it’s not the huge performance hit it might seem at first glance.

Summary

MethodProsCons
No swapSimple
Fastest
Wastes RAM
Can lead to serious problems if you run out of RAM
Dedicated Swap Drive(s)Simple
Optimal performance
Each drive is a single point of failure for the whole system
Multiple Swap PartitionsImproved performance
Lower cost than dedicated
Each drive is a single point of failure for the whole system
Single swap partition (multi-drive system)Simple
Lower probability of single point of failure occurring.
Reduced performance
Still has single point of failure
Mirrored drives or partitionsNo single point of failure for the whole systemReduced performance
Swap fileFlexible even on live system
Redundancy the same as drive array
Reduced performance
Quick summary of different swap/paging device strategies.

Conclusion

Having swap paritions on multiple drives increases your risk of a fault taking down a server that would otherwise keep running. Either use mirrored swap partitions/drives, or use a swap file on redundant storage. The choice depends on the amount of virtual memory you use in normal circumstances.

Microsoft releases WSL open Source

Microsoft has just open-sourced its Windows Subsystem for Linux (WSL).

https://blogs.windows.com/windowsdeveloper/2025/05/19/the-windows-subsystem-for-linux-is-now-open-source/

This is major. WSL runs the FOSS Unix knock-off on their closed source and expensive operating system, making it possible to host Unix applications on it. Cynics might think this was a ploy to still sell a Windows server license instead of people running Linux direct on the hardware. Or you could say it allows lower skilled Windows administrators who couldn’t cope with a command line to still access Linux applications.

Since it first appeared, people have been questioning Microsoft’s open source credentials, as WSL was closed source. Not now. You can get at the source code, customise it and run your own version.

This is great news, but as with anything Microsoft, it’s probably another cyber security attack vector for Windows.

How do run Docker on Debian

This is about how to run Docker on Debian Linux, not why you should want to. But it deserves an answer.

Supposing you’re running FreeBSD and someone really, really, really wants to you run something that’s only available as a Docker container? The only practical way is on a Linux VM running under bhyve. RHEL is expensive (and I no longer have an employer willing to stand me a developers’ license), CentoOS is no more. If you want to stay mainstream that leaves Debian and Arch. In my experience, Debian runs easily enough under bhyve, so Debian it is.

So log in to your new Debian installation as root and run the following, which took a while to work out so this is really a cheat sheet…

apt update
apt install curl ca-certificates

# Get docker GPG key
curl -fsSL https://download.docker.com/linux/debian/gpg \
   -o /etc/apt/keyrings/docker.asc

# This adds the latest Docker repo info to your APT sources list
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc]   https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
   | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update

# Finally install Docker
apt install docker-ce docker-ce-cli containerd.io -y

# You can check it's there by running   docker --version

systemctl enable docker

You can check it’s running with systemctl stop docker, and stop it with systemctl stop docker.

If you’re going to run this as a non-root user (probably a good idea) you’ll probably need to add yourself to the docker group:

usermod -aG docker your-user-id

This is just the Linux way of adding you to the /etc/group file.


Having a good argument

I’ve seen all sorts of stuff on forums about how to process command line argument in C or C++. What a load of fuss and bother. There’s a standard getopt() function in the ‘C’ library, similar to the shell programming command, but it’s not great.

The main problem with getopt() is that it produces its own error message. Although this saves you the trouble, it can be a bit user unfriendly for the user, especially when things get complex. For example, you might have mutually exclusive arguments and want to print out a suitable message. That said, it’ll work most of the time.

Here’s a page showing a good explanation for the GCC version, whcih is pretty standard.

Example of Getopt (The GNU C Library)

But rolling your own is not hard. Here’s a skeleton I use. It’s pretty self-explanatory. My rules allow single options (e.g. -a -b) or combined options (e.g. -ab), or any mixture. “–” ends options, meaning subsequent arguments are going to be a actual arguments.

If you want to pass something as an option, such as a filename, you can. -fmyfile or -f myfile are both handled in the example.

You can add code to detect a long option by adding “if (!strcmp(p,”longname”)) … just after the char c. But I don’t like long options.

#include <stdio.h>

void process(char *s)
{
    printf("Processing %s\n",s);
}

int main (int cnt, char **a)
{
    int i;
    for (i=1; i<cnt && a[i][0] == '-'; i++)
    {
        char *p = &a[i][1];
        char c;
        if (*p == '-')
        {
            i++;
            break;
        }
        while (c = *p)
            switch (*p++)
            {
            case 'a':
                printf("Option a\n");
                break;
                
            case 'b':
                printf("Option b\n");
                break;

            case 'f':
                if (!*p && i+1 < cnt)
                    printf("Value for f=%s\n", a[++i]);
                else
                {
                    printf("Value for f=%s\n", p);
                    while (*p)
                        p++;
                }
                break;
                
            default:
                printf("Bad switch %c\n",c);

            }
    }
    for (;i<cnt;i++)
        process(a[i]);
}

The above code assumes that options precede arguments. If you want to mix them the following code allows for complete anarchy – but you can end it using the “–” option, which will take any following flags as arguments. As a bonus it shows how to add a long argument.

#include <stdio.h>
#include <string.h>

void process(char *s)
{
    printf("Processing %s\n",s);
}

int main (int cnt, char **a)
{
    int i;
    int moreargs=1;

    for (i=1; i<cnt; i++)
    {
            if (moreargs && a[i][0] == '-')
            {
            char *p = &a[i][1];
            char c;
                if (*p == '-')
                {
                    moreargs = 0;
                    continue;
                }
                if (!strcmp(p,"long"))
                {
                    printf("Long argument\n");
                    i++;
                    continue;
                }

                while (c = *p)
                    switch (*p++)
                    {
                    case 'a':
                        printf("Option a\n");
                        break;

                    case 'b':
                        printf("Option b\n");
                        break;

                    case 'f':
                        if (!*p && i+1 < cnt)
                            printf("Value for f=%s\n", a[++i]);
                        else
                        {
                            printf("Value for f=%s\n", p);
                            while (*p)
                                p++;
                        }
                        break;

                        default:
                            printf("Bad switch %c\n",c);

            }
        }
    else
        process(a[i]);
    }
}



Systemd Network Configuration

Unless you’ve been living in a very Linux-free environment for a while, you’ll know about systemd – the collection of daemons intended to replace the System V init system commonly found on Linux, with something more complicated. I’m not a fan of System V startup, but they might have done better by going for the Research Unix or BSD /etc/rc approach for robustness, simplicity and compatibility. But Linux, to many, is a launcher stub for graphical desktops running LibreOffice and games, and these probably work better with systemd syntax when controlled by a simple GUI.

Systemd is more than an init system – in fact it has daemons from everything from the keyboard to DNS resolution – and network interface configuration (networkd)

This nightmare came out of Red Hat, and Linux distributions like Debian, Ubuntu, openSUSE, Arch, and their derivatives have started using it. One result, amongst other things, is that it’s suddenly not possible to configure networks the way you used to using ifconfig and /etc/resolv.conf.

You can install the missing ifconfig and suchlike using a package called net-tools, which is present on most major Linux distributions and is installed in the appropriate way (dnf, apt, yum etc). This may be the best way to keep scripts working.

Otherwise, you might be hoping systemd-networkd has simplified things, with less to type. But I’m afraid not.

So for those who are struggling, here’s a cheat sheet.

Names

The first think you’ll have to remember is that systemd-networkd doesn’t call your Ethernet interfaces eth0:, eth1. It doesn’t even call them by their driver name+enum BSD style. Instead it mungs a name from indices provided by the firmware, PCIe slot number and even the MAC address. Look out for some very strange interface names.

The idea is that the NIC/port has a predictable name, which is great in theory. I can see two problems: Firstly this doesn’t really help you find the RJ45 any better unless you have a schematic. Secondly, if you pull the system from one host and put it in another it all goes to hell in a handcart anyway. On the plus side I guess it means that adding or removing a NIC isn’t going to change the name of the existing ports.

For what it’s worth, eno# is an onboard device, ens# is a PCI slot index, enp#s# is port number on NIC and PCI slot index. enx1122334455667788 is the MAC address but this behaviour seems to be turned off on most systems. If it can’t determine anything it will fall back to eth#.

There are ways of selecting the old behaviour using kernel parameters or knobbling the /etc/systemd/network/… something “default” depending on system but you should check that out in the man page. Oh, hang on, this is Linux there probably no man pages.

Cheat Sheet

OldNew
ifconfig eth0 192.168.1.2/24ip addr add 192.168.1.2/24 dev eth0
ifconfig eth0 192.168.1.2 deleteip addr del 192.168.1.2/24 dev eth0
ifconfig eth0 netmask 255.255.255.0? Set address and netmask together ?
ifconfig eth0 mtu 5000ip link set eht0 mtu 5000
ifconfig eth0 down (or up)ip link set eth0 down (or up)
ifconfigip a
netstatss
netstat -rip route show
routeip r
route add default 192.168.1.254ip route add default via 192.168.1.254
arp -aip n
ifconfig eht0 name wan1? Not possible from command line ?

The last entry in the table is about renaming an interface, which given the user-hostile names now generated is even more useful. I haven’t figured out how to do this from the command line, but the assumption is that all interface configuration is done in configuration files by default, which brings us neatly on to these.

Configuring at startup

At one time you could just edit /etc/network/interfaces, and it might still work (it does int he latest Debian, for example). In BSD stick simple definitions in rc.conf, but that’s too easy. Anyway, /etc/network/interfaces could look something like this:

auto eth0
iface eth0 inet static
address 192.168.1.2
netmask 255.255.255.0
gateway 192.168.1.253

auto eth1
iface eth1 inet dhcp

After editing the configuration files(s) you could restart:

/etc/init.d/networking [start | stop | restart]

But some systemd Linux distributions are different. Systemd-networkd has a directory tree full of configuration stuff and I can only scratch the surface here.

Basically a load of *.network stored in /etc/systemd/network/ get run in sort order. It’s normal to prefix each file with two digits and a dash to set this order. I don’t think there’s any reason not to use a single file, but in the Linux world people don’t, often choosing to make the rest of the filename the NIC name, such as “04-enp0s5.network“, although the name you choose is only for your reference (or that of some GUI configuration tool).

To force every NIC to configure using dhcp create a file 02-dhcpall.network:

[Match]
Name=en*
[Network]
DHCP=yes

Note the wildcard on the NIC Name=*

On the other hand if you want to make one specific card static, have a file which you might want to call 01-enp5s2.network:

[Match]
Name=enp5s2
 
[Network]
Address=192.168.1.2/24
Gateway=192.168.1.254
DNS=192.168.1.254 8.8.8.8
Domains=example.com test.example.com

This should be fairly self-explanatory. You can specify multiple Address= lines (aliases) but for some reason DNS servers tend to be listed on one line, although multiple lines do work in my experience. I’ve used IPv4 in the examples but IPv6 works too.

Domains=example.com test.example.com is basically the DNS search domains (as normally found in resolv.conf). As systemd has its own resolver, systemd-resolved, it’s not just a matter of edit one file any longer, and is also less flexible.

You can restart systemd-networkd with:

systemctl restart systemd-networkd

If you haven’t made any mistakes you might still be connected to your server.