Comp 141 Class 6 notes

June 27

Shotts:

Chapter 24: Writing Your First Script

Chapter 27: Flow Control: Branching with if

Chapter 33: Flow Control: Looping with for

Editing Review

We'll consider these editors:

Basic use of nano

Nano is modeless. Every displayable character is entered when you press it. Nano uses control characters for actions; see the bottom two lines.

Some Nano commands are implemented with the "Meta" key, which is usually mapped to "Alt".


The environment, revisited

Commands: env, export, execve()

What is export all about?

settings in .bashrc

Shell Scripting

A shell script is a sequence of shell commands, encapsulated into a file.

We can make the file executable (chmod +x filename) and launch the script as ./filename; otherwise we need "bash filename".

The first line of an executable shell script should be

    #!/bin/bash

The '#!' is often read as "shebang". Technically it is a comment, because it begins with #. It tells the system what program to use to interpret the script (and it ca be #!/usr/bin/python3, for example). But the default is usually /bin/sh (though that is not the same as bash).

A first script:

    #!/bin/bash

    # Here is a comment

    echo "Hello world!"

Let's put this in a file hello.sh (we'll use the extension .sh for a while, but typically executables in Unix do not get extensions). Now we'd like to run it. At this point, typing

    ./hello.sh

fails, but we can type bash hello.sh. To get ./hello.sh to work. we must make the file executable:

    chmod +x hello.sh

But we still need that ./ to run it. Also, our current directory has to be the same as the directory the program is in. One common strategy is:

We do the latter with

    PATH=$PATH:~/bin

The righthand side is the existing PATH, namely $PATH, followed by ~/bin (and separated by a ':')

Putting ~/bin at the end of the path means that we can't override any existing commands. If that's what we want to do, though, then PATH=~/bin:$PATH is a better choice.

We probably also want to add

    export PATH

Finally, Ubuntu-type distributions often add ~/bin to PATH automatically. Your Loyola virtual machines do this. From ~/.profile:

# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/bin" ] ; then
    PATH="$HOME/bin:$PATH"
fi

Long options

In scripts, the long options to commands are usually a better choice: instead of ls -ad, consider ls --all --directory

Continuation lines

You can terminate lines with \ (make sure there is no space following!), and they will be seen by the shell as continued on the following line. This allows breaking up very long lines for readability

bash for

This one is frequently useful directly from the keyboard:

for i in *.text
do
   echo $i
done

for i in A B C D; do        # alternative do location
    echo $i
done

for i in *; do echo $i; done    # one-line version; prone to problems with too many or too few semicolons

This gives us a way of taking a long list of filenames (or command arguments) and dealing with them one at a time.

We've used "i" as the index variable in both cases, but it can be anything; "file" is also popular (assuming the index variable is taking on filenames)

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.

bash if

Suppose we want our script to check for some condition, and have the result of that check affect its further action. The bash if command helps here. We also need bash conditions. Any command can be used as a condition. Bash looks at the command's exit code, with true represented by an exit code of 0 (the normal case) and false represented by anything else. (Note that this is the reverse of the C convention.) We will start with the test command, and the equivalent [ command:

    test expression

    expression  ]

There is a newer version of test, [[   ]], that also includes support for regular-expression matching.

Here are some file tests:

    test -f file    # file is a regular file

    test -d file    # file is a directory

    test -e file    # file exists; useful for wildcard matching

    test -L file    # file is a symlink

    test -r file    # file is readable (also -w for writable)

    test file1 -nt file2    # file1 is newer than file2, in terms of the file-modification date

Here is nodirs.sh, which checks if there are no subdirectories of the current directory. It also uses the return code to signal this.

for i in *
do
    # echo $i
    if test -d $i
    then
        echo "directories found"
        exit 1
    fi
done

echo "no directories"
exit 0

Here are some string tests.


Shotts answer.sh, modified to take a command-line argument


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.

((  )) tests for numeric zero

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".

Example:

    ls -l $(which dash)

This gets "ls -l" information about the command

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 how we can increment a variable:

    i=0

    i=$(expr $i + 1)

We can also use the double-parentheses trick, i=$(($i + 1)). Note the inner $ is needed to get the value of i, and the outer one is part of the $((  )) construct.

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)

What does this do to foo.bar.baz?

There's a fancier way:

    echo "${file%.*}"

From the manual

${parameter%%word}

The word is expanded to produce a pattern [here .*] and matched according to the rules described below. If the pattern matches a trailing portion of the expanded value of parameter, then the result of the expansion is the value of parameter with the shortest matching pattern deleted.

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

ifaddrs