Writing Simple Scripts

bash command lines can get to be very long, especially when pipes are used. A script is a text file that contains shell commands that may itself be executed as a command, providing an easy way to reuse complex sequences of commands. In fact, bash provides a complete programming language for use in scripts.

How Do I Do That?

To create a script, simply place commands in a text file. For example, this script will display the ten largest files in the current directory:

ls -lS | tail -n +2 | head -10

Save this file as topten. In order to run the script, you will need to set read and execute permission:

$ chmod a+rx 
               
                  topten
               

The script can be executed by specifying the directory and filename (or an absolute pathname):

$ ./topten
-rw-r--r--  1 root root    807103 Jul 12 21:18 termcap
-rw-r--r--  1 root root    499861 Jul 17 08:08 prelink.cache
-rw-r--r--  1 root root    362031 Feb 23 08:09 services
-rw-r--r--  1 root root     97966 Jul 15 11:19 ld.so.cache
-rw-r--r--  1 root root     92794 Jul 12 12:46 Muttrc
-rw-r--r--  1 root root     83607 Mar 23 07:23 readahead.files
-rw-r--r--  1 root root     73946 Jul 13 02:23 sensors.conf
-rw-r--r--  1 root root     45083 Jul 12 18:33 php.ini
-rw-r--r--  1 root root     30460 Jul 13 20:36 jwhois.conf
-rw-r--r--  1 root root     26137 Mar 23 07:23 readahead.early.files

The directory name is required because the current directory (.) is not in the list of directories normally searched for commands (called the PATH). To make your script accessible to all users, move it to the /usr/local/bin directory, which appears by default in everyone’s PATH:

# mv 
               
                  topten
               
                /usr/local/bin

Shell and environment variables

bash uses shell variables to keep track of current settings. These shell variables are private to the shell and are not passed to processes started by the shell—but they can be exported, which converts them into environment variables, which are passed to child processes.

You can view all shell and environment variables using the set command:

