Bash interactive scripts

Bash interactive scripts
bash interactive scripts

Some scripts run without any interaction from the user at all. Advantages of non-interactive scripts include:

  • The script runs in a predictable way every time.
  • The script can run in the background.

Many scripts, however, require input from the user, or give output to the user as the script is running. The advantages of interactive scripts are, among others:

  • More flexible scripts can be built.
  • Users can customize the script as it runs or make it behave in different ways.
  • The script can report its progress as it runs.

When writing interactive scripts, never hold back on comments. A script that prints appropriate messages is much more user-friendly and can be more easily debugged. A script might do a perfect job, but you will get a whole lot of support calls if it does not inform the user about what it is doing. So include messages that tell the user to wait for output because a calculation is being done. If possible, try to give an indication of how long the user will have to wait. If the waiting should regularly take a long time when executing a certain task, you might want to consider integrating some processing indication in the output of your script.

When prompting the user for input, it is also better to give too much than too little information about the kind of data to be entered. This applies to the checking of arguments and the accompanying usage message as well.

Bash has the echo and printf commands to provide comments for users, and although you should be familiar with at least the use of echo by now, we will discuss some more examples in the next sections.

echo built-in command

The echo built-in command outputs its arguments, separated by spaces and terminated with a newline character. The return status is always zero. echo takes a couple of options:

  • -e: interprets backslash-escaped characters.
  • -n: suppresses the trailing newline.

The following table gives an overview of sequences recognized by the echo command:

Escape sequences used by the echo command

Sequence Meaning
\a Alert (bell).
\b Backspace.
\c Suppress trailing newline.
\e Escape.
\f Form feed.
\n Newline.
\r Carriage return.
\t Horizontal tab.
\v Vertical tab.
\\ Backslash.
\0NNN The eight-bit character whose value is the octal value NNN (zero to three octal digits).
\NNN The eight-bit character whose value is the octal value NNN (one to three octal digits).
\xHH The eight-bit character whose value is the hexadecimal value (one or two hexadecimal digits).

For more information about the printf command and the way it allows you to format output, see the Bash info pages. Keep in mind that there might be differences between different versions of Bash.

The read built-in command

The read built-in command is the counterpart of the echo and printf commands. The syntax of the read command is as follows:

read [options] NAME1 NAME2 ... NAMEN

One line is read from the standard input, or from the file descriptor supplied as an argument to the -u option. The first word of the line is assigned to the first name, NAME1, the second word to the second name, and so on, with leftover words and their intervening separators assigned to the last name, NAMEN. If there are fewer words read from the input stream than there are names, the remaining names are assigned empty values.

The characters in the value of the IFS variable are used to split the input line into words or tokens. The backslash character may be used to remove any special meaning for the next character read and for line continuation.

If no names are supplied, the line read is assigned to the variable REPLY.

The return code of the read command is zero, unless an end-of-file character is encountered, if read times out or if an invalid file descriptor is supplied as the argument to the -u option.

The following options are supported by the Bash read built-in:

Options to the read built-in

Option Meaning
-a ANAME The words are assigned to sequential indexes of the array variable ANAME, starting at 0. All elements are removed from ANAME before the assignment. Other NAME arguments are ignored.
-d DELIM The first character of DELIM is used to terminate the input line, rather than newline.
-e readline is used to obtain the line.
-n NCHARS read returns after reading NCHARS characters rather than waiting for a complete line of input.
-p PROMPT Display PROMPT, without a trailing newline, before attempting to read any input. The prompt is displayed only if input is coming from a terminal.
-r If this option is given, backslash does not act as an escape character. The backslash is considered to be part of the line. In particular, a backslash-newline pair may not be used as a line continuation.
-s Silent mode. If input is coming from a terminal, characters are not echoed.
-t TIMEOUT Cause read to time out and return failure if a complete line of input is not read within TIMEOUT seconds. This option has no effect if read is not reading input from the terminal or from a pipe.
-u FD Read input from file descriptor FD.

Redirection and file descriptors

As you know from basic shell usage, input and output of a command may be redirected before it is executed, using a special notation – the redirection operators – interpreted by the shell. Redirection may also be used to open and close files for the current shell execution environment.

Redirection can also occur in a script, so that it can receive input from a file, for instance, or send output to a file. Later, the user can review the output file, or it may be used by another script as input.

File input and output are accomplished by integer handles that track all open files for a given process. These numeric values are known as file descriptors. The best known file descriptors are stdin, stdout and stderr, with file descriptor numbers 0, 1 and 2, respectively. These numbers and respective devices are reserved. Bash can take TCP or UDP ports on networked hosts as file descriptors as well.

 

