POSIXly, when searching for a $PATH
'd executable command, you should use command
:
command -v
- Write a string to standard output that indicates the pathname or command that will be used by the shell, in the current shell execution environment (see Shell Execution Environment, to invoke command_name, but do not invoke command_name.
- ... Otherwise, no output shall be written and the exit status shall reflect that the name was not found.
If called eithout a switch, command
will invoke command_name, but you do still need a fully resolved path to fully resolve a shell glob.
So, to do that, you should check the glob against each :
colon-separated element in $PATH
in order, as spec'd:
PATH
- This variable shall represent the sequence of path prefixes that certain functions and utilities apply in searching for an executable file known only by a filename. The prefixes shall be separated by a colon
:
. When a non-zero-length prefix is applied to this filename, a slash shall be inserted between the prefix and the filename. A zero-length prefix is a legacy feature that indicates the current working directory. It appears as two adjacent colons ::
, as an initial colon preceding the rest of the list, or as a trailing colon following the rest of the list. A strictly conforming application shall use an actual pathname (such as .
) to represent the current working directory in PATH
. The list shall be searched from beginning to end, applying the filename to each prefix, until an executable file with the specified name and appropriate execution permissions is found. If the pathname being sought contains a slash, the search through the path prefixes shall not be performed. If the pathname begins with a slash, the specified path is resolved (see Pathname Resolution). If PATH
is unset or is set to null, the path search is implementation-defined.
You can split out $PATH
into a field per $PATH
component on $IFS
, but you can only do this safely if you also set -f
ilename expansion as disabled while you do, in case a component in $PATH
contains any of [?*
characters, because filename generation (read: globbing) occurs after field-splitting in the shell's expansion order.
In that way, in general, the only really robust way to do this POSIXly is to first expand the field list on $IFS
while filename generation is disabled and to somehow save that array result, then to once again enable filename generation and disable field-splitting (so that you might store and expand your glob pattern unquoted in a variable w/out any danger of it being affected by $IFS
first), and operate on the results each in turn.
Fortunately, this is not so difficult:
g='libreoffice?.?'
( IFS=:; set -f #split on :; disable filename gen
set +f -- $PATH #store split array, enable filename gen after
IFS= #disable field splitting
for p do for x in ${p:+"$p/"$g}
do command -v "$x"
done;done
) #done in subshell to avoid affecting current shell
The last bits there are not commented because they're better explained here.
for p do
- The first for
loop iteratively assigns $p
the value of each positional parameter in the shell's "$@"
array, which are set
to the the field-split expansion of $PATH
as divided by :
colons.
for x in ${p:+"$p/"$g}
- The inner for
loop iteratively sets $x
to each (if any) of the expansions of ${p:+"$p/"$g}
- which is to say, it iterates over nothing at all unless $p
is both set and not-null. This is important because ::
or a leading :
would split out to null fields, and, in this way, the two loops strictly conform to the $PATH
spec (as quoted above) by not acknowledging zero-length fields.
Importantly, because of the way $PATH
is split while filename generation is disabled and "$p/"$g
is resolved while it is enabled but field-splitting has been disabled, none of the elements in $PATH
can expand to anything but their fields as split on :
(regardless of whatever other characters they may contain) and neither can any value for "$p/"$g
expand to anything but itself or whatever filenames it might match (regardless of any characters in $p
or other than pattern-matching characters $g
might contain).
command
- and last command
only prints to standard-out those values for $x
which are executable commands.
Here's the whole thing wrapped in a shell function, with the additional capability of handling multiple $g
arguments:
glob_path()(
for g do IFS=:; set -f
set +f -- ${PATH:-.}; IFS=
for p do for x in ${p:+"$p/"$g}
do command -v "$x"
done; done; done
)
That should get you a list of executables matching each argument you hand it printed one per line ordered first by argument order, then by $PATH
priority, and last by your locale's sort order. It should not fail on newlines or special characters in the pathnames or the glob patterns you hand it. It might be altered (see the edit history here) to faithfully populate the shell "$@"
array at one executable per positional parameter.
You could also alter it to execute the first successful glob match and quit searching for that particular glob argument and to move onto the next if the command exits successfully like...
glob_path()(
for g do IFS=:; set -f
set +f -- ${PATH:-.}; IFS=
for p do for x in ${p+:"$p/"$g}
do command -v "$x" &&
command "$x" &&
break 2
done; done; done
)
This is a POSIX portable means of finding your glob match, but some shells don't even need the the second for g...
loop. In bash
, for example, this will also work like...
glob_path()(
for g do IFS=:; set -f
set +f -- ${PATH:-.}; IFS=
for p do command -v ${p:+"$p/"$g}
done; done
)
...which still lists every executable glob match for all arguments and for each element in $PATH
regardless of whether any of those elements or glob patterns contain newlines, or special characters, etc.
You can use it like:
glob_path '?sh' '??sh' #I'd rather not install libreoffice
...which for me prints...
/usr/bin/ksh
/usr/bin/rsh
/usr/bin/ssh
/usr/bin/zsh
/usr/local/bin/dash
/usr/local/bin/yash
/usr/bin/bash
/usr/bin/bssh
/usr/bin/chsh
/usr/bin/dash
/usr/bin/mksh
/usr/bin/posh
/usr/bin/slsh
/usr/bin/yash
As already discussed, the glob_path
function interprets the $PATH
environment variable as the spec states a strictly conforming application should by ignoring leading, trailing or consecutive :
occurrences in $PATH
's value, but it does interpret an empty or unset $PATH
to mean .
.
If you did want that legacy feature though, the function could be written:
glob_path()(
for g do IFS=:; set -f
set +f -- ${0+$PATH:}; IFS=
for p do for x in "${p:-.}/"$g
do command -v "$x"
done; done; done
)
...which would interpret all occurrences of leading, trailing, or two consecutive :
colons to mean .
(and so search .
as many times as a zero-length $PATH
component appears in $PATH
's value), but that's not strictly conformant.