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

Simple guide to the vi editor

vi is the standard text editor that’s been around on Unix and Unix-like systems since 1976. It was written by Bill Joy in one weekend as an enhancement of the original ex editor, which lacked “full screen” mode because, to put it bluntly, full screen terminals hadn’t been invented.

It’s part of the POSIX standard and you’ll never find a Unix that hasn’t got it. Love it or hate it, vi is standard; so you’d better make friends with it because you’re going to need it, even if it’s only briefly while you compile something more modern as a replacement.

There are numerous tutorials and “cheat sheets” for vi, but they’re over-complicated to my mind. Yes, you can do a lot with ex and vi if you remember and type various key sequences correctly, but most people just want to edit a text file, quickly. There are much better editors out there for big jobs, although they weren’t written in a weekend.

So how do you use it, if you don’t want to remember more complex keystrokes than necessary?

The modes

The first thing you need to understand about vi is that it has “modes”. With any normal editor, you move the cursor to wherever you want to type something, and type away. Not so with vi, but it’s not that bad when you understand it. When you first start vi it’s in Command Mode, which would be better described as “move the cursor mode”. It’s waiting for you to move the cursor somewhere. You can enter a command by prefixing it with a colon (“:”), when it will jump the cursor to the bottom of the screen so you can enter the command. Other modes let you enter text or search for things. To return to the Command Mode (i.e. move the cursor mode) you generally press the [Esc] key. Remember that if you’re stuck.

In Command Mode most of the keys do things, usually moving the cursor, often in expected ways. Be careful what you press.

In this text I’m representing the Enter key on the keyboard with [Enter], and Escape with [Esc]. Other characters in double quotes to make them stand out, but are typed without the double quotes. So let’s get started…

Loading a file to edit

If you’re new to vi you might want to make a backup of the file you’re about to mess with. There are mechanisms to have vi do this for you, but for safety just make a copy yourself:

cp filename filename.safe

Then on to edit the file. Type “vi filename”

This will load the file named filename into the editor. To make the editor a little more friendly you can type:

:set verbose showmode

Followed by Enter.

Quitting or saving a file.

This uses a colon command (see above if you skipped the introduction). To quit type :q followed by the Enter key. If you’ve made changes to the file it will say “File modified since last complete write” or similar, and won’t let you. You can override this by adding a ! to the command to show you really mean it

:q! [Enter]

To write changes to disk use :w <enter> This is the equivalent to “Save” on a modern editor. To write the current file to another file (i.e. Save As…) use :w newfilename [Enter]

A quick shortcut if you want to save and exit the editor in one go is :x [Enter]

Moving around the file

When the file is loaded you’re in Command Mode, which might more usefully be thought of as Movement Mode. You can’t type anything but you can move the cursor around. On anything reasonably modern you can do this using the arrow keys on the keyboard and [PgUp] and [PgDn]. [Home] and [End] probably work too. There are other ways of doing this using original keyboards that lacked cursor keys, which I’ll cover later if needed.

Changing stuff

Let’s assume you’ve got the cursor to the place in the file where you want to make the change. Most of the time you’ll want to either delete stuff or insert stuff. To delete what’s under the cursor type the “x” key. If you want to delete a lot, “D” deletes everything to the end of the line and “dd” deletes the whole line, but if you can’t remember just stick to “x”. Note that it’s case sensitive – “D” and “d” are not the same.

To insert something, type the “i” key to get into Insert Mode. Other modes are available, but Insert Mode is what most people are used to.

When you’re in Insert Mode, everything you type will be inserted, including new lines if you press the return key. To get out of insert mode press the [Esc] key. And this is a general rule, if you’re in a mess with vi keep hitting the [Esc] key until you get back into Command Mode.

You might, of course, have made a mess of the edit. A single “u” will undo the last change you made in Insert Mode. If you press “u” a second time it will toggle the undone changes back.

If you make a real mess of it just quit out using “:q!” and start again. It sometimes pays to do a “:w” while editing to save good changes.

Cut+paste with a mouse

vi has all sorts of ways of moving text around, but to keep it simple I’m going to assume you’re using a virtual terminal (something like PuTTY) and have mouse. Just select the text you want to copy, move to where you want it to go and put vi in insert mode with “i”. If you can’t remember which mode you’re in type [Esc] “i” to be sure. Then right-click the mouse and it will paste.

Search and replace

To search for something in the file hit [Esc] to make sure you’re in Command Mode and type “/” followed by whatever you’re looking for, followed by Enter. If you want to search backwards use “?” instead of “/”. The cursor will jump to the first occurrence it finds.

If you want to search for the next occurrence use the “n” key, if you want to go backwards use the capital “N”.

