which Command in Linux: Complete Guide to Locating Executable Files and Parameters

which command in Linux and it perimeters

which command in Linux and it perimeters

Every time I’ve debugged a “wrong version of a tool is running” problem — and there have been many, especially with Python and Node version managers in the mix — which has been the first command I reach for. It answers a deceptively simple question: when I type a command name, exactly which file on disk is my shell actually going to execute? This guide covers which in full: how it searches, its parameters, its internals, and where it can mislead you if you don’t understand how your shell resolves commands in the first place.

What Is the which Command?

which searches the directories listed in your PATH environment variable, in order, and reports the full path to the first executable file matching the given command name. It’s the tool you use to answer “if I run python3 right now, which binary am I actually invoking?”

Basic Syntax

which [OPTIONS] COMMAND_NAME...

A Basic Example

$ which ls
/usr/bin/ls

This tells you that when you type ls, the shell (assuming no alias or shell function intercepts it first — more on that caveat below) will execute /usr/bin/ls.

Checking Multiple Commands at Once

$ which ls cat grep
/usr/bin/ls
/usr/bin/cat
/usr/bin/grep

Full Parameter Reference

which‘s option set is intentionally small — it’s a focused, single-purpose tool:

OptionDescription
-aPrint all matching executables in PATH, not just the first one found
-iSkip alias/function checks (behavior depends on shell integration; see below)
-sSilent mode — no output, only sets the exit status
-vPrint version information
--skip-dotSkip directories in PATH that begin with a dot (relative paths)
--skip-tildeSkip directories in PATH that begin with ~
--show-dotShow a ./ command if found in a dot-prefixed PATH directory rather than skipping it
--show-tildeDon’t expand a leading ~ when displaying results (has no effect for root)

Note: which implementations vary somewhat between distributions (some ship the simple debianutils version, others ship a more feature-rich version) — always check which --help on your system, since not every flag above is guaranteed present everywhere.

-a: Finding Every Match in PATH

This is the flag I use constantly when I suspect there are multiple versions of a tool installed and I want to see the full list, in PATH priority order:

$ which -a python3
/usr/bin/python3
/bin/python3

On a system with a version manager like pyenv, nvm, or rbenv installed, -a is what reveals whether a shim is shadowing the system binary — the first result is what actually runs, and any results after it are shadowed alternatives sitting further down PATH.

Checking Exit Status Silently

$ which nonexistcmd123
$ echo "exit code: $?"
exit code: 1

When a command isn’t found anywhere in PATH, which prints nothing and returns a non-zero exit status. This makes it directly usable in conditionals:

if which docker > /dev/null 2>&1; then
    echo "Docker is installed"
else
    echo "Docker is not installed"
fi

How which Works Internally

which performs a straightforward algorithm:

  1. Read the PATH environment variable, which is a colon-separated list of directories (e.g. /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/bin).
  2. Split PATH into individual directory entries, preserving order.
  3. For each directory, check whether a file matching the requested command name exists and has the executable permission bit set for the current user.
  4. Report the first match found (or all matches, with -a), following PATH order exactly — because that order is exactly what your shell itself uses when resolving a bare command name.

This is why which‘s answer is meaningful: it mirrors the same resolution logic your shell uses internally (via the execvp() family of functions, which also walks PATH looking for the first executable match).

The Critical Caveat: Aliases, Functions, and Builtins

This is the single most important thing to understand about which: it only searches PATH for external executable files. It does not know about, and cannot detect:

For a fully accurate picture of what will actually execute — including aliases, functions, builtins, and keywords — the shell’s own built-in type command is more reliable than which:

