Overview

In this tutorial, learn to customize your Linux bash shell environment to meet user needs. Learn to:

  • Modify global and user profiles
  • Set environment variables when you log in or spawn a new shell
  • Create bash functions for frequently used sequences of commands
  • Maintain skeleton directories for new user accounts
  • Set your command search path

Linux shells

You use a Linux shell program to interact with your system by typing commands (the input stream) at a terminal and seeing output (the output stream) and error messages (the error stream) on the same terminal. Sometimes you need to run commands before the system has booted far enough to allow terminal connections, and sometimes you need to run commands periodically, whether you are logged in. A shell can do these tasks for you, too. The standard input and output streams do not have to come from, or be directed to, a real user at a terminal. In this tutorial, you learn more about shells and customizing the user’s environment. In particular, you learn about the bash (Bourne again) shell — an enhancement of the original Bourne shell — and about changes to bash that make it more compliant with the Portable Operating System Interface (POSIX). I point out features of some other shells along the way.

This tutorial helps you prepare for Objective 105.1 in Topic 105 of the Linux Server Professional (LPIC-1) exam 102. The objective has a weight of 4.

Prerequisites

To get the most from the tutorials in this series, you need a basic knowledge of Linux and a working Linux system on which you can practice the commands covered in this tutorial. You should be familiar with GNU and UNIX commands. This tutorial builds on material covered in the tutorials for Topic 103 of exam 101. Sometimes different versions of a program format output differently, so your results might not always look exactly like the listings and figures shown here.

The examples in this tutorial are largely distribution independent. Unless otherwise noted, the examples use Fedora 22, with a 4.2.3 kernel.

Shells and environments

A shell provides a layer between you and the intricacies of an operating system. With Linux (and UNIX) shells, you can build complex operations by combining basic functions. Using programming constructs, you can then build functions for direct execution in the shell or save functions as shell scripts. A shell script is a sequence of commands that is stored in a file that the shell can run as needed.

POSIX is a series of IEEE standards collectively referred to as IEEE 1003. (The first POSIX standard was IEEE Standard 1003.1-1988, released in 1988.) Bash implements many POSIX features and can run in a mode that complies even more closely with POSIX. Other well-known shells include the Korn shell (ksh), the C shell (csh) and its derivative tcsh, and the Almquist shell (ash) and its Debian derivative (dash). I recommend that you know at least enough about these other shells to recognize when a script uses features from any of them.

Recall from the tutorial “Learn Linux, 101: The Linux command line” that when you are running in a bash shell, you have a shell environment. The environment is a set of name-value pairs that define the form of your prompt, your home directory, your working directory, the name of your shell, files that you have opened, functions that you have defined, and more. The environment is available to every shell process. When a shell starts, it assigns values from the environment to shell variables. You can use shells, including bash, to create and modify additional shell variables. You can then export such variables to be part of the environment that is inherited by child processes that you spawn from your current shell.

Shell variables have a name (or identifier). Table 1 shows a few common bash environment variables that are usually set for you automatically.

Table 1. Common bash environment variables
Name Purpose
USER The name of the logged-in user
UID The numeric user ID of the logged-in user
HOME The user’s home directory
PWD The current working directory
SHELL The name of the shell
PPID The parent process PID — the process ID of the process that started this process

In addition to variables, the shell has some special parameters that are set but cannot be modified. Table 2 shows some examples.

Table 2. Common bash parameters
Name Purpose
$ The process ID (or PID of the running bash shell [or other] process)
? The exit code of the last command
0 The name of the shell or shell script

Using variables

You use the value of a variable by prefixing its name with a $, as shown in Listing 1.

Listing 1. Using variable values

[ian@atticf22 ~]$ echo $UID
1000
[ian@atticf22 ~]$ echo $HOME
/home/ian

Setting variable values and making them available

You create or set a shell variable in the bash shell by typing a name followed immediately by an equals sign (=) with no intervening space. Variable names are words consisting only of alphanumeric characters and underscores. The names begin with an alphabetic character or an underscore. Variables names are case sensitive, so var1 and VAR1 are different variables. Variable names, particularly for exported variables, are often uppercase, like the examples in Table 1, but this is a convention and not a requirement. Some variables, such as $$ and $?, are really shell parameters rather than variables — they can only be referenced; you cannot assign a value to them.

Shell variables are visible only to the process in which you create them unless you export them so that child processes can see and use them. A child process cannot export a variable to a parent process. Use the export command to export variables. In the bash shell, you can assign and export in one step, but not all shells support this capability.

A good way to explore these concepts is to create child processes by using another shell. You can use the ps command to help you keep track of where you are and what’s running. Listing 2 shows some examples with inline comments to help you.

Listing 2. Setting shell variables and exporting them to the environment

[ian@atticf22 ~]$ #Use the ps command to list current PID, parent PID and running command name
[ian@atticf22 ~]$ ps ‑p $$ ‑o "pid ppid cmd"
  PID  PPID CMD
