7

I'd like to find and execute a command in the current $PATH matching this wildcard libreoffice?.? (eg. libreoffice4.0, libreoffice4.3, etc.)

EDIT: if multiple matches are found, you can pick one randomly.

I prefer a POSIX compliant solution.

3

Set IFS to : to split the value of PATH on colons. If your find has the -quit action and the -maxdepth primary (e.g. FreeBSD, OSX, GNU), you know the command will exist and you don't care about the command's return code, you can use this one-liner:

pattern='libreoffice?.?'
IFS=:; find $PATH -maxdepth 1 -type f -name "$pattern" -exec {} \; -quit; unset IFS

This doesn't provide an easy way to report whether the command was found. Furthermore, to be more robust, turn off globbing in case the value of PATH contains wildcards. Also, it's possible to have an empty component in PATH to mean the current directory (but my advice is to use . instead). The code below takes care of all these complications.

pattern='libreoffice?.?'
case $PATH in
  :*) directories=.$PATH;;
  *::*) directories=${PATH%%::*}:.:${PATH#*::};;
  *:) directories=$PATH.;;
  *) directories=$PATH;;
esac
set -f; IFS=:
cmd=
for d in $directories; do
  set +f
  for x in "$d"/$pattern; do
    if [ -x "$x" ] && ! [ -d "$x" ]; then
      cmd=$x
      break
    fi
  done
  if [ -n "$cmd" ]; then break; fi
done
set +f; unset IFS
if [ -z "$cmd" ]; then
  echo 1>&2 "$pattern: not found in PATH"
  exit 127
else
  exec "$cmd"
fi

If you happen to be using zsh (as opposed to plain sh, bash, ksh, …), it's a lot simpler to make a robust solution.

