Comp 141 Class 7 notes

July 11

Shotts:

I apparently messed up two weeks ago, and never put homework 6 online. So it's there for today. It's four shell scripts. For each, submit the entire script, as well as a sample run if possible.

For next week, there will be a series of quizzes on Sakai, each involving the writing of one bash script. You can do them all together, or one at a time. These will count like a midterm exam.

More Shell Scripting

A few nice, significant examples: www.macs.hw.ac.uk/~hwloidl/Courses/LinuxIntro/x864.html

Control structures:

    expressions and test

    if

    case

    for

    while

   

bash arguments

Shell scripts can be given arguments on the command line. The first ten commands are $0 through $9, with $0 being the command name itself. The list of all the arguments is $* (though all the arguments in a bash array is $@, and is usually a better choice). Below is echo3.sh, which echoes arguments 0 through 3.

echo $0
echo $1
echo $2
echo $3

If more arguments are provided, they are ignored. If only two are provided, then $3 is the empty string, and is echoed as such, creating a blank line. Note the appearance of $0. Also, note what happens to ./echo3.sh foo 'bar baz' quux

Next, consider this echoeach command:

#!/bin/bash

for i in $*
do
    echo $i
done

Try ./echoeach.sh foo bar baz.

A more interesting demo is ./echoeach foo 'bar baz' quux. What does it do that is wrong?

A fix is to use "$@"; this does not work with "$*". You are strongly encouraged to use "$@" for the list/array of command-line arguments.

There are a couple other shell argument variables. One is $#, which is the number of arguments in all, not including $0.

Here are the basic file tests:

file1 -nt file2            file1 is newer than file2.
file1 -ot file2            file1 is older than file2.
-d file                        file exists and is a directory.
-e file                        file exists.
-f file                        file exists and is a regular file (not a directory or a device file)
-L file                       file exists and is a symbolic link
-s file                        file exists and has nonzero size

Here are some string tests.

-n string        The length of string is greater than zero.
-z string        The length of string is zero.
string1 = string2
string1 == string2        string1 and string2 are equal. Single or double equal signs may be used. Most people use ==, but it is not Posix.
string1 != string2        string1 and string2 are not equal.
string1 > string2        string1 sorts after string2.  Warning: > must be escaped from the shell
string1 < string2        string1 sorts before string2.   Warning: < must be escaped from the shell

It is often necessary to have the string-containing shell variables be in quotes, because an empty-string shell variable just disappears and leads to syntax errors. For example, if s=foo and t='', then

    -z $t

becomes

    -z

which is not what you want. But

    -z "$t"

becomes

    -z ""

which correctly tests as true.

Older shells didn't handle empty strings, so sometimes people put a letter in:

    if test "x$s" == "x$t"

This becomes

    if test "xfoo" == "x"

which is false, but at least syntactically correct.

See stringer and qstringer. stringer fails if you give it an argument with a space:

    stringer 'foo bar' baz

