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

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

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?

  • 6
    Why not use type -a? – Stéphane Chazelas Apr 15 '16 at 3:41
  • @StéphaneChazelas because I didn't know it existed. :/ – Gregory Nisbet Apr 15 '16 at 3:43
  • 2
    See also Debian's which script for how to loop over $PATH properly. – Stéphane Chazelas Apr 15 '16 at 3:44
  • @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? – Gregory Nisbet Apr 15 '16 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 '16 at 4:49

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

(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

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:


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

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

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

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

This script works similar to the command where

for i in `echo $PATH|tr ':' '\n'`
  if [[ -e "$i/$fn" ]] ; then
    echo $i

$fn is something you want to find.

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


Shell utils:

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

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.