pattern='libreoffice?.?'
matches=($^path/$~pattern(N.*[1]))
if ((!#matches)); then
  $matches[1]
else
  echo 1>&2 "$pattern: not found in PATH"
  exit 127
fi
  • Strictly speaking, :: could occur more than once in $PATH. You may want to consider the case where $PATH is set but empty as well (replace with .). – Stéphane Chazelas Feb 22 '15 at 23:13
  • @StéphaneChazelas - per our previous conversation :: assuming . for :: is only legacy conformant anyway - and newer applications should make such an assumption. – mikeserv Feb 22 '15 at 23:26
  • I prefer the oneliner solution, but with posix-compatible switches: find $PATH -type f -perm /u=x,g=x,o=x -prune -name "$@" | head -n1 – eadmaster Feb 22 '15 at 23:26
  • @eadmaster You're recursing into subdirectories into directories in $PATH (and yes, that does happen). – Gilles 'SO- stop being evil' Feb 22 '15 at 23:53
  • 2
    @eadmaster Make that "$1", since there can only be one pattern here. – Gilles 'SO- stop being evil' Feb 22 '15 at 23:55
2

With zsh:

$commands[(i)libreoffice?.?]

In zsh, $commands is a special associative array whose keys are command names and value their path.

i above is an array subscript flag that tells zsh to match the pattern against the array keys and return the first matching key.

The elements of the associative array are not in any particular order though, so the first matching key will not necessarily be the first that occurs in $PATH. If you want the libreoffice with the greatest version number, you could do:

${${(nO)${commands[(I)libreoffice?.?]}}[1]}

The I subscript flag expands to all the matching keys. We use the n (numerical sort), and O (reverse order) parameter expansion flags to sort that list from greatest to smallest version number, and then [1] to select the first one.

See also:

whence -m 'libreoffice?.?'

to find the paths of the corresponding commands.

  • 1
    @mikeserv, it reports the key, so the command name, the normal $PATH lookup will invoke the first in $PATH for the greatest version of libreoffice (and $commands[cmd] is the first cmd in $PATH, it's just that if you have /bin/libreoffice4.2 and /usr/bin/libreoffice4.1, the first solution may very well return libreoffice4.1 even if /bin is before /usr/bin in $PATH). In zsh, the $path array is tied to the $PATH scalar (a feature borrowed from csh) – Stéphane Chazelas Feb 23 '15 at 11:48
  • But I thought $path can be altered without altering $PATH? Anyway, I know very little about that - only that they were separate things. Nope - tested it - changing $path also changed $PATH. – mikeserv Feb 23 '15 at 11:51
  • @mikerserv, no, $path and $PATH are tied. Any modification of one is reflected in the other. See the description of typeset -T. – Stéphane Chazelas Feb 23 '15 at 11:55
  • Interesting... The first two arguments are the name of a scalar and an array parameter (in that order) that will be tied together in the manner of $PATH and $path. The optional third argument is a single-character separator which will be used to join the elements of the array to form the scalar; if absent, a colon is used. zsh sure does some neat stuff, it's just that it does so much of it that I often get lost. That's why, for the complicated stuff, I've learned to like ksh93 much better - it's more straight-forward, as I think. But that's just preference. – mikeserv Feb 23 '15 at 12:03
1

Here's a simpler alternative:

$(compgen -c libreoffice)

It assumes bash, and assumes there's only one libreoffice* installed.

It emulates what bash tab completion would do if you typed libreofficeTab.

If you were deliberately trying to exclude libreoffice without a version number, and want to handle the existence of multiple versions, try:

run_libreoffice() {
    compgen -c libreoffice |
        while read -r exe; do
            case "$exe" in libreoffice?.?)
                "$exe" "$@"
                return
                ;;
            esac
        done
}
run_libreoffice "$@"

The case statement makes it match only libreoffice?.?, and we loop over the results, only running the first one.

  • 1
    It assumes more than bash - it assumes one full result per line, and no $IFS or glob characters in results. – mikeserv Feb 22 '15 at 22:29
  • that's an interesting option, but it is limited to wildcards starting with a fixed string... – eadmaster Feb 22 '15 at 23:22
  • The second form isn't limited to fixed strings, but having to specify the substring to compgen then the pattern to case isn't ideal. – Mikel Feb 23 '15 at 0:08
0

You can use the command "find" as follows

find ./ -name "libreoffice?.?"
  • 1
    does not work, note that i meant the system $PATH, not the $PWD – eadmaster Feb 22 '15 at 3:08
  • 1
    find $(echo $PATH|sed 's/:/ /g') -name "libreoffice?.?" – dgsleeps Feb 22 '15 at 3:43
  • ok, it works! :) – eadmaster Feb 22 '15 at 3:45
  • In order to find into all $PATH directories we should do a search in all of it. This means something like this: 'find dir1 dir2 dir3 -name "libreoffice?.?"'. Therefore, what we just replace the semicolon for spaces with command $(echo $PATH|sed 's/:/ /g') – dgsleeps Feb 22 '15 at 3:50
0

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

  • i prefer dgsleeps' answer because looks simpler... – eadmaster Feb 22 '15 at 4:21
  • Fails if $PATH contains empty components. Also, note that IFS considers : as a delimiter, not separator as you'd need to process $PATH (values of $PATH like /bin:/usr/bin: are not unusual) – Stéphane Chazelas Feb 22 '15 at 23:03
  • @StéphaneChazelas - Good point - I had that in the older one. What does the Also... mean? Are you saying it is possible for a $PATH component to contain :? Anyway; I fixed the :: thing. – mikeserv Feb 22 '15 at 23:05
  • IFS=:; $PATH splits /bin:/usr/bin: into (/bin /usr/bin) in POSIX shells, not (/bin /usr/bin '') (exception with zsh in sh emulation, yash, pdksh and older versions of mksh) – Stéphane Chazelas Feb 22 '15 at 23:08
  • An empty $PATH means search in the current directory, /bin: means search in /bin and current directory. For an unset $PATH, that depends on the shell, check my answer on the why not use which question. See @Gilles' for a more correct method. – Stéphane Chazelas Feb 22 '15 at 23:12

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.