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 Answers 5
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 thewhile 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
.)
-
2Why the downvote? I see three other answers have been given downvotes just now with no comments...why?Wildcard– Wildcard2016年04月15日 19:34:32 +00:00Commented Apr 15, 2016 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
#!/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.
Shell utils:
echo $PATH | tr ':' '\n'
type -a
?which
script for how to loop over$PATH
properly.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 thefor
andwhile
loop treatIFS
differently?while
that's doing anything withIFS
in this case, it'sread
, which reads a whole line at once, regardless ofIFS
—as Stephane explained, actually.