$ set
BASH=/bin/bash
BASH_ARGC=()
BASH_ARGV=()
BASH_LINENO=()
BASH_SOURCE=()
BASH_VERSINFO=([0]="3" [1]="1" [2]="17" [3]="1" [4]="release" [5]="i686-redhat-linux-gnu")
BASH_VERSION='3.1.17(1)-release'
COLORS=/etc/DIR_COLORS.xterm
COLORTERM=gnome-terminal
COLUMNS=172
CVS_RSH=ssh
DBUS_SESSION_BUS_ADDRESS=unix:abstract=/tmp/dbus-I4CWWfqvE6,guid=e202bd44a31ea8366b20151327662e00
DESKTOP_SESSION=default
DESKTOP_STARTUP_ID=
DIRSTACK=()
DISPLAY=:0.0
EUID=503
GDMSESSION=default
GDM_XSERVER_LOCATION=local
GNOME_DESKTOP_SESSION_ID=Default
GNOME_KEYRING_SOCKET=/tmp/keyring-FJyfaw/socket
GROUPS=()
GTK_RC_FILES=/etc/gtk/gtkrc:/home/hank/.gtkrc-1.2-gnome2
G_BROKEN_FILENAMES=1
HISTFILE=/home/hank/.bash_history
HISTFILESIZE=1000
HISTSIZE=1000
HOME=/home/hank
HOSTNAME=bluesky.fedorabook.com
HOSTTYPE=i686
IFS=$' \t\n'
INPUTRC=/etc/inputrc
KDEDIR=/usr
KDE_IS_PRELINKED=1
LANG=en_US.UTF-8
LESSOPEN='|/usr/bin/lesspipe.sh %s'
LINES=55
LOGNAME=hank
LS_COLORS='no=00:fi=00:di=00;34:ln=00;36:pi=40;33:so=00;35:bd=40;33;01:cd=40;33;01:or=01;05;37;41:mi=01;05;37;41:ex=00;32:*.cmd=00;32:*.exe=00;32:*.com=00;32:*.btm=00;32:*.bat=00;32:*.sh=00;32:*.csh=00;32:*.tar=00;31:*.tgz=00;31:*.arj=00;31:*.taz=00;31:*.lzh=00;31:*.zip=00;31:*.z=00;31:*.Z=00;31:*.gz=00;31:*.bz2=00;31:*.bz=00;31:*.tz=00;31:*.rpm=00;31:*.cpio=00;31:*.jpg=00;35:*.gif=00;35:*.bmp=00;35:*.xbm=00;35:*.xpm=00;35:*.png=00;35:*.tif=00;35:'
MACHTYPE=i686-redhat-linux-gnu
MAIL=/var/spool/mail/hank
MAILCHECK=60
OLDPWD=/usr/share/wallpapers
OPTERR=1
OPTIND=1
OSTYPE=linux-gnu
PATH=/usr/lib/qt-3.3/bin:/usr/kerberos/bin:/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/home/hank/bin
PIPESTATUS=([0]="0" [1]="141" [2]="0")
PPID=3067
PRELINKING=yes
PRELINK_FULL_TIME_INTERVAL=14
PRELINK_NONRPM_CHECK_INTERVAL=7
PRELINK_OPTS=-mR
PROMPT_COMMAND='echo -ne "\033]0;${USER}@${HOSTNAME%%.*}:${PWD/#$HOME/~}"; echo -ne "\007"'
PS1='$ '
PS2='> '
PS4='+ '
PWD=/etc
QTDIR=/usr/lib/qt-3.3
QTINC=/usr/lib/qt-3.3/include
QTLIB=/usr/lib/qt-3.3/lib
SESSION_MANAGER=local/beige.fedorabook.com:/tmp/.ICE-unix/2621
SHELL=/bin/bash
SHELLOPTS=braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor
SHLVL=2
SSH_AGENT_PID=2659
SSH_ASKPASS=/usr/libexec/openssh/gnome-ssh-askpass
SSH_AUTH_SOCK=/tmp/ssh-dNhrfX2621/agent.2621
TERM=xterm
UID=503
USER=hank
USERNAME=hank
WINDOWID=58721388
XAUTHORITY=/home/hank/.Xauthority
_=
qt_prefix=/usr/lib/qt-3.3

Many of these variables contain settings for particular programs. Some of the common variables used by many programs are shown in Table 4-16.

Table 4-16. Key environment variables

NamePurpose Format
                                 DISPLAY
Information on which X display is being used hostname:display.screen hostname is the hostname or IP address of the X server or blank for the local host, display is the display number, and screen is the screen number (optional; the screen number specifies the monitor in a multimonitor, single-person display configuration).
                                 HOME
Home directoryAbsolute pathname of the user’s home directory.
                                 HOSTNAME
Name of this computerFully qualified domain name of the local host.
                                 MAIL
Location of the user’s default mailboxAbsolute pathname of the user’s mailbox (usually /var/spool/mail/<username>).
                                 PATH
List of directories to be searched to find a commandAbsolute pathnames of directories to be searched, separated by colons.
PS1, PS2 Primary and secondary shell promptsPlain text. Special characters sequences consisting of \\ and a letter are replaced with other information; for example, \\w is replaced by the current working directory (see the manpage for bash for a complete list).
                                 TERM
