The cd command remembers the previous working directory, and cd - will return to it. There is another command that will change the directory and remember an unlimited number of directories: pushd. The directories are stored in an array, DIRSTACK. To return to a previous directory, popd pulls the top entry off DIRSTACK and makes that the current directory. I use two functions that make handling DIRSTACK easier, and I’ve added a third one here just for the sake of completeness.
Note The names of some of the functions that are created in this chapter are similar to the commands available in Bash. The reason for this is to use your existing shell scripts without making any changes to them and still availing of some additional functionality.
cd
The cd function replaces the built-in command of the same name. The function uses the built-in command pushd to change the directory and store the new directory on DIRSTACK. If no directory is given, pushd uses $HOME. If changing the directory fails, cd prints an error message, and the function returns with a failing exit code (Listing 11-1).
Listing 11-1. cd, Change Directory, Saving Location on the Directory Stack
cd() #@ Change directory, storing new directory on DIRSTACK { local dir error ## variables for directory and return code while : ## ignore all options do case $1 in --) break ;; -*) shift ;; *) break ;; esac done dir=$1 if [ -n "$dir" ] ## if a $dir is not empty then pushd "$dir" ## change directory else pushd "$HOME" ## go HOME if nothing on the command line fi 2>/dev/null ## error message should come from cd, not pushd error=$? ## store pushd's exit code if [ $error -ne 0 ] ## failed, print error message then builtin cd "$dir" ## let the builtin cd provide the error message fi return "$error" ## leave with pushd's exit code } > /dev/null
The standard output is redirected to the bit bucket because pushd prints the contents of DIRSTACK, and the only other output is sent to standard error (>&2).
Note A replacement for a standard command such as cd should accept anything that the original accepts. In the case of cd, the options -L and -P are accepted, even though they are ignored. That said, I do sometimes ignore options without even making provisions for them, especially if they are ones I never use.
pd
The pd function is here for the sake of completeness (Listing 11-2). It is a lazy man’s way of calling popd; I don’t use it.
Listing 11-2. pd, Return to Previous Directory with popd
pd () { popd } >/dev/null ### for the same reason as cd
cdm
The reason I don’t use pd isn’t because I’m not lazy. Far from it, but I prefer to leave DIRSTACK intact so I can move back and forth between directories. For that reason, I use a menu that presents all the directories in DIRSTACK.
The cdm function sets the input field separator (IFS) to a single newline (NL or LF) to ensure that the output of the dirs built-in command keeps file names together after word splitting (Listing 11-3). File names containing a newline would still cause problems; names with spaces are an annoyance, but names with newlines are an abomination.
The function loops through the names in DIRSTACK (for dir in $(dirs -l -p)), adding each one to an array, item, unless it is already there. This array is then used as the argument to the menu function (discussed below), which must be sourced before cdm can be used.
DIRS BUILT-IN COMMAND
The dirs built-in command lists the directories in the DIRSTACK array. By default, it lists them on a single line with the value of HOME represented by a tilde. The -l option expands ~ to $HOME, and -p prints the directories, one per line.
Listing 11-3. cdm, Select New Directory from a Menu of Those Already Visited
cdm() #@ select new directory from a menu of those already visited { local dir IFS=$'\n' item for dir in $(dirs -l -p) ## loop through diretories in DIRSTACK[@] do [ "$dir" = "$PWD" ] && continue ## skip current directory case ${item[*]} in *"$dir:"*) ;; ## $dir already in array; do nothing *) item+=( "$dir:cd '$dir'" ) ;; ## add $dir to array esac done menu "${item[@]}" Quit: ## pass array to menu function }
When run, the menu looks like this:
$ cdm 1. /public/music/magnatune.com 2. /public/video 3. /home/jayant 4. /home/jayant/tmp/qwe rty uio p 5. /home/jayant/tmp 6. Quit (1 to 6) ==>
menu
The calling syntax for the menu function comes from 9menu, which was part of the Plan 9 operating system. Each argument contains two colon-separated fields: the item to be displayed and the command to be executed. If there is no colon in an argument, it is used both as the display and as the command:
$ menu who date "df:df ." 1. who 2. date 3. df (1 to 3) ==> 3 Filesystem 1K-blocks Used Available Use% Mounted on /dev/hda5 48070472 43616892 2011704 96% /home $ menu who date "df: df ." 1. who 2. date 3. df (1 to 3) ==> 1 jayant tty8 Jun 18 14:00 (:1) jayant tty2 Jun 21 18:10
A for loop numbers and prints the menu; read gets the response; and a case statement checks for the exit characters q, Q, or 0 in the response. Finally, indirect expansion retrieves the selected item, further expansion extracts the command, and eval executes it: eval "${!num#*:}" (Listing 11-4).
Listing 11-4. menu, Print Menu, and Execute-Associated Command
menu() { local IFS=$' \t\n' ## Use default setting of IFS local num n=1 opt item cmd echo ## Loop though the command-line arguments for item do printf " %3d. %s\n" "$n" "${item%%:*}" n=$(( $n + 1 )) done echo ## If there are fewer than 10 items, set option to accept key without ENTER if [ $# -lt 10 ] then opt=-sn1 else opt= fi read -p " (1 to $#) ==> " $opt num ## Get response from user ## Check that user entry is valid case $num in [qQ0] | "" ) return ;; ## q, Q or 0 or "" exits *[!0-9]* | 0*) ## invalid entry printf "\aInvalid response: %s\n" "$num" >&2 return 1 ;; esac echo if [ "$num" -le "$#" ] ## Check that number is <= to the number of menu items then eval "${!num#*:}" ## Execute it using indirect expansion else printf "\aInvalid response: %s\n" "$num" >&2 return 1 fi }