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.
A few nice, significant examples: www.macs.hw.ac.uk/~hwloidl/Courses/LinuxIntro/x864.html
Control structures:
expressions and test
if
case
for
while
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.
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.