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

matt
  • 21
  • 1
    Remove your script's directory from PATH? http://unix.stackexchange.com/questions/108873/removing-a-directory-from-path – muru Apr 02 '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 'SO- stop being evil' Apr 02 '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 03 '15 at 09: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 'SO- stop being evil' Apr 03 '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 03 '15 at 15:05
  • @Janis: I presume you understand what this question is about: a user who wants to “wrap” a system command with a customization that tweaks the operation of the system command.  The classic, well known, trivial example is alias mv="mv -i".  More complicated examples need to be done in shell functions or scripts.  … (Cont’d) – G-Man Says 'Reinstate Monica' Apr 29 '15 at 21:00
  • (Cont’d) …  Now, I believe that you’re misinterpreting/twisting Gilles’s comment.  He didn’t say that a system program might depend on a private program.  But consider, (on my system, at least) /bin/ps2html is a script that runs gs (Ghostscript) without specifying a path.  Suppose I have wrapped /bin/gs with a front-end ~/bin/gs.  If I have ~/bin before /bin in my search PATH, then, when I run /bin/ps2html, I probably want it to run ~/bin/gs (which will probably invoke /bin/gs with a tweak) — even/especially if I am running /bin/ps2html from a ~/bin/ps2html front-end. – G-Man Says 'Reinstate Monica' Apr 29 '15 at 21:01
  • @Gilles: if the “real” foo might in turn invoke other commands, some of which may be in the same directory as the foo wrapper, then change PATH *without exporting it, so, when ~/bin/foo says “foo”, it will get /usr/bin/foo, but, when /usr/bin/foo says “bar”, it will use the original* $PATH, which it gets from the environment, and so it can find ~/bin/bar. – G-Man Says 'Reinstate Monica' Apr 29 '15 at 21:05
  • @G-Man You can't change the value a variable without exporting it in Bourne-style shells. Either a variable is exported, and the environment variable has the same value as the shell variable, or it is not exported, and the variable is not present in the environment. – Gilles 'SO- stop being evil' Apr 29 '15 at 21:07
  • @Gilles: Dang; I misread something. – G-Man Says 'Reinstate Monica' Apr 29 '15 at 21:29

4 Answers4

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.

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.

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.

muru
  • 72,889
Xofo
  • 639
  • 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 Says 'Reinstate Monica' Apr 03 '15 at 00: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 Says 'Reinstate Monica' 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 Says 'Reinstate Monica' 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
  • This question is about a user who wants to “wrap” a system command with a customization that tweaks the operation of the system command.  The classic, well known, trivial example is alias mv="mv -i".  More complicated examples need to be done in shell functions or scripts.  Are you saying that alias mv="mv -i" is bad design?  For that matter, if I write a script called do_stuff that uses commands like mv, cp, and ls, then the shell (that runs that script) needs to find those programs.  Is that bad design?  I don’t understand your argument. – G-Man Says 'Reinstate Monica' Apr 29 '15 at 21:47
  • You know very well I am not saying that. What I am saying is that burying a dependency and having to discover it (through some script trickery) is not elegant nor wise.

    Yes it is possible ... but that is not the way to go. Entry and exit conditions as well as dependencies should be known to the end user if he is DEPLOYING it - As he stated in his/her post.

    – Xofo Apr 29 '15 at 22:02
  • I *do not "know very well"* what you are and are not saying.  I still do not understand the difference between ~/bin/do_stuff running mv and expecting to get /bin/mv, versus ~/bin/mv running mv and expecting to get /bin/mv. – G-Man Says 'Reinstate Monica' Apr 29 '15 at 23:33
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.