To do a Search and Replace you’ll just have to go with me on this. Again, make sure you’re in Command Mode and hit “:” for a colon command. To replace “old” with “new” in the entire file the colon command looks like this:

:%s/old/new/g

Basically the %s means search every line and the g means change every occurrence in a line. The “/” marks the old and new fields. If you want to replace something with a “/” character in it use a different separator character like “.” or “|” – it’s just looking for a punctuation mark and it will carry on using whatever punctuation mark it finds first.

That’s it?

You want it to be more complicated? I’ve seen many tutorials and cheat sheets explain a lot of stuff you don’t need for simple editing. Commands to move the cursor quickly, repeated commands and so on. Yes, you can scroll ten lines down using 10j but who cares? Just hit the down arrow ten times – assuming you even know that your destination is that far below. I might add a batch of secondary very useful commands later but they’re not essential. However, read on if you don’t have cursor keys or can’t use a mouse for cut and paste, or care about line numbers.

Line numbers

If you care which line you’re on, possibly because you’re getting a message line “error in line 123” of your config file and need to fix it, there are a few extras that might help. If you want to jump to line 123 use “:123”. As you can imagine, to get to the top of the file quickly you can also use “:1”, and use “:$” to go to the bottom.

You can turn on a display of the current line number and cursor column with “:set ruler” and display line numbers with “:set number”. To turn these things off prefix them with “no” – e.g. “:set nonumber

Cut+paste with keyboard

If you’re using a real hardware terminal instead of a virtual software one (with a mouse) you can still cut and paste, but I’ll need to explain something about buffers and deleting first. When you delete anything in vi it goes into a buffer. The basic delete command is “d” followed by any movement command. Anything between the old and new cursor positions gets deleted and put in the buffer. There are a lot of movement commands, but the one we’re interested in is “jump to marker”.

To place a marker in the file at the cursor position you must be in Command Mode (hit [Esc] if you’re not sure) and then type “ma”. This sets marker “a”. You can also set marker b, c, d and so on, which is useful if you want to bookmark different places in a file.

To jump to a marker use a “ ` ” (single back quote, normally to the left of the “1” on the keyboard) followed by the marker letter – i.e. `a

So to cut some text, set the marker at the start, move to the end and type d`a and the text will disappear into the paste buffer. If you wanted to Copy instead of Cut use y`a instead – y is Copy.

In order to paste the contents of the buffer, go to where you need it and type capital “P”. This will insert at the current cursor position, which is what most people want.

A variation in this is to use a ‘ instead of a ` (a single quote instead of a backquote), which will cut/paste whole lines between the cursor and the marker. Beware, a lower case “p” instead of a capital for paste puts the buffer after the current cursor position by either one character or one line depending on whether you used a ` or a ‘ originally. Stick with P to avoid confusion.

No cursor keys?

If your keyboard doesn’t have cursor keys, or they’re not working, you can use the following to move around:

left=h, right=l, k=up, j=down. Ctrl-B and Ctrl-F give you Page Up and Page Down.

A note on VIM

vim is a “Vi Improved” editor found in GNU/Linux. It claims to be mostly compatible with VI and most of the above should work. One of its improvements is a multi-buffer undo facility, presumably because Linux users make more mistakes.

It’s curious that anyone would want to improve in vi, and I say this having used it for over 45 years now. It’s not an editor that I’d choose as a starting point to write a better one. Improved editors are out there, including the straightforward nano editor that is bundled with most GNU/Linux distros; you may find that more friendly if it’s available.

After all this time using vi I certainly know a few tricks and can use it very quickly and efficiently, but the main reason for learning it now is that it will be there when you need to edit something, on every Unix-like system.

Singapore Airlines appears to have data breach

How do I know this? My alarm systems have just triggered on an email address known only to Singapore Airlines, that’s how. For decades I’ve seeded special email addresses with various organisations and waited to see if criminals got hold of them.

But hang on – they suffered a supply chain attack in 2021. Except this one was seeded on 9th April 2025, so my guess is that it’s happened again.

Watch this space!

BBC Uncanny – the White Mountain UFO

Anyone interested in supernatural folklore is likely to have heard of the Danny Robins series “Uncanny” on Radio 4. Episodes are written and presented by Danny Robins, with guest various experts – notably Ciarán O’Keeffe for the septics and Evelyn Hollow for the believers. The latest venture is “Uncanny Cold Cases”, currently running, presenting older stories instead witness testimony.

This is worth a listen if you’re interested in what leads people to believe what they do, and an awful lot of people do think they’ve seen a ghost. But for some reason, Uncanny can’t keep away from UFO stories. With ghosts, people say they’ve seen something strange and you either believe them or don’t, but UFOs are more objective.