12761  9169 bash
[ian@atticf22 ~]$ #Start a child bash process
[ian@atticf22 ~]$ bash
[ian@atticf22 ~]$ #Assign two variables
[ian@atticf22 ~]$ VAR1=v1
[ian@atticf22 ~]$ VAR2="Variable 2"
[ian@atticf22 ~]$ #Export examples
[ian@atticf22 ~]$ export VAR2
[ian@atticf22 ~]$ export VAR3="Assigned and exported in one step"
[ian@atticf22 ~]$ #Use the $ character to reference the variables
[ian@atticf22 ~]$ echo $VAR1 '/' $VAR2 '/' $VAR3
v1 / Variable 2 / Assigned and exported in one step
[ian@atticf22 ~]$ #What is the value of the SHELL variable?
[ian@atticf22 ~]$ echo $SHELL
/bin/bash
[ian@atticf22 ~]$ #Now start ksh child and export VAR4
[ian@atticf22 ~]$ ksh
$ ps ‑p $$ ‑o "pid ppid cmd"
  PID  PPID CMD
26212 22923 ksh
$ export VAR4=var4
$ #See what is visible
$ echo $VAR1 '/' $VAR2 '/' $VAR3 '/' $VAR4 '/' $SHELL
/ Variable 2 / Assigned and exported in one step / var4 / /bin/bash
$ #No VAR1 and shell is /bin/bash ‑ is that right?
$ exit
[ian@atticf22 ~]$ ps ‑p $$ ‑o "pid ppid cmd"
  PID  PPID CMD
22923 12761 bash
[ian@atticf22 ~]$ #See what is visible
[ian@atticf22 ~]$ echo $VAR1 '/' $VAR2 '/' $VAR3 '/' $VAR4 '/' $SHELL
v1 / Variable 2 / Assigned and exported in one step / / /bin/bash
[ian@atticf22 ~]$ #No VAR4 ‑ our child cannot export back to us
[ian@atticf22 ~]$ exit
exit
[ian@atticf22 ~]$ ps ‑p $$ ‑o "pid ppid cmd"
  PID  PPID CMD
12761  9169 bash
[ian@atticf22 ~]$ #See what is visible
[ian@atticf22 ~]$ echo $VAR1 '/' $VAR2 '/' $VAR3 '/' $VAR4 '/' $SHELL
/ / / / /bin/bash
[ian@atticf22 ~]$ #None of VAR1 through VAR4 is exported back to parent

Did you notice in Listing 2 that ksh did not set the SHELL variable? This variable is usually set when you log in or when you use the su command to switch to another user with options that create a login shell. You learn more about login shells later in this tutorial.

Use the echo command to see what’s in some of the common bash variables and parameters from Table 1 and Table 2, as illustrated in Listing 3.

Listing 3. Common environment and shell variables

[ian@atticf22 ~]$ echo $USER $UID
ian 1000
[ian@atticf22 ~]$ echo $SHELL $HOME $PWD
/bin/bash /home/ian /home/ian
[ian@atticf22 ~]$ (exit 0);echo $?;(exit 4);echo $?
0
4

[ian@atticf22 ~]$ echo $0
bash
[ian@atticf22 ~]$ echo $$ $PPID
12761 9169
[ian@atticf22 ~]$ #see what my process and its parent are running
[ian@atticf22 ~]$ ps ‑p $$,$PPID ‑o "pid ppid cmd"
  PID  PPID CMD
 9169 1 /usr/libexec/gnome‑terminal‑server
12761  9169 bash

In the bash shell, you can also set environment values for the duration of a single command by prefixing the command with name=value pairs, as shown in Listing 4.

Listing 4. Setting bash environment values for a single command
[ian@atticf22 ~]$ echo "$VAR5 / $VAR6" / [ian@atticf22 ~]$ VAR5=5 VAR6="some value" bash [ian@atticf22 ~]$ echo "$VAR5 / $VAR6" 5 / some value [ian@atticf22 ~]$ exit [ian@atticf22 ~]$ echo "$VAR5 / $VAR6" /

readonly and other variable attributes

I mentioned that some shell parameters cannot be modified. You can also constrain variables to be readonly, integer, or string, among other possibilities. You can use the declare command to set variable attributes. Use the -p option to display variables with various attributes. To find out more about the declare command, try running:

info bash "Shell Builtin Commands" "Bash Builtins" ‑‑index‑search declare

or

help declare

Listing 5 shows a few examples.

Listing 5. Variable attributes

