4

For example, my script is $HOME/bin/sudo. I'd like to search for an executable in $PATH with the same name, sudo, and run it - but not the script $HOME/bin/sudo itself, otherwise I will run into an infinite loop!

EDIT: the point of this is that in some cases i want my replacement scripts to have an higher priority over the regular system commands, while in some other cases i want the opposite. So i've set "$HOME/bin" as first in the PATH, so now i can define priorities for each command individually. Also i want some kind of "portability" so the scripts will be able to run in different systems.

eadmaster
  • 1,643
  • 1
    I don't fully understand your question - you have your script located in $HOME/bin and you've put it in some folder in your $PATH and now you don't know, where it is? – Eenoku Oct 24 '14 at 10:01
  • no, i want to write a script that works as an alternative to a system command, but only if the system command does not exist... I guess that without an explicit check the replacement script could override the system command... – eadmaster Oct 24 '14 at 10:04
  • 2
    What are you actually trying to do? Sounds like an XY problem to me. – Bernhard Oct 24 '14 at 13:18

6 Answers6

4

There is no need for scripting or functions. If you simply put $HOME/bin last in your path, it will only be used if there is no matching command name in any of the previous directories in the $PATH.

Example:

[jenny@sameen ~]$ export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin
[jenny@sameen ~]$ which foo
/usr/bin/which: no foo in (/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin)
[jenny@sameen ~]$ export PATH=$PATH:$HOME/bin
[jenny@sameen ~]$ which foo
~/bin/foo
[jenny@sameen ~]$ sudo cp bin/foo  /usr/local/bin/
[jenny@sameen ~]$ which foo
/usr/local/bin/foo

If you don't trust that you can set your own $PATH, so that you still want to do the check from within the script, here's an example:

#!/bin/bash
export ORIGPATH=$PATH  # to revert the path after checking for the binary
export PATH=`echo $PATH |sed -e 's{/home/jenny/bin:{{'`

MYNAME=`basename $0`

if  which $MYNAME
then
    BINFILE=`which $MYNAME`
    export PATH=$ORIGPATH 
    echo "Found $MYNAME in $BINFILE "
    $BINFILE
else
    export PATH=$ORIGPATH
    echo "Here goes the rest of the script"