The latest episode is “The White Mountain UFO”, which is referring to the alleged abduction of Barney and Betty Hill in 1961. The couple were driving back from a holiday in Montreal to their home in Lancaster, New Hampshire, USA. Seeing a light in the sky they stopped to have a better look. Having seen a flying saucer and aliens they fell into a daze and woke up in their car, close to home, with no recollection of how they got there. The story has become embellished over time.

A mundane explanation is that they were over-tired, hallucinating and it’s lucky they didn’t fall asleep and drive into a tree. However, under hypnosis they both told a similar story about being abducted by aliens in the missing hours.

In an era of cold war paranoia, and in a country gripped in the 1960s flying saucer hysteria, the Hills were interviewed by the USAF and NICAP, the (American) National Investigations Committee On Aerial Phenomena.

So far so meh. Where Uncanny suddenly became interesting was with some physical evidence. Apparently Mrs Hill’s dress had some unexplained pink stains that were investigated later and found to be contaminated in a way that was impossible by natural means. They found Rhodium and Tellurium on the dress, which are present at the earth’s surface at 0.0002 parts per million – but possibly more common in space. These are the “facts” presented.

But is this the smoking gun Uncanny is suggesting? Some physical indisputable evidence? I hadn’t heard of the dress being tested before before, so I did some digging. You’ll never guess what I found.

The tests were actually done by analytical chemist Phyllis Budinger, who had become interested in the case. She believes in UFOs. She contacted Kathleen Marden, Betty Hill’s niece, and obtained five fabric samples in 2001; the lining, three from the discoloured areas and a fourth from a clean patch as a control. Over the next couple of years she conducted tests – infrared spectra on the surface and of solvent extractions, and good old microscope analysis. She also got some X-ray fluorescence tests done by the University of Pittsburgh. The results appeared in a book, “Captured!”, written none other than Ms Marden and Stanton Friedman, where it’s stated that this new analysis found rare and unusual heavy metals. And this appears to be the source used in Uncanny.

However, this is a very selective interpretation. These heavy metals are indeed seriously rare. However the figure giving their prevalence is an average over the earth’s surface but this says nothing about concentrations. Being rare on average is not the same as being rare on early 1960s dresses.

Those doing the XRF were somewhat more circumspect. They noted it was unusual, and suggested the graduate chemistry department conduct more extensive scans to corroborate, describing their own tests as “cursory”. As far as I can tell, no such further testing was carried out.


Budinger herself suggests getting a biologist involved and looking at the DNA, but concluded the pink was a result of mould caused by whatever was contaminating the fabric in some areas.

The results can be found as Technical Service Response No UT025 from Frontier Analysis Ltd.

So that’s that for the physical evidence. What of the hypnosis? Both of the hills recounted a story with details very similar to episodes of a TV programme “The Outer Limits” that aired in the weeks before the hypnotic regressions. The Hills may have been accessing repressed memories of their abduction three years earlier, or they may have been remembering a TV programme they watched three weeks earlier. You decide.

The sessions were conducted by psychiatrist Dr. Benjamin Simon, who concluded that the abduction experience as recalled under hypnosis “did not take place but was a reproduction of Betty’s dream which occurred right after the sighting.” The story revealed under hypnosis is often presented as true, but this is a misrepresentation. Dr Simon’s letters to Philip J. Klass make his scepticism clear. Meanwhile other people, including the aforementioned Ms Marden, need it to be true to sell books. This may affect their credulity, again you decide.

Further corroborating evidence of the Hills account was presented in the episode – specifically radar returns (contacts) of an unknown object at the time of their disappearance. These may have been taken from various books on the subject – quite likely Jacques Vallée’s “Dimensions”, which describes a return signal monitored by Pease Air Force Base. This was on the approach radar, and the extract reads:

observed unidentified aircraft come on precision approach radar 4 miles out, aircraft made approach and pulled up at half a mile, shortly after observed weak target on downwind, then when it made low approach, tower unable to see any aircraft at any time.

This was at 2:14am local time, after the Hill’s sighting, possibly the UFO flying away after being undetected for the last four hours? Or perhaps it was an unidentified aircraft on an aborted approach, which isn’t exactly unusual.

Another return comes from the North Concord Air Force in Vermont, which tracked an object for 18 minutes – fitting the podcast. It was recorded by Project Blue Book. However, it’s not corroboration either:

…one object detected on height-finder radar at 62,000 feet, appearing at 196 degrees at 84 miles, lost at 199 degrees at 80 miles, moving north then south, observed for 18 minutes.

It was high in the air, moving slowly and erratically, exactly like a weather balloon – which was their actual assessment. It was also seen at 5:22pm Eastern time – long before the Hills’ sighting.

