singpolyma
singpolyma

Reputation: 11251

Find out if a command exists on POSIX system

I want to be able to tell if a command exists on any POSIX system from a shell script.

On Linux, I can do the following:

if which <command>; then
   ...snip...
fi

However, Solaris and MacOS which do not give an exit failure code when the command does not exist, they just print an error message to STDOUT.

Also, I recently discovered that the which command itself is not POSIX (see http://pubs.opengroup.org/onlinepubs/9699919799/idx/utilities.html)

Any ideas?

Upvotes: 31

Views: 15481

Answers (6)

Ernest Izdebski
Ernest Izdebski

Reputation: 31

To make this work in zsh or bash, such that aliases & functions are not considered while still being POSIX-compliant, just shell out:

xdg_open_path="$(sh -c "command -v xdg-open")"
if [ "$?" -eq 0 ]; then
    echo "xdg-open: is ${xdg_open_path}"
else
    echo 'xdg-open: not found' 1>&2
fi

Upvotes: 1

jarno
jarno

Reputation: 856

One which utility is available as shell script in the Git repository of debianutils package of Debian Linux. The script seems to be POSIX compatible and you could use it, if you take into account copyright and license. Note that there have been some controversy whether or not and how the which utility should be deprecated; (at time of writing) current version in Git shows deprecation message whereas an earlier version added later removed -s option to enable silent operation.

command -v as such is problematic as it may output a shell function name, an alias definition, a keyword, a builtin or a non-executable file path. On the other hand some path(s) output by which would not be executed by shell if you run the respective argument as such or as an argument for command. As an alternative for using the which script, a POSIX shell function using command -v could be something like

#!/bin/sh
# Argument $1 should be the basename of the command to be searched for.
# Outputs the absolute path of the command with that name found first in
# a directory listed in PATH environment variable, if the name is not
# shadowed by a special built-in utility, a regular built-in utility not
# associated with a PATH search, or a shell reserved word; otherwise
# outputs nothing and returns 1. If this function prints something for
# an argument, it is the path of the same executable as what 'command'
# would execute for the same argument.
executable() {
    if cmd=$(unset -f -- "$1"; command -v -- "$1") \
        && [ -z "${cmd##/*}" ] && [ -x "$cmd" ]; then
        printf '%s\n' "$cmd"
    else
        return 1
    fi
}

Disclaimer: Note that the script using command -v above does not find an executable whose name equals a name of a special built-in utility, a regular built-in utility not associated with a PATH search, or a shell reserved word. It might not find an executable either in case if there is non-executable file and executable file available in PATH search.

Upvotes: 1

Tom Hale
Tom Hale

Reputation: 46963

The following works in both bash and zsh and avoids both functions and aliases.

It returns 1 if the binary is not found.

bin_path () {
        if [[ -n ${ZSH_VERSION:-} ]]; then
                builtin whence -cp "$1" 2> /dev/null
        else
                builtin type -P "$1"
        fi
}

Upvotes: -1

Jimmix
Jimmix

Reputation: 6526

A function_command_exists for checking if a command exists:

#!/bin/sh

set -eu

function_command_exists() {
    local command="$1"
    local IFS=":" # paths are delimited with a colon in $PATH

    # iterate over dir paths having executables
    for search_dir in $PATH
    do
        # seek only in dir (excluding subdirs) for a file with an exact (case sensitive) name
        found_path="$(find "$search_dir" -maxdepth 1 -name "$command" -type f 2>/dev/null)"

        # (positive) if a path to a command was found and it was executable
        test -n "$found_path" && \
        test -x "$found_path" && \
            return 0
    done
    
    # (negative) if a path to an executable of a command was not found
    return 1
}

# example usage
echo "example 1";

command="ls"
if function_command_exists "$command"; then
    echo "Command: "\'$command\'" exists"
else
    echo "Command: "\'$command\'" does not exist"
fi

command="notpresent"
if function_command_exists "$command"; then
    echo "Command: "\'$command\'" exists"
else
    echo "Command: "\'$command\'" does not exist"
fi

echo "example 2";

command="ls"
function_command_exists "$command" && echo "Command: "\'$command\'" exists"

command="notpresent"
function_command_exists "$command" && echo "Command: "\'$command\'" does not exist"

echo "End of the script"

output:

example 1
Command: 'ls' exists
Command: 'notpresent' does not exist
example 2
Command: 'ls' exists
End of the script

Note that even the set -eu that turns -e option for the script was used the script was executed to the last line "End of the script"

There is no Command: 'notpresent' does not exist in the example 2 because of the && operator so the execution of echo "Command: "\'$command\'" does not exist" is skipped but the execution of the script continues till the end.

Note that the function_command_exists does not check if you have a right to execute the command. This needs to be done separately.

Solution with command -v <command-to-check>

#!/bin/sh
set -eu;

# check if a command exists (Yes)
command -v echo > /dev/null && status="$?" || status="$?"
if [ "${status}" = 127 ]; then
   echo "<handle not found 1>"
fi

# check if a command exists (No)
command -v command-that-does-not-exists > /dev/null && status="$?" || status="$?"
if [ "${status}" = 127 ]; then
   echo "<handle not found 2>"
fi

produces:

<handle not found 2>

because echo was found at the first example.

Solution with running a command and handling errors including command not found.

#!/bin/sh

set -eu;

# check if a command exists (No)
command -v command-that-does-not-exist > /dev/null && status="$?" || status="$?"
if [ "${status}" = 127 ]; then
   echo "<handle not found 2>"
fi

# run command and handle errors (no problem expected, echo exist)
echo "three" && status="$?" || status="$?"
if [ "${status}" = 127 ]; then
   echo "<handle not found 3>"

elif [ "${status}" -ne 0 ]; then
   echo "<handle other error 3>"
fi

# run command and handle errors (<handle not found 4> expected)
command-that-does-not-exist && status="$?" || status="$?"
if [ "${status}" = 127 ]; then
   echo "<handle not found 4>"

elif [ "${status}" -ne 0 ]; then
   echo "<handle other error 4>"
fi

# run command and handle errors (command exists but <handle other error 5> expected)
ls non-existing-path && status="$?" || status="$?"
if [ "${status}" = 127 ]; then
   echo "<handle not found 5>"

elif [ "${status}" -ne 0 ]; then
   echo "<handle other error 5>"
fi

produces:

<handle not found 2>
three
./function_command_exists.sh: 34: ./function_command_exists.sh: command-that-does-not-exist: not found
<handle not found 4>
ls: cannot access 'non-existing-path': No such file or directory
<handle other error 5>

Upvotes: -1

singpolyma
singpolyma

Reputation: 11251

command -v is a POSIX specified command that does what which does.

It is defined to to return >0 when the command is not found or an error occurs.

Upvotes: 37

Andy White
Andy White

Reputation: 88385

You could read the stdout/stderr of "which" into a variable or an array (using backticks) rather than checking for an exit code.

If the system does not have a "which" or "where" command, you could also grab the contents of the $PATH variable, then loop over all the directories and search for the given executable. That's essentially what which does (although it might use some caching/optimization of $PATH results).

Upvotes: 3

Related Questions