When excuting a given command, the following steps are excuted, in order:

  • If the standard output of a previous command is being piped to the standard input of the current command, then /proc/<current_process_ID>/fd/0 is updated to target the same anonymous pipe as /proc/<previous_process_ID/fd/1.
  • If the standard output of the current command is being piped to the standard input of the next command, then /proc/<current_process_ID>/fd/1 is updated to target another anonymous pipe.
  • Redirection for the current command is processed from left to right.
  • Redirection “N>&M” or “N<&M” after a command has the effect of creating or updating the symbolic link /proc/self/fd/N with the same target as the symbolic link /proc/self/fd/M.
  • The redirections “N> file” and “N< file” have the effect of creating or updating the symbolic link /proc/self/fd/N with the target file.
  • File descriptor closure “N>&-“ has the effect of deleting the symbolic link /proc/self/fd/N.
  • Only now is the current command executed.

Redirection of errors

When redirecting errors, note that the order of precedence is significant. For example, this command, issued in /var/spool

ls -l * 2> /var/tmp/unaccessible-in-spool

will redirect standard output of the ls command to the file unaccessible-in-spool in /var/tmp. The command

ls -l * > /var/tmp/spoollist 2>&1

will direct both standard input and standard error to the file spoollist. The command

ls -l * 2 >& 1 > /var/tmp/spoollist

directs only the standard output to the destination file, because the standard error is copied to standard output before the standard output is redirected.

For convenience, errors are often redirected to /dev/null, if it is sure they will not be needed. Hundreds of examples can be found in the startup scripts for your system.

Bash allows for both standard output and standard error to be redirected to the file whose name is the result of the expansion of FILE with this construct:

&> FILE

This is the equivalent of > FILE 2>&1, the construct used in the previous set of examples. It is also often combined with redirection to /dev/null, for instance when you just want a command to execute, no matter what output or errors it gives.

File input and output

Using /dev/fd

The /dev/fd directory contains entries named 0, 1, 2, and so on. Opening the file /dev/fd/N is equivalent to duplicating file descriptor N. If your system provides /dev/stdin, /dev/stdout and /dev/stderr, you will see that these are equivalent to /dev/fd/0, /dev/fd/1 and /dev/fd/2, respectively.

The main use of the /dev/fd files is from the shell. This mechanism allows for programs that use pathname arguments to handle standard input and standard output in the same way as other pathnames. If /dev/fd is not available on a system, you’ll have to find a way to bypass the problem. This can be done for instance using a hyphen () to indicate that a program should read from a pipe

example:

localhost ~> filter body.txt.gz | cat header.txt - footer.txt
This text is printed at the beginning of each print job and thanks the sysadmin
for setting us up such a great printing infrastructure.

Text to be filtered.

This text is printed at the end of each print job.

Read and exec

Assigning file descriptors to files

Another way of looking at file descriptors is thinking of them as a way to assign a numeric value to a file. Instead of using the file name, you can use the file descriptor number. The exec built-in command can be used to replace the shell of the current process or to alter the file descriptors of the current shell. For example, it can be used to assign a file descriptor to a file. Use

exec fdN> file

for assigning file descriptor N to file for output, and

exec fdN< file

for assigning file descriptor N to file for input. After a file descriptor has been assigned to a file, it can be used with the shell redirection operators, as is demonstrated in the following example:

localhost ~> exec 4> result.txt

localhost ~> filter body.txt | cat header.txt /dev/fd/0 footer.txt >& 4

localhost ~> cat result.txt
This text is printed at the beginning of each print job and thanks the sysadmin
for setting us up such a great printing infrastructure.

Text to be filtered.

This text is printed at the end of each print job.

Closing file descriptors

Since child processes inherit open file descriptors, it is good practice to close a file descriptor when it is no longer needed. This is done using the

exec fd<&-

syntax. In the above example, file descriptor 7, which has been assigned to standard input, is closed each time the user needs to have access to the actual standard input device, usually the keyboard.

The following is a simple example redirecting only standard error to a pipe:

localhost ~> cat listdirs.sh
#!/bin/bash

# This script prints standard output unchanged, while standard error is 
# redirected for processing by awk.

INPUTDIR="$1"

# fd 6 targets fd 1 target (console out) in current shell
exec 6>&1

# fd 1 targets pipe, fd 2 targets fd 1 target (pipe),
# fd 1 targets fd 6 target (console out), fd 6 closed, execute ls
ls "$INPUTDIR"/* 2>&1 >&6 6>&- \
				# Closes fd 6 for awk, but not for ls.

| awk 'BEGIN { FS=":" } { print "YOU HAVE NO ACCESS TO" $2 }' 6>&-

# fd 6 closed for current shell
exec 6>&-

Now you should be able to build interactive bash scripts.

Comments are closed.