. . . .

Bash Examples

 

updated:2018.05.14

Prev   Next   Site Map   Home  
Text Size
Please reload page for style change

BASH

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.



IMPROVED CD


# 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.

PURPOSE

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.

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.

GENERAL OPERATION

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.

PERSISTENCE

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).

PROGRAM DESIGN

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.



MULTI-STAGE FIND FILTER


# 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.

findiftext

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.

findtext

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.



IMPROVED DIFF


#!/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:




Prev   Next   Site Map   Home   Top   Valid HTML   Valid CSS