$ type ls
ls is aliased to `ls --color=auto'

$ type cd
cd is a shell builtin

$ type python3
python3 is /usr/bin/python3

I treat which as the right tool specifically for “what’s the actual file on disk,” and type as the right tool for “what will genuinely execute when I type this,” since they can legitimately give different answers.

Real-World Use Cases

1. Verifying a Tool Is Installed Before Using It in a Script

#!/bin/bash
if ! which jq > /dev/null 2>&1; then
    echo "Error: jq is required but not installed." >&2
    exit 1
fi

2. Diagnosing “Wrong Version” Problems

$ which -a node
/home/user/.nvm/versions/node/v20.11.0/bin/node
/usr/bin/node

If you expected the system node at /usr/bin/node to run but a shell script or CI job is picking up an nvm-managed version instead, -a immediately shows you why — the nvm shim is earlier in PATH.

3. Confirming a Package Manager Installed a Binary in the Expected Location

$ sudo apt install ripgrep -y
$ which rg
/usr/bin/rg

4. Building Portable Installation Scripts

#!/bin/bash
for tool in curl wget git; do
    if which "$tool" > /dev/null 2>&1; then
        echo "$tool: found at $(which "$tool")"
    else
        echo "$tool: NOT FOUND"
    fi
done

Shell Scripting and Automation

A practical dependency-check pattern I reuse across deployment scripts:

#!/bin/bash
# check_deps.sh - verify required tools exist before running a deployment
set -euo pipefail

REQUIRED_TOOLS=(docker docker-compose git rsync jq)
MISSING=()

for tool in "${REQUIRED_TOOLS[@]}"; do
    if ! which "$tool" > /dev/null 2>&1; then
        MISSING+=("$tool")
    fi
done

if [[ ${#MISSING[@]} -gt 0 ]]; then
    echo "Missing required tools: ${MISSING[*]}" >&2
    exit 1
fi

echo "All required tools present."

This kind of preflight check is standard practice at the top of any deployment or CI script — failing fast with a clear message beats a cryptic “command not found” three steps into a longer process.

which vs Related Commands

CommandPurpose
whichLocates the executable file that would run from PATH, external commands only
typeReports how a name would be interpreted by the shell — alias, function, builtin, or external file
command -vPOSIX-standard equivalent to type, more portable across shells; commonly preferred in portable scripts
whereisLocates binary, source, and manual page files, using a fixed set of standard system directories rather than PATH
locateSearches a prebuilt filesystem-wide database by filename, not limited to executables or PATH
findGeneral-purpose, real-time filesystem search with rich filtering, but slower since it walks the filesystem live

For portable shell scripts, command -v tool is frequently recommended over which tool, because command -v is a POSIX shell builtin available consistently across bash, dash, zsh, and others without depending on an external which binary being installed at all (some minimal container images genuinely don’t ship which).

if command -v jq > /dev/null 2>&1; then
    echo "jq available"
fi

Troubleshooting Common Issues

Problem: which reports a path, but running the command behaves differently than expected. Check for an alias or function shadowing it with type command_name — this is the single most common cause of “which says X but running it does Y.”

Problem: which somecommand returns nothing, but the command works fine when I type it. It’s almost certainly a shell builtin or function, not a PATH-resolved external file. Confirm with type somecommand.

Problem: which finds an old/wrong version of a tool after installing a new one. Your PATH likely lists an old install location before the new one. Check order with echo $PATH and which -a toolname, then adjust your PATH ordering in your shell profile (.bashrc, .zshrc, /etc/environment) so the intended directory comes first.

Problem: Script works interactively but fails with “command not found” when run via cron or a different shell context. Cron jobs and non-interactive shells often have a much more minimal PATH than your interactive login shell. Use which toolname interactively to find the full path, then hardcode that full path (or explicitly set PATH at the top of the cron script) rather than relying on the bare command name.

Performance Optimization

which is essentially instantaneous — it performs a handful of stat()-equivalent filesystem lookups across the directories listed in PATH, which is typically fewer than 10-15 entries. There’s no meaningful performance tuning needed; if you’re calling which in a tight loop thousands of times (e.g. checking the same tool repeatedly across many script invocations), caching the result in a variable once is a reasonable micro-optimization, but this is rarely a real bottleneck in practice.

Security Implications

Because which‘s answer depends entirely on PATH and the order of directories within it, it’s directly relevant to a well-known class of security issue: PATH hijacking. If a directory earlier in your PATH is writable by an untrusted user (for example, if . — the current directory — appears early in PATH, or a shared/world-writable directory is included), an attacker can place a malicious executable with a common name (like ls or sudo) that gets found and run instead of the real system binary.

Using which -a periodically to audit what’s actually resolvable, and inspecting echo $PATH for unexpected or writable-by-others directories, is a reasonable habit — especially on shared or multi-user systems. Never include a relative or current-directory entry (.) in PATH for a privileged account; this remains one of the classic, still-relevant Unix privilege-escalation vectors.

Compatibility Across Distributions

which behavior differs slightly between distributions because there isn’t one single canonical implementation. Debian and Ubuntu ship a minimal version from the debianutils package; other distributions may ship the GNU which package with a slightly richer feature set (including some alias-detection behavior when properly configured with shell integration). Because of this inconsistency, POSIX-portable scripts commonly prefer command -v over which specifically to avoid depending on implementation-specific behavior. If you’re writing something that must behave identically on Ubuntu, Fedora, Alpine (BusyBox which), and macOS, test explicitly — BusyBox’s minimal which in particular lacks most of the flags described above.

Best Practices

Summary

which answers one specific, well-defined question — which file on disk would run for a given command name, based on PATH — and it’s genuinely useful for that purpose in scripting dependency checks and diagnosing version conflicts. Its blind spot is equally well-defined: it knows nothing about aliases, shell functions, or builtins, which is where type and command -v take over. Understanding that boundary is what turns which from “the tool I vaguely associate with finding programs” into a precise diagnostic instrument.

References

Exit mobile version