. | . | . | . | David McCracken |
Bash Examplesupdated:2018.05.14 |
Bash is not the easiest language to use. The main difficulty is that, like Perl, it has multiple ways to express the same thing without a clear rationale for selection. Unlike Perl, which has no justification for arbitrarily different forms of expression, Bash was designed to recognize scripts written in sh, Bourne, and C shell languages and to augment these predecessors with improved constructs. It does a remarkably good job of this.
Like most other non-OO languages, Bash requires the programmer to “roll their own” complex objects. This is not much of a burden for the routine tasks required of shell scripts. They are not intended for writing big programs, although with Python this is relatively easy. We don’t have to decide when a job is too much for Python because it works well throughout the full range of trivial to massive. I would use Python instead of Bash for all scripts except that (in a Bash shell) Bash can outperform Python. Bash scripts start without delay, execute quickly, and can modify the user’s environment.
# File: cdxterm dir="$(cat ~/.DirList)" dir="${dir%%\\*}" dir="${dir#\`}" mate-terminal --working-directory="$dir"
# File: cdx # ---------------------------------------------------------------------------- # Function: cdLimitList # Purpose: Chops off the tail of DIRLIST, if needed, to prevent the list # from growing to more than 10. # Returns: true (0) # Arguments: None # ............................................................................ cdLimitList() { local tempStr=${DIRLIST//[!\\]/} # Compress to all (suffix) delimiter. if [ ${#tempStr} -gt 10 ] ; then DIRLIST="${DIRLIST%\`*\\}" # Remove the tail. fi } # ---------------------------------------------------------------------------- # Function: cdInList # Purpose: Determines whether the given directory ($1) is in the DIRLIST. # Returns: 1 if the directory is in the list else 0. # Arguments: $1 is the directory to try to find in the list. # ................................. Notes .................................... # It does this by comparing the list string length before and after attempting # to remove the directory. The dir list itself is not changed in any case. # ............................................................................ cdInList() { local lvCnt=${#DIRLIST} local lvDirs="${DIRLIST//\`${1}\\}" return $(($lvCnt != ${#lvDirs})) } # ---------------------------------------------------------------------------- # Function: cdForBack # Purpose: Implements the -f and -b commands to move forward and backward # through the directory list. -f moves to the list tail directory and - b to # the head. If PWD is not already in the list, it is added to the head if -f or # tail if -b. # Returns: error (NZ) if DIRLIST is empty else reflected return from # builtin cd. # Arguments: $1 is F to move forward through the list or B (or any non-F) # to move backward. # ................................. Notes .................................... # This always calls builtin cd unless DIRLIST is empty. # ............................................................................ cdForBack() { local dir if [ -n "$DIRLIST" ] then cdInList $PWD local lvPwdInList=$? if [ $lvPwdInList -eq 0 ] ; then cdLimitList fi if [ $1 = 'F' ] ; then # forward dir="${DIRLIST##*\`}" # Tail is forward target. dir="${dir%\\}" DIRLIST="${DIRLIST%\`*\\}" # Rotate right, losing tail if [ $lvPwdInList -eq 0 ] ; then DIRLIST="\`${PWD}\\$DIRLIST" fi else # backward dir="${DIRLIST%%\\*}" # Head is backward target. dir="${dir#\`}" DIRLIST="${DIRLIST#\`*\\}" # Rotate left, losing head if [ $lvPwdInList -eq 0 ] ; then DIRLIST="$DIRLIST\`${PWD}\\" fi fi builtin cd "$dir" return # Be sure to reflect return from builtin cd (although fi won't change it). fi # [ -n "$DIRLIST" ] } # ---------------------------------------------------------------------------- # Function: cdRotateList # Purpose: Rotates the directory list to move the given directory ($1) to # the head. Do not call this without first ascertaining that the directory is # in the list or else this will run forever. # Returns: true (0) # Arguments: None # ............................................................................ cdRotateList() { local dir while dir=${DIRLIST%%\\*} ; [ "$dir" != "\`${1}" ] ; do DIRLIST=${DIRLIST#*\\}"$dir\\" done } # ---------------------------------------------------------------------------- # Function: cd # Purpose: Front end over builtin cd. # Returns: Reflected return from builtin cd in most cases but always true # (0) if command argument is -c or -s and the user aborts (^C). If -f, -b, or -s # and DIRLIST is empty then false (NZ). # Arguments: $1 is -h/H, -f/F, -bB, -cC, -s/S or a directory name. # ............................................................................ cd() { if [ -z $CDINIT ] ; then CDINIT=1 trap savedirlist EXIT if [ -e ~/.DirList ] ; then DIRLIST="$(cat ~/.DirList)" if [ "$1" = "" ] ; then local dir="${DIRLIST%%\\*}" dir="${dir#\`}" builtin cd "$dir" return fi fi fi case "$1" in -h | -H | -\? ) echo 'cd front end: -b backward, -f forward, -s select from menu, -c clear' userinp='' read -p '1) cd help 2) help cd <enter> ' userinp case $userinp in 1) builtin cd --help ;; 2) help builtin cd ;; esac return ;; # case -h | -H | -\? -f | -F ) # ----- Move forward through dir list -------- cdForBack F return ;; # case -f | -F -b | -B ) # ------ Move backward through dir list -------- cdForBack B return ;; # case -b | -B -c | -C ) # ------ Clear dir list ---------------- DIRLIST= return 0 ;; # case -c | -C -s | -S ) # ---------- Select from directory list --------- # If PWD isn't already in the list then add it to the head. Checking for its # inclusion in the list and adding it are done in the same statement by # removing it if it in in the list and unconditionally adding it. This would # also have the effect of moving it to the head if it were in the list and not # at the head, potentially changing the order. This situation can exist only # if the user has typed the name of a directory that is already in the list. DIRLIST="\`${PWD}\\${DIRLIST//\`${PWD}\\/}" IFS=\` PS3="Select directory or ^C to stay in $PWD > " local dirlist="${DIRLIST//\\/}" local err="0" select seldir in ${dirlist#\`} ; do if [ "$seldir" ] ; then cdRotateList "$seldir" cdForBack B err=$? fi break done # select seldir unset IFS unset PS3 return $err ;; # case -s | -S * ) # -------- Normal cd but always push departing dir onto list --------- # If a directory already in the list is typed, nothing special will happen # immediately because we don't do anything immediately with the target anyway. # However, on leaving the directory, its name is moved from its previous position # in the list to the head. if [ "$1" != "$PWD" ] ; then local dir="$PWD" builtin cd $1 $2 $3 # $1 must not be quoted or blank (home) fails. local err=$? if [ $err = "0" ] then # If the list is empty then it becomes simply the pre-cd PWD. Otherwise, # put pre-cd PWD at the head. if [ -z "$DIRLIST" ] ; then DIRLIST="\`$dir\\" else DIRLIST="\`${dir}\\${DIRLIST//\`${dir}\\/}" fi cdLimitList return 0 else # cd failure return $err # Reflect return from builtin cd fi # if cd fi # [ "$1" != "$PWD" ] ;; # case * esac # case $1 in } # ---------------------------------------------------------------------------- # Function: savedirlist # Purpose: Shell on-exit callback. Move or add the closing directory to # the head of DIRLIST and write the list out to ~/.DirList. # .......................... Notes ......................................... # Closing Directory # There are two reasons for move/add the closing directory. Directories are # added to the list only when we leave them. Unless we are revisiting the # closing directory, it won't be in the list. Also, we want the closing # directory at the head of the list (which is normally done by remove/insert) # to make it easy to extract at the beginning of the next session to assist # opening a fresh terminal at the closing directory of the previous. # ............................................................................ savedirlist() { DIRLIST="\`${PWD}\\${DIRLIST//\`${PWD}\\/}" echo "$DIRLIST" > ~/.DirList }
See Windows equivalent implemented with cooperating Python dSup.py and BAT d.bat scripts.
When I’m working at the command line I find it distracting to have to
type long path names to change the directory, especially to return to one
recently visited. The intrinsic change directory command cd
is
easy to remember but doesn’t help. If you can remember to use them,
pushd directory
and popd
afford primitive retrace
but they don’t provide forward retrace, which can be even more useful
than backward. I would also like to be able to select randomly from a menu of
recently visited directories. popd +/-N
somewhat provides
this but it is too unfriendly to be of much value. In all cases
popd
destroys traversal history, making it impossible to move back
and forth between two or more directories. My cdx script provides all of the
capabilities of cd
, pushd
, and popd
plus
my improvements and it is easier to use.
The startup bashrc (e.g. Fedora /etc/bashrc, Ubuntu /etc/bash.bashrc) invokes
the cdx source script, which only defines functions, one of which is called
cd
. Subsequently invoking cd
executes this function
instead of builtin cd
command.
The cd
function accepts a few arguments that would be illegal to
builtin cd
. For all other command lines, it silently pushes the
current directory onto its own list and then invokes builtin cd
.
If the argument to cd
is a directory name then this appears to the
user to be the standard cd
command. cd -b
goes back
to the previous directory. cd -f
is the inverse, going forward in
the directory list. cd -s
displays the directory list for random
selection by menu item number. cd -c
clears the list.
cd -h
displays help for the cd
function and then invokes help
for builtin cd
. Normally, cd
without argument is
passed through to builtin cd
to go to the user’s home
directory.
The central feature is a circular path list, which enables both forward and
backward traversal. cd directory
operates like
pushd
. cd -b
is similar to popd
but
doesn’t remove the target directory from the list. This is essential to
being able to move forward through the list. cd -f
and
cd -s
have no counterparts. On cd directory
, the
departing directory is added to the list only if not already in it. If the list
is already full the oldest element (relative to the current head) is removed.
The list is limited to 10 elements for convenience of use.
The basic operation doesn’t afford persistence of either the directory
list or the closing-opening directory. List persistence is achieved by saving
DIRLIST to the ~/.DirList file on shell EXIT and initializing the new DIRLIST
of the next session by reading back the file. This doesn’t happen when a
fresh terminal opens but on the first invocation of cd in a fresh shell. We
can’t use the fact that DIRLIST is uninitialized to indicate first
invocation because one option is cd -c
to clear the list. Instead,
we create the variable CDINIT, which serves only as an initialization done
flag. We also set a trap to call savedirlist on shell EXIT.
cdx alone cannot fully address automatically opening a fresh terminal in the
closing directory of the previous terminal shell, i.e. closing-opening
directory persistence. A partial solution is provided by a special
interpretation of cd
without arguments, which normally is passed
through to intrinsic cd
for the conventional interpretation as
user’s home directory. However, in a fresh shell (under the CDINIT block)
this is interpreted as change to the previous closing directory, which
savedirlist has placed at the head of the list.
Extracting the head of the list is a bit tricky because the list syntax has to
avoid any overlap with normal directory syntax and, because Bash is rather
primitive, the extraction requires two commands. First,
local dir="${DIRLIST%%\\*}"
removes the rest of the list. Then,
dir="${dir#\`}"
removes the ` prefix. For example, given
"`/usr/local/bin\`/index/d\`/etc\"
dir="${DIRLIST%%\\*}" produces
"`/usr/local/bin"; then
dir="${dir#\`}" produces the usable directory "/user/local/bin".
This special interpretation of cd
works for both TTY and terminal
window, such as mate-terminal. This is the best we can do for TTY but not
necessarily for a terminal window. Many GUI shells allow the opening directory
of a terminal window to be established by the command that invokes the terminal
program. The cdxterm script does this for Mate (Gnome) terminal. It invokes
mate-terminal with the command argument --working-directory set to the first
item it reads in the DirList file as in the CDINIT block in the
cd
function. Given the complexity of extracting the head of the list, it
would be desirable for cd
and cdxterm
to share this
code but they execute in different environments and can’t easily share
variables (they only share the ~/.DirList file).
Functions cdLimitList, cdInList, cdForBack, and cdRotateList are helper functions for the cd function, which is an alias for the builtin cd command.
I initially implemented the directory list similarly to PATH, with
: separating the directories. When directories are only added or
removed from either end, this is ok, although it requires some finesse since it
produces three different directory forms: head = name: , middles = :name: , and
tail = :tail. However, I needed to rotate the list so that it would align with
PWD after mid-list jump either by cd -s
or
cd directory
when directory is already in the list.
The expressions to do this with the : list were too
complicated. I changed to make all directory forms the same and with different
leading and trailing characters and without using any character that can be
used in a legitimate directory name. There are only two of these that are also
printable, ` (back-quote) and \ (back-slash).
Thus, environment variable DIRLIST comprises "`name\`name\...". Both of the
delimiter characters have to be escaped in expressions. e.g.
DIRLIST="\`$dir\\"
Initializes the list with one directory name.
DIRLIST="\`${dir}\\${DIRLIST//\`${dir}\\/}"
inserts dir at the
head of the list while removing it from any other place where it might already
be in the list. i.e. move it to the head if it exists or add it at the head.
Similarly,
DIRLIST="\`${PWD}\\${DIRLIST//\`${PWD}\\/}"
moves or adds PWD to the head.
local dirlist="${DIRLIST//\\/}"
removes all /
delimiters from the list. This was done to make a list that could be presented
to select
, which allows only the one delimiter character defined
by IFS.
IFS=\`
prepares the delimiter for select
.
while dir=${DIRLIST%%\\*} ; [ "$dir" != "\`${1}" ] ; do DIRLIST=${
DIRLIST#*\\}"$dir\\"
rotates the list to move the directory matching $1
to the head. Each iteration does one left shift until the head matches $1. Some
directory must match $1.
${DIRLIST%%\\*}
returns the head by chopping off the first \ and everything after it, i.e. "`name" is all that remains (note back-quote).
[ "$dir" != "\`${1}" ]
compares the chopped off head to $1 with prefixed back-quote for matching.
${DIRLIST#*\\}"$dir\\"
rotates the list by chopping off the first \ and everything that precedes it, leaving "`name\..."
, and appending the head, "`name"
, and a final \
.
dir="${DIRLIST##*\`}" # Tail is forward target
.
dir="${dir%\\}"
These two statements are required to get the tail with both delimiters removed. The first chops off the last back-quote and everything preceding it. The second chops off the trailing \.
DIRLIST="${DIRLIST%\`*\\}" # Rotate right, losing tail
Despite the comment, this does not rotate but shifts the list, losing the tail. This is done in response to cd -f. The tail directory is not actually lost but goes into PWD. In effect, PWD serves as a “carry” register for the list. Moving backward and forward effectively rotates the list right or left through PWD.
cdLimitList
is called before adding a new directory to the list. This isn’t done to prevent the list from becoming temporarily too long but because removal after insertion is complicated by having to avoid removing the new addition.
# File: findiftext # Companion to findtext script. if file $2 | grep -q text ; then grep -Hni "$1" $2 ; fi
# File: findtext # Search text files for a given case-insensitive string. # Usage: findtext searchString findArgs # findtext invokes find, passing it all arguments after the first. On every # matching file find -exec invokes findiftext, which, if the file is text, # invokes grep to search for the string in it. # ..................... notes ............................................... # The companion script findiftext must be executable because find -exec won't # source a script. findtext could be sourced but that would just make it more # difficult for the user to invoke. The slight invocation time improvement # would be immaterial beause it is just invoked one time for a complex process. # ............................................................................. if test "$1" = "" ; then echo Text argument is missing. ; exit -1 ; fi set -f # Disable path globbing to simplify script and use syntax. echo Search for \"$1\" in text files found by find ${@:2} find ${@:2} -type f -exec findiftext "$1" {} \;
The Linux/Unix find
command is the standard general-purpose file
searcher. It has many options and no simplification for common situations.
Despite the pain that it exacts even for simple uses, it is still incomplete.
In particular, it cannot perform a two-stage filter, for example to apply
grep
only to text files. Option -type f
can filter
out non-files (i.e. directories, links, etc.) but there is no intrinsic filter
for text files. Option -exec
can invoke a program or script but it
is not a command line interpreter and does not support multistage filtering by
piping. There are many situations where this is a serious impediment. Searching
for text only in text files is just one of the most common and obvious
examples. Limiting the search to only text files can easily reduce the search
time by several orders of magnitude.
The only means available to effect a multistage find filter is by creating a
script for -exec
to invoke. The script is quite simple
if file $2 | grep -q text ; then grep -Hni "$1" $2 ; fi
. The
file
program opens and analyzes the given file ($2) to deduce its
type. Its only means of reporting the type is through stdout so we have to pipe
this into grep to see the result. -q
tells grep to not display
anything but simply return true if it finds the word text. If so, grep is
invoked again, this time to search for the given text in the given file. The
options -Hni
instruct grep to print the file name and line number
of each match and to do a case-insensitive search.
If this one-line script were an environment function, its invocation would be
very quick, which would be desirable, as it is invoked on every file. However,
-exec
is so picky that it won’t even invoke a single source
script. Instead, the script is in file findiftext
, which must be
opened and closed on every invocation. But this is a minor penalty compared to
the cost of unwanted grepping on binary files.
The find
command including -exec findiftext
is too
complicated for normal use. The findtext
script hides this
complexity behind the simple command line
findtext searchString findArgs
, where
findArgs are any arguments that would normally be used in the find
command line except -exec
.
Any command that frequently recurses, like find
would be better
off without globbing. Unfortunately, although we can globally turn off
globbing, we can’t do it selectively for specific commands.
findtext
is no exception. If the search string contains wildcards it
must be quoted (as well as if it contains space). The globbing issue becomes a
serious problem in a script that, like findtext
, invokes another
command line because globbing would normally occur again in that, requiring the
user to double-quote some command arguments. However, since
findtext
executes in a child shell, it can globally disable globbing without
affecting its parent. Thus, the user only sees the normal single-globbing
problem that most are used to dealing with.
#!/bin/bash # xdif # This script is an improved version of the standard diff utility. The main # improvement is to provide regular recursion. diff treats subdirectories as if # they were files, reporting content differences but not differences between # the file themselves. Files are compared only in the root directories # (arguments to diff). xdif corrects this by providing its own recursion, # reinvoking diff at each level. xdif also affords more reporting options than # diff, including #- whether to report each directory change when recursing #- whether to report missing directories when recursing #- whether to report missing files. #- whether to report "Permission denied" files. # ............... notes .......................................... # diff output filters # "Permission denied" and missing files are filtered out by piping diff into # grep. Diff sends these messages to stderr instead of stdout, so they could be # filtered by redirection 2> /null/def. In fact, to present a unified stream of # diff's normal and error output to grep requires redirection 2>&1 # --------------------------------------------------------------------------- # ------------------------------------------------------------------------------ # Function: filterDif # Purpose: This is the core. Given two filespecs, it invokes diff on each file # in $1 compared to the presumed corresponding file in $2. For each directory # in $1, if recursion is turned on, filterDif calls itself. # Returns: Nothing # Arguments: # - $1 is the reference or source directory. Its files (and subdirectories if # recursing) are the ones chosen for comparison. # - $2 is the comparison or destination directory. # Files that exist under $1 but not under $2 are reported as missing (unless # option -f). Files that exist under $2 but not under $1 are ignored. # Globals: # - patterns is an array of file filter patterns, e.g. "*.cpp" # - patternCnt tells the number of elements in patterns. There is always at least # one. If the script is invoked without patterns then patterns is assigned one # element "*". This simplifies filterDif a bit. # ........................ notes .............................................. # GREP -F -P # Diff's output is piped to grep only if the -f or -p filter is turned on. Grep # is not need for -i (Identical) filtering. # # FILES VS DIRECTORIES # In each invocation (initial and recursive) iterate over files first and then, # if recursive, directories. This not only produces a less confusing output but # simplifies handling filter patterns. It would be difficult to merge files # and directories into one loop because directories, not being subject to # filtering, can only be iterated over by *. But then the only way to determine # whether to skip a file due to filtering would be to, for each file, iterate # over the filter list. It is much simpler to, for each filter pattern, iterate # over the files in the current source directory # ............................................................................. filterDif() { if [ -n "$announceDirs" ] ; then echo "Diff ${patterns[@]} files in $1 to $2" fi # First iterate over all files (in $1 = source directory) over all patterns. # If no file filter pattern then the pattern list comprises just "*". for (( idx=0 ; idx < $patternCnt ; idx++ )) ; do for f in $1/${patterns[$idx]} ; do # For some unknown reason, if the directory doesn't contain any file matching # a wildcard pattern, the interpreter makes one f that is the same as the # pattern, e.g. *.h. So we need to verify the file's existance before using # it. We needed to do this anyway to avoid matching directories at this stage. # Note that both invocations of diff redirect stderr to stdout. When piping # to grep this is essential, but it is useful in the other case as well, so # that the output can be directed into a file. if [ -f "$f" ] ; then if [ "$grepArg" ] ; then diff $diffArg $f "$2" 2>&1 | eval egrep $grepArg\" else diff $diffArg $f "$2" 2>&1 fi fi done done # Now, if recursive, iterate over all names (*) in the source directory and # recurse on each directory for which there is a matching destination. if [ -n "$recurse" ] ; then for f in "$1"/* ; do if [ -d "$f" ] ; then dir="$root2${f/#$root1/}" if [ -d "$dir" ] ; then filterDif "$f" "$dir" elif [ -z "$noMissDirs" ] ; then echo "Directory $dir does not exist" fi # if [ -d "$dir" ] fi # if [ -d "$f" ] done # for f in "$1"/* fi } # ----------------------------------------------------------------------------- # Function: grepExclude # Purpose: Called when command line parsing finds an option that requires # grep (-f or -p). On the first invocation, it sets up the basic grep # argument '-v "' and the given filter string. On subsequent invocation it just # adds the filter string. Additional filters may be added in the future but # with only -f and -p the maximum resulting value is '-v "(No such)|(Permission # denied)" # Returns: Nothing # Arguments: $1 is a string filter, e.g. "No such" or "Permission denied", which # added to the grepArg variable, whose value comprises command arguments passed # to grep (if command arguments to xdif trigger a need for grep). # Globals: assigns grepArg # .............................................................................. grepExclude() { if [ "$grepArg" ] ; then grepArg="$grepArg|($1)" else grepArg="-v \"($1)" fi } # ----------------------------------------------------------------------------- # Function: tellUse # Pupose: Tells basic usage. If called in response to an error, the caller # should explain the error. #.............................................................................. tellUse() { echo "Usage: xdif [-radsifphH] [fileset1] fileset2 'filter' 'filter' ..." echo "Show differences between fileset1 and fileset2." echo "Empty fileset1 defaults to all PWD files." echo "Filters limit selected files. e.g. '*.c'." echo "Options:" echo "-s Show file difference details" echo "-r Recurse fileset1 tree" echo "-a Announce subdirectories" echo "-d Don't report missing directories (under filespec1 but not filespec2)" echo "-f Don't report missing files (in filespec1 but not filespec2)" echo "-p Don't report Permission denied files" echo "-i Don't report Identical files" } # ------------------ SCRIPT BEGINS HERE --------------------------------------- # Assign default control variables. # Process - options # Check that the command line contains at least one filespec argument. # If there is only one argument then PWD is taken as the root of filespec1 # and $1 is taken as the root of filespec2. # If there at least two arguments, $1 is taken as the root of filespec1, $2 as # the root of filespec2, and any additional arguments as file filters. # Unless recursion is turned on by -r, only files in root1 and root2 will be # compared. Otherwise, files in all subdirectories in the root1 tree will be # compared to matching directories under root2. In any case, filterDif is # called only once and it invokes itself for any recursion. recurse= # -r turns on recurse directories announceDirs= # -a turns on subdirectory announcement noMissDirs= # -d turns off reporting of missing directories diffArg="-qsU 2" # Options only remove components of the argument string passed to diff. The # "U 2" cannot be removed so diffArg will never be null. # q = don't show details. Removed by -s option. # s = report identicals. Removed by -i option. # U 2 = "Universal" format with smallest context. Print context of differences. # This produces more text than is appropriate but otherwise diff doesn't even # tell the file name when it prints difference details. Has no effect unless # -s and corresponding files have differences. grepArg= # grep is invoked after diff to exclude things for which diff affords no # filtering. grepArg is initially null and changes only in response to command # line options. If it is still null after processing any options, grep is not # invoked at all. Otherwise grep is invoked with grepArg telling it what to # filter out. Note that "Identical" is not a grep filter but a diff control # option. # -f Filter = "No such" for diff output "No such file or directory" Don't # report files that exist only in filespec1 (including implied by empty # filespec1). # -p Filter = "Permission denied" while getopts 'radsifphH' opt ; do case $opt in s ) diffArg=${diffArg//q} ;; r ) recurse=1 ;; a ) announceDirs=1 ;; d ) noMissDirs=1 ;; f ) grepExclude 'No such' ;; p ) grepExclude 'Permission denied' ;; i ) diffArg=${diffArg//s/} ;; h | H ) tellUse ; exit 0 ;; \? ) tellUse ; exit 1 ;; esac done shift $((OPTIND - 1)) if (( $# == 0 )) ; then echo "Error: At least one filespec is required" tellUse exit 1 fi declare -i patternCnt=0 if (( $# == 1 )) ; then root1="$PWD" root2="$1" else root1="$1" root2="$2" let patternCnt=$#-2 shift 2 patterns=("$@") fi if ! [ -e "$root1" ] ; then echo "$root1 doesn't exist" exit 1 fi if ! [ -e "$root2" ] ; then echo "$root2 doesn't exist" exit 1 fi if (( patternCnt == 0 )) ; then patterns=("*") patternCnt=1 fi filterDif "$root1" "$root2"
This script is an improved version of the standard diff
utility.
The main improvement is to provide regular recursion. diff
treats
subdirectories as if they were files, reporting content differences but not
differences between the file themselves. Files are compared only in the root
directories (arguments to diff
). Sometimes this is useful but more
often I would like to recurse into all subdirectories and the command to do
this is too complex for routine use. My xdif
script improves
diff
by providing its own recursion, reinvoking diff
at
each level. xdif
also affords more reporting options than
diff
, including:
Functions grepExclude
and xdifUse
are helpers.
filterDif
is the recursive core. There is no function called xdif
.
The script just begins at processing options. Note that /?
option
case is not the option -?
but any illegal -character
.
Much of this script is taken up with processing command arguments. This
demonstrates a design pattern, where a usage reporting function is called after
reporting a specific error or for the -h option. xdif
also
illustrates:
patterns=("$@")
eval
diff $diffArg $f "$2" 2>&1 | eval egrep $grepArg\"
echo "Diff ${patterns[@]} files in $1 to
$2" ${patterns[@]
it must be quoted to avoid exapansion of any of its
elements that contain wild character. In for f in $1/${patterns[$idx]}
it must not be quoted because we want the shell to expand it to all
matching file names.