2

Say I have PATH="home/bob/bin:/usr/bin". I am writing a bash script /home/bob/bin/foo that will do some munging and then call /usr/bin/foo. Of course I want to be able to use this script on different systems which have different path structures. In practice the real foo might be in many different places, so I want to just find it from the PATH. My new foo script is on my path too, so I can't just call foo, that will result in a recursive call.

Is there an easy way of doing this in a bash script? (Other than looping through elements of PATH and doing the search manually?)

  • 1
    Remove your script's directory from PATH? unix.stackexchange.com/questions/108873/… – muru Apr 2 '15 at 22:33
  • @muru Not a good solution if the “real” foo might in turn invoke other commands, some of which may be in the same directory as the foo wrapper. – Gilles Apr 2 '15 at 23:41
  • 1
    @Gilles; is it realistic to assume a system's global /usr/bin/foo is accessing "private" commands like /home/bob/bin/foo? - At best that appears to me to be a misdesign. – Janis Apr 3 '15 at 9:48
  • @Janis I very very often run /bin/sh with the expectation that it runs “private” commands. Or run programs in /usr/bin that invoke an editor which is ~/bin/EDITOR. Etc. – Gilles Apr 3 '15 at 11:49
  • @Gilles; what you write in the last comment is different from what we have here in your first comment of the question context; i.e. that scripts in the systems context depend on scripts in private context. (Nevermind, it was anyway only a rhetorical question.) – Janis Apr 3 '15 at 15:05
2

You can always get the path to the second foo with:

foo=$(type -Pa foo | tail -n+2 | head -n1)

(provided file paths don't contain newline characters).

Beware that may be a relative path which would stop to be valid after you run cd.

You could then do:

hash -p "$foo" foo

So that foo be invoked when you run foo.

  • You're assuming that foo is being executed through the PATH. This has the potential to recurse if it's executed with an explicit PATH and there's one other foo in front. – Gilles Apr 2 '15 at 23:40
1

I don't think there's an easier way to do this robustly than enumerating the directories in PATH. It isn't hard.

#!/bin/bash
set -f; IFS=:
for d in $PATH; do
  if [[ -f $d/foo && -x $d/foo && ! $d/foo -ef /home/bob/bin/foo ]]; then
    exec "$d/foo" "$@"
    exit 126
  fi
done
echo "$0: foo (real) not found in PATH"
exit 127

I assume there are no empty entries in PATH. Empty PATH entries are evil, write . explicitly (or better don't include it at all).

If you'll only ever run foo from the command line and not from other programs, make it a function instead of a script. From the function, run command foo to hide the function.

  • Now that I look at it again, shouldn't it be exec $d/foo "$@"? – muru Apr 3 '15 at 1:11
0

If foo is important, then it should be configured in the script. This I think is good practice, because you then EXPLICITLY peg it to the script. I do not like indirection, implicit or hidden stuff ... scripts should be very simple if they are to be deployed.

Otherwise if you insist on finding it, then it can be found by doing

whereisfoo="$(which foo)"

If this is not enough, then I fear what you may be doing is too complex.

  • But the OP has a script called foo in has private bin directory ($HOME/bin), which appears at the beginning of his $PATH, so which foo will report /home/bob/bin/foo — which is not what the question is asking for. – G-Man Apr 3 '15 at 0:07
  • I think it is bad practice to hide stuff in scripts. It only gets you into trouble. Making the user stumble if a configuration file is not set is better practice – Xofo Apr 29 '15 at 16:08
  • (1) “… hide stuff in scripts”?  What?  I wonder whether you understand the question.  (2) “configuration file”?  Where did that come from?  (3) “[S]cripts should be very simple if they are to be deployed.”  OK, that’s your opinion, which (3a) doesn’t really help answer this question, and (3b) is, I suspect, a minority opinion.  Albert Einstein is credited with saying “Everything should be made as simple as possible, but not simpler.”  A script (or any other piece of software) should be as complicated as it needs to be to satisfy its requirements.  … (Cont’d) – G-Man Apr 29 '15 at 20:52
  • (Cont’d) …  And (I’m not sure whether this is 3c or 4) you say scripts should be very simple, but that a script that executes a mv command should be distributed with a configuration file that tells it whether mv is in /bin or /usr/bin?  Seriously? – G-Man Apr 29 '15 at 20:53
  • I do not think he is doing 'just' an mv. He is doing some munging and then calls /user/bin/foo ... Since /user/bin/foo may be somewhere else he needs to find it. I see this as bad design (my opinion). Because foo is a dependency that must be implicitly found (unbeknownst to the user). – Xofo Apr 29 '15 at 21:36
0

Here’s an approach that I believe is slightly less messy than Gilles’ answer (although I concede that to be a subjective judgment).

#!/bin/sh
this_dir=$(dirname "$0")        # Alternatively, hard-code this as this_dir=$HOME/bin
redacted_PATH=$(echo ":${PATH}:" | sed -e "s:\:$this_dir\::\::" -e "s/^://" -e 's/:$//')
if obscured_prog=$(PATH=$redacted_PATH which foo)
then
            ⋮           # Munging to do before running /usr/bin/foo
        "$(obscured_prog)" argument(s)          # may be "$@", but might not be.
            ⋮           # Munging to do after running /usr/bin/foo
else
        echo "$0: foo (real) not found in PATH."
            ⋮           # Any code that you want to do anyway.
fi

This constructs redacted_PATH to be $PATH minus the $HOME/bin directory where this private copy of foo lives.  echo ":${PATH}:" adds colons to the beginning and end of $PATH, so every component of it will be preceded and followed by a colon — even the first and last one.  The sed searches for

: $this_dir :

(with spaces added for “clarity”) and replaces it with

:

i.e., it excises $this_dir from ":${PATH}:".  It then removes the colons from the beginning and end.

Then we temporarily set PATH to $redacted_PATH and search for foo using which.  If successful, we get a full path to it (e.g., /bin/foo or /usr/bin/foo), which we use to run the real (public/shared/system) copy of foo.  Since we changed PATH only temporarily, /bin/foo has access to the user’s ambient $PATH, and so, if /bin/foo runs brillig, it can find $HOME/bin/brillig (if it exists).

This will have a problem if $HOME/bin appears in $PATH multiple times, but that’s not too hard to remedy.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.