When the [ / test command is processed, the strings are written out into the command line, and the command line is re-parsed, and re-globbed if appropriate. This is true even though test is built into bash (as well as existing as a standalone program).

Here are some numeric tests. These only work for integers

    integer1 -eq integer2            integer1 is equal to integer2.
    integer1 -ne integer2            integer1 is not equal to integer2.
    integer1 -le integer2             integer1 is less than or equal to integer2.
    integer1 -lt integer2              integer1 is less than integer2.
    integer1 -ge integer2            integer1 is greater than or equal to integer2.
    integer1 -gt integer2             integer1 is greater than integer2.

You can make Boolean combinations with -a for and, -o for or, and ! for not.

command substitution

Sometimes we want to test the output of a subcommand. We us a special syntax that converts the output of the command to a string, which can be tested in the parent script. Just enclose the subcommand in $(    ). This is traditionally called "command substitution".

The expr command evaluates an arithmetic expression (where operands have to be separated from numbers by spaces, and '*' must be escaped from the shell to avoid globbing). If we create a shell "loop", here is one way to increment a variable:

    i=0

    i=$(expr $i + 1)

But the double-parentheses method has some advantages, i=$(($i + 1)). Note the inner $ is needed to get the value of i, and the outer one is part of the $((  )) construct.

First, with the double-parentheses method, the parts of the expression do not have to be separated by spaces: $(($i+1)) works. Second, in expr the multiplication operator '*' must be escaped from the shell. Examples:

echo $(expr 4 * 3)

echo $(expr 4 '*' 3)

echo $((4 * 3))

(Note expr is a command, and its output is stdout, which must use command substitution to be converted to a string. $((  )) generates a string in the first place.

The cut command is useful for extracting particular fields from the output of another command. It is possible to use it to extract a set of columns by column numbers, thus allowing the parsing of the output of ls, but the use of cut is easier for output that is essentially in "csv" (comma-separated values") format (the separator does not have to be a comma).

Linux has a builtin command basename that takes a filename like /usr/bin/dircmp and returns the "base" part, "dircmp". What if we want to remove the "extension" from a file; that is, convert foo.text or foo.pdf to just foo? We'll treat the string as two fields with separator '.', and use cut to get the first field. Note that fields in cut are numbered starting with 1, not 0.

    echo foo.pdf | cut -d. -f1

Or, if the filename is in $file and we want to set the "root" filename to the variable rootname,

    rootname=$(echo $file | cut -d. -f1)

On my laptop, I often prefer to disable the touchpad quickly from the keyboard. I have a command that works like the list below. I'm trying to extract the number in the string "id=12", so I first use cut with the separator =, and then with the default TAB separator.

DEVNAME='Synaptics TM3625-010'

TOUCHPADNUM=$(xinput list | grep "$DEVNAME" | cut -d= -f2 | cut -f1)

echo "TOUCHPADNUM = $TOUCHPADNUM   ARG=$ARG"

xinput --disable $TOUCHPADNUM

ifaddr

This command parses the output of ip addr show to get just the IP address. The hard part is getting the interface name; the command ifaddrs gets them all. 

Here is the output of ip addr list dev enx00e04c680a51:


3: enx00e04c680a51: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1492 qdisc fq_codel state UP group default qlen 1000
    link/ether 00:e0:4c:68:0a:51 brd ff:ff:ff:ff:ff:ff
    inet 10.0.6.6/24 brd 10.0.6.255 scope global dynamic noprefixroute enx00e04c680a51
       valid_lft 3376sec preferred_lft 3376sec
    inet6 2001:470:1f11:17b:6a7c:ab79:b8b1:45fd/64 scope global temporary dynamic
       valid_lft 86392sec preferred_lft 14392sec
    inet6 2001:470:1f11:17b:93bf:7c1e:7f9b:419e/64 scope global temporary deprecated dynamic
       valid_lft 86392sec preferred_lft 0sec
    inet6 2001:470:1f11:17b:4b6d:9613:de1c:2b85/64 scope global temporary deprecated dynamic
       valid_lft 86392sec preferred_lft 0sec
    inet6 2001:470:1f11:17b:3bc3:2e83:b897:3a97/64 scope global temporary deprecated dynamic
       valid_lft 86392sec preferred_lft 0sec
    inet6 2001:470:1f11:17b:186e:872f:8dda:5ab3/64 scope global temporary deprecated dynamic
       valid_lft 86392sec preferred_lft 0sec
    inet6 2001:470:1f11:17b:cbaf:7aa7:e383:6bd1/64 scope global temporary deprecated dynamic
       valid_lft 86392sec preferred_lft 0sec
    inet6 2001:470:1f11:17b:516e:9575:27c2:9b64/64 scope global temporary deprecated dynamic
       valid_lft 39961sec preferred_lft 0sec
    inet6 2001:470:1f11:17b:e31:522c:6a40:ca41/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 86392sec preferred_lft 14392sec
    inet6 fe80::125:7d03:f2c6:9688/64 scope link noprefixroute
       valid_lft forever preferred_lft forever

We need to find the string 'inet' (but not 'inet6'!), get the token that comes after it, and split it at the '/'. Here is the main loop:

ip addr show dev $1 | while read -r line
do
   if [[ "$line" =~ 'inet ' ]]        # a regular-expression match
   then
       ippair=$(echo $line | cut -d' ' -f2)        # cut out the addr/len string
       addr=$(echo $ippair|cut -d/ -f1)            # get the part before the /
       len=$(echo $ippair|cut -d/ -f2)             # get the part after the /
       echo $addr $len
       exit 0
   fi  
done

Note that I piped the output of ip addr dev into the while read. Below, when we look at linecounter, this will be a total fail.


udemo

This demonstrates the function of set -u, which disallows the use of any undefined variables. By default, undefined variables are treated as empty strings.

tolower1 and tolower2

These rename a file to lower case.

tolower2 checks that the new filename isn't already in use.

The two non-file parameters to tr are the two character sets. If they are ranges, they are expressed without the [ ], which are used in globbing to match a character range.

What if we add the [ ]? The script (renamed tolower[, which needs to be quoted to run) seems to work fine!

But now let's create a file that would match [A-Z], and one that would match [a-z]:  >D  >z

What is going on?

What if we forget the $ in front of the (...)?


numsum  N

Adds the integers from 1 to N. Note the $# check.


isempty

This is a script I wrote that I use all the time. It checks if its single argument represents an empty file or an empty directory

#!/bin/bash
# if $1 is a plain file, returns TRUE if the file has size 0, otherwise FALSE
# if $1 is a directory, returns TRUE if it has no files, otherwise FALSE

if [ $# -eq 0 ]
then
    echo "usage: isempty filename"
    exit 2
fi

FILENAME=$1

if test -d $FILENAME
then
    if [ $(ls -a $FILENAME |wc -l) -eq 2 ]
    then
           exit 0
    else
        exit 1
    fi
elif test -f $FILENAME
then
    # echo "isempty: $FILENAME is regular file"
    FILESIZE=$(ls -l $FILENAME | awk '{ print $5 }')
    # if [ $FILESIZE -eq 0 ]
    if test ! -s $FILENAME
    then
        exit 0
    else
        exit 1
    fi
else
    echo "isempty: $FILENAME is weird file"
    exit 2
fi

Note that no output is returned, only the exit code.

The right way to check if a file is empty: test ! -s $file

checking if a directory is empty

After checking that the directory exists, I often use test $(ls -A $dir |wc -l) -eq 2

This is a hack.

One alternative is find $dir -mindepth 1 | read; if the directory is empty, this is false. Of course you end up reading the entire directory, but most are smallish.

But I did fix the much worse hack of trying to parse the output of ls with awk

isempty is used by listempty in homework 6. I used to use it all the time in processing downloads from Sakai.


linecounter

This attempts to recreate the functionality of wc -l in bash. It serves as an example of a while loop, and also of the use of read.

linecounter with pipe:

    cat $FOO | while read x

This is horribly unexpected! What is going on?


filecounter: counts the files in the current directory owned by the current user

Note that I parsed the output of ls to get the owner name of each file. This is almost never recommended, but the real problem is parsing anything after the file size. For very large files the size can get messed up. The date output of ls is notorious; for files older than 6 months it's one format, and for newer it's another. Finally, the file's name may have newline characters, or additional spaces, or tabs. None of this applies here.