9

I am trying to write a bash function that behaves similarly to the where builtin in tcsh. In tcsh, where lists all the builtins, aliases, and the absolute paths to executables on the PATH with a given name, even if they are shadowed, e.g.

tcsh> where tcsh
/usr/bin/tcsh
/bin/tcsh

As part of this I want to loop over everything in the $PATH and see if an executable file with the appropriate name exists.

The following bash snippet is intended to loop over a colon-delimited list of paths and print each component followed by a newline, however, it just seems to print the entire contents of $PATH all on one line

#!/bin/bash
while IFS=':' read -r line; do
    printf "%s\n" "$line"
done <<< "$PATH"

As is stands now, bash where and ./where just print /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games

So, how do I set up my while loop so that the value of the loop variable is each segment of the colon-separated list of paths in turn?

5
  • 6
    Why not use type -a? Apr 15, 2016 at 3:41
  • 2
    @StéphaneChazelas because I didn't know it existed. :/ Apr 15, 2016 at 3:43
  • 2
    See also Debian's which script for how to loop over $PATH properly. Apr 15, 2016 at 3:44
  • 1
    @StéphaneChazelas I see so if you use IFS=':' ; for x in $PATH ; do echo "$x" ; done, then the loop variable will be set to each element of $PATH in turn. Why do the for and while loop treat IFS differently? Apr 15, 2016 at 4:44
  • 2
    @GregoryNisbet it's not while that's doing anything with IFS in this case, it's read, which reads a whole line at once, regardless of IFS—as Stephane explained, actually.
    – Wildcard
    Apr 15, 2016 at 4:49

5 Answers 5

9

read uses IFS to separate the words in the line it reads, it doesn't tell read to read until the first occurrence of any of the characters in it.

IFS=: read -r a b

Would read one line, put the part before the first : in $a, and the rest in $b.

IFS=: read -r a

would put the whole line (the rest) in $a (except if that line contains only one : and it's the last character on the line).

If you wanted to read until the first :, you'd use read -d: instead (ksh93, zsh or bash only).

printf %s "$PATH" | while IFS= read -rd: dir || [ -n "$dir" ]; do
  ...
done

(we're not using <<< as that adds an extra newline character).

Or you could use standard word splitting:

IFS=:; set -o noglob
for dir in $PATH""; do
  ...
done

Now beware of few caveats:

  • An empty $PATH component means the current directory.
  • An empty $PATH means the current directory (that is, $PATH contains one component which is the current directory, so the while read -d: loop would be wrong in that case).
  • //file is not necessary the same as /file on some system, so if $PATH contains /, you need to be careful with things like $dir/$file.
  • An unset $PATH means a default search path is to be used, it's not the same as a set but empty $PATH.

Now, if it's only the equivalent of tcsh/zsh's where command, you could use bash's type -a.

More reading:

4

Don't use shell loops to process text.

Instead, use awk, or tr, or even sed.

printf %s\\n "$PATH" | tr ':' '\n'

printf %s "$PATH" | awk 'BEGIN {RS=":"}; 1'

Or, since this is a shell variable you are processing, just use bash pattern substitution:

echo "${PATH//:/
}"

(See LESS=+/parameter/pattern man bash.)

1
  • 2
    Why the downvote? I see three other answers have been given downvotes just now with no comments...why?
    – Wildcard
    Apr 15, 2016 at 19:34
1

Pure bash, it's the same thing as Wildcard's 2nd method, but this is on one line:

echo -e "${PATH//:/"\n"}"
0

This script works similar to the command where

#!/bin/bash
fn="foo"
for i in `echo $PATH|tr ':' '\n'`
do
  if [[ -e "$i/$fn" ]] ; then
    echo $i
  fi
done

$fn is something you want to find.

$i/$fn may be $i$fn sometimes.

-1

Shell utils:

echo $PATH | tr ':' '\n'

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .