Would you want to work in an environment where you couldn’t adjust things to your liking? Imagine not being able to adjust the height of your chair, or being forced to walk the long way to the lunchroom, just because someone else thought that was the “right way.” That sort of inflexibility wouldn’t be acceptable for long; however, that’s what most users expect, and accept, from their computing environments. But if you’re used to thinking of your user interface as something inflexible and unchangeable, relax—the user interface is not carved in stone. bash lets you customize it so that it works with you, rather than against you.
bash gives you a very powerful and flexible environment. Part of that flexibility is the extent to which it can be customized. If you’re a casual Unix user, or if you’re used to a less flexible environment, you might not be aware of what’s possible. This chapter shows you how to configure bash to suit your individual needs and style. If you think the Unix cat command has a ridiculous name (most non-Unix people would agree), you can define an alias that renames it. If you use a few commands all the time, you can assign abbreviations to them, too—or even misspellings that correspond to your favorite typing errors (e.g., “mroe” for the more command). You can create your own commands, which can be used the same way as standard Unix commands. You can alter the prompt so that it contains useful information (like the current directory). And you can alter the way bash behaves; for example, you can make it case-insensitive, so that it doesn’t care about the difference between upper- and lowercase. You will be surprised and pleased at how much you can improve your productivity with a few simple bash tweaks, especially to readline.
For more information about customizing and configuring bash, see Chapter 3 of Learning the bash Shell, 3rd Edition, by Cameron Newham (O’Reilly).
bash sometimes has several different ways to set the same option, and this is an example of that. You can set an option on startup (for example, bash -x), then later turn the same option off interactively using set +x.
Customize the $PS1 and $PS2 variables as you desire.
The default prompt varies depending on your system. bash itself will show its major and minor version (\s-\v\$); for example, bash-3.00$. However, your operating system may have its own default, such as [user@host~]$ ([\u@\h\W]\$) for some versions of Fedora. Our solution presents eight basic prompts and three fancier prompts.
Here are eight examples of more useful prompts that will work with bash 1.14.7 or newer. The trailing \$ displays # if the effective UID is zero (i.e., you are root) and $ otherwise:
Username@hostname, the date and time, and the current working directory:
$ export PS1='[\u@\h \d \A] \w \$ ' [jp@freebsd Wed Dec 28 19:32] ~ $ cd /usr/local/bin/ [jp@freebsd Wed Dec 28 19:32] /usr/local/bin $
Username@long-hostname, the date and time in ISO 8601 format, and the base-name of the current working directory (\W):
$ export PS1='[\u@\H \D{%Y-%m-%d %H:%M:%S%z}] \W \$ '
[jp@freebsd.jpsdomain.org 2005-12-28 19:33:03-0500] ~ $ cd /usr/local/
[jp@freebsd.jpsdomain.org 2005-12-28 19:33:06-0500] local $
Username@hostname, the bash version, and the current working directory (\w):
$ export PS1='[\u@\h \V \w] \$ ' [jp@freebsd 3.00.16] ~ $ cd /usr/local/bin/ [jp@freebsd 3.00.16] /usr/local/bin $
Newline, username@hostname, base PTY, shell level, history number, newline, and full working directory name ($PWD):
$ export PS1='\n[\u@\h \l:$SHLVL:\!]\n$PWD\$ ' [jp@freebsd ttyp0:3:21] /home/jp$ cd /usr/local/bin/ [jp@freebsd ttyp0:3:22] /usr/local/bin$
PTY is the number of the pseudoterminal (in Linux terms) to which you are connected. This is useful when you have more than one session and are trying to keep track of which is which. Shell level is the depth of subshells you are in. When you first log in it’s 1, and as you run subprocesses (for example, screen) it increments, so after running screen it would normally be 2. The history line is the number of the current command in the command history.
Username@hostname, the exit status of the last command, and the current working directory. Note the exit status will be reset (and thus useless) if you execute any commands from the prompt:
$ export PS1='[\u@\h $? \w \$ ' [jp@freebsd 0 ~ $ cd /usr/local/bin/ [jp@freebsd 0 /usr/local/bin $ true [jp@freebsd 0 /usr/local/bin $ false [jp@freebsd 1 /usr/local/bin $ true [jp@freebsd 0 /usr/local/bin $
Newline, username@hostname, and the number of jobs the shell is currently managing. This can be useful if you run a lot of background jobs and forget that they are there:
$ export PS1='\n[\u@\h jobs:\j]\n$PWD\$ ' [jp@freebsd jobs:0] /tmp$ ls -lar /etc > /dev/null & [1] 96461 [jp@freebsd jobs:1] /tmp$ [1]+ Exit 1 ls -lar /etc >/dev/null [jp@freebsd jobs:0] /tmp$
This example goes really crazy and shows everything.
Newline, username@hostname, terminal, shell, level, history, number of jobs, bash version, and full working directory:
$ export PS1='\n[\u@\h t:\l l:$SHLVL h:\! j:\j v:\V]\n$PWD\$ ' [jp@freebsd t:ttyp1 l:2 h:91 j:0 v:3.00.16] /home/jp$
Newline, username@hostname, T for terminal, L for shell level, C for command number, and date and time in ISO 8601 format:
$ PS1='\n[\u@\h:T\l:L$SHLVL:C\!:\D{%Y-%m-%d_%H:%M:%S_%Z}]\n$PWD\$ '
[jp@freebsd:Tttyp1:L1:C337:2006-08-13_03:47:11_EDT]
/home/jp$ cd /usr/local/bin/
[jp@freebsd:Tttyp1:L1:C338:2006-08-13_03:47:16_EDT]
/usr/local/bin$
This prompt is one you will either love or hate. It shows very clearly who did what, when, and where and is great for documenting steps you took for some task via a simple copy and paste from a scrollback buffer—but some people find it much too cluttered and distracting.
Here are three fancy prompts that use ANSI escape sequences for colors, or to set the contents of the title bar in an xterm—but be aware that these will not always work. There are a bewildering array of variables in system settings, xterm emulation, and SSH and Telnet clients, all of which can affect these prompts.
Also, note that such escape sequences should be surrounded by \[ and \], which tells bash that the enclosed characters are nonprinting. Otherwise, bash (technically, really readline) will be confused about line lengths and wrap lines in the wrong place:
Username@hostname, and the current working directory in light blue (color not shown in print):
$ export PS1='\[\033[1;34m\][\u@\h:\w]\$\[\033[0m\] ' [jp@freebsd:~]$ [jp@freebsd:~]$ cd /tmp [jp@freebsd:/tmp]$
Username@hostname, and the current working directory in both the xterm title bar and the prompt itself. If you are not running in an xterm this may produce garbage in your prompt:
$ export PS1='\[\033]0;\u@\h:\w\007\][\u@\h:\w]\$ ' [jp@ubuntu:~]$ [jp@ubuntu:~]$ cd /tmp [jp@ubuntu:/tmp]$
Both color and xterm updates:
$ PS1='\[\033]0;\u@\h:\w\007\]\[\033[1;34m\][\u@\h:\w]\$\[\033[0m\] ' [jp@ubuntu:~]$ [jp@ubuntu:~]$ cd /tmp [jp@ubuntu:/tmp]$
To save you some tedious typing if you want to try them out, all of these prompts are available in the file ./ch16/prompts in this book’s GitHub repository. The contents of that file are shown in Example 16-1.
# cookbook filename: prompts# Username @ short hostname, the date and time, and the current working# directory (CWD):exportPS1='[\u@\h \d \A] \w \$ '# Username @ long hostname, the date and time in ISO 8601 format, and the# basename of the current working directory (\W):exportPS1='[\u@\H \D{%Y-%m-%d %H:%M:%S%z}] \W \$ '# Username @ short hostname, bash version, and the current working# directory (\w):exportPS1='[\u@\h \V \w] \$ '# Newline, username @ short hostname, base PTY, shell level, history number,# newline, and full working directory name ($PWD):exportPS1='\n[\u@\h \l:$SHLVL:\!]\n$PWD\$ '# Username @ short hostname, the exit status of the last command, and the# current working directory:exportPS1='[\u@\h $? \w \$ '# Newline, username @ short hostname, and the number of jobs# in the background:exportPS1='\n[\u@\h jobs:\j]\n$PWD\$ '# Newline, username @ short hostname, terminal, shell, level, history, jobs,# version and full working directory name:exportPS1='\n[\u@\h t:\l l:$SHLVL h:\! j:\j v:\V]\n$PWD\$ '# Newline, username @ short hostname, T for terminal, L for shell level, C# command number, and the date and time in ISO 8601 format:exportPS1='\n[\u@\h:T\l:L$SHLVL:C\!:\D{%Y-%m-%d_%H:%M:%S_%Z}]\n$PWD\$ '# Username @ short hostname, and the current working directory in light# blue:exportPS1='\[\033[1;34m\][\u@\h:\w]\$\[\033[0m\] '# Username @ short hostname, and the current working directory in both the# xterm title bar and the prompt itself:exportPS1='\[\033]0;\u@\h:\w\007\][\u@\h:\w]\$ '# Both color and xterm updates:exportPS1='\[\033]0;\u@\h:\w\007\]\[\033[1;34m\][\u@\h:\w]\$\[\033[0m\] '
Note that the export command need only be used once to flag a variable to be exported to child processes.
Assuming the promptvars shell option is set, which it is by default, prompt strings are decoded and expanded (via parameter expansion, command substitution, and arithmetic expansion), quotes are removed, and they are finally displayed. Prompt strings are $PS0, $PS1, $PS2, $PS3, and $PS4.
$PS0 is only available in bash version 4.4 or newer. For more on this “pre-execution” prompt, see the next recipe.
The $PS2 prompt is the secondary prompt displayed when bash needs more information to complete a command. It defaults to > but you may use anything you like.
$PS3 is the select prompt (see Recipe 3.7, “Selecting from a List of Options” and Recipe 6.16, “Creating Simple Menus”), which defaults to #?.
$PS4 is the xtrace (debugging) prompt, with a default of +. Note that the first character of $PS4 is replicated as many times as needed to denote levels of indirection in the currently executing command:
$ export PS2='Secondary> ' $ for i in * Secondary> do Secondary> echo $i Secondary> done cheesy_app data_file hard_to_kill mcd mode $ export PS3='Pick me: ' $ select item in 'one two three'; do echo $item; done 1) one two three Pick me: ^C $ export PS4='+ debugging> ' $ set -x $ echo $( echo $( for i in *; do echo $i; done ) ) +++ debugging> for i in '*' +++ debugging> echo cheesy_app +++ debugging> for i in '*' +++ debugging> echo data_file +++ debugging> for i in '*' +++ debugging> echo hard_to_kill +++ debugging> for i in '*' +++ debugging> echo mcd +++ debugging> for i in '*' +++ debugging> echo mode ++ debugging> echo cheesy_app data_file hard_to_kill mcd mode + debugging> echo cheesy_app data_file hard_to_kill mcd mode cheesy_app data_file hard_to_kill mcd mode
Since the $PS1 prompt is only useful when you are running bash interactively, the best place to set it is either globally in /etc/bashrc or locally in ~/.bashrc.
As a style note, we recommend putting a space character as the last character in the $PS1 string. It makes it easier to read what is on your screen by separating the prompt string from the commands that you type. For this reason, and because your string may contain other spaces or special characters, it is a good idea to use double or even single quotes to quote the string when you assign it to $PS1.
There are at least three easy ways to display your current working directory (CWD) in your prompt: \w, \W, and $PWD. \W will print the basename, or last part of the directory path, while \w will print the entire path. Note that both will print ~ instead of whatever $HOME is set to when you are in your home directory. That drives some people crazy, so to print the entire CWD, use $PWD. Printing the entire CWD will cause the prompt to change length, and it can even wrap in deep directory structures. That can drive other people crazy. If you have bash 4 or newer, just use $PROMPT_DIRTRIM with \w or \W (it does not affect $PWD). The Bash Reference Manual describes this variable as follows:
If set to a number greater than zero, the value is used as the number of trailing directory components to retain when expanding the
\wand\Wprompt string escapes…. Characters removed are replaced with an ellipsis.
If you can use $PROMPT_DIRTRIM you should, but if you can’t, Example 16-2 provides a function to truncate the working directory and a prompt to use the function.
# cookbook filename: func_trunc_PWDfunctiontrunc_PWD{# $PWD truncation code adapted from The Bash Prompt HOWTO:# 11.10. Controlling the Size and Appearance of $PWD# http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x783.html# How many characters of the $PWD should be keptlocalpwdmaxlen=30# Indicator that there has been directory truncation:localtrunc_symbol='...'# Temp variable for PWDlocalmyPWD=$PWD# Replace any leading part of $PWD that matches $HOME with '~'# OPTIONAL, comment out if you want the full path!myPWD=${PWD/$HOME/~}if[${#myPWD}-gt$pwdmaxlen];thenlocalpwdoffset=$((${#myPWD}-$pwdmaxlen))echo"${trunc_symbol}${myPWD:$pwdoffset:$pwdmaxlen}"elseecho"$myPWD"fi}
Here’s a demonstration:
$ source file/containing/trunc_PWD
[jp@freebsd ttyp0:3:60]
~/this is a bunch/of really/really/really/long directories/did I mention really/
really/long$export PS1='\n[\u@\h \l:$SHLVL:\!]\n$(trunc_PWD)\$ '
[jp@freebsd ttyp0:3:61]
...d I mention really/really/long$
You will notice that the prompts here are single-quoted so that $ and other special characters are taken literally. The prompt string is evaluated at display time, so the variables are expanded as expected. Double quotes may also be used, though in that case you must escape shell metacharacters, e.g., by using \$ instead of $.
The command number and the history number are usually different: the history number of a command is its position in the history list, which may include commands restored from the history file, while the command number is the position in the sequence of commands executed during the current shell session.
There is also a special variable called $PROMPT_COMMAND, which if set is interpreted as a command to execute before the evaluation and display of $PS1. The issue with that, and with using command substitution from within the $PS1 prompt, is that these commands are executed every time the prompt is displayed, which is often. For example, you could embed a command substitution such as $(ls-1 | wc-l) in your prompt to give you a count of files in the current working directory. But on an old or heavily utilized system in a large directory, that may result in significant delays before the prompt is presented and you can get on with your work. Prompts are best left short and simple (notwithstanding some of the monsters shown in the Solution section). Define functions or aliases to easily run on demand instead of cluttering up and slowing down your prompt.
To work around ANSI or xterm escapes that produce garbage in your prompt if they are not supported, you can use something like this in your rc file:
case $TERM in
xterm*) export \
PS1='\[\033]0;\u@\h:\w\007\]\[\033[1;34m\][\u@\h:\w]\$\[\033[0m\]' ;;
*) export PS1='[\u@\h:\w]\$ ' ;;
esac
See the section “Prompt String Customizations” in Appendix A for more on this topic.
In the ANSI example we just discussed, 1;34m means “set the character attribute to light, and the character color to blue.” 0m means “clear all attributes and set no color.” See “ANSI Color Escape Sequences” in Appendix A for the codes. The trailing m indicates a color escape sequence.
Example 16-3 is a script that displays all the possible combinations. If this does not display colors on your terminal, then ANSI color is not enabled or supported.
#!/usr/bin/env bash# cookbook filename: colors## Daniel Crisman's ANSI color chart script from# The Bash Prompt HOWTO: 6.1. Colours# http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x329.html.## This file echoes a bunch of color codes to the# terminal to demonstrate what's available. Each# line is the color code of one foreground color,# out of 17 (default + 16 escapes), followed by a# test use of that color on all nine background# colors (default + 8 escapes).#T='gYw'# The test textecho-e"\n 40m 41m 42m 43m\44m 45m 46m 47m";forFGs in' m'' 1m'' 30m''1;30m'' 31m''1;31m'' 32m'\'1;32m'' 33m''1;33m'' 34m''1;34m'' 35m''1;35m'\' 36m''1;36m'' 37m''1;37m';doFG=${FGs// /}echo-en"$FGs\033[$FG$T"forBG in 40m 41m 42m 43m 44m 45m 46m 47m;doecho-en"$EINS\033[$FG\033[$BG$T\033[0m";doneecho;doneecho
If you’d like a simple way to try out some colorful themes for your terminal, check out Bashish.
The Bash Reference Manual
./examples/scripts.noah/prompt.bash in the bash source tarball
This solution is only for bash 4.4 or newer. That version of bash introduced the $PS0 prompt. If set, the prompt string will be evaluated and printed prior to the execution of any command that you have typed.
Here’s a way to use both $PS0 and $PS1 to display start and end timestamps of commands that you run:
PS0=' \t\n'PS1='----------------------------------------------- \t\n\! \$ '
The pre-execution prompt is $PS0.
It will be displayed just before the shell begins to execute your command.
The leading blanks are there to move the output more to the right; feel free to add more blanks to move it farther to the right, or fewer to move it more to the left.
Similarly, the dashes in $PS1 move the timestamp to the right, and also delineate between commands, for easier visual scanning. Again, feel free to add more (or replace them with spaces) to taste.
The key element of both of these prompts is the \t. It will be translated into the timestamp. The \n is just a newline for proper formatting.
If you set both prompts, as shown in the solution, and then run a command that may take a bit of time, like sleep 5, then you can see the resulting timestamps:
1037$echo'sleep...';sleep5;echo'awake!'21:36:59sleep...awake!-----------------------------------------------21:37:041038$
If you’d like the $PS0 prompt to print on the same line as the command that you typed, put an \e[A just before the \t. You’ll probably want to add more spaces, too, to get the timestamp farther to the right.
To try out the prompt string before setting it, you can use another feature that is only for bash version 4.4 or newer. Assign the string to some variable and echo its value with the @P operator. For example:
$ MYTRY=' \! \h \t\n'
$ echo "${MYTRY}"
\! \h \t\n
$ echo "${MYTRY@P}"
1015 monarch 14:07:45
$
Without the @P it will echo the characters as you typed them; with the @P it will interpret the special sequences as if it were a prompt string. When you have the variable showing what you want, then assign it to $PS0.
The timestamp resolution is only in seconds, so it is not meant for precise performance measurements, but it can be very useful in looking back over a series of commands (especially if you walked away from your screen to get coffee) to see what transpired and which commands took a long time to run.
You need to permanently change your path.
First you need to discover where the path is set, and then update it. For your local account, it’s probably set in ~/.profile or ~/.bash_profile. Find the file with grep -l PATH ~/.[^.]* and edit it with your favorite editor; then source the file to have the change take effect immediately.
If you are root and you need to set the path for the entire system, the basic procedure is the same, but there are different files in /etc where the $PATH may be set, depending on your operating system and version. The most likely file is /etc/profile, but /etc/bashrc, /etc/rc, /etc/default/login, ~/.ssh/environment, and the PAM /etc/environment files are also possible.
On some systems there is a directory called /etc/profile.d which contains shell scripts to be run on startup. You can modify an existing script or add a new script to this directory to accomplish your change. The various scripts in this directory are just there as a way to organize or modularize the various settings rather than having them all in one big file.
The grep -l PATH ~/.[^.]* command is interesting because of the nature of shell wildcard expansion and the existence of the . and .. directories. See Recipe 1.7 for details.
The locations listed in the $PATH have security implications, especially when you are root. If a world-writable directory is in root’s path before the typical directories (i.e., /bin, /sbin), then a local user can create files that root might execute, doing arbitrary things to the system. This is the reason that the current directory (.) should not be in root’s path either.
To avoid this issue:
Make root’s path as short as possible, and never use relative paths.
Avoid having world-writable directories in root’s path.
Consider setting explicit paths in shell scripts run by root.
Consider hardcoding absolute paths to utilities used in shell scripts run by root.
Put user or application directories last in the $PATH, and then only for unprivileged users.
There are several ways to handle this problem.
You can prepend or append a new directory using PATH="newdir:$PATH" or PATH="$PATH:newdir", though you should make sure the directory isn’t already in the $PATH first.
If you need to edit something in the middle of the path, you can echo the path to the screen, then use your terminal’s kill and yank (copy and paste) facility to duplicate it on a new line and edit it. Or, you can add the “[m]acros that are convenient for shell interaction” from the readline documentation. Specifically:
# edit the path"\C-xp":"PATH=${PATH}\e\C-e\C-a\ef\C-f"# [...]# Edit variable on current line."\M-\C-v":"\C-a\C-k$\C-y\M-\C-e\C-a\C-y="
Then pressing Ctrl-X P will display the $PATH on the current line for you to edit, while typing any variable name and pressing Meta-Ctrl-V will display that variable for editing. Very handy.
For simple cases you can also use the function in Example 16-4 (adapted slightly from Red Hat Linux’s /etc/profile).
# cookbook filename: func_pathmunge# Adapted from Red Hat Linuxfunctionpathmunge{if!echo$PATH|/bin/egrep -q"(^|:)$1($|:)";thenif["$2"="after"];thenPATH="$PATH:$1"elsePATH="$1:$PATH"fifi}
The egrep pattern looks for the value in $1 between two pipe characters (|) or colons (:), at the beginning (^) or end ($) of the $PATH string. We chose to use a case statement in our function, and to force a leading and trailing : to do the same thing. It’s theoretically faster since it uses a shell builtin, but the Red Hat version is more concise. Our version is also an excellent illustration of the fact that the if command works on exit codes, so the first if works by using the exit code set by grep, while the second requires the use of the test operator ([]).
For more complicated cases when you’d like a lot of error checking, you can source and then use the more generic functions in Example 16-5.
# cookbook filename: func_tweak_path#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# Add a directory to the beginning or end of your path as long as it's not# already present. Does not take into account symbolic links!# Returns: 1 or sets the new $PATH# Called like: add_to_path <directory> (pre|post)functionadd_to_path{locallocation=$1localdirectory=$2# Make sure we have something to work withif[-z"$location"-o -z"$directory"];thenecho"$0:$FUNCNAME: requires a location and a directory to add">&2echo"e.g. add_to_path pre /bin">&2return1fi# Make sure the directory is not relativeif[$(echo$directory|grep'^/')];then:echo"$0:$FUNCNAME: '$directory' is absolute">&2elseecho"$0:$FUNCNAME: can't add relative directory '$directory' to \$PATH">&2return1fi# Make sure the directory to add actually existsif[-d"$directory"];then:echo"$0:$FUNCNAME: directory exists">&2elseecho"$0:$FUNCNAME: '$directory' does not exist--aborting">&2return1fi# Make sure it's not already in the $PATHif[$(contains"$PATH""$directory")];thenecho"$0:$FUNCNAME: '$directory' already in \$PATH--aborting">&2else:echo"$0:$FUNCNAME: adding directory to \$PATH">&2fi# Figure out what to docase$locationin pre*)PATH="$directory:$PATH";;post*)PATH="$PATH:$directory";;*)PATH="$PATH:$directory";;esac# Clean up the new path, then set itPATH=$(clean_path$PATH)}# end of function add_to_path#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# Remove a directory from your path, if present.# Returns: sets the new $PATH# Called like: rm_from_path <directory>functionrm_from_path{localdirectory=$1# Remove all instances of $directory from $PATHPATH=${PATH//$directory/}# Clean up the new path, then set itPATH=$(clean_path$PATH)}# end of function rm_from_path#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# Remove leading/trailing or duplicate ':', remove duplicate entries# Returns: echoes the "cleaned up" path# Called like: cleaned_path=$(clean_path $PATH)functionclean_path{localpath=$1localnewpathlocaldirectory# Make sure we have something to work with[-z"$path"]&&return1# Remove duplicate directories, if anyfordirectory in${path//:/};docontains"$newpath""$directory"&&newpath="${newpath}:${directory}"done# Remove any leading ':' separators# Remove any trailing ':' separators# Remove any duplicate ':' separatorsnewpath=$(echo$newpath|sed's/^:*//; s/:*$//; s/::/:/g')# Return the new pathecho$newpath}# end of function clean_path#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# Determine if the path contains a given directory# Return 1 if target is contained within pattern, 0 otherwise# Called like: contains $PATH $dirfunctioncontains{localpattern=":$1:"localtarget=$2# This will be a case-sensitive comparison unless nocasematch is setcase$patternin *:$target:*)return1;;*)return0;;esac}# end of function contains
Use them as follows:
$ source chpath $ echo $PATH /bin:/usr/bin:/usr/local/bin:/usr/bin/X11:/usr/X11R6/bin:/home/jp/bin $ add_to_path pre foo -bash:add_to_path: can't add relative directory 'foo' to the $PATH $ add_to_path post ~/foo -bash:add_to_path: '/home/jp/foo' does not exist--aborting $ add_to_path post '~/foo' -bash:add_to_path: can't add relative directory '~/foo' to the $PATH $ rm_from_path /home/jp/bin $ echo $PATH /bin:/usr/bin:/usr/local/bin:/usr/bin/X11:/usr/X11R6/bin $ add_to_path /home/jp/bin -bash:add_to_path: requires a location and a directory to add e.g. add_to_path pre /bin $ add_to_path post /home/jp/bin $ echo $PATH /bin:/usr/bin:/usr/local/bin:/usr/bin/X11:/usr/X11R6/bin:/home/jp/bin $ rm_from_path /home/jp/bin $ add_to_path pre /home/jp/bin $ echo $PATH /home/jp/bin:/bin:/usr/bin:/usr/local/bin:/usr/bin/X11:/usr/X11R6/bin
There are four interesting things about this problem and the functions presented in Example 16-5 in the Solution.
First, if you try to modify your $PATH or other environment variables in a shell script, it won’t work because scripts run in subshells that go away when the scripts terminate, taking any modified environment variables with them. So instead, we source the functions into the current shell and run them from there.
Second, you may notice that add_to_path post ~/foo returns “does not exist” while add_to_path post'~/foo' returns “can’t add relative directory.” That’s because ~/foo is expanded by the shell to /home/jp/foo before the function ever sees it. Not accounting for shell expansion is a common mistake. Use the echo command to see what the shell will actually pass to your scripts and functions.
Next, you may note the use of lines such as echo "$0:$FUNCNAME:requires a location and a directory to add" >&2. $0:$FUNCNAME is a handy way to identify exactly where an error message is coming from. $0 is always the name of the current program (-bash in the usage examples, and the name of your script or program in other cases). Adding the function name makes it easier to track down problems when debugging. Echoing to >&2 sends the output to STDERR, where runtime user feedback, especially including warnings or errors, should go.
Finally, you can argue that the functions have inconsistent interfaces, since add_to_path and remove_from_path actually set $PATH, while clean_path displays the cleaned-up path and contains returns true or false. We might not do it that way in production either, but it makes this example more interesting and shows different ways to do things. And we might argue that the interfaces make sense given what the functions do.
Set your $CDPATH appropriately. Your commonly used directories will likely be unique, so for a contrived example, suppose you spend a lot of time working with init’s rc directories:
/home/jp$ cd rc3.d bash: cd: rc3.d: No such file or directory /home/jp$ export CDPATH='.:/etc' /home/jp$ cd rc3.d /etc/rc3.d /etc/rc3.d$ cd rc5.d /etc/rc5.d /etc/rc5.d$ cd games bash: cd: games: No such file or directory /etc/rc5.d$ export CDPATH='.:/etc:/usr' /etc/rc5.d$ cd games /usr/games /usr/games$
According to the Bash Reference Manual, $CDPATH is “a colon-separated list of directories used as a search path for the cd builtin command.” Think of it as $PATH for cd. It’s a little subtle, but can be very handy.
If the argument to cd begins with a slash, $CDPATH will not be used. If $CDPATH is used, the absolute pathname to the new directory is printed to STDOUT, as in our example.
Watch out when running bash in POSIX mode (e.g., as /bin/sh or with --posix). As the Bash Reference Manual notes:
If a non-empty directory name from
$CDPATHis used, or if - is the first argument, and the directory change is successful, the absolute pathname of the new working directory is written to the standard output.
In other words, pretty much every time you use cd it will echo the new path to STDOUT, which is not the standard behavior.
Common directories to include in $CDPATH are:
.The current directory (optional because this is implied)
Your home directory
The parent directory
The grandparent directory
A hidden directory containing nothing but symbolic links to other commonly used directories
These suggestions result in this:
export CDPATH='.:~/:..:../..:~/.dirlinks'
Add something like this to the top of your script, or better, to an rc file:
functioncommand_not_found_handle(){echo"Sorry.$0:$1not there."return1}
In bash 4.3 and later there is a special function that is called if the shell cannot find the executable you want to run. The function is called command_not_found_handle, and you can (re)define it for your custom purposes. In this example we had the function simply echo the name of the shell and then the command that couldn’t be found.
It is important that your function return a nonzero value to indicate that the invocation of the command did not succeed. Other parts of your script, or other callers of your script, may be depending on that information.
Some administrators put a definition for the command_not_found_handle function in a system-wide bashrc file like /etc/profile or similar. In it, they look in /usr/lib or /usr/share for a Python script called command-not-found (note the dashes, not underscores). That script looks in packages for the command that just failed, to see if it can suggest installing a package to provide the missing command. While helpful in some situations, it is just noise for those cases where the command was simply mistyped.
Do not manually rename or move executable files, as many aspects of Unix and Linux depend on certain commands existing in certain places; instead, you should use aliases, functions, and possibly symbolic links.
According to the Bash Reference Manual, "Aliases allow a string to be substituted for a word when it is used as the first word of a simple command. The shell maintains a list of aliases that may be set and unset with the alias and unalias builtin commands.” This means that you can rename commands, or create a macro, by listing many commands in one alias; for example, alias copy='cp' or alias ll.='ls -ld .*'.
Aliases are only expanded once, so you can change how a command works, as with alias ls='ls -F', without going into an endless loop. In most cases only the first word of the command line is checked for alias expansion, and aliases are strictly text substitutions; they cannot use arguments to themselves. In other words, you can’t do alias='mkdir $1 && cd $1' because that doesn’t work.
Functions are used in two different ways. First, they can be sourced into your interactive shell, where they become, in effect, shell scripts that are always held in memory. They are usually small, and are very fast since they are already in memory and are executed in the current process, not in a spawned subshell. Second, they may be used within a script as subroutines. Functions do allow arguments. For example, as seen in Example 16-6 (from Recipe 6.19):
# cookbook filename: func_calc# Trivial command-line calculatorfunctioncalc{# INTEGER ONLY! --> echo The answer is: $(( $* ))# Floating pointawk"BEGIN {print \"The answer is: \"$*}";}# end of calc
For personal or system-wide use, you are probably better off using aliases or functions to rename or tweak commands, but symbolic links are very useful in allowing a command to be in more than one place at a time. For example, Linux systems almost always use /bin/bash while other systems may use /usr/bin/bash, /usr/local/bin/bash, or /usr/pkg/bin/bash. While there is a better way to handle this particular issue (using env; see Recipe 15.1), in general symbolic links may be used as a workaround. We do not recommend using hard links, as they are harder to see if you are not looking for them, and they are more easily disrupted by badly behaved editors and such. Symbolic links are just more obvious and intuitive.
Usually, only the first word of a command line is checked for alias expansion. How-ever, if the last character of the value of that alias is a space, the next word will be checked as well. In practice, this is rarely an issue.
Since in bash aliases can’t use arguments (unlike in csh), you’ll need to use a function if you need to pass in arguments. Because both aliases and functions reside in memory, this is not a big difference.
Unless the expand_aliases shell option is set, aliases are not expanded when the shell is not interactive. Best practices for writing scripts dictate that you not use aliases, since they may not be present on another system. You also need to define functions inside your script, or explicitly source them before use (see Recipe 19.14). Thus, the best place to define them is in your global /etc/bashrc or your local ~/.bashrc.
See the tables in the sections “Builtin Shell Variables”, “set Options”, and “shopt Options” in Appendix A.
There are three ways to adjust various aspects of your environment. set is standardized in POSIX and uses one-letter options. shopt is specifically for bash shell options. And there are many environment variables in use for historical reasons, as well as for compatibility with many third-party applications. How you adjust what, and where, can be be very confusing. The tables in Appendix A will help you sort it out, but they’re too big to duplicate here.
help set
help shopt
The bash documentation (see http://www.bashcookbook.com)
Edit or create a ~/.inputrc or /etc/inputrc file, as appropriate. There are many parameters you can adjust to your liking. To have readline use your file when it initializes, set $INPUTRC; for example, use INPUTRC=~/.inputrc. To reread the file and apply or test after making changes, use bind -f filename.
We recommend you explore the readline documentation and the bind command—especially bind -v, bind -l, bind -s, and bind -p, though the last one is rather long and cryptic.
For more on configuring readline, see “readline Init File Syntax” in Appendix A. Some useful settings for users from other environments, notably Windows, are:
# This is a SUBSET of interesting inputrc settings, see Chapter 16:# "Getting Started with a Custom Configuration" for a longer example# To reread (and implement changes to this file) use:# bind -f $SETTINGS/inputrc# First, include any system-wide bindings and variable# assignments from /etc/inputrc# (fails silently if file doesn't exist)$include/etc/inputrc$ifBash# Ignore case when doing completionsetcompletion-ignore-case on# Completed dir names have a slash appendedsetmark-directories on# Completed names which are symlinks to dirs have a slash appendedsetmark-symlinked-directories on# List ls -F for completionsetvisible-stats on# Cycle through ambiguous completions instead of list"\C-i": menu-complete# Set bell to audiblesetbell-style audible# List possible completions instead of ringing bellsetshow-all-if-ambiguous on# From the readline documentation at# https://cnswww.cns.cwru.edu/php/chet/readline/readline.html#SEC12# Macros that are convenient for shell interaction# edit the path"\C-xp":"PATH=${PATH}\e\C-e\C-a\ef\C-f"# prepare to type a quoted word -- insert open and close double quotes# and move to just after the open quote"\C-x\"":"\"\"\C-b"# insert a backslash (testing backslash escapes in sequences and macros)"\C-x\\":"\\"# Quote the current or previous word"\C-xq":"\eb\"\ef\""# Add a binding to refresh the line, which is unbound"\C-xr": redraw-current-line# Edit variable on current line.#"\M-\C-v": "\C-a\C-k$\C-y\M-\C-e\C-a\C-y=""\C-xe":"\C-a\C-k$\C-y\M-\C-e\C-a\C-y="$endif
You will want to experiment with these and other settings. Also note the $include to use the system settings, but make sure you can change them if you like. See Recipe 16.22 for the downloadable file.
Many people are not aware of how customizable, not to mention powerful and flexible, the GNU Readline library is. Having said that, there is no “one size fits all” approach. You should work out a configuration that suits your needs and habits.
Note the first time readline is called it performs its normal startup file processing, including looking at $INPUTRC, or defaulting to ~/.inputrc if that’s not set.
Create a ~/bin directory, place your utilities in it, and add it to your path:
PATH="$PATH:~/bin"
You’ll want to make this change in one of your shell initialization files, such as ~/.bashrc. Some systems already add $HOME/bin as the last directory in a nonprivileged user account by default, so check first.
As a fully qualified shell user (well, you bought this book), you’ll certainly be creating lots of scripts. It’s inconvenient to invoke scripts with their full pathname. By collecting your scripts in a ~/bin directory, you can make your scripts look like regular Unix programs—at least to you.
For security reasons, don’t put your bin directory at the start of your path. Starting your path with ~/bin makes it easy to override system commands, which is inconvenient if it happens accidentally (we’ve all done it), and dangerous if it’s done maliciously.
$PS2 is called the secondary prompt string and is used when you are interactively entering a command that you have not completed yet. It is usually set to >, but you can redefine it. For example:
$ export PS2='Secondary: ' $ for i in $(ls) Secondary: do Secondary: echo $i Secondary: done colors deepdir trunc_PWD
$PS3 is the select prompt, and is used by the select statement to prompt the user for a value. It defaults to #?, which isn’t very intuitive. You should change it before using the select command; for example:
$ select i in $(ls) Secondary: do Secondary: echo $i Secondary: done 1) colors 2) deepdir 3) trunc_PWD #? 1 colors #? ^C $ export PS3='Choose a directory to echo: ' $ select i in $(ls); do echo $i; done 1) colors 2) deepdir 3) trunc_PWD Choose a directory to echo: 2 deepdir Choose a directory to echo: ^C
$PS4 is displayed during trace output. Its first character is shown as many times as necessary to denote the nesting depth. The default is +. For example:
$ cat demo #!/usr/bin/env bash set -o xtrace alice=girl echo "$alice" ls -l $(type -path vi) echo line 10 ech0 line 11 echo line 12 $ ./demo + alice=girl + echo girl girl ++ type -path vi + ls -l /usr/bin/vi -r-xr-xr-x 6 root wheel 285108 May 8 2005 /usr/bin/vi + echo line 10 line 10 + ech0 line 11 ./demo: line 11: ech0: command not found + echo line 12 line 12 $ export PS4='+xtrace $LINENO: ' $ ./demo +xtrace 5: alice=girl +xtrace 6: echo girl girl ++xtrace 8: type -path vi +xtrace 8: ls -l /usr/bin/vi -r-xr-xr-x 6 root wheel 285108 May 8 2005 /usr/bin/vi +xtrace 10: echo line 10 line 10 +xtrace 11: ech0 line 11 ./demo: line 11: ech0: command not found +xtrace 12: echo line 12 line 12
Using the default settings, the last shell to gracefully exit will overwrite your history file, so unless it is synchronized with any other shells you had open at the same time, it will clobber their histories. Using the shell option shown in Recipe 16.14 to append rather than overwrite the history file helps, but keeping your history in sync across sessions may offer additional benefits.
Manually synchronizing history involves writing an alias to append the current history to the history file (history -a), then rereading anything new in that file into the current shell’s history (history -n):
alias hs='history -a ; history -n'
The disadvantage to this approach is that you must manually run the commands in each shell when you want to synchronize your history.
To automate that approach, you could use the $PROMPT_COMMAND variable:
PROMPT_COMMAND='history -a ; history -n'
The value of $PROMPT_COMMAND is interpreted as a command to execute each time the default interactive prompt, $PS1, is displayed. The disadvantage to that approach is that it runs those commands every time $PS1 is displayed. That is very often, and on a heavily loaded or slower system that can cause a significant slowdown in your shell, especially if you have a large history.
help history
The $HISTFILESIZE variable sets the number of lines permitted in the $HISTFILE. The default for $HISTFILESIZE is 500 lines, and $HISTFILE is ~/.bash_history unless you are in POSIX mode, in which case it’s ~/.sh_history. Increasing $HISTFILESIZE may be useful, and unsetting it causes the $HISTFILE length to be unlimited. Changing $HISTFILE probably isn’t necessary, except that if it is not set or the file is not writable, no history will be written to disk. The $HISTSIZE variable sets the number of lines permitted in the history stack in memory.
$HISTIGNORE and $HISTCONTROL control what goes into your history in the first place. $HISTIGNORE is more flexible since it allows you to specify patterns to decide what command lines to save to the history. $HISTCONTROL is more limited in that it sup-ports only the few keywords listed here (any other value is ignored):
ignorespaceCommand lines that begin with a space character are not saved in the history list.
ignoredupsCommand lines that match the previous history entry are not saved in the history list.
ignorebothShorthand for both ignorespace and ignoredups.
erasedupsAll previous command lines that match the current line are removed from the history list before that line is saved.
If $HISTCONTROL is not set, or does not contain any of these keywords, all commands are saved to the history list, subject to processing $HISTIGNORE. The second and subsequent lines of a multiline compound command are not tested, and are added to the history regardless of the value of $HISTCONTROL.
(Material in the preceding paragraphs has been adapted from the Bash Reference Manual.)
If set and non-null, the $HISTTIMEFORMAT variable available in bash 3 and later specifies an strftime format string to use when displaying or writing the history. If you don’t have bash version 3, but you do use a terminal with a scroll-back buffer, adding a date and timestamp to your prompt can also be very helpful (see Recipe 16.2). Watch out because stock bash does not put a trailing space after the format, but some systems (e.g., Debian) have patched it to do so:
$ history
1 ls -la
2 help history
3 help fc
4 history
# Ugly
$ export HISTTIMEFORMAT='%Y-%m-%d_%H:%M:%S'
$ history
1 2006-10-25_20:48:04ls -la
2 2006-10-25_20:48:11help history
3 2006-10-25_20:48:14help fc
4 2006-10-25_20:48:18history
5 2006-10-25_20:48:39export HISTTIMEFORMAT='%Y-%m-%d_%H:%M:%S'
6 2006-10-25_20:48:41history
# Better
$ HISTTIMEFORMAT='%Y-%m-%d_%H:%M:%S; '
$ history
1 2006-10-25_20:48:04; ls -la
2 2006-10-25_20:48:11; help history
3 2006-10-25_20:48:14; help fc
4 2006-10-25_20:48:18; history
5 2006-10-25_20:48:39; export HISTTIMEFORMAT='%Y-%m-%d_%H:%M:%S'
6 2006-10-25_20:48:41; history
7 2006-10-25_20:48:47; HISTTIMEFORMAT='%Y-%m-%d_%H:%M:%S; '
8 2006-10-25_20:48:48; history
# Getting tricky now
$ HISTTIMEFORMAT=': %Y-%m-%d_%H:%M:%S; '
$ history
1 : 2006-10-25_20:48:04; ls -la
2 : 2006-10-25_20:48:11; help history
3 : 2006-10-25_20:48:14; help fc
4 : 2006-10-25_20:48:18; history
5 : 2006-10-25_20:48:39; export HISTTIMEFORMAT='%Y-%m-%d_%H:%M:%S'
6 : 2006-10-25_20:48:41; history
7 : 2006-10-25_20:48:47; HISTTIMEFORMAT='%Y-%m-%d_%H:%M:%S; '
8 : 2006-10-25_20:48:48; history
The last example uses the : builtin with the ; metacharacter to encapsulate the date stamp into a “do nothing” command (e.g., : 2006-10-25_20:48:48;). This allows you to reuse a literal line from the history file without having to bother parsing out the date stamp. Note the space after the : is required.
There are also shell options to configure history file handling. If histappend is set, the shell appends to the history file; otherwise, it overwrites the history file. Note that it is still truncated to $HISTFILESIZE. If cmdhist is set, multiline commands are saved as a single line, with semicolons added as needed. If lithist is set, multiline commands are saved with embedded newlines.
Use the function in Example 16-7.
# cookbook filename: func_cd# Allow use of 'cd ...' to cd up 2 levels, 'cd ....' up 3, etc. (like 4NT/4DOS)# Usage: cd ..., etc.functioncd{localoption=length=count=cdpath=i=# Local scope and start clean# If we have a -L or -P symlink option, save then remove itif["$1"="-P"-o"$1"="-L"];thenoption="$1"shiftfi# Are we using the special syntax? Make sure $1 isn't empty, then# match the first 3 characters of $1 to see if they are '...', then# make sure there isn't a slash by trying a substitution; if it fails,# there's no slash.if[-n"$1"-a"${1:0:3}"='...'-a"$1"="${1%/*}"];then# We are using special syntaxlength=${#1}# Assume that $1 has nothing but dots and count themcount=2# 'cd ..' still means up one level, so ignore first two# While we haven't run out of dots, keep cd'ing up 1 levelfor((i=$count;i<=$length;i++));docdpath="${cdpath}../"# Build the cd pathdone# Actually do the cdbuiltin cd$option"$cdpath"elif[-n"$1"];then# We are NOT using special syntax; just plain old cd by itselfbuiltin cd$option"$*"else# We are NOT using special syntax; plain old cd by itself to home dirbuiltin cd$optionfi}# end of cd
The cd command takes an optional -L or -P argument that, respectively, follows symbolic links or the physical directory structure. Either way, we have to take them into account if we want to redefine how cd works.
Then, we make sure $1 isn’t empty and match the first three characters of $1 to see if they are .... We then make sure there isn’t a slash by trying a substitution; if it fails, there’s no slash. Both of these string routines require bash version 2.0+. After that, we build the actual cd command using a portable for loop and finally use the builtin command to use the shell cd and not create an endless loop by recursively calling our cd function. We also pass in the -L or -P argument if present.
help cd
http://jpsoft.com for the 4NT shell, which is the source of this idea
Recipe 16.16, “Creating and Changing Into a New Directory in One Step”
Add the function in Example 16-8 to an appropriate configuration file, such as your ~/.bashrc file, and source it.
# cookbook filename: func_mcd# mkdir newdir then cd into it# usage: mcd (<mode>) <dir>functionmcd{localnewdir='_mcd_command_failed_'if[-d"$1"];then# Dir exists, mention that...echo"$1exists..."newdir="$1"elseif[-n"$2"];then# We've specified a modecommandmkdir -p -m$1"$2"&&newdir="$2"else# Plain old mkdircommandmkdir -p"$1"&&newdir="$1"fifibuiltin cd"$newdir"# No matter what, cd into it}# end of mcd
For example:
$ source mcd $ pwd /home/jp $ mcd 0700 junk $ pwd /home/jp/junk $ ls -ld . drwx------ 2 jp users 512 Dec 6 01:03 .
This function allows you to optionally specify a mode for the mkdir command to use when creating the directory. If the directory already exists, it will mention that fact but still cd into it. We use the command command to make sure that we ignore any shell functions for mkdir, and the builtin command to make sure we only use the shell cd.
We also assign _mcd_command_failed_ to a local variable in case the mkdir fails. If it works, the correct new directory is assigned. If it fails, when the cd tries to execute it will display a reasonably useful message, assuming you don’t have a lot of _mcd_command_failed_ directories lying around:
$ mcd /etc/junk mkdir: /etc/junk: Permission denied -bash: cd: _mcd_command_failed_: No such file or directory $
You might think that we could easily improve this using break or exit if the mkdir fails. However, break only works in a for, while, or until loop, and exit will actually exit our shell, since a sourced function runs in the same process as the shell (we could use return, but we will leave that as an exercise for the reader):
commandmkdir -p"$1"&&newdir="$1"||exit1# This will exit our shellcommandmkdir -p"$1"&&newdir="$1"||break# This will fail
You could also place the following in a trivial function, but we obviously prefer the more robust version given in the solution:
functionmcd{mkdir"$1"&&cd"$1";}
This use of find in a large directory structure such as /usr could take a while and isn’t recommended.
Depending on how your directory structure is set up, this may not work for you; you’ll have to try it and see. The find . will simply list all the files and directories in the current directory and below, the tail -n 1 will grab the last line, dirname will extract just the path, and cd will take you there. It may be possible for you to tweak the command to get it to put you in the right place. For example:
alias bot='cd $(dirname $(find . | sort -r | tail -n 5 | head -1))'
or:
alias bot='cd $(dirname $(find . | sort -r | grep -v 'X11' | tail -n 3 \ | head -1))'
Keep trying the part in the innermost parentheses, especially tweaking the find command, until you get the results you need. Perhaps there is a key file or directory at the bottom of the structure, in which case the following function might work:
function bot { cd $(dirname $(find . | grep -e "$1" | head -1)); }
Note that aliases can’t use arguments, so this must be a function. We use grep rather than a -name argument to find because grep is much more flexible. Depending on your structure, you might want to use tail instead of head. Again, test the find command first.
man find
man dirname
man head
man tail
man grep
man sort
Recipe 16.16, “Creating and Changing Into a New Directory in One Step”
The material in this recipe also appears in Learning the bash Shell, 3rd Edition, by Cameron Newham (O’Reilly).
Use the dynamically loadable builtins introduced in bash version 2.0. The bash archive contains a number of prewritten builtins in the directory ./examples/loadables/, especially the canonical hello.c. You can build them by uncommenting the lines in the file Makefile that are relevant to your system, and typing make. We’ll take one of these builtins, tty, and use it as a case study for builtins in general.
The following is a list of the builtins provided in bash version 4.4’s ./examples/loadables/ directory:
basename.c |
id.c |
necho.c |
printenv.c |
strftime.c |
uname.c |
cat.c |
ln.c |
pathchk.c |
push.c |
sync.c |
unlink.c |
dirname.c |
loadables.h |
perl |
realpath.c |
tee.c |
whoami.c |
finfo.c |
logname.c |
perl/bperl.c |
rmdir.c |
template.c |
head.c |
mkdir.c |
perl/iperl.c |
setpgid.c |
truefalse.c |
hello.c |
mypid.c |
On systems that support dynamic loading, you can write your own builtins in C, compile them into shared objects, and load them at any time from within the shell with the enable builtin.
We will discuss briefly how to go about writing a builtin and loading it in bash. This discussion assumes that you have experience with writing, compiling, and linking C programs.
tty will mimic the standard Unix command tty. It will print the name of the terminal that is connected to standard input. The builtin will, like the command, return true if the device is a TTY and false if it isn’t. In addition, it will take an option, -s, which specifies that it should work silently (i.e., print nothing and just return a result).
The C code for a builtin can be divided into three distinct sections: the code that implements the functionality of the builtin, a help text message definition, and a structure describing the builtin so that bash can access it.
The description structure is quite straightforward and takes the form:
struct builtinbuiltin_name_struct = {"builtin_name",function_name, BUILTIN_ENABLED,help_array,"usage",0 };
The trailing _struct is required on the first line to give the enable builtin a way to find the symbol name. builtin_name is the name of the builtin as it appears in bash. The next field, function_name, is the name of the C function that implements the builtin. We’ll look at this in a moment. BUILTIN_ENABLED is the initial state of the builtin, whether it is enabled or not. This field should always be set to BUILTIN_ ENABLED. help_array is an array of strings that are printed when help is used on the builtin. usage is the shorter form of help: the command and its options. The last field in the structure should be set to 0.
In our example we’ll call the builtin tty, the C function tty_builtin, and the help array tty_doc. The usage string will be tty [-s]. The resulting structure looks like this:
structbuiltintty_struct="tty", tty_builtin, BUILTIN_ENABLED, tty_doc,"tty [-s]", 0};
The next section is the code that does the work. It looks like this:
tty_builtin(list)WORD_LIST *list;{int opt, sflag;char *t;reset_internal_getopt();sflag=0;while((opt=internal_getopt(list,"s"))!=-1){switch(opt){case's':sflag=1;break;default: builtin_usage();return(EX_USAGE);}}list=loptend;t=ttyname(0);if(sflag==0)puts(t ? t :"not a tty");return(t ? EXECUTION_SUCCESS : EXECUTION_FAILURE);}
builtin functions are always given a pointer to a list of type WORD_LIST. If the builtin doesn’t actually take any options, you must call no_options(list) and check its return value before any further processing. If the return value is nonzero, your function should immediately return with the value EX_USAGE.
You must always use internal_getopt rather than the standard C library getopt to process the built-in options. Also, you must reset the option processing first by calling reset_internal_getopt.
Option processing is performed in the standard way, except if the options are incorrect, in which case you should return EX_USAGE. Any arguments left after option processing are pointed to by loptend. Once the function is finished, it should return the value EXECUTION_SUCCESS or EXECUTION_FAILURE.
In the case of our tty builtin, we then just call the standard C library routine ttyname and, if the -s option wasn’t given, print out the name of the TTY (or “not a tty” if the device wasn’t). The function then returns success or failure, depending upon the result from the call to ttyname.
The last major section is the help definition. This is simply an array of strings, the last element of the array being NULL. Each string is printed to standard output when help is run on the builtin. You should, therefore, keep the strings to 76 characters or less (an 80-character standard display minus a 4-character margin). In the case of tty, our help text looks like this:
char *tty_doc[]={"tty writes the name of the terminal that is opened for standard","input to standard output. If the `-s' option is supplied, nothing","is written; the exit status determines whether or not the standard","input is connected to a tty.",(char *)NULL};
The last things to add to our code are the necessary C header files. These are stdio.h and the bash header files config.h, builtins.h, shell.h, and bashgetopt.h.
Example 16-9 shows the C program in its entirety.
# cookbook filename: builtin_tty.c#include "config.h"#include <stdio.h>#include "builtins.h"#include "shell.h"#include "bashgetopt.h"extern char *ttyname();tty_builtin(list)WORD_LIST *list;{int opt, sflag;char *t;reset_internal_getopt();sflag=0;while((opt=internal_getopt(list,"s"))!=-1){switch(opt){case's':sflag=1;break;default: builtin_usage();return(EX_USAGE);}}list=loptend;t=ttyname(0);if(sflag==0)puts(t ? t :"not a tty");return(t ? EXECUTION_SUCCESS : EXECUTION_FAILURE);}char *tty_doc[]={"tty writes the name of the terminal that is opened for standard","input to standard output. If the `-s' option is supplied, nothing","is written; the exit status determines whether or not the standard","input is connected to a tty.",(char *)NULL};structbuiltintty_struct={"tty", tty_builtin, BUILTIN_ENABLED, tty_doc,"tty [-s]", 0};
We now need to compile and link this as a dynamic shared object. Unfortunately, different systems have different ways to specify how to compile dynamic shared objects.
The configure script should put the correct commands into the Makefile automatically. If for some reason it doesn’t, Table 16-1 lists some common systems and the commands needed to compile and link tty.c. Replace archive with the path of the top level of the bash archive.
| System | Command |
|---|---|
SunOS 4 |
|
SunOS 5 |
|
SVR4, SVR4.2, Irix |
|
AIX |
|
Linux |
|
NetBSD, FreeBSD |
|
After you have compiled and linked the program, you should have a shared object called tty. To load this into bash, just type enable -f tty tty. You can remove a loaded builtin at any time with the -d option; e.g., enable -d tty.
You can put as many builtins as you like into one shared object as long as the three main sections for each builtin are in the same C file. However, bash loads a shared object as a whole, so if you ask it to load one builtin from a shared object that has 20 builtins, it will load all 20 (but only one will be enabled). It’s best to keep the number of builtins small to save loading memory with unnecessary things, and to group similar builtins (e.g., pushd, popd, dirs) so that if the user enables one of them, all of them will be loaded and ready in memory for enabling.
./examples/loadables in any bash tarball newer than 2.0
This recipe was adapted directly from Learning the bash Shell, 3rd Edition, by Cameron Newham (O’Reilly).
Find and install additional programmable completion libraries, or write your own. Some examples are provided in the bash tarball, in ./examples/complete. Some distributions (e.g., SUSE) have their own version in /etc/profile.d/complete.bash. However, the largest and most well known of the third-party libraries is certainly Ian Macdonald’s, which you may download as a tarball or RPM from http://www.caliban.org/bash/index.shtml#completion or https://github.com/scop/bash-completion/. This library is already included in Debian (and derivatives like Ubuntu and Mint), and it is present in Fedora Extras as well as other third-party repositories.
According to Ian’s README: “Many of the completion functions assume GNU versions of the various text utilities that they call (e.g., grep, sed, and awk). Your mileage may vary.”
At the time of this writing there are 103 modules provided by the bash-completion-20060301.tar.gz library. The following is an excerpted list:
bash alias completion
bash export completion
bash shell function completion
chown(1) completion
chgrp(1) completion
Red Hat & Debian GNU/Linux if{up,down}
completion
cvs(1) completion
rpm(8) completion
chsh(1) completion
chkconfig(8) completion
ssh(1) completion
GNU make(1) completion
GNU tar(1) completion
jar(1) completion
Linux iptables(8) completion
tcpdump(8) completion
ncftp(1) bookmark completion
Debian dpkg(8) completion
Java completion
PINE address-book completion
mutt completion
Python completion
Perl completion
FreeBSD package management tool completion
mplayer(1) completion
gpg(1) completion
dict(1) completion
cdrecord(1) completion
yum(8) completion
smartctl(8) completion
vncviewer(1) completion
svn completion
Programmable completion is a feature that was introduced in bash version 2.04. It extends the built-in textual completion by providing hooks into the completion mechanism. This means that it is possible to write virtually any form of completion desired. For instance, if you were typing the man command, wouldn’t it be nice to be able to hit Tab and have the manual sections listed for you? Programmable completion allows you to do this and much more.
This recipe will only look at the basics of programmable completion. If you need to delve into the inner depths and actually write your own completion code, first check the libraries of completion commands developed by other people to see if what you want has already been done or is available for use as an example. We’ll just outline the basic commands and procedures needed to use the completion mechanism, should you ever need to work on it yourself.
To do textual completion in a particular way, you first have to tell the shell how to do it when you press the Tab key. This is done via the complete command.
The main argument of complete is a name that can be the name of a command or anything else that you want textual completion to work with. As an example, we will look at the gunzip utility that allows compressed archives of various types to be uncompressed. Normally, if you were to type:
gunzip [TAB][TAB]
you would get a list of filenames from which to complete. This list would include all kinds of things that are unsuitable for gunzip. What you would really like is the subset of those files that are suitable for the utility to work on. You can set this up by using complete:
complete -A file -X '!*.@(Z|gz|tgz)' gunzip
Note that in order for @(Z|gz|tgz) to work, you will need extended pattern matching switched on via shopt -s extglob.
Here we are telling the completion mechanism that when the gunzip command is typed in we want it to do something special. The -A flag is an action and takes a variety of arguments. In this case we provide file as the argument, which asks the mechanism to provide a list of files as possible completions. The next step is to cut this down by selecting only the files that we know will work with gunzip. We’ve done this with the -X option, which takes as its argument a filter pattern. When applied to the completion list, the filter removes anything matching the pattern—i.e., the result is everything that doesn’t match the pattern. gunzip can uncompress a number of file types, including those with the extensions .Z, .gz, and .tgz. We want to match all filenames with extensions that have one of these three patterns. We then have to negate this with a ! (remember, the filter removes the patterns that match).
We can actually try this out first and see what completions would be returned without having to use complete to install the completion. We can do this via the compgen command:
compgen -A file -X '!*.@(Z|gz|tgz)'
This produces a list of completion strings (assuming you have some files in the current directory with these extensions). compgen is useful for trying out filters to see what completion strings are produced. It is also needed when more complex completion is required. We’ll see an example of this later in the recipe.
Once we install the preceding complete command, either by sourcing a script that contains it or by executing it on the command line, we can use the augmented completion mechanism with the gunzip command:
$ gunzip [TAB][TAB] archive.tgz archive1.tgz file.Z $ gunzip
You can probably see that there are other things we could do. What about providing a list of possible arguments for specific options to a command? For instance, the kill command takes a process ID, but can optionally take a signal name preceded by a dash (-) or a signal name following the option -n. We could complete with PIDs, but if there is a dash or a -n, it’ll have to be done with signal names.
This is slightly more complex than the previous one-line example. Here we will need some code to distinguish what has already been typed in. We’ll also need to get the PIDs and the signal names. We’ll put the code in a function and call the function via the completion mechanism. Here’s the code to call our function, which we’ll name _kill:
complete -F _kill kill
The -F option to complete tells it to call the function named _kill when it is performing textual completion for the kill command. The next step is to code the function, as seen in Example 16-10.
# cookbook filename: func_kill_kill(){localcurlocalsignCOMPREPLY=()cur=${COMP_WORDS[COMP_CWORD]}if(($COMP_CWORD==2))&&[[${COMP_WORDS[1]}==-n]];then# return list of available signals_signalselif(($COMP_CWORD==1))&&[["$cur"==-*]];then# return list of available signalssign="-"_signalselse# return list of available PIDsCOMPREPLY=($(compgen-W'$( command ps axo pid | sed 1d )'$cur))fi}
The code is fairly standard, apart from the use of some special environment variables and a call to a function called _signals, which we’ll come to shortly.
The variable $COMPREPLY is used to hold the result that is returned to the completion mechanism. It is an array that holds a set of completion strings. Initially this is set to an empty array.
The local variable $cur is a convenience variable to make the code more readable because the value is used in several places. Its value is derived from an element in the array $COMP_WORDS. This array holds the individual words on the current command line. $COMP_CWORD is an index into the array; it gives the word containing the current cursor position. The value of $cur is the word currently containing the cursor.
The first if statement tests for the condition where the kill command is followed by the -n option. If the first word was -n and we are on the second word, then we need to provide a list of signal names for the completion mechanism.
The second if statement is similar, except this time we are looking to complete on the current word, which starts with a dash and is followed by anything else. The body of this if again calls _signals, but this time it sets the sign variable to a dash. The reason for this will become obvious when we look at the _signals function.
The remaining part in the else block returns a list of process IDs. It uses the compgen command to help create the array of completion strings. First it runs the ps command to obtain a list of PIDs, and then it pipes the result through sed to remove the first line (which is the heading “PID”). This is then given as an argument to the -W option of compgen, which takes a word list. compgen returns all the completion strings that match the value of the variable $cur, and the resulting array is assigned to $COMPREPLY.
compgen is important here because we can’t just return the complete list of PIDs provided by ps. The user may have already typed part of a PID and then attempted completion. As the partial PID will be in the variable $cur, compgen restricts the results to those that match or partially match that value. For example if $cur had the value 5, then compgen would return only values beginning with a 5, such as 5, 59, or 562.
The last piece of the puzzle is the _signals function (Example 16-11).
# cookbook filename: func_signals_signals(){localiCOMPREPLY=($(compgen-A signal SIG${cur#-}))for((i=0;i <${#COMPREPLY[@]};i++));doCOMPREPLY[i]=$sign${COMPREPLY[i]#SIG}done}
While we can get a list of signal names by using complete’s -A signal, the names are unfortunately not in a form that is very usable, so we can’t use this to directly generate the array of names. The names generated begin with the letters “SIG”, while the names needed by the kill command don’t. The _signals function should assign an array of signal names to $COMPREPLY, optionally preceded by a dash.
First we generate the list of signal names with compgen. Each name starts with the letters “SIG”. In order to get complete to provide the correct subset if the user has begun to type a name, we add “SIG” to the beginning of the value in $cur. We also take the opportunity to remove any preceding dash that the value has so it will match.
We then loop on the array, removing the letters “SIG” and adding a dash if needed (the value of the variable sign) to each entry.
Both complete and compgen have many other options and actions; far more than we can cover here. If you are interested in taking programmable completion further, we recommend looking in the Bash Reference Manual and downloading some of the many examples that are available on the internet or in the bash tarball, in ./examples/complete.
help complete
help compgen
./examples/complete in any bash tarball newer than 2.04
Here’s a cheat sheet for these files and what do with them. Some or all of these files may be missing from your system, depending on how it is set up. Systems that use bash by default (e.g., Linux) tend to have a complete set, while systems that use some other shell by default are usually missing at least some of them:
Global login environment file for Bourne and similar login shells. We recommend you leave this alone unless you are the system administrator and know what you are doing.
Global environment file for interactive bash subshells. We recommend you leave this alone unless you are the system administrator and know what you are doing.
If this exists, it’s almost certainly the configuration file for Ian Macdonald’s programmable completion library (see Recipe 16.19). We recommend looking into it—it’s pretty cool.
Other possible distribution-specific bash completion files. Common in Fedora these days, but may be on other systems.
Global GNU readline configuration. We recommend tweaking this as desired for the entire system (if you are the administrator), or tweaking ~/.inputrc for just you (see Recipe 16.22). This is not executed or sourced but read in via readline and $INPUTRC, and $include (or bind -f). Note that it may contain include statements to other readline files.
Personal environment file for interactive bash subshells. We recommend that you place your aliases, functions, and fancy prompts here.
Personal profile file for bash login shells. We recommend that you make sure this sources ~/.bashrc, then ignore it.
Personal profile file for Bourne login shells; only used by bash if ~/.bash_profile is not present. We recommend you ignore this.
Personal profile file for Bourne login shells; only used by bash if ~/.bash_profile and ~/.bash_login are not present. We recommend you ignore this unless you also use other shells that use it.
Default storage file for your shell command history. We recommend you use the history tools (Recipe 16.14) to manipulate it instead of trying to directly edit it. This is not executed or sourced; it’s just a datafile.
Executed when you log out. We recommend you place any cleanup routines (see Recipe 17.7) here. This is only executed on a clean logout (i.e., not if your session dies due to a dropped WAN link).
Personal customizations for GNU readline. We recommend tweaking this as desired (see Recipe 16.22). This is not executed or sourced but read in via readline and $INPUTRC, and $include (or bind -f). Note that it may contain include statements to other readline files.
We realize this list is a bit is tricky to follow; however, each OS or distribution may differ, since it’s up to the vendor exactly how these files are written. To really understand how your system works, read each of the files listed here. You can also temporarily add echo name_of_file>&2 to the very first line of any of them that are executed or sourced (i.e., skip /etc/inputrc, ~/.inputrc, and ~/.bash_history). Note that may interfere with some programs (notably scp and rsync) that are confused by extra output on STDOUT or STDERR, so remove these statements when you are finished. See the warning in Recipe 16.21 for more details.
Use Table 16-2 as a guideline only, since it’s not necessarily how your system will work. (In addition to the login-related rc files listed in Table 16-2, the ~/.bash_logout rc file is used when you log out cleanly from an interactive session.)
| Interactive login shell | Interactive non-login shell (bash) | Noninteractive shell (script) (bash /dev/null) | Noninteractive (bash -c ':') |
|---|---|---|---|
Ubuntu 6.10: |
Ubuntu 6.10: |
Ubuntu 6.10: |
Ubuntu 6.10: |
/etc/profile |
N/A |
N/A |
|
/etc/bash.bashrc |
/etc/bash.bashrc |
||
~/.bash_profilea |
|||
~/.bashrc |
~/.bashrc |
||
/etc/bash_completion |
/etc/bash_completion |
||
Fedora Core 5: |
Fedora Core 5: |
Fedora Core 5: |
Fedora Core 5: |
N/A |
N/A |
||
/etc/profile.d/colorls.sh |
|||
/etc/profile.d/glib2.sh |
|||
/etc/profile.d/krb5.sh |
|||
/etc/profile.d/lang.sh |
|||
/etc/profile.d/less.sh |
|||
/etc/profile.d/vim.sh |
|||
/etc/profile.d/which-2.sh |
|||
~/.bash_profiled |
|||
~/.bashrc |
~/.bashrc |
||
/etc/bashrc |
/etc/bashrc |
||
a If ~/.bash_profile is not found, then ~/.bash_login or ~/.profile will be attempted, in that order. b If c Red Hat’s /etc/profile also sources /etc/profile.d/*.sh files; see Recipe 4.10 for details. d If ~/.bash_profile is not found, then ~/.bash_login or ~/.profile will be attempted, in that order. | |||
For more detail see the “Bash Startup Files” section in the Bash Reference Manual.
One of the tricky things in Unix or Linux is figuring out where to change something like the $PATH or prompt on the rare occasions when you do want to do it for the whole system. Different operating systems and even versions can put things in different places. This command has a pretty good chance of finding out where your system $PATH is set, for example:
grep 'PATH=' /etc/{profile,*bash*,*csh*,rc*}
If that doesn’t work, the only thing you can really do is grep all of /etc to find it, as in:
find /etc -type f | xargs grep 'PATH='
Note that unlike most of the code in this book, this is better run as root. You can run it as a regular user and get some results, but you may miss something and you’ll almost certainly get some “Permission denied” errors.
One of the other tricky things is figuring out what you can tweak and where to do that for your personal account. We hope this chapter has given you a lot of great ideas in that regard.
man grep
man find
man xargs
The “Bash Startup Files” section in the Bash Reference Manual
Put all of your customizations in files in a settings subdirectory, copy or rsync that directory to a location such as ~/ or /etc, and use includes and symbolic links (e.g., ln -s ~/settings/screenrc ~/.screenrc) as necessary. Use logic in your customization files to account for criteria such as operating system, location, etc.
You may also choose not to use leading dots in the filenames to make it a little easier to manage the files. As you saw in Recipe 1.7, the leading dot causes ls not to show the files by default, thus eliminating some clutter in your home directory listing. But since we’ll be using a directory that exists only to hold configuration files, using the dot is not necessary. Note that dot files are usually not used in /etc either, for the same reason.
See Recipe 16.22 for a sample to get you started.
Let’s take a look at the assumptions and criteria we used in developing this solution. First, the assumptions:
You have a complex environment in which you control some, but not all, of the machines you use.
For machines you control, one machine exports /opt/bin and all other machines NFS-mount it, so all configuration files reside there. We used /opt/bin because it’s short and less likely to collide with existing directories than /usr/local/bin, but feel free to use whatever makes sense.
For some machines with partial control, a system-wide configuration in /etc is used.
For machines on which you have no administrative control, dot files are used in ~/.
You have settings that will vary from machine to machine, and in different environments (e.g., home or work).
The criteria were as follows:
Require as few changes as possible when moving configuration files between operating systems and environments.
Supplement, but do not replace, operating system default or system administrator–supplied configurations.
Provide enough flexibility to handle the demands made by conflicting settings (e.g., work and home CVS).
While it may be tempting to put echo statements in your configuration files to see what’s going on, be careful. If you do that, scp, rsync, and probably any other rsh-like programs will fail with mysterious errors such as:
# scp # protocol error: bad mode # rsync # protocol version mismatch - is your shell clean? # (see the rsync manpage for an explanation) # rsync error: protocol incompatibility (code 2) at compat. # c(62)
ssh itself works since it is actually interactive and the output is displayed on the screen rather than confusing the data stream. See the discussion in Recipe 14.22 for details on why this happens.
For debugging, put these two lines near the top of /etc/profile or ~/.bash_profile, but see the warning we just gave about confusing the data stream:
exportPS4='+xtrace $LINENO: 'set-x
As an alternative (or in addition) to using set -x, you can add lines such as the following to any or all of your configuration files:
# E.g. in ~/.bash_profilecase"$-"in *i*)echo"$(date'+%Y-%m-%d_%H:%M:%S_%Z')Interactive"\"~/.bash_profile ssh=$SSH_CONNECTION">> ~/rc.log;;*)echo"$(date'+%Y-%m-%d_%H:%M:%S_%Z')Noninteractive"\"~/.bash_profile ssh=$SSH_CONNECTION">> ~/rc.log;;esac# In ~/.bashrccase"$-"in *i*)echo"$(date'+%Y-%m-%d_%H:%M:%S_%Z')Interactive"\"~/.bashrc ssh=$SSH_CONNECTION">> ~/rc.log;;*)echo"$(date'+%Y-%m-%d_%H:%M:%S_%Z')Noninteractive"\"~/.bashrc ssh=$SSH_CONNECTION">> ~/rc.log;;esac
Since there is no output to the terminal this will not interfere with commands, as we noted in the warning. Run a tail -f ~/rc.log command in one session and run your troublesome command (e.g., scp, cvs) from elsewhere to determine which configuration files are in use. You can then more easily track down the problem.
When making any changes to your configuration files, we strongly advise that you open two sessions. Make all your changes in one session and then log out and back in. If you broke something so that you can’t log back in, fix it from the second session and then try again from the first one. Do not log out of both terminals until you are absolutely sure you can log back in again. This goes triple if any changes you’re making could affect root.
You really do need to log out and back in again. Sourcing the changed files is a help, but leftovers from the previous environment may allow things to work temporarily, until you start clean—and then things are broken. Make changes to the running environment as necessary, but don’t change the files until you are ready to test; otherwise you’re likely to forget and possibly be locked out if something is wrong.
Here are some samples to give you an idea of what you can do. We follow the suggestion in Recipe 16.21 to keep customizations separate for easy backouts and portability between systems.
For system-wide profile settings, add the contents on Example 16-12 to /etc/profile. Since that file is also used by the true Bourne shell, be careful not to use any bash-only features (e.g., source instead of .) if you do this on a non-Linux system. Linux uses bash as the default shell for both /bin/sh and /bin/bash (except when it doesn’t, as in Ubuntu 6.10+, which uses dash). For user-only settings, add it to only one of ~/.bash_profile, ~/.bash_login, or ~/.profile, in that order (whichever exists first.)
# cookbook filename: add_to_bash_profile# Add this code to your ~/.bash_profile# If we're running in bash, search for then source our settings# You can also just hardcode $SETTINGS, but this is more flexibleif[-n"$BASH_VERSION"];thenforpath in /opt/bin /etc ~;do# Use the first one foundif[-d"$path/settings"-a -r"$path/settings"-a -x"$path/settings"]thenexportSETTINGS="$path/settings"fidonesource"$SETTINGS/bash_profile"#source "$SETTINGS/bashrc" # If necessaryfi
For system-wide environment settings, add the contents in Example 16-13 to /etc/bashrc (or /etc/bash. bashrc).
# cookbook filename: add_to_bashrc# Add this code to your ~/.bashrc# If we're running in bash, and it isn't already set,# search for then source our settings# You can also just hard code $SETTINGS, but this is more flexibleif[-n"$BASH_VERSION"];thenif[-z"$SETTINGS"];thenforpath in /opt/bin /etc ~;do# Use the first one foundif[-d"$path/settings"-a -r"$path/settings"-a -x"$path/settings"]thenexportSETTINGS="$path/settings"fidonefisource"$SETTINGS/bashrc"fi
Example 16-14 is a bash_profile.
# cookbook filename: bash_profile# settings/bash_profile: Login shell environment settings# To reread (and implement changes to this file) use:# source $SETTINGS/bash_profile# Only if interactive bash with a terminal![-t1-a -n"$BASH_VERSION"]||return# Failsafe. This should be set when we're called, but if not, the# "not found" error messages should be pretty clear.# Use leading ':' to prevent this from being run as a program after# it is expanded.:${SETTINGS:='SETTINGS_variable_not_set'}# DEBUGGING only--will break scp, rsync# echo "Sourcing $SETTINGS/bash_profile..."# export PS4='+xtrace $LINENO: '# set -x# Debugging/logging--will not break scp, rsync#case "$-" in# *i*) echo "$(date '+%Y-%m-%d_%H:%M:%S_%Z') Interactive" \# "$SETTINGS/bash_profile ssh=$SSH_CONNECTION" >> ~/rc.log ;;# * ) echo "$(date '+%Y-%m-%d_%H:%M:%S_%Z') Noninteractive" \# "$SETTINGS/bash_profile ssh=$SSH_CONNECTION" >> ~/rc.log ;;#esac# Use the keychain (http://www.funtoo.org/Keychain/) shell script# to manage ssh-agent, if it's available. If it's not, you should look# into adding it.forpath in$SETTINGS${PATH//:/};doif[-x"$path/keychain"];then# Load default id_rsa and/or id_dsa keys, add others here as needed# See also --clear --ignore-missing --noask --quiet --time-out$path/keychain ~/.ssh/id_?sa ~/.ssh/${USER}_?sabreakfidone# Apply interactive subshell customizations to login shells too.# The system profile file in /etc probably already does this.# If not, it's probably better to do it manually in wherever you:# source "$SETTINGS/bash_profile"# But just in case...# for file in /etc/bash.bashrc /etc/bashrc ~/.bashrc; do# [ -r "$file" ] && source $file && break # Use the first one found#done# Do site- or host-specific things herecase$HOSTNAMEin *.company.com)# source $SETTINGS/company.com;;host1.*)# host1 stuff;;host2.company.com)# source .bashrc.host2;;drake.*)# echo DRAKE in bash_profile.jp!;;esac# Do this last because we basically fork off from here. If we exit screen# we return to a fully configured session. The screen session gets configured# as well, and if we never leave it, well, this session isn't that bloated.# Only run if we are interactive and not already running screen# AND '~/.use_screen' exists.if["$PS1"-a$TERM!="screen"-a"$USING_SCREEN"!="YES"-a -f ~/.use_screen];\then# We'd rather use 'type -P' here, but that was added in bash-2.05b and we# use systems we don't control with versions older than that. We can't# easily use 'which' since on some systems that produces output whether# the file is found or not.forpath in${PATH//:/};doif[-x"$path/screen"];then# If screen(1) exists and is executable, run our wrapper[-x"$SETTINGS/run_screen"]&&$SETTINGS/run_screenfidonefi
Example 16-15 is a sample bashrc (we know this is long, but read it for ideas).
# cookbook filename: bashrc# settings/bash_profile: subshell environment settings# To reread (and implement changes to this file) use:# source $SETTINGS/bashrc# Only if interactive bash with a terminal![-t1-a -n"$BASH_VERSION"]||return# Failsafe. This should be set when we're called, but if not, the# "not found" error messages should be pretty clear.# Use leading ':' to prevent this from being run as a program after# it is expanded.:${SETTINGS:='SETTINGS_variable_not_set'}# DEBUGGING only--will break scp, rsync# echo "Sourcing $SETTINGS/bash_profile..."# export PS4='+xtrace $LINENO: '# set -x# Debugging/logging--will not break scp, rsync# case "$-" in# *i*) echo "$(date '+%Y-%m-%d_%H:%M:%S_%Z') Interactive" \# "$SETTINGS/bashrc ssh=$SSH_CONNECTION" >> ~/rc.log ;;# * ) echo "$(date '+%Y-%m-%d_%H:%M:%S_%Z') Noninteractive" \# "$SETTINGS/bashrc ssh=$SSH_CONNECTION" >> ~/rc.log ;;#esac# In theory this is also sourced from /etc/bashrc (/etc/bash.bashrc)# or ~/.bashrc to apply all these settings to login shells too. In practice# if these settings only work sometimes (like in subshells), verify that.# Source keychain file (if it exists) for SSH and GPG agents[-r"$HOME/.keychain/${HOSTNAME}-sh"]\&&source"$HOME/.keychain/${HOSTNAME}-sh"[-r"$HOME/.keychain/${HOSTNAME}-sh-gpg"]\&&source"$HOME/.keychain/${HOSTNAME}-sh-gpg"# Set some more useful prompts# Interactive command-line prompt# ONLY set one of these if we really are interactive, since lots of people# (even us sometimes) test to see if a shell is interactive using# something like: if [ "$PS1" ]; thencase"$-"in *i*)#export PS1='\n[\u@\h t:\l l:$SHLVL h:\! j:\j v:\V]\n$PWD\$ '#export PS1='\n[\u@\h:T\l:L$SHLVL:C\!:\D{%Y-%m-%d_%H:%M:%S_%Z}]\n$PWD\$ 'exportPS1='\n[\u@\h:T\l:L$SHLVL:C\!:J\j:\D{%Y-%m-%d_%H:%M:%S_%Z}]\n$PWD\$ '#export PS2='> ' # Secondary (i.e. continued) prompt#export PS3='Please make a choice: ' # Select prompt#export PS4='+xtrace $LINENO: ' # xtrace (debug) promptexportPS4='+xtrace $BASH_SOURCE::$FUNCNAME-$LINENO: '# xtrace prompt# If this is an xterm set the title to user@host:dircase"$TERM"in xterm*|rxvt*)PROMPT_COMMAND='echo -ne "\033]0;${USER}@${HOSTNAME}:$PWD\007"';;esac;;esac# Make sure custom inputrc is handled, if we can find it; note different# names. Also note different order, since for this one we probably want# our custom settings to override the system file, if present.forfile in$SETTINGS/inputrc ~/.inputrc /etc/inputrc;do[-r"$file"]&&exportINPUTRC="$file"&&break# Use first founddone# No core files by default# See also /etc/security/limits.conf on many Linux systems.ulimit-S -c0> /dev/null 2>&1# Set various aspects of the bash historyexportHISTSIZE=5000# Num. of commands in history stack in memoryexportHISTFILESIZE=5000# Num. of commands in history file#export HISTCONTROL=ignoreboth # bash < 3, omit dups & lines starting with spacesexportHISTCONTROL='erasedups:ignoredups:ignorespace'exportHISTIGNORE='&:[ ]*'# bash >= 3, omit dups & lines starting with spaces#export HISTTIMEFORMAT='%Y-%m-%d_%H:%M:%S_%Z=' # bash >= 3, timestamp hist fileshopt-s histappend# Append rather than overwrite history on exitshopt-q -s cdspell# Auto-fix minor typos in interactive use of 'cd'shopt-q -s checkwinsize# Update the values of LINES and COLUMNSshopt-q -s cmdhist# Make multiline commands 1 line in historyset-o notify# (or set -b) # Immediate notif. of background job termination.set-o ignoreeof# Don't let Ctrl-D exit the shell# Other bash settingsPATH="$PATH:/opt/bin"exportMANWIDTH=80# Manpage width, use < 80 if COLUMNS=80 & less -NexportLC_COLLATE='C'# Set traditional C sort order (e.g. UC first)exportHOSTFILE='/etc/hosts'# Use /etc/hosts for hostname completionexportCDPATH='.:~/:..:../..'# Similar to $PATH, but for use by 'cd'# Note that the '.' in $CDPATH is needed so that cd will work under POSIX mode# but this will also cause cd to echo the new directory to STDOUT!# And see also "cdspell" above!# Import bash completion settings, if they exist in the default location# and if not already imported (e.g. "$BASH_COMPLETION_COMPAT_DIR" NOT set).# This can take a second or two on a slow system, so you may not always# want to do it, even if it does exist (which it doesn't by default on many# systems, e.g. Red Hat).if[-z"$BASH_COMPLETION_COMPAT_DIR"]&&!shopt-oq posix;thenif[-f /usr/share/bash-completion/bash_completion];then. /usr/share/bash-completion/bash_completionelif[-f /etc/bash_completion];then. /etc/bash_completionfifi# Use a lesspipe filter, if we can find it. This sets the $LESSOPEN variable.# Globally replace the $PATH ':' delimiter with space for use in a list.forpath in$SETTINGS/opt/bin ~/${PATH//:/};do# Use first one found of 'lesspipe.sh' (preferred) or 'lesspipe' (Debian)[-x"$path/lesspipe.sh"]&&eval$("$path/lesspipe.sh")&&break[-x"$path/lesspipe"]&&eval$("$path/lesspipe")&&breakdone# Set other less & editor prefs (overkill)exportLESS="--LONG-PROMPT --LINE-NUMBERS --ignore-case --QUIET --no-init"exportVISUAL='vi'# Set a default that should always work# We'd rather use 'type -P' here, but that was added in bash-2.05b and we use# systems we don't control with versions older than that. We can't easily# use 'which' since that produces output whether the file is found or not.#for path in ${PATH//:/ }; do# # Overwrite VISUAL if we can find nano# [ -x "$path/nano" ] \# && export VISUAL='nano --smooth --const --nowrap --suspend' && break#done# See above notes re: nano for why we're using this for loopforpath in${PATH//:/};do# Alias vi to vim in binary mode if we can[-x"$path/vim"]&&aliasvi='vim -b'&&breakdoneexportEDITOR="$VISUAL"# Yet Another PossibilityexportSVN_EDITOR="$VISUAL"# Subversionaliasedit=$VISUAL# Provide a command to use on all systems# Set ls options and aliases.# Note all the colorizing may or may not work depending on your terminal# emulation and settings, esp. ANSI color. But it shouldn't hurt to have.# See above notes re: nano for why we're using this for loop.forpath in${PATH//:/};do[-r"$path/dircolors"]&&eval"$(dircolors)"\&&LS_OPTIONS='--color=auto'&&breakdoneexportLS_OPTIONS="$LS_OPTIONS-F -h"# Using dircolors may cause csh scripts to fail with an# "Unknown colorls variable 'do'." error. The culprit is the ":do=01;35:"# part in the LS_COLORS environment variable. For a possible solution see# http://forums.macosxhints.com/showthread.php?t=7287# eval "$(dircolors)"aliasls="ls$LS_OPTIONS"aliasll="ls$LS_OPTIONS-l"aliasll.="ls$LS_OPTIONS-ld"# Usage: ll. ~/.*aliasla="ls$LS_OPTIONS-la"aliaslrt="ls$LS_OPTIONS-alrt"# Useful aliases# Moved to a function: alias bot='cd $(dirname $(find . | tail -1))'#alias clip='xsel -b' # pipe stuff into right "X" clipboardaliasgc='xsel -b'# "GetClip" get stuff from right "X" clipboardaliaspc='xsel -bi'# "PutClip" put stuff to right "X" clipboardaliasclr='cd ~/ && clear'# Clear and return $HOMEaliascls='clear'# DOS-ish for clearaliascal='cal -M'# Start calendars on Mondayaliascopy='cp'# DOS-ish for cp#alias cp='cp -i' # Annoying Red Hat default from /root/.bashrcaliascvsst='cvs -qn update'# Hack to get concise CVS status (like svn st)aliasdel='rm'# DOS-ish for rmaliasdf='df --print-type --exclude-type=tmpfs --exclude-type=devtmpfs'aliasdiff='diff -u'# Make unified diffs the defaultaliasjdiff="\diff --side-by-side --ignore-case --ignore-blank-lines\--ignore-all-space --suppress-common-lines"# Useful GNU diff commandaliasdir='ls'# DOS-ish for lsaliashu='history -n && history -a'# Read new hist. lines; append current linesaliashr='hu'# "History update" backward compat to 'hr'aliasinxi='inxi -c19'# (Ubuntu) system information scriptaliasipconfig='ifconfig'# Windows-ish for ifconfigaliaslesss='less -S'# Don't wrap linesaliaslocate='locate -i'# Case-insensitive locatealiasman='LANG=C man'# Display manpages properlyaliasmd='mkdir'# DOS-ish for mkdiraliasmove='mv'# DOS-ish for mv#alias mv='mv -i' # Annoying Red Hat default from /root/.bashrcaliasntsysv='rcconf'# Debian rcconf is pretty close to Red Hat ntsysv#alias open='gnome-open' # Open files & URLs using GNOME handlers; see run belowaliaspathping='mtr'# mtr - a network diagnostic toolaliasping='ping -c4'# Only 4 pings by defaultaliasr='fc -s'# Recall and execute 'command' starting with...aliasrd='rmdir'# DOS-ish for rmdir# Tweaked from http://bit.ly/2fc4e8Zaliasrandomwords="shuf -n102 /usr/share/dict/words \| perl -ne 'print qq(\u\$_);' | column"aliasren='mv'# DOS-ish for mv/rename#alias rm='rm -i' # Annoying Red Hat default from /root/.bashrcaliasreloadbind='rndc -k /etc/bind/rndc.key freeze \&& rndc -k /etc/bind/rndc.key reload && rndc -k /etc/bind/rndc.key thaw'# Reload dynamic BIND zones after editing db.* filesaliassvndiff='meld'# Cool GUI diff, similar to TortoiseMergealiassvnpropfix='svn propset svn:keywords "id url date"'aliassvnkey='svn propset svn:keywords "id url"'aliassvneol='svn propset svn:eol-style'# One of 'native', 'LF', 'CR', 'CRLF'aliassvnexe='svn propset svn:executable on'aliastop10='sort | uniq -c | sort -rn | head'aliastracert='traceroute'# DOS-ish for traceroutealiasvzip='unzip -lvM'# View contents of ZIP filealiaswgetdir="wget --no-verbose --recursive --no-parent --no-directories \--level=1"# Grab a whole directory using wgetaliaswgetsdir="wget --no-verbose --recursive --timestamping --no-parent \--no-host-directories --reject 'index.*'"# Grab a dir and subdirsaliaszonex='host -l'# Extract (dump) DNS zone# Date/timealiasiso8601="date '+%Y-%m-%dT%H:%M:%S%z'"# ISO 8601 timealiasnow="date '+%F %T %Z(%z)'"# More readable ISO 8601 localaliasutc="date --utc '+%F %T %Z(%z)'"# More readable ISO 8601 UTC# Neat stuff from http://xmodulo.com/useful-bash-aliases-functions.htmlaliasmeminfo='free -m -l -t'# See how much memory you have leftaliaswhatpid='ps auwx | grep'# Get PID and process infoaliasport='netstat -tulanp'# Show which apps are connecting to the network# If the script exists and is executable, create an alias to get# web server headersforpath in${PATH//:/};do[-x"$path/lwp-request"]&&aliashttpdinfo='lwp-request -eUd'&&breakdone# Useful functions# Use 'gnome-open' to "run" thingsfunctionrun{[-r"$*"]&&{gnome-open"$*">&/dev/null}||{echo"'$*' not found or not readable!"}}# Python version of 'perl -c'functionpython-c{python -m py_compile"$1"&&rm -f"${1}c"}# cd to the bottom of a narrow but deep dir treefunctionbot{localdir=${1:-.}#\cd $(dirname $(find $dir | tail -1))\cd$(find . -name CVS -prune -o -type d -print|tail -1)}# mkdir newdir then cd into it# usage: mcd (<mode>) <dir>functionmcd{localnewdir='_mcd_command_failed_'if[-d"$1"];then# Dir exists, mention that...echo"$1exists..."newdir="$1"elseif[-n"$2"];then# We've specified a modecommandmkdir -p -m$1"$2"&&newdir="$2"else# Plain old mkdircommandmkdir -p"$1"&&newdir="$1"fifibuiltin cd"$newdir"# No matter what, cd into it}# end of mcd# Trivial command-line calculatorfunctioncalc{# INTEGER ONLY! --> echo The answer is: $(( $* ))# Floating pointawk"BEGIN {print \"$*= \"$*}";#awk "BEGIN {printf \"$* = %f\", $* }";}# end of calcfunctionaddup{awk'{sum += $1} END {print sum}'}# Allow use of 'cd ...' to cd up 2 levels, 'cd ....' up 3, etc. (like 4NT/4DOS)# Usage: cd ..., etc.functioncd{localoption=length=count=cdpath=i=# Local scope and start clean# If we have a -L or -P symlink option, save then remove itif["$1"="-P"-o"$1"="-L"];thenoption="$1"shiftfi# Are we using the special syntax? Make sure $1 isn't empty, then# match the first 3 characters of $1 to see if they are '...', then# make sure there isn't a slash by trying a substitution; if it fails,# there's no slash.if[-n"$1"-a"${1:0:3}"='...'-a"$1"="${1%/*}"];then# We are using special syntaxlength=${#1}# Assume that $1 has nothing but dots and count themcount=2# 'cd ..' still means up one level, so ignore first two# While we haven't run out of dots, keep cd'ing up 1 levelfor((i=$count;i<=$length;i++));docdpath="${cdpath}../"# Build the cd pathdone# Actually do the cdbuiltin cd$option"$cdpath"elif[-n"$1"];then# We are NOT using special syntax; just plain old cd by itselfbuiltin cd$option"$*"else# We are NOT using special syntax; plain old cd by itself to home dirbuiltin cd$optionfi}# end of cd# Do site- or host-specific things herecase$HOSTNAMEin *.company.com)# source $SETTINGS/company.com;;host1.*)# host1 stuff;;host2.company.com)# source .bashrc.host2;;drake.*)# echo DRAKE in bashrc.jp!exportTAPE=/dev/tape;;esac
Example 16-16 is a sample inputrc.
# cookbook filename: inputrc# settings/inputrc: # readline settings# To reread (and implement changes to this file) use:# bind -f $SETTINGS/inputrc# First, include any system-wide bindings and variable# assignments from /etc/inputrc# (fails silently if file doesn't exist)$include/etc/inputrc$ifBash# Ignore case when doing completionsetcompletion-ignore-case on# Completed dir names have a slash appendedsetmark-directories on# Completed names which are symlinks to dirs have a slash appendedsetmark-symlinked-directories on# List ls -F for completionsetvisible-stats on# Cycle through ambiguous completions instead of list"\C-i": menu-complete# Set bell to audiblesetbell-style audible# List possible completions instead of ringing bellsetshow-all-if-ambiguous on# From the readline documentation at# https://cnswww.cns.cwru.edu/php/chet/readline/readline.html#SEC12# Macros that are convenient for shell interaction# edit the path"\C-xp":"PATH=${PATH}\e\C-e\C-a\ef\C-f"# prepare to type a quoted word -- insert open and close double quotes# and move to just after the open quote"\C-x\"":"\"\"\C-b"# insert a backslash (testing backslash escapes in sequences and macros)"\C-x\\":"\\"# Quote the current or previous word"\C-xq":"\eb\"\ef\""# Add a binding to refresh the line, which is unbound"\C-xr": redraw-current-line# Edit variable on current line.#"\M-\C-v": "\C-a\C-k$\C-y\M-\C-e\C-a\C-y=""\C-xe":"\C-a\C-k$\C-y\M-\C-e\C-a\C-y="$endif# some defaults / modifications for the emacs mode$ifmode=emacs# allow the use of the Home/End keys"\e[1~": beginning-of-line"\e[4~": end-of-line# allow the use of the Delete/Insert keys"\e[3~": delete-char"\e[2~": quoted-insert# mappings for "page up" and "page down" to step to beginning/end of the history# "\e[5~": beginning-of-history# "\e[6~": end-of-history# alternate mappings for "page up" and "page down" to search the history# "\e[5~": history-search-backward# "\e[6~": history-search-forward# MUCH nicer up-arrow search behavior!"\e[A":history-search-backward## up-arrow"\e[B":history-search-forward## down-arrow# mappings for Ctrl-left-arrow and Ctrl-right-arrow for word moving### These were/are broken, and /etc/inputrc has better anyway# "\e[5C": forward-word# "\e[5D": backward-word# "\e\e[C": forward-word# "\e\e[D": backward-word# for non RH/Debian xterm, can't hurt for RH/Debian xterm"\eOH": beginning-of-line"\eOF": end-of-line# for FreeBSD console"\e[H": beginning-of-line"\e[F": end-of-line$endif
Example 16-17 is a sample bash_logout.
# cookbook filename: bash_logout# settings/bash_logout: execute on shell logout# Clear the screen on logout to prevent information leaks, if not already# set as an exit trap elsewhere[-n"$PS1"]&&clear
Finally, Example 16-18 is a sample run_screen (for GNU screen, which you may need to install).
#!/usr/bin/env bash# cookbook filename: run_screen# run_screen--Wrapper script intended to run from a "profile" file to run# screen at login time with a friendly menu.# Sanity checkif["$TERM"=="screen"-o"$TERM"=="screen-256color"];thenprintf"%b""According to \$TERM = '$TERM' we're *already* using"\" screen.\nAborting...\n"exit1elif["$USING_SCREEN"=="YES"];thenprintf"%b""According to \$USING_SCREEN = '$USING_SCREEN' we're"" *already* using screen.\nAborting...\n"exit1fi# The "$USING_SCREEN" variable is for the rare cases when screen does NOT set# $TERM=screen. This can happen when 'screen' is not in TERMCAP or friends,# as is the case on a Solaris 9 box we use but don't control. If we don't# have some way to tell when we're inside screen, this wrapper goes into an# ugly and confusing endless loop.# Seed list with Exit and New options and see what screens are already running.# The select list is whitespace-delimited, and we only want actual screen# sessions, so use perl to remove whitespace, filter for sessions, and show# only useful info from 'screen -ls' output.available_screens="Exit New$(screen -ls\|perl -ne's/\s+//g; print if s/^(\d+\..*?)(?:\(.*?\))?(\(.*?\))$/$1$2\n/;')"# Print a warning if using runtime feedbackrun_time_feedback=0["$run_time_feedback"==1]&&printf"%b""+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++'screen' Notes:1) Sessions marked 'unreachable' or 'dead' should be investigated andremoved with the -wipe option if appropriate.\n\n"# Present a list of choicesPS3='Choose a screen for this session: 'selectselection in$available_screens;doif["$selection"=="Exit"];thenbreakelif["$selection"=="New"];thenexportUSING_SCREEN=YESexecscreen -c$SETTINGS/screenrc -a\-S$USER.$(date'+%Y-%m-%d_%H:%M:%S%z')breakelif["$selection"];then# Pull out just the part we need using cut# We'd rather use a 'here string' [$(cut -d'(' -f1 <<< $selection)]# than this echo, but they are only in bash-2.05b+screen_to_use="$(echo$selection|cut -d'('-f1)"# Old: exec screen -dr $screen_to_use# Alt: exec screen -x $USER/$screen_to_useexecscreen -r$USER/$screen_to_usebreakelseprintf"%b""Invalid selection.\n"fidone
See the code and the code’s comments for details.
Something interesting happens if you set $PS1 at inappropriate times, or if you set traps using clear. Many people use code like this to test to see if the current shell is interactive:
if[-n"$PS1"];then: Interactive code herefi
If you arbitrarily set $PS1 if the shell isn’t interactive, or if you set a trap using just clear instead of ["$PS1"]&&clear, you’ll get errors like this when using scp or ssh noninteractively:
# e.g. from tputNo valuefor$TERMand no -T specified# e.g. from clearTERM environment variable not set.