Mr Hill died before his time in 1969 aged 43, but his wife survived until 2004 and was well known in UFO circles, although not universally believed according to many accounts.

So where does this leave the BBC’s Uncanny podcast? The evidence that this story doesn’t amount to a white hill of beans is out there, so why didn’t Dr O’Keeffe blow it out of the water? I guess that’s entertainment!

FreeBSD error: leapsecond file expired

You might see something odd in the console log about a leapsecond file:

ntpd[905]: leapsecond file ('/var/db/ntpd.leap-seconds.list'): expired 841 days ago

It just means that the leap second file is out of date, and couldn’t be updated automatically. It’s not a big deal as this data rarely changes. In fact the last one was in 2017. You can force an update with the following line:

service ntpd fetch

However, this might well give you another error like “fetch: https://www.ietf.org/timezones/data/leap-seconds.list: Not Found” The reason is that the IETF is not longer hosting the leap-seconds file although it’s baked into the FreeBSD (and likely other) configs.

On FreeBSD you can easily point it to another host at IANA by adding the following to /etc/rc.conf:

ntp_leapfile_sources="https://data.iana.org/time-zones/data/leap-seconds.list"

After you’ve done this you can trigger it manually using the fetch line above. The old source is actually configured originally in defaults/rc.conf, but it’s best to override it in the local rc.conf – and newer releases of FreeBSD have updated it anyway.

The Crazy Politics of Age Verification

The UK government required age verification for pornographic website to protect children. Australia was the first country in the world to ban children from social media. Children, of course, circumvented all such restrictions immediately. And now the UK government and others around the world are thinking “This plays well with the public” and is trying to follow suit, in spite of the fact the teenagers are laughing at the restrictions.

Rather than getting an informed opinion on the practicality of such measures from technology experts, the politicians, as usual, get their technical understanding from some random teenagers on social media.

This lack of tech savvy would be hilarious if it wasn’t such a serious matter. It’s nothing new. And these proposed technological solutions to problems in society are doing nothing to fix the very real issues they’re trying to address. You cannot fix social problems using technology. Leave a comment if you can think of any example where this has been the case.

Next Monday there’s a vote in the commons on just this, and I fear grandstanding arts-graduate politicians will let it through, regardless of the technical impossibility of complying. And there’s no end to this nonsense.

There’s crazy, then there’s California

And just when you think it can’t get any worse, the Californians have taken it to the next level with a new bill called Age Verification signals: software applications and online services.

Here’s an extract:

1798.501. (a) An operating system provider shall do all of the following:

(1) Provide an accessible interface at account setup that requires an account holder to indicate the birth date, age, or both, of the user of that device for the purpose of providing a signal regarding the user’s age bracket to applications available in a covered application store.

(2) Provide a developer who has requested a signal with respect to a particular user with a digital signal via a reasonably consistent real-time application programming interface that identifies, at a minimum, which of the following categories pertains to the user:

(A) Under 13 years of age.
(B) At least 13 years of age and under 16 years of age.
(C) At least 16 years of age and under 18 years of age.
(D) At least 18 years of age.

Having an handy OS function to determine the age of the user seems like a good idea, doesn’t it? Naughty software can refuse to run if the user is under-age. But it’s technical nonsense; not thought through at all. For example, it’s referring to a “real-time application programming interface” – so this only applies to a RTOS? Obviously they saw the phrase, thought it sounded good and included it.

But the concept has some merit – rather than everything doing age verification, why not dump the problem on Microsoft, Apple and Google? They’ve got the resources, and with their penchant for knowing everything about their customers, their OS could easily provide age verification information along with their shoe size and taste in pot plants.

Except… you need to consider the chilling effect this will have on anything called an Operating System. The lawmakers probably wouldn’t recognise an OS if it jumped out at them waving a flag, but this is important here. For starters, every embedded system has software that fits the definition of Operating System. Do they have to provide age verification?

And what about, say, servers? They don’t have a user as such, but they have people using them (administrators). How can you tell the age of the administrator at any given time? The account may well be shared by a number of people.

But the most damage from this nonsense will be to FOSS operating systems like Linux. They don’t have a KYC culture like Microsoft/Apple/Google to make it easy and don’t have the time, money or resources to solve the problem. An open source operating system is community owned, and the community has no incentive to track the personal details of who might be using it.

Although Linux is free, it’s still licensed using the GPL and has a vendor – even if no money changes hands. If this bill ever passes, the State of California may well go after the vendor for non-compliance and try to force them into it by fines and suchlike.

The logical solution to the problem for Linux distributions (and FreeBSD and whatever) is to change the terms of their license saying “You may not download or use this software in the State of California”. Microsoft, Apple and Google will love that.