Model number of the current terminalMust correspond to a filename in /usr/share/terminfo/?/*.

To set a shell variable, type the variable name, an equal sign, and the value you wish to assign (all values are treated as text):

$ A=
                  
                     red
                  

Once a variable has been assigned a value, you can use it in commands, preceded by a dollar sign:

$ ls -l 
                  
                     red
                  
ls: red: No such file or directory
$ touch $A
$ ls -l red
-rw-r--r-- 1 hank hank 0 Jul 18 15:26 red

The echo command can be used to view the value of a variable:

$ echo $A
red

To destroy a variable, use the unset command:

$ echo $A
red
$ unset A
$ echo $A
$

Finally, to make a variable accessible to processes started by the current process, use the export command:

$ unset A
$ TEST=blue
$ echo $TEST                    # variable is known to the shell
blue
$ bash                          # start a child shell
[hank@beige foo]$ echo $TEST    # variable is not known to child

[hank@beige foo]$ exit        # exit back to parent shell
exit
$ export 
                  TEST                   # export the variable
$ echo 
                  $TEST                    # value is still known to the shell
blue
$ bash                          # start a new child shell
[hank@beige foo]$ echo $TEST    # exported value is known to the child
blue

The PATH value is stored in an environment variable of the same name. Its value can be viewed like any other environment variable:

$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin

To add a directory to the existing directories, use $PATH on the righthand side of an assignment to insert the current value of the variable into the new value:

$ PATH=$PATH:
                  
                     /home/hank/bin
                  
$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/home/hank/bin

Tip

You don’t need to export PATH in this case because it has already been exported; assigning a new value does not changes its exported status.

Assuming that the topten script is saved in /home/hank/bin, you can now execute it by just typing its name:

$ topten
-rw-r--r--  1 root root    807103 Jul 12 21:18 termcap
-rw-r--r--  1 root root    499861 Jul 17 08:08 prelink.cache
-rw-r--r--  1 root root    362031 Feb 23 08:09 services
-rw-r--r--  1 root root     97966 Jul 15 11:19 ld.so.cache
-rw-r--r--  1 root root     92794 Jul 12 12:46 Muttrc
-rw-r--r--  1 root root     83607 Mar 23 07:23 readahead.files
-rw-r--r--  1 root root     73946 Jul 13 02:23 sensors.conf
-rw-r--r--  1 root root     45083 Jul 12 18:33 php.ini
-rw-r--r--  1 root root     30460 Jul 13 20:36 jwhois.conf
-rw-r--r--  1 root root     26137 Mar 23 07:23 readahead.early.files

Within a script, you can prompt the user using the echo command, and then use the read command to read a line from the user and place it in an environment variable:

echo "Please enter your name:"
read NAME
echo "Hello $NAME!"

Or you can collect the standard output of a command and assign it to a variable using the $() symbols:

$ NOW=$(date)
$ echo $NOW
Tue Jul 18 22:25:48 EDT 2006

Special variables

There are several special parameters, or special variables, that bash sets automatically; Table 4-17 contains a list of the most important ones.

Table 4-17. Important special variables

NameDescriptionNotes
$$
Process ID of the shellSince process IDs are unique (at any one point in time), this can be used to ensure unique filenames (e.g., /tmp/$$.txt will never conflict with the same filename used by another copy of the same script).
$0
Name of the scriptUseful to generate error messages, and when one script is invoked through more than one name.
$1, $2, $3, ...
Arguments given on the script’s command lineThe shift command will eliminate $1 and then shift all of the parameters accordingly ($2 becomes $1, $3 becomes $2, and so forth).
$#
Number of arguments from the script’s command lineIf $# is 0, then no options were given on the command line.
$*
$@
All of the arguments from the script’s command lineWhen quoted, "$*" becomes a single block of text containing all of the arguments, while "$@" becomes separate words. If the script is called with the arguments "green" and "yellow", then "$*" would evaluate to "green yellow”, while "$@" would evaluate to "green" "yellow".
$?
Exit status of the last commandManpages document the possible exit-status values for most commands.

Control structures

Like most programming languages, bash features a number of control structures to enable looping and conditional execution. The three most common control structures are listed in Table 4-18; there is also a C-style for loop that I’ll discuss in the next section.

Table 4-18. Common bash control structures

StructureNotesExample
                                 for variable in list
do
   loop-commands
done
The variable is assigned the first value in list, and loop-commands are executed. The process is then repeated for all of the other values in list.
# Set X to 'hosts', then
# display the filename
# and file contents. 
# Repeat for 'services'
for X in hosts services
do
  echo "==== $X"
  cat /etc/$X
done
                                 if control-command
then
   if-commands
[else
   else-commands]
fi
If the control-command succeeds, the if-commands are executed; otherwise, the else-commands are executed.
# Tell the user if
# the text 'test'
# appears in file1
if grep -q test file1
then
  echo "Found it!"
else
  echo "Not found."
fi
                                 while control-command
do
   loop-commands
done
As long as control-command executes successfully, loop-commands are repeated.
# Display the free
# disk space every
# 2 seconds, forever
while sleep 2
do
  df -h
done

The for..in control structure is great for looping over a range of values. This loop will display the status of the httpd, ftpd, and NetworkManager services:

for SERVICE in httpd ftpd NetworkManager
do
    /sbin/service $SERVICE status
done

for...in is even more useful when the list of values is specified as an ambiguous filename. In this script, the loop is repeated once for each file in the directory /etc/ that ends in .conf:

mkdir backup
for FILE in /etc/*.conf
do
    echo "Backing up the file $FILE..."
    cp $FILE backup/
done

For the if and while control structures, a control-command determines the action taken. The control-command can be any command on the system; an exit status of zero is considered true and any other exit status is considered false.

For example, the grep command exits with a value of zero if a given pattern is found in the file(s) specified or in the standard input. When combined with an if structure, you can cause a program to take a particular action if a pattern is found. For example, this code displays the message “Helen is logged in!” if the output of who contains the word helen:

if who | grep -q helen
then
    echo "Helen is logged in!"
fi

Tip

The exit status of the last command is taken as the exit status of a pipeline, which is grep in this case. The -q argument to grep suppresses the output—otherwise, matching lines are sent to standard output.

The built-in command test can be used to test conditions; the exit status will be zero if the condition is true. The most common conditional expressions are listed in Table 4-19.

Table 4-19. Common bash conditional operators

OperatorTests whether...Example using an environment variable
-f file
File exists and is a regular file
-f "$A"
-d file
File exists and is a directory
-d "$B"
-r file
File exists and is readable
-r "$C"
-w file
File exists and is writable
-w "$D"
-x file
File exists and is executable
-x "$E"
                                 value1 == value2
Strings match
"$F" == "red"
                                 value1 != value2
Strings don’t match
"$G" != "blue"
                                 value1 -eq value2
Integer values are equal
"$H" -eq 2
                                 value1 -ne value2
Integer values are unequal
"$J" -ne 10
                                 value1 -gt value2
value1 integer value is greater than value2
"$K" -gt 25
                                 value1 -ge value2
value1 integer value is greater than or equal to value2
"$L" -ge 25
                                 value1 -lt value2
value1 integer value is less than value2
"$M" -lt 75
                                 value1 -le value2
value1 integer value is less than or equal to value2
"$N" -le 75
                                 expression1 -a expression2
expression1 and expression2 are both true
"$P" -gt 36 -a "$P" -lt 71
                                 expression1 -o expression2
expression1 or expression2 (or both) are true
"$P" -lt 12 -o "$P" -eq 50

So if you wanted to print “Too high!” if the value of the variable A was over 50, you would write:

if test "$A" -gt 50 
then
    echo "Too high!"
fi

The variable expression $A is quoted in case A has a null value (“”) or doesn’t exist—in which case, if unquoted, a syntax error would occur because there would be nothing to the left of -gt.

The square brackets ([]) are a synonym for test, so the previous code is more commonly written:

if [ "$A" -gt 50 ]
then
    echo "Too high!"
fi

You can also use test with the while control structure. This loop monitors the number of users logged in, checking every 15 seconds until the number of users is equal to or greater than 100, when the loop will exit and the following pipeline will send an email to the email alias alert:

while [ "$(who | wc -l)" -lt 100 ]
do
    sleep 15
done
echo "Over 100 users are now logged in!"|mail -s "Overload!" alert

Integer arithmetic

bash provides very limited integer arithmetic capabilities. An expression inside double parentheses (( )) is interpreted as a numeric expression; an expression inside double parentheses preceded by a dollar sign $(( )) is interpreted as a numeric expression that also returns a value.

Tip

Inside double parentheses, you can read a variable’s value without using the dollar sign (use A=B+C instead of A=$B+$C).

Here’s an example using a while loop that counts from 1 to 20 using integer arithmetic:

A=0
while [ "$A" -lt 20 ]
do
        (( A=A+1 ))
        echo $A
done

The C-style increment operators are available, so this code could be rewritten as:

A=0
while [ "$A" -lt 20 ]
do
        echo $(( ++A ))
done

The expression $(( ++A )) returns the value of A after it is incremented. You could also use $(( A++ )), which returns the value of A before it is incremented:

A=1
while [ "$A" -le 20 ]
do
        echo $(( A++ ))
done

Since loops that count through a range of numbers are often needed, bash also supports the C-style for loop. Inside double parentheses, specify an initial expression, a conditional expression, and a per-loop expression, separated by semicolons:

# Initial value of A is 1
# Keep looping as long as A<=20
# Each time you loop, increment A by 1
for ((A=1; A<=20; A++))
do
        echo $A
done

Note that the conditional expression uses normal comparison symbols (<=) instead of the alphabetic options (-le) used by test.

Tip

Don’t confuse the C-style for loop with the for...in loop!

Making your scripts available to users of other shells

So far we have been assuming that the user is using the bash shell; if the user of another shell (such as tcsh) tries to execute one of your scripts, it will be interpreted according to the language rules of that shell and will probably fail.

To make your scripts more robust, add a shebang line at the beginning—a pound-sign character followed by an exclamation mark, followed by the full path of the shell to be used to interpret the script (/bin/bash):

#!/bin/bash
# script to count from 1 to 20

for ((A=1; A<=20; A++))
do
        echo $A
done

I also added a comment line (starting with #) after the shebang line to describe the function of the script.

Tip

The shebang line gets its name from sharp and bang, common nicknames for the #! characters.

An example

Here is an example of a longer script, taking advantage of some of the scripting features in bash:

#!/bin/bash
#
# number-guessing game
# 

# If the user entered an argument on the command
# line, use it as the upper limit of the number 
# range.
if [ "$#" -eq 1 ]
then
        MAX=$1
else
        MAX=100
fi

# Set up other variables
SECRET=$(( (RANDOM % MAX) + 1 )) # Random number 1-100
TRIES=0
GUESS=-1

# Display initial messages
clear
echo "Number-guessing Game"
echo "--------------------"
echo
echo "I have a secret number between 1 and $MAX."

# Loop until the user guesses the right number
while [ "$GUESS" -ne "$SECRET" ]
do

        # Prompt the user and get her input
        ((TRIES++))
        echo -n "Enter guess #$TRIES: "
        read GUESS

        # Display low/high messages
        if [ "$GUESS" -lt "$SECRET" ]
        then
                echo "Too low!"
        fi

        if [ "$GUESS" -gt "$SECRET" ]
        then
                echo "Too high!"
        fi

done

# Display final messages
echo
echo "You guessed it!"
echo "It took you $TRIES tries."
echo

This script could be saved as /usr/local/bin/guess-it and then made executable:

# chmod a+rx 
                  
                     /usr/local/bin/guess-it
                  

Here’s a test run of the script:

$ guess-it
Number-guessing Game
--------------------

I have a secret number between 1 and 100.
Enter guess #1: 
                     50
                  
Too low!
Enter guess #2: 
                     75
                  
Too low!
Enter guess #3: 
                     83
                  
Too low!
Enter guess #4: 
                     92
                  
Too high!
Enter guess #5: 
                     87
                  
Too high!
Enter guess #6: 
                     85
                  
Too low!
Enter guess #7: 
                     86
                  

You guessed it!
It took you 7 tries.

Another test, using an alternate upper limit:

$ guess-it 50
Number-guessing Game
--------------------

I have a secret number between 1 and 50.
Enter guess #1: 
                     25
                  
Too low!
Enter guess #2: 
                     37
                  
Too low!
Enter guess #3: 
                     44
                  
Too high!
Enter guess #4: 
                     40
                  

You guessed it!
It took you 4 tries.

Login and initialization scripts

When a user logs in, the system-wide script /etc/profile and the per-user script ~/.bash_profile are both executed. This is the default /etc/profile:

# /etc/profile

# System wide environment and startup programs, for login setup
# Functions and aliases go in /etc/bashrc

pathmunge () {
        if ! echo $PATH | /bin/egrep -q "(^|:)$1($|:)" ; then
           if [ "$2" = "after" ] ; then
              PATH=$PATH:$1
           else
              PATH=$1:$PATH
           fi
        fi
}

# ksh workaround
if [ -z "$EUID" -a -x /usr/bin/id ]; then 
        EUID=\Qid -u\Q
        UID=\Qid -ru\Q
fi

# Path manipulation
if [ "$EUID" = "0" ]; then
        pathmunge /sbin
        pathmunge /usr/sbin
        pathmunge /usr/local/sbin
fi

# No core files by default
ulimit -S -c 0 > /dev/null 2>&1

if [ -x /usr/bin/id ]; then
        USER="\Qid -un\Q"
        LOGNAME=$USER
        MAIL="/var/spool/mail/$USER"
fi

HOSTNAME=\Q/bin/hostname\Q
HISTSIZE=1000

if [ -z "$INPUTRC" -a ! -f "$HOME/.inputrc" ]; then
    INPUTRC=/etc/inputrc
fi

export PATH USER LOGNAME MAIL HOSTNAME HISTSIZE INPUTRC

for i in /etc/profile.d/*.sh ; do
    if [ -r "$i" ]; then
        . $i
    fi
done

unset i
unset pathmunge

This script adds /sbin, /usr/sbin, and /usr/local/sbin to the PATH if the user is the root user. It then creates and exports the USER, LOGNAME, MAIL, HOSTNAME, and HISTSIZE variables, and executes any files in /etc/profile.d that end in .sh.

The default ~/.bash_profile looks like this:

# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/bin

export PATH

You can edit /etc/profile to change the login process for all users, or ~/.bash_profile to change just your login process. One useful change that I make to every Fedora system I install is to comment out the if statements for path manipulation in /etc/profile so that every user has the superuser binary directories in his path:

# Path manipulation
#if [ "$EUID" = "0" ]; then
        pathmunge /sbin
        pathmunge /usr/sbin
        pathmunge /usr/local/sbin
#fi

Tip

bash comments start with # and are not executed—so commenting out code means adding # at the start of selected lines to disable them.

Environment variables are inherited by child processes, so any environment variables set up during the login process are accessible to all shells (and other programs) you start. bash also supports the use of aliases, or nicknames, for commands, but since these are not inherited by child processes, they are instead placed in the file ~/.bashrc, which is executed each time a shell starts. If you log in once and then start three shells, ~/.bash_profile is executed once at login and ~/.bashrc is executed three times, once for each shell that starts.

This is the default ~/.bashrc:

# .bashrc

# Source global definitions
if [ -f /etc/bashrc ]; then
        . /etc/bashrc
fi

# User-specific aliases and functions

As you can see, there aren’t any alias definitions in there (but you can add them). The file /etc/bashrc is invoked by this script, and it contains common aliases made available to all users:

# System-wide functions and aliases
# Environment stuff goes in /etc/profile

# By default, we want this to get set.
# Even for noninteractive, nonlogin shells.
umask 022

# Are we an interactive shell?
if [ "$PS1" ]; then
    case $TERM in
        xterm*)
                if [ -e /etc/sysconfig/bash-prompt-xterm ]; then
                        PROMPT_COMMAND=/etc/sysconfig/bash-prompt-xterm
                else
                PROMPT_COMMAND='echo -ne ↵
                    "\033]0;${USER}@${HOSTNAME%%.*}:${PWD/#$HOME/~}"; 
                    echo -ne "\007"'
                fi
                ;;
        screen)
                if [ -e /etc/sysconfig/bash-prompt-screen ]; then
                        PROMPT_COMMAND=/etc/sysconfig/bash-prompt-screen
                else
                PROMPT_COMMAND='echo -ne "\033_${USER}@${HOSTNAME%%.*}:${PWD/#$HOME/~}"; echo -ne "\033\\"'
                fi
                ;;
        *)
                [ -e /etc/sysconfig/bash-prompt-default ] && PROMPT_COMMAND=/etc/sysconfig/bash-prompt-default
            ;;
    esac
    # Turn on checkwinsize
    shopt -s checkwinsize
    [ "$PS1" = "\\s-\\v\\\$ " ] && PS1="[\u@\h \W]\\$ "
fi

if ! shopt -q login_shell ; then # We're not a login shell
        # Need to redefine pathmunge, it get's undefined at the end of /etc/profile
    pathmunge () {
                if ! echo $PATH | /bin/egrep -q "(^|:)$1($|:)" ; then
                        if [ "$2" = "after" ] ; then
                                PATH=$PATH:$1
                        else
                                PATH=$1:$PATH
                        fi
                fi
        }

        for i in /etc/profile.d/*.sh; do
                if [ -r "$i" ]; then
                        . $i
        fi
        done
        unset i
        unset pathmunge
fi
# vim:ts=4:sw=4

This script sets up the umask, configures a command that will be executed before the display of each prompt (which sets the terminal-window title to show the user, host, and current directory), and then executes each of the files in /etc/profile.d that end in .sh.

Packages installed on your Fedora system can include files that are placed in /etc/profile.d, providing a simple way for each package to globally add aliases or other shell configuration options. There are a few command aliases defined in these script files, including:

alias l.='ls -d .* --color=tty'
alias ll='ls -l --color=tty'
alias ls='ls --color=tty'
alias vi='vim'

If you type ll at a command prompt, ls -l will be executed, due to the alias highlighted in the preceding listing:

$ ll /
total 138
drwxr-xr-x   2 root root  4096 Jul 17 08:08 bin
drwxr-xr-x   4 root root  1024 Jul 15 11:16 boot
drwxr-xr-x  12 root root  3900 Jul 19 07:56 dev
drwxr-xr-x 102 root root 12288 Jul 18 18:14 etc
drwxr-xr-x   8 root root  4096 Jul 16 22:51 home
drwxr-xr-x  11 root root  4096 Jul 17 07:58 lib
drwx------   2 root root 16384 Jun  9 19:34 lost+found
drwxr-xr-x   4 root root  4096 Jul 18 18:14 media
drwxr-xr-x   2 root root     0 Jul 18 11:48 misc
drwxr-xr-x   6 root root  4096 Jul 15 11:38 mnt
drwxr-xr-x   2 root root     0 Jul 18 11:48 net
drwxr-xr-x   2 root root  4096 Jul 12 04:48 opt
dr-xr-xr-x 126 root root     0 Jul 18 11:46 proc
drwxr-x---   9 root root  4096 Jul 18 00:18 root
drwxr-xr-x   2 root root 12288 Jul 17 08:08 sbin
drwxr-xr-x   4 root root     0 Jul 18 11:46 selinux
drwxr-xr-x   2 root root  4096 Jul 12 04:48 srv
drwxr-xr-x  11 root root     0 Jul 18 11:46 sys
drwxrwxrwt  98 root root  4096 Jul 19 11:04 tmp
drwxr-xr-x  14 root root  4096 Jul 14 04:17 usr
drwxr-xr-x  26 root root  4096 Jul 14 04:17 var

Similarly, if you type vi the shell will execute vim.

You can create your own aliases using the alias command; for example, I like to use l for ls -l, sometimes use cls to clear the screen, and like to have machine report the hostname (old habits):

$ alias l='ls -l
$ alias cls='clear'
$ alias machine='hostname'

Adding the same lines to ~/.bashrc will make them available every time you start a new shell; adding them to ~/.bashrc will make them available to all users.

You can see the currently defined aliases by typing alias alone as a command:

$ alias
alias cls='clear'
alias l='ll'
alias l.='ls -d .* --color=tty'
alias ll='ls -l --color=tty'
alias ls='ls --color=tty'
alias machine='hostname'
alias vi='vim'

To destroy an alias, use the unalias command:

$ unalias 
                  
                     machine
                  
$ alias
alias cls='clear'
alias l='ll'
alias l.='ls -d .* --color=tty'
alias ll='ls -l --color=tty'
alias ls='ls --color=tty'
alias vi='vim'

How Does It Work?

When the kernel receives a request to execute a file (and that file is executable), it uses magic number codes at the start of the file to determine how to execute it. For example, there are magic numbers for standard Executable and Linking Format (ELF) binaries and historical assembler output (a.out) binaries; the kernel will use them to set up the correct execution environment and then start the program.

If the first two bytes of the file are #!, which counts as a magic number, the file is treated as a script: a pathname is read from the file starting at the third byte and continuing to the end of the first line. The shell or interpreter program identified by this pathname is executed, and the script name and all arguments are passed to the interpreter.

If a file has no magic number or shebang line, the kernel will attempt to execute it as though the value of the SHELL environment variable were given on the shebang line.

What About...

...interacting with the user through the graphical user interface?

Other scripting languages such as Perl and Python can be used to construct full-scale GUI applications, but the zenity program enables a shell script to interact with a GUI user.

zenity presents a simple dialog or information box to the user. There are a number of dialog types available, including information and error boxes, text entry and editing boxes, and date-selection boxes; the type of dialog as well as the messages that appear in the dialog are configured by zenity options.

Here is the number-guessing script rewritten to use zenity for the user interface:

#!/bin/bash
#
# number-guessing game - GUI version
# 

# If the user entered an argument on the command
# line, use it as the upper limit of the number 
# range
if [ "$#" -eq 1 ]
then
        MAX=$1
else
        MAX=100
fi

# Set up other variables
SECRET=$(( (RANDOM % MAX) + 1 )) # Random number 1-100
TRIES=0
GUESS=-1

# Display initial messages
zenity --info --text \
"I have a secret number between 1 and $MAX. Try and guess it!" \
--title "Guess-It"

# Loop until the user guesses the right number
while [ "$GUESS" -ne "$SECRET" ]
do

        # Prompt the user and get her input
        ((TRIES++))
        GUESS=$(zenity --entry --text "Enter guess #$TRIES:" --title "Guess...")

        # Display low/high messages
        if [ "$GUESS" -lt "$SECRET" ]
        then
                zenity --info --text "Too low!"
        fi

        if [ "$GUESS" -gt "$SECRET" ]
        then
                zenity --info --text "Too high!"
        fi

done

# Display final messages
zenity --info --text "You guessed it! It took you $TRIES tries." --title "Congratulations!"

Figure 4-16 shows the zenity dialogs produced by this script. Obviously, this user interface is not as refined as one that could be provided by a full-featured GUI application, but it is perfectly suitable for simple interactions.

zenity dialogs

Figure 4-16. zenity dialogs

Where Can I Learn More?

  • The manpages for bash, chmod, and zenity

Get Fedora Linux now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.