[ian@atticf22 ~]$ declare ‑r rov1="this is readonly"
[ian@atticf22 ~]$ rov="Who says it's read only?"
[ian@atticf22 ~]$ readonly rov2="another constant value"
[ian@atticf22 ~]$ rov2=3
bash: rov2: readonly variable
[ian@atticf22 ~]$ UID=99
bash: UID: readonly variable
[ian@atticf22 ~]$ declare ‑pr
declare ‑r BASHOPTS="checkwinsize:cmdhist:complete_fullquote:expand_aliases:extglob:extquote:
force_fignore:histappend:interactive_comments:progcomp:promptvars:sourcepath"
declare ‑ir BASHPID
declare ‑r BASH_COMPLETION_COMPAT_DIR="/etc/bash_completion.d"
declare ‑ar BASH_VERSINFO='([0]="4" [1]="3" [2]="42" [3]="1" [4]="release" [5]=
"x86_64‑redhat‑linux‑gnu")'
declare ‑ir EUID="1000"
declare ‑ir PPID="12761"
declare ‑r SHELLOPTS="braceexpand:emacs:hashall:histexpand:history:interactive‑comments:monitor"
declare ‑ir UID="1000"
declare ‑r rov1="this is readonly"
declare ‑r rov2="another constant value"
[ian@atticf22 ~]$ help declare
declare: declare [‑aAfFgilnrtux] [‑p] [name[=value] ...]
    Set variable values and attributes.
    
    Declare variables and give them attributes.  If no NAMEs are given,
    display the attributes and values of all variables.
    
    Options:
      ‑f   restrict action or display to function names and definitions
      ‑F   restrict display to function names only (plus line number and
       source file when debugging)
      ‑g   create global variables when used in a shell function; otherwise
       ignored
      ‑p   display the attributes and value of each NAME
    
    Options which set attributes:
      ‑a   to make NAMEs indexed arrays (if supported)
      ‑A   to make NAMEs associative arrays (if supported)
      ‑i   to make NAMEs have the 'integer' attribute
      ‑l   to convert NAMEs to lower case on assignment
      ‑n   make NAME a reference to the variable named by its value
      ‑r   to make NAMEs readonly
      ‑t   to make NAMEs have the 'trace' attribute
      ‑u   to convert NAMEs to upper case on assignment
      ‑x   to make NAMEs export
    
    Using '+' instead of '‑' turns off the given attribute.
    
    Variables with the integer attribute have arithmetic evaluation (see
    the let' command) performed when the variable is assigned a value.
    
    When used in a function, 'declare' makes NAMEs local, as with thelocal'
    command.  The `‑g' option suppresses this behavior.
    
    Exit Status:
    Returns success unless an invalid option is supplied or a variable
    assignment error occurs.

Unsetting variables

Use the unset command to remove a variable from the bash shell. Use the -v option to ensure that you are removing a variable definition. Functions (which I cover later in this tutorial) can have the same names as variables, so use -f if you want to remove a function definition. Without either -f or -v, the bash unset command removes a variable definition if it exists; otherwise, it removes a function definition if one exists. Listing 6 shows some examples.

Listing 6. Unsettng bash variables

[ian@atticf22 ~]$ bash
[ian@atticf22 ~]$ VAR1=v1
[ian@atticf22 ~]$ declare ‑i VAR2
[ian@atticf22 ~]$ VAR2=3+4
[ian@atticf22 ~]$ echo $VAR1 $VAR2
v1 7
[ian@atticf22 ~]$ unset VAR1
[ian@atticf22 ~]$ echo $VAR1 $VAR2
7
[ian@atticf22 ~]$ unset ‑v VAR2
[ian@atticf22 ~]$ echo $VAR1 $VAR2

[ian@atticf22 ~]$ exit
exit

Note that if a variable is defined as integer, an assignment to it is evaluated as an arithmetic expression.

By default, bash treats unset variables as if they had an empty value. So why unset a variable rather than assign it an empty value? With bash and many other shells, you can generate an error if you reference an undefined variable. Use the set -u command to generate an error for undefined variables, and set +u to disable the warning. Listing 7 illustrates these points.

Listing 7. Errors and unset variables
[ian@atticf22 ~]$ bash [ian@atticf22 ~]$ set -u [ian@atticf22 ~]$ VAR1=var1 [ian@atticf22 ~]$ echo $VAR1 var1 [ian@atticf22 ~]$ unset VAR1 [ian@atticf22 ~]$ echo $VAR1 bash: VAR1: unbound variable [ian@atticf22 ~]$ VAR1= [ian@atticf22 ~]$ echo $VAR1 [ian@atticf22 ~]$ unset VAR1 [ian@atticf22 ~]$ echo $VAR1 bash: VAR1: unbound variable [ian@atticf22 ~]$ unset -v VAR1 [ian@atticf22 ~]$ set u [ian@atticf22 ~]$ echo $VAR1 [ian@atticf22 ~]$ exit exit

As you see, it is not an error to unset a variable that does not exist, even when set -u has been specified.

Environments, variables, and the C shell

The csh and tcsh shells handle environments and variables slightly differently from bash. You use the set command to set variables in your shell, and the setenv command to set and export variables. The syntax differs slightly from that of the bash export command, as illustrated in Listing 8. Note the equals (=) sign when set is used.

Listing 8. Setting variables and environment values in the C and tcsh shells

[ian@atticf22 ~]$ tcsh
[ian@atticf22 ~]$ setenv E1 "Env variable 1"
[ian@atticf22 ~]$ set V2="variable 2"
[ian@atticf22 ~]$ echo "$E1 / $V2"
Env variable 1 / variable 2
[ian@atticf22 ~]$ tcsh
[ian@atticf22 ~]$ echo $E1
Env variable 1
[ian@atticf22 ~]$ echo $V2
V2: Undefined variable.
[ian@atticf22 ~]$ echo "$?E1 / $?V2"
1 / 0
[ian@atticf22 ~]$ exit

Note that the second tcsh shell did not inherit the V2 variable. Trying to reference V2 produces an error, as happens in bash when set -u is set. In csh and tcsh, you can check whether NAME is set by using the $?NAME construct, which returns 1 if NAME is set and 0 otherwise.

Another difference in environment handling is that csh and tcsh maintain separate namespaces for variables and environment values. If the same name appears in both places, the variable definition takes precedence. As with bash, the unset command unsets a variable. Use the unsetenv command to unset an environment value. Listing 9 shows these commands.

Listing 9. Unsetting csh and tcsh variables and environment values

[ian@atticf22 ~]$ echo "$E1 / $V2"
Env variable 1 / variable 2
[ian@atticf22 ~]$ set E1="I'm now a regular variable"
[ian@atticf22 ~]$ echo $E1
I'm now a regular variable
[ian@atticf22 ~]$ unset E1
[ian@atticf22 ~]$ echo $E1
Env variable 1
[ian@atticf22 ~]$ unsetenv E1
[ian@atticf22 ~]$ echo $E1
E1: Undefined variable.

Profile files

When you log in to a Linux system, your ID has a default shell, which is your login shell. The shell program is specified in your entry in the /etc/passwd file, and this is the value that is used to set the SHELL environment variable. Use the man 5 passwdman5passwd command to find out more about the /etc/passwd file.

If your login shell is bash, it runs several profile scripts before you get control. If /etc/profile exists, bash runs it first, along with any .sh files in /etc/profile.d. After bash runs the system scripts, it checks your home directory for the ~/.bash_profile, ~/.bash_login, and ~/.profile files, in that order, and runs the first one that it finds.

The distribution files in /etc, such as /etc/profile, can be modified by system updates, so do not edit them directly. Rather, create additional files in /etc/profile.d if you need to customize the login environment for all users on your system. Needless to say, you can modify the files in your home directory to suit your individual working style.

After you log in and are using bash, you will probably start more shells to run commands. If the shell accepts commands from the keyboard, it is an interactive shell. If the shell input comes from a file, it is a noninteractive shell. For interactive shells, bash runs the ~/.bashrc script if it exists. It is customary to check for this script in your ~/.bash_profile so that you can run it at login and when you start an interactive shell. Depending on your distribution, your ~/.bashrc can in turn source common aliases and global variables from /etc/bash.bashrc or /etc/bashrc. When you log off, bash runs the ~/.bash_logout script from your home directory if the script exists. My .bash_profile is shown in Listing 10.

Listing 10. My .bash_profile

[ian@atticf22 ~]$ cat ~/.bash_profile 
#.bash_profile

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

#User specific environment and startup programs

PATH=$PATH:$HOME/.local/bin:$HOME/bin

export PATH

The --login option forces bash to read profiles as if it were a login shell, and the --noprofile option tells bash to skip the profiles. Similarly, the --norc option disables execution of the ~/.bashrc file for an interactive shell. To force bash to use a file other than ~/.bashrc, specify the --rcfile option with the name of the file you want to use. Listing 11 shows how to create an a simple example file to use with the --rcfile option.

Listing 11. Using a file with the –rcfile parameter

[ian@atticf22 ~]$ echo VAR1=var1>testrc
[ian@atticf22 ~]$ echo $VAR1

[ian@atticf22 ~]$ bash ‑‑rcfile testrc
bash‑4.3$ echo $VAR1
var1
bash‑4.3$ exit
exit

Key bindings

Many programs, including bash, use the readline library to read commands from the keyboard. You customize how a keystroke or key combination is interpreted by using the built-in bash bind command. For example, bind ‘”\C-t”:”pwd\n”‘bind'"\C-t":"pwd\n"' binds the Ctrl-t (pressing Ctrl and t together) to run the pwd command.

The global file for default key bindings is /etc/inputrc. You can create your own bindings in the .inputrc file in your home directory. If you do, include the following line so that the global definitions are not left out:


$include /etc/inputrc

See the bash info pages for more information.

Other ways to start bash

You have already seen how to start login and interactive shells. When you run a shell script, bash starts the script in a noninteractive shell, and the profile files are not read. However, if the BASH_ENV variable is set, bash expands the value and assumes that it is the name of a file. If that file exists, bash runs it before running whatever script or command you are running in the noninteractive shell. Listing 12 illustrates this behavior.

Listing 12. Using BASH_ENV

[ian@atticf22 ~]$ cat testenv.sh
#!/bin/bash
echo "Test of BASH_ENV"
export VAR_ENV="my value"
[ian@atticf22 ~]$ cat testscript.sh
#!/bin/bash
echo "Running $0"
echo "VAR_ENV is set to $VAR_ENV"
[ian@atticf22 ~]$ ./testscript.sh
Running ./testscript.sh
VAR_ENV is set to 
[ian@atticf22 ~]$ export BASH_ENV="~/testenv.sh"
[ian@atticf22 ~]$ ./testscript.sh
Test of BASH_ENV
Running ./testscript.sh
VAR_ENV is set to my value

If a script runs in a child shell, any variables it might export are lost when it returns to the parent. So if .bash_profile runs the ~/.bashrc script, why aren’t the variables and functions that are exported from ~/.bashrc lost? The answer is that you run the script in the current environment by using the source (or .) command. Refer back to Listing 10 for an example.

I mentioned POSIX earlier. You can start bash with the --posix option to make it more POSIX-compliant. You might do this if you are trying to develop code that will work across systems with different levels of POSIX compliance. Startup for this mode is similar to startup of a noninteractive shell, except that an initial file to run, if any, is determined from the ENV environment variable rather than the BASH_ENV variable.

Linux systems often have /bin/sh as a symbolic link to bash. When bash detects that it is being run under the name sh, it tries to follow the startup behavior of the older Bourne shell while still conforming to POSIX standards. When run as a login shell, bash tries to read and run /etc/profile and ~/.profile. When run as an interactive shell via the sh command, bash tries to run the file specified by the ENV variable, as it does when invoked in POSIX mode. When run interactively as sh, bash only uses a file specified by the ENV variable; the --rcfile option is ignored.

Finally, if bash is invoked by the remote shell daemon, it behaves as an interactive shell, using the ~/.bashrc file if it exists.

Pipelines and command lists

Before moving on to aliases and functions, you need a good grasp of pipelines and command sequences (or lists). See “Learn Linux, 101: Streams, pipes, and redirects” to review pipes and “Learn Linux, 101: The Linux command line” to review lists or sequences.

Sometimes you must group commands in one stage of a pipeline and then redirect the output of all those commands to the next stage of the pipeline. You can use parentheses to run a command list in a subshell, or you can use curly braces to run the command list in the current shell. Curly braces must be separated from other content by white space, whereas parentheses are recognized as operators and do not need to be separated. Listing 13 uses these two methods to find the largest file in /usr/bin and the total amount of space used by the /usr/bin directory.

Listing 13. Command grouping with parentheses and braces

[ian@atticf22 ~]$ (ls ‑lrS /usr/bin; du /usr/bin) | tail ‑n 3
‑rwxr‑xr‑x.   1 root root    10669512 Sep 18 10:41 mysql
‑rwxr‑xr‑t.   1 root root    15148768 Sep 14 05:11 emacs‑24.5
386296   /usr/bin
[ian@atticf22 ~]$ { ls ‑lrS /usr/bin; du /usr/bin; } | tail ‑n 3
‑rwxr‑xr‑x.   1 root root    10669512 Sep 18 10:41 mysql
‑rwxr‑xr‑t.   1 root root    15148768 Sep 14 05:11 emacs‑24.5
386296   /usr/bin

Shell aliases

In bash, you can define aliases for commands. You use aliases to provide an alternative name for a command, to provide default parameters for the command, or sometimes to construct a new or more-complex command. You set aliases or list aliases with the alias command and remove them with the unalias command. Listing 14 shows some examples.

Listing 14. Simple use of the alias and unalias commands

[ian@atticf22 ~]$ #Two commonly set aliases
[ian@atticf22 ~]$ alias ls ll
alias ls='ls ‑‑color=auto'
alias ll='ls ‑l ‑‑color=auto'
[ian@atticf22 ~]$ #Add one for a long listing in reverse chronological order
[ian@atticf22 ~]$ alias llrt='ls ‑lrt ‑‑color=auto'
[ian@atticf22 ~]$ llrt /bin
lrwxrwxrwx. 1 root root 7 Aug 16  2014 /bin ‑> usr/bin
[ian@atticf22 ~]$ #Remove our new alias
[ian@atticf22 ~]$ unalias llrt
[ian@atticf22 ~]$ 

Figure 1, a screenshot of the text from Listing 12, shows the colored output of the new llrt alias.

Figure 1. Colored output from the llrt command
Screenshot with colors showing the text from the previous listing

Bash alias expansion operates on the first word of a command if that word is not enclosed in quotation marks. If the first word of the expansion is an unquoted word that has not already been expanded, bash does alias expansion again, and so on. So the llrt alias could have been alias llrt=’ls -lrtaliasllrt='ls-lrt, and the llrt alias could also have skipped the --color=auto parameter — because both aliases would have gone through further substitution for the ls word. Try experimenting with these and other variations.

Another common use of aliases is for the root user. The cp, rm, and mv commands are usually aliased to include the -i parameter, to help prevent accidental destruction of files. Check out these aliases on your own system.

As a final and more complex example, look at the way the which command is aliased in Listing 15.

Listing 15. Alias for the which command

[ian@atticf22 ~]$ alias which
alias which='(alias; declare ‑f) | /usr/bin/which ‑‑tty‑only ‑‑read‑alias 
‑‑read‑functions ‑‑show‑tilde ‑‑show‑dot'
[ian@atticf22 ~]$ which which
alias which='(alias; declare ‑f) | /usr/bin/which ‑‑tty‑only ‑‑read‑alias 
‑‑read‑functions ‑‑show‑tilde ‑‑show‑dot'
   /usr/bin/which

Here the alias and declare -fdeclare-f commands are grouped with parentheses and run in a subshell. This group lists the aliases and functions known to the shell. The output of the group is piped to the /usr/bin/which executable. If the executable is running on a terminal (--tty-only option), /usr/bin/which reads alias (--read-alias option) and function (--read-functions option) definitions from stdin and includes output for any matching aliases or functions, and the basic output for executables found on the current PATH. Read the man pages for which to find out more about the other options.

Aliases, command lists, and quoting

You need to be careful when defining aliases that use command lists or other shell meta characters. You also need to be careful with quoting if you want to use shell variables in your alias.

In Listing 15, the alias for which uses a command list and single quotation marks. Now, suppose you want a command called lsdu to list the contents of the current directory and also the amount of space used by it and all its subdirectories. Listing 16 shows the right and wrong ways to create the alias.

Listing 16. Right and wrong way to use command lists and quoting in aliases

[ian@atticf22 developerworks]$ #Wrong way
[ian@atticf22 developerworks]$ alias lsdu=ls;du ‑sh
8.2M    .
[ian@atticf22 developerworks]$ lsdu
new‑article.py        new‑article.vbs  schema     tools        web
new‑article‑rich.vbs  readme           templates  validate.py  xsl
[ian@atticf22 developerworks]$ #Right way
[ian@atticf22 developerworks]$ alias lsdu='ls;du ‑sh'
[ian@atticf22 developerworks]$ lsdu
new‑article.py        new‑article.vbs  schema     tools        web
new‑article‑rich.vbs  readme           templates  validate.py  xsl
8.2M    .

In the example of the wrong way, the alias command only includes the text up to the ; meta character, and the remaining part of the line immediately executes. In cases like this, you need to quote the full text that makes up the alias. But should you use single or double quotation marks?

Use double quotation marks if you want the shell to evaluate any shell variable references at the time the alias is defined. Use single quotation marks if you want the reference evaluated when the alias is executed. Listing 17 shows the difference, using the PWD (print working directory) variable.

Listing 17. Using single and double quotation marks in aliases

[ian@atticf22 ~]$ alias mypwd1='echo Current directory is $PWD'
[ian@atticf22 ~]$ alias mypwd2="echo Current directory is $PWD"
[ian@atticf22 ~]$ #cd to a different directory
[ian@atticf22 ~]$ cd developerworks
[ian@atticf22 developerworks]$ mypwd1
Current directory is /home/ian/developerworks
[ian@atticf22 developerworks]$ mypwd2
Current directory is /home/ian
[ian@atticf22 developerworks]$ alias mypwd1 mypwd2
alias mypwd1='echo Current directory is $PWD'
alias mypwd2='echo Current directory is /home/ian'

Shell functions

Aliases are useful, but what happens if you want to handle parameters? Aliases expand only the first word, and everything else on the command line is appended to the expansion. If you want to run a command with some parameters and then process the output somehow, you are out of luck with an alias. In this case, you need a shell function.

Shell functions have a couple of advantages over aliases:

  • You can handle parameters.
  • You can use programming constructs, such as testing and looping, to enhance your processing.

This tutorial covers only simple functions. The next tutorial in the series covers the enhanced programming constructs that you can use in shell functions and scripts. Here, I show you how to build the necessary commands and then how to put them into a function.

Suppose that you want a command called ldirs that works much like ls but lists only directories. Your first thought might be to start with a long listing and filter out the lines that start with d and then do further processing to eliminate the extra information. Listing 18 shows the first stage of this thinking.

Listing 18. A first try at filtering out directories

[ian@atticf22 ~]$ ls ‑l developerworks/ | grep "^d"
drwxrwxr‑x. 2 ian ian  4096 Nov 13 13:06 my first article
drwxrwxr‑x. 2 ian ian  4096 Sep 12  2014 readme
drwxrwxr‑x. 4 ian ian  4096 Sep 12  2014 schema
drwxrwxr‑x. 3 ian ian  4096 Sep 12  2014 templates
drwxrwxr‑x. 3 ian ian  4096 Sep 12  2014 tools
drwxrwxr‑x. 3 ian ian  4096 Mar  5  2013 web
drwxrwxr‑x. 4 ian ian  4096 Sep 12  2014 xsl

Clearly, it won’t do just to strip off the last word from the output, because some directory names might include spaces. Rather than using a long listing, it would be better to start from a classified listing in which directory names include a trailing / character. Listing 19 shows how this approach might look.

Listing 19. Classifying ls output to find directories

[ian@atticf22 ~]$ ls ‑F developerworks/ | grep "/$"
my first article/
readme/
schema/
templates/
tools/
web/
xsl/

Much better. Now it remains only to strip off the trailing /, which you can easily do with sed, as shown in Listing 20.

Listing 20. An ldirs command list example

[ian@atticf22 ~]$ ls ‑F developerworks/ | grep "/$" | sed ‑e 's/\/$//'
my first article
readme
schema
templates
tools
web
xsl

Now that you have the complex command that you want for your ldirs function, it’s time to learn how to make it a function. A function consists of a name followed by () and then a compound command. For this tutorial, a compound command is any command or command list, terminated by a semicolon and surrounded by braces (which must be separated from other tokens by white space). I cover more-complex compound commands in the next tutorial in this series.

In the bash shell, a function name can be preceded by the word function, but this is not part of the POSIX specification and is not supported by more minimalist shells such as dash. Another tutorial will show you how to make sure that a script is interpreted by a particular shell, even if you normally use a different shell.

The commands in Listing 18, Listing 19, and Listing 20 list a directory on my system named developerworks, but you don’t want to list the same directory every time — and you can’t substitute a parameter for the directory name if you use aliases. So, you’ll use a function.

Before you write the function, you need to know how to use parameters. Inside a function, you reference parameters by using the bash special variables in Table 3. You prefix these variables with a $ symbol to reference them, as with other shell variables.

Table 3. Shell parameters for functions
Parameter Purpose
0, 1, 2, … The positional parameters starting from parameter 0. Parameter 0 refers to the name of the program that started bash, or the name of the shell script if the function is running within a shell script. See the bash man pages for information on other possibilities, such as when bash is started with the -c parameter. A string enclosed in single or double quotation marks is passed as a single parameter, and the quotation marks are stripped. In the case of double quotation marks, any shell variables, such as $HOME, are expanded before the function is called. You need to use single or double quotation marks to pass parameters that contain embedded blanks or other characters that might have special meaning to the shell.
* The positional parameters starting from parameter 1. If the expansion is done within double quotation marks, the expansion is a single word with the first character of the interfield separator (IFS) special variable separating the parameters or no intervening space if IFS is null. The default IFS value is a blank, tab, and newline. If IFS is unset, the separator used is a blank, as for the default IFS.
@ The positional parameters starting from parameter 1. If the expansion is done within double quotation marks, each parameter becomes a single word, so that "$@" is equivalent to "$1", "$2", …. If your parameters are likely to contain embedded blanks, use this form.
# The number of parameters, not including parameter 0.

If you have more than nine parameters, you cannot use $10 to refer to the tenth one. You must first either process or save the first parameter ($1), and then use the shift command to drop parameter 1 and move all remaining parameters down 1, so that $10 becomes $9, and so on. The value of $# is updated to reflect the remaining number of parameters.

To start, try defining a simple function to do nothing more than tell you how many parameters it has and display them, as shown in Listing 21.

Listing 21. A simple function to display its parameters

[ian@atticf22 ~]$ testfunc () { echo "$#parameters"; echo "$@"; }
[ian@atticf22 ~]$ testfunc
0 parameters

[ian@atticf22 ~]$ testfunc a b c
3 parameters
a b c
[ian@atticf22 ~]$ testfunc a "b c"
2 parameters
a b c

In this example, you can use $*, "$*", $@, or "$@" without seeing much difference in the output — except for the number of parameters and the way any variable references are handled. When things become more complex, the distinctions will definitely matter.

Now create your ldirs function, as shown in Listing 22, using the command list in Listing 20.

Listing 22. The ldirs function

[ian@atticf22 ~]$ #You can enter the function on one line
[ian@atticf22 ~]$ ldirs () { ls ‑F "$@" |  grep "/$" | sed ‑e 's/\/$//'; }
[ian@atticf22 ~]$ #Or you can enter it on multiple lines
[ian@atticf22 ~]$ #in which case you do not need the final semi‑colon
[ian@atticf22 ~]$ ldirs ()
> {
> ls ‑F "$@" |  grep "/$" |
> sed ‑e 's/\/$//'
> }

Listing 23 shows output from running ldirs on my system to list the developerWorks XML authoring package directories.

Listing 23. Output from the ldirs function

[ian@atticf22 ~]$ ldirs developerworks/ ~/developerworks/xsl/7.0/
my first article
readme
schema
templates
tools
web
xsl
en_US
en_VN
es_AR
ja_JP
ko_KR
pt_BR
ru_RU
zh_CN
[ian@atticf22 ~]$ ldirs ‑ilrt developerworks
2511035 drwxrwxr‑x. 3 ian ian  4096 Mar  5  2013 web
3019425 drwxrwxr‑x. 4 ian ian  4096 Sep 12  2014 xsl
2511022 drwxrwxr‑x. 3 ian ian  4096 Sep 12  2014 templates
2511036 drwxrwxr‑x. 4 ian ian  4096 Sep 12  2014 schema
3023428 drwxrwxr‑x. 3 ian ian  4096 Sep 12  2014 tools
3282516 drwxrwxr‑x. 2 ian ian  4096 Sep 12  2014 readme
3277408 drwxrwxr‑x. 2 ian ian  4096 Nov 13 13:06 my first article
[ian@atticf22 ~]$ ldirs  developerworks/my\ first\ article/
[ian@atticf22 ~]$ mkdir developerworks/my\ first\ article/"dir example"
[ian@atticf22 ~]$ ldirs  developerworks/my\ first\ article/
dir example

Although the ldirs example is fairly simple, it illustrates the technique of combining small building blocks to create more-complex or custom commands. Both pipelines and lists are essential parts of your Linux (or UNIX) toolbox.

Skeleton directories

You might be wondering how files like ~/.bash_profile, ~/.bashrc, or ~/.bash_logout got created in your home directory. These are skeleton files that are copied from /etc/skel. Listing 24 shows the files in /etc/skel on my system. Note that you need the -a option of ls; otherwise, the directory will probably appear empty.

Listing 24. Contents of /etc/skeleton

[ian@atticf22 ~]$ ls ‑a /etc/skel/
.  ..  .bash_logout  .bash_profile  .bashrc  .emacs  .kshrc  .mozilla

When the useradd command is run with the -m option to create a home directory, these files are copied to the new home directory. Programs such as Firefox or the Korn shell might add additional directories to /etc/skel, as you see here. If you want to customize the directories that are created for a new user, add your new directories and any appropriate files to /etc/skel.

The /etc/skel location is configurable through the /etc/defaults/useradd file. The file as it exists on my system is shown in Listing 25.

Listing 25. /etc/defaults/useradd

[ian@atticf22 ~]$ cat /etc/default/useradd 
#useradd defaults file
GROUP=100
HOME=/home
INACTIVE=‑1
EXPIRE=
SHELL=/bin/bash
SKEL=/etc/skel
CREATE_MAIL_SPOOL=yes

Command search paths

You know that bash searches for executables by using the directories listed in the PATH variable. The PATH variable contains a list of directories with a colon separating the entries. The root user’s PATH is usually different from that of an ordinary user, as illustrated in Listing 26.

Listing 26. PATH for normal and root users

[ian@atticf22 ~]$ echo $PATH
/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:/home/ian/.local/bin:/home/ian/bin
[ian@atticf22 ~]$ su ‑
Password: 
[root@atticf22 ~]#echo $PATH
/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin

Now suppose that your company has a program library, say /opt/company-bin, that you’d like on the path of every non-root user. Assume that you’d like to add the library to the front of the PATH variable, but only if it is not already there. Further assume that you’d like a function or alias, say add2path, that users can use to add additional directories to their PATH variables more easily. You will use what you have learned here so far to do both of these tasks.

First, you need to decide which is the better choice here: an alias or a function. The fact that you want to add to the PATH only if the entry is not already there suggests that you need to handle a parameter. Aliases don’t handle parameters, so you need a function.

An easy way to see if a directory is already on your path is to wrap both the directory name and the PATH variable with a leading and trailing colon (:) and test whether the delimited directory name is part of the delimited PATH. If you don’t delimit in this way, you might be testing for /bin and actually find /usr/bin, which contains the string /bin. Use shell brace expansion to remove a string from a parameter and then compare the result with the original. Listing 27 shows the basic constructs you could use.

Listing 27. Testing whether one string is part of another

[ian@atticf22 ~]$ #Augment the path
[ian@atticf22 ~]$ augpath=":$PATH:"
[ian@atticf22 ~]$ #demonstrate with /usr/bin
[ian@atticf22 ~]$ augdir=":/usr/bin:"
[ian@atticf22 ~]$ echo ‑e "$augdir\n$augpath\n${augpath/$augdir}":/usr/bin:
:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:/home/ian/.local/bin:/home/ian/bin:
:/usr/local/bin:/usr/local/sbin/usr/sbin:/bin:/sbin:/home/ian/.local/bin:/home/ian/bin:
[ian@atticf22 ~]$ test "$augpath" != "${augpath/$augdir}" && echo Found || echo Not
Found

Constructing the add2path function is now easy. If you declare the intermediate variables as local, they are not known outside the function. Listing 28 shows an example of the add2path function and its usage.

Listing 28. The add2path function and its usage

[ian@atticf22 ~]$ add2path ()
> {
> local augpath augdir
> augpath=":$PATH:"
> augdir=":$1:"
> test "$augpath" = "${augpath/$augdir}" && PATH="$1:$PATH"
> }
[ian@atticf22 ~]$ add2path /opt/company‑bin
[ian@atticf22 ~]$ echo $PATH
/opt/company‑bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:
    /sbin:/home/ian/.local/bin:/home/ian/bin

Having learned about profiles earlier in this tutorial, you know that the best way to add this sequence of commands as a system-wide function is to create a company-bin.sh file in /etc/profile.d. You need root authority to create this file. Remember that you need to add /opt/company-bin to the PATH of non-root users. Listing 29 shows what the file might look like.

Listing 29. The /etc/profile.d/company-bin.sh file

[ian@atticf22 ~]$ cat /etc/profile.d/company‑dir.sh 
#Declare the add2path function

add2path () 
{ 
    local augpath augdir
    augpath=":$PATH:"
    augdir=":$1:"
    test "$augpath" = "${augpath/$augdir}" && PATH="$1:$PATH"
}

#Add /opt/company‑bin if not already on non‑root PATH
[ $(id ‑u) ‑ne 0 ] && add2path /opt/company‑bin

Try this example out on your own system.

This concludes your introduction to using and customizing the bash shell environment.