#!/bin/ksh # # Check elements of PATH for accessibilty, and for possibly conflicting program names. # # $Header: /afs/northstar/ufac/richard/bin/RCS/checkpath,v 1.5 2009/02/07 08:41:15 richard Exp $ # # This script checks all the directories in $PATH for accessibility, then it checks specific # named arguments for possible conflicts between different directories in the $PATH. # If no arguments are named, all programs in the $PATH are examined for conflicts. # # Directories which are equivalent (symlinks) are removed from the list before the # file analysis is made, to cut down on spurious conflicts. Apparent conflicts which are symlinks # to the same file are also not reported. Most systems seem to have many of these. # # This cannot get all possible conflicts for all shells and all situations. # Specifically, it does not address shell aliases, built-ins or global shell functions, # all of which are shell-dependant. # Nor can it address temporary conflicts caused by a startup script which augments $PATH # and then spawns child processes which perform a $PATH search. # # If "." is in the path, and the current directory also happens to be in the path, spurious # conflicts are not reported because of the path trimming performed on equivalent directories. # # Warning: A path element containing "~" is not valid in sh/ksh, but is valid in bash and some csh/tcsh. # Normally the "~" is expanded when the path is set. We test (with "-d") for the presence of a directory # and this fails because "~" is not expanded inside the test operation. In bash, the test fails, # the "~" still works as a path element. It is most reliable to use $HOME explicitly, not "~", in $PATH # # Options: # -v verbosity # 0 - terse output if conflicts are found. # 1 - medium output if conflicts are found (default) # 2 - long output. List all potential conflicts, even if multiple pathnames resolve to the same file. # Follow symlinks to their final destinations. Run 'file' on each conflicting pathname. # 3 - Additional debugging information # -d directory check only - don't analyse any filenames # -p path check only, no directory report. # # Exit status: # 0 = all directories in PATH are accessible. # >0 = a count of the inaccessible directories. # (the exit status does not reflect whether pathname conflicts were discovered) # # 2004/11/03 Richard Brittain, Dartmouth College Computing Services. # Collect options and set defaults verbosity=1 dircheckonly=0 pathcheckonly=0 while getopts pdv: o ; do case $o in p) pathcheckonly=1;; d) dircheckonly=1;; v) verbosity=$OPTARG;; esac done shift $OPTIND-1 # Functions for use later - control starts near the end of the script wordsplit() { # Take a string in $1, a set of delimiters in $2, and print the token # indexed by $3 # Set noglob in the calling routine to avoid expanding wildcards in the result typeset arg=$3 IFS=$2 ; set -- $1 eval print -R \${$arg} } verboseprint() { # Print erguments if $verbosity (global) is set high enough threshold=$1; shift if [[ $verbosity -ge $threshold ]]; then for arg in "$@"; do print "$arg" done fi } follow_links() { ( # Run in a subshell since we need to change directories # Follow the symlinks in $1 and return the final location. val=$1 line=$(ls -ld $val) link=$(wordsplit "$line" " " 11) # $link now contains something if there is a link while [ -n "$link" ]; do # $val is the full pathname of the file we searched on # $link is the linked-to name (path may be relative or absolute) # $line is the output of ls -ld $val # change directories to the location of $val, and try again # Note that dirname is not the same as ${val%/*} if there is only one / cd $(dirname $val) # if $link is a relative pathname, stick it onto current directory # otherwise, use it as an absolute name case $link in /*) val=$link ;; *) case $PWD in /) val=/$link ;; *) val=$PWD/$link ;; esac ;; esac line=$(ls -ld $val) # now see if we have another link link=$(wordsplit "$line" " " 11) done # no [more] links - just return the final pathname print $val ) } check_equivs() { # Check the path $1 for equivalence with each of the remaining arguments. # Echo all that match p=$1; shift for d in "$@" ; do [ $p -ef $d ] && print -R "$d" done } elim_equivs() { # Check the path $1 for equivalence with each of the remaining arguments # Echo the ones that do NOT match (i.e., are unique) # If less than two arguments, echo the argument and return if [ $# -lt 2 ]; then print -R "$1" else p=$1; shift for d in "$@"; do [ ! $p -ef $d ] && print -R "$d" done fi } listdirs() { # list all the files in each directory argument. No directory parts or headers for dir in "$@"; do # arguments should be clean, but double check [ -d "$dir" ] && ls -1 $dir done } check_path() { # For each directory in a list passed as $1 ($PATH format), make sure the # directory exists, and is read/execute # If a directory is a symlink, check whether the linked-to directory is also in the path. # $pathels is created as a space-separated list of path elements, with duplicates # (e.g. symlinks) and inaccessible elements removed. This is returned to the caller # as a global variable. pathels= # Uses global $verbosity to control messages to stdout. # For each path element in turn, output is # "seq#: directory [errors or warnings]" verboseprint 1 "Path directories, in search order\n" status=0 seq=0 path=$1 OIFS=$IFS; IFS=:; set $path; IFS=$OIFS for dir in "$@"; do seq=$((seq + 1)) verboseprint 1 "$seq: $dir\c" if [[ -L $dir ]]; then linked_dir=$(follow_links $dir) verboseprint 1 " \tWARNING: $dir symlinks to $linked_dir\c" fi if [[ ! -d $dir ]]; then verboseprint 1 " \tERROR: Missing directory\c" status=$((status + 1)) elif [[ ! ( -x $dir && -r $dir ) ]]; then # Note - directories owned by the current user always seem to pass this test # regardless of permissions verboseprint 1 " \tERROR: Inaccessible directory: check permissions\c" status=$((status + 1)) else # No access problems, but check for duplicates (symlinks or real duplicates) # and do not add those to $pathels or we'll get bogus conflicts. equivdir=$(check_equivs "$dir" $pathels) if [ "$equivdir" ] ; then verboseprint 1 " (equivalent to $equivdir, already in PATH)\c" else pathels="$pathels $dir" fi fi # Generate a newline - any mesages above are on the same line. verboseprint 1 "" # Debugging - show the directory details, but suppress errors from missing directories etc. [ $verbosity -gt 1 ] && ls -ld $dir 2>/dev/null done # output spacing only. verboseprint 1 "" # Return an exit status indicating bad path elements. return $status } searchpath() { # Look for the program given as $1 in each of the directories given in the remaining arguments. # Print warning messages to stdout for each real conflict. Ignore apparent conflicts which # actually resolve to the same file. # Return an exit status which is the number of real conflicts located. [ $verbosity -ge 4 ] && set -x prog=$1; shift confpaths= nconf=0 for dir in "$@"; do if [[ -f $dir/$prog && -x $dir/$prog ]]; then confpaths="$confpaths $dir/$prog" nconf=$((nconf + 1)) fi done # We have a list of $nconf items, but some may be equivalent to others. We need to # eliminate the duplicates and return the number of real conflicts. The list can be # empty or have just one item, in which case we have no conflicts, but may want to # present the file details anyway if [ $nconf -eq 0 ]; then # Could get here if the user specified a program name which doesn't exist # OR, files appear in the path but are not executable. verboseprint 1 "$prog not found in \$PATH" elif [ $nconf -eq 1 ]; then # Found the program, but only once - don't report anything. return 0 else # We have two or more potential pathnames in conflict # Detect linked files. Do not count paths which resolve to the same file # Reset the arguments to the function, for easier parsing rconf=0 rconfpaths= set -- $confpaths p1=$1; shift remainder=$(elim_equivs "$p1" "$@") while [ -n "$remainder" ]; do rconfpaths="$rconfpaths $p1" rconf=$((rconf + 1)) p1=$1 [ $# -gt 0 ] && shift remainder=$(elim_equivs "$p1" "$@") done # $rconf now contains a count of the non-equivalent pathnames, which may be 0 (no real conflicts) if [ $rconf -eq 0 ] ; then # No real conflicts, but print the info anyway if we are being verbose verboseprint 2 "$prog has 0 conflicts" if [ $verbosity -ge 2 ]; then set -- $confpaths for path in "$@"; do print "0: \c" ; ls -l $path if [[ -L $path ]] ; then print " -> $(follow_links $path)" fi done print for path in "$@"; do if [[ -L $path ]]; then print -R "-> $(file $(follow_links $path))" else print -R " $(file $path)" fi done print fi return 0 else # We have 2 or more real conflicts - list them, with 'ls -l' and 'file' output verboseprint 0 "$prog has $rconf conflicts" if [ $verbosity -ge 1 ]; then # At this point, $rconfpaths has the conflicting pathnames in order, so we should # be able to do "ls -l $rconfpaths". However, 'ls' sometimes generates output not in the # same order as the arguments, so step through explicitly and add a counter. set -- $rconfpaths i=0 for path in "$@"; do print "$i: \c" ; ls -l $path if [[ $verbosity -ge 2 && -L $path ]] ; then print " -> $(follow_links $path)" fi i=$((i+1)) done print # repeat for the 'file' information for path in "$@"; do if [[ $verbosity -ge 2 && -L $path ]] ; then print -R "-> $(file $(follow_links $path))" else print -R " $(file $path)" fi done print fi fi return $rconf fi } # Control starts here. # First check once that all the directories in the $PATH exist and are read/execute. # This is always performed, to validate the path elements for later, # but the report to stdout may be suppressed if [ $pathcheckonly -eq 1 ]; then check_path $PATH >/dev/null estat=$? else check_path $PATH; estat=$? estat=$? fi verboseprint 3 "Modified path elements:" "$pathels" # Next check all the named arguments, or default to all the executables in the path. dupcount=0 if [ $dircheckonly -ne 1 ]; then if [ $# -gt 0 ]; then # Examine specific programs named as arguments for pathname in "$@"; do prog=${pathname##*/} searchpath $prog $pathels [ $? -gt 0 ] && dupcount=$((dupcount + 1)) done else # No arguments given - analyse all the directories on the path. # The pattern to grep has a space and a tab. totcount=$(listdirs $pathels | wc -l) listdirs $pathels | sort | uniq -c | grep -v '^ *1[ ]' | while read ndups conflict ; do # searchpath function will check $conflict against $pathels and print messages to stdout. # The exit status is the number of real conflicts found (not counting symlinks to the same file) # By using $pathels we already eliminate most of the spurious conflicts caused by the same # directory appearing the in the path multiple times. searchpath $conflict $pathels [ $? -gt 0 ] && dupcount=$((dupcount + 1)) done verboseprint 1 "Total files examined: $totcount" fi verboseprint 1 "Total conflicting names: $dupcount" fi exit $estat