fi
Jenny D
  • 13,172
  • ok, but i cannot make assumptions where "$HOME/bin" is in my $PATH. Also maybe the $PATH search order is not standardized... – eadmaster Oct 24 '14 at 10:23
  • 1
    The search order is standardized. Since you can set your own path, you can set it so that $HOME/bin is last. Or you can put these particular scripts in a different directory (e.g. $HOME/scripts) and add that directory last to your path. – Jenny D Oct 24 '14 at 10:46
  • ok, but i still prefer an explicit check in case i forget this or some system scripts modify my $PATH variable... – eadmaster Oct 24 '14 at 10:50
  • @eadmaster Code snippet added. (note that I used { as the separator because it gets boring writing a \ before each / in the sed command...) – Jenny D Oct 24 '14 at 11:16
  • 1
    the side effect of this solution is that "$MYNAME" will run with a modified $PATH too... – eadmaster Oct 25 '14 at 13:42
  • Putting ~/bin last won't work here since it would cause the original command to be executed in the first place instead of the wrapper script as intended. – Gilles 'SO- stop being evil' Oct 25 '14 at 14:21
  • @Gilles I think one of us has misunderstood the problem, because that comment does not make sense to me from how the question was phrased. – Jenny D Oct 25 '14 at 18:55
  • @eadmaster Edited in a fix for that. – Jenny D Oct 25 '14 at 18:59
  • if you don't trust $PATH you just do command -p getconf PATH – mikeserv Oct 26 '14 at 08:00
  • another problem is that there could be multiple entries of "$HOME/bin" in $PATH, some of which could be symlinks (in my case there is "$HOME/my-applications/bin/" which is an alias to "$HOME/bin"). So, resetting $PATH as suggested could be another solution. – eadmaster Oct 26 '14 at 16:27
  • 1
    @eadmaster At some point you really have to step up and take responsibility for setting up your environment the way you want to. Multiple entries are easy to deal with; man sed will tell you how to fix it. Symlinks - less easy, still possible. But, as I said from the start, if you want your $PATH to look a particular way, then you need to set it yourself (preferably in your profile/.bashrc), and then simply use common sense. – Jenny D Oct 27 '14 at 08:04
  • ^this is very well said. i didn't mean to take away from this answer - which is excellent and already had my vote long before I wrote mine. i learned my lesson a long time ago with ruby scripts for colorizing terminal output - which worked by usurping $PATH. compiles were horrible. the only good command -p getconf PATH can do anyone as I believe is to get you an editor or two so you can turn right around and do as you say. – mikeserv Oct 28 '14 at 05:19
2

POSIX defines the -p option to the command builtin so...

  • -p Perform the command search using a default value for PATH that is guaranteed to find all of the standard utilities.

Taken with the -v and -V options for either parsable or human-friendly (respectively) output regarding a command's location, and you can pretty well rely on it to get you the intended utility when you ask it for one. Here's a little script to demo how it works:

(   cd ~; mkdir -p bin
    cat >./bin/cat
    chmod +x ./bin/cat
    export "PATH=$HOME/bin:$PATH"
    command -V cat | cat
) <<\NOTCAT
#!/bin/sh
command -p cat
! printf "I'm not REALLY cat, but '%s' is!\n" \
         "$(command -pv cat)"
NOTCAT

OUTPUT

cat is /home/mikeserv/bin/cat
I'm not REALLY cat, but '/bin/cat' is!

The first couple statements build an executable script in ~/bin/cat. $PATH is also modified to insert ~/bin at its head.

So when I do command -V cat | cat command writes to my fake cat's stdin. And yet its output still makes it to my screen. This is because command -p cat gets the real one regardless of how I have mangled my $PATH.

mikeserv
  • 58,310
  • 1
    The only problem is that is guaranteed to find only the standard utilities, so it could not work in some cases. Btw this is a very concise solution using only POSIX standard features, so i prefer it over the others. (i just hope they won't change the syntax in later revisions ;) – eadmaster Oct 26 '14 at 16:36
  • @eadmaster - this is a legitimate concern. However, if you look at man command, you'll find that in the Rationale they cite the purpose of finding the standard utilities is really to enable you always at least to do: command -p getconf PATH. – mikeserv Oct 26 '14 at 20:28
0

Search for the command with a modified PATH:

PATH=$(awk -F: 'sub(FS "'$HOME/bin'","")' <<<"$PATH") which sudo

For example:

$ which sudo
/usr/local/bin/sudo
$ PATH=$(awk -F: 'sub(FS "/usr/local/bin","")' <<<"$PATH") which sudo
/usr/bin/sudo
muru
  • 72,889
  • this is not working: PATH=$(awk -F: 'sub(FS "$( dirname $0 )","")' <<<"$PATH") which $( basename $0 ) – eadmaster Oct 24 '14 at 10:55
  • @eadmaster Take note of the quotes you are using. Use something like: PATH=$(awk -F: 'sub(FS "'$(dirname $0)'"... – muru Oct 24 '14 at 11:09
0

I've just found this alternative that seems quite reliable.

EXEPATHSLIST="/usr/bin /usr/sbin /bin /sbin /opt/$1/bin /usr/games /usr/local/bin /usr/local/games /system/bin /system/xbin"
for exepath in $EXEPATHSLIST
do
    if [ -x  $exepath/$1 ]; then
        $exepath/"$@"
        exit $?
    fi
done

# alternative using command
command -p "$@"
_ES=$? ; [ $_ES -ne 127 ] && exit $_ES

# more alternative search methods here
# ...

# else command not found
exit 127
eadmaster
  • 1,643
-1

If your $HOME/bin path are not in $PATH you could test it with

if [ ! -a $(command -v $(basename $0)) ]; then
        YOUR SCRIPT
else
   echo "$(basename $0 already exists in $PATH"
fi
UsersUser
  • 409
-1

I'm not entirely sure I understand what you're after either but type and which should help. For example:

$ type -a sudo
sudo is aliased to `/usr/bin/sudo'
sudo is /home/terdon/scripts/sudo
sudo is /usr/bin/sudo

In the example above, there are 3 possible sudos, one is an alias and two are in my $PATH. You can just parse that to do what you want. If you are using a shell that does not have the type builtin, you can do the same with which (though that does not detect aliases but that shouldn't be an issue with shell scripts anyway.):

$ which -a sudo
/home/terdon/scripts/sudo
/usr/bin/sudo

With this in mind, if your objective is to avoid an infinite loop based on the name of the current script, you can do something like:

#!/usr/bin/env bash

## Get the name of the matching command from your
## $PATH, excluding the script itself ($0).
com=$(type -a $(basename $0) | grep -v $0 | head -n 1 | awk '{print $NF'})
## Run the command on the arguments passed
$com "$@"

As @edimaster pointed out in the comments, -a is not defined by POSIX so it may not be present in all systems. A more portable approach then, would be to search through the directories of your $PATH:

#!/usr/bin/env bash

## Get the name of the matching command from your
## $PATH, excluding the script itself ($0).
com=$(find $(printf "%s" "$PATH" | sed 's/:/ /g') -name $(basename $0) |
      grep -v $0 | head -n 1)
## Run the command on the arguments passed
$com "$@"
terdon
  • 242,166
  • Although i like the idea of leaving the PATH variable unchanged, the "-a" option is not standard. – eadmaster Oct 26 '14 at 15:58
  • @eadmaster so it seems, thanks. In that case, you could use find instead. See updated answer. – terdon Oct 26 '14 at 16:14
  • unfortunately any mistrust for $PATH must always also lend itself to find. It is arguable however, that, in any case, any mistrust for $PATH is baseless and that the admin that set it up should either know what they are doing or will learn soon enough and we should all stop trying to circumvent their wishes.... – mikeserv Oct 26 '14 at 21:31