1. Introduction

Most major operating systems have the concept of a PATH variable. Usually part of the core configuration, PATH most often holds colon-separated paths to (directories with) system and user binaries, libraries, and other similar files with executable code. Considering this, the Linux $PATH variable is critical to the functionality of the shell.

In this tutorial, we explore ways to list all executables from the $PATH variable in Linux. First, we look at standard universal solutions to generate the list. Next, we check the native method to get executables from $PATH in one of the most common Linux shells. After that, we explore another major shell and its toolset with regard to our aims.

We tested the code in this tutorial on Debian 11 (Bullseye) with GNU Bash 5.1.4. It should work in most POSIX-compliant environments unless otherwise specified.

2. Universal POSIX

As usual, the POSIX standard provides means to deal with core operating system (OS) functionality.

Importantly, since we rely on special environment variables, our solutions below are within {} curly braces, limiting changes to the subshell they create. In fact, this way, we can more easily pipe the output from any of the examples through the classic sort.

2.1. Using Basic Tooling

Indeed, we can use only the most basic of POSIX commands to get the executable file list from $PATH:

$ {
  set -f;
  IFS=:;
  for p in $PATH; do
    set +f;
    [ -n "$p" ] || p=.;
    for f in "$p"/.[!.]* "$p"/..?* "$p"/*; do
      [ -f "$f" ] && [ -x "$f" ] && printf '%s\n' "${f##*/}";
    done;
  done;
}
cpan
cpan-mirrors
adduser
agetty
anacron
[...]

Let’s dissect this seemingly complex combination.

First, the f flag of set controls globbing:

  • set -f – disable globbing
  • set +f – enable globbing

Next, we change the value of $IFS to a : colon, since that’s the $PATH separator. This causes the for loop to store each path in $p, without considering globbing.

Within the loop, we first reenable globbing. After that, we use the || shorthand to check whether the current $PATH component $p is empty. If so, we assign a . period to it to avoid issues.

The nested for loop goes through and stores in $f any hidden files of the current path, as well as /* all other files (since globbing is enabled at this point) by adding the respective suffix. In each case, *the && shorthand checks whether any found [-f]iles in $f are e[-x]ecutable*. If they are, we use printf to output their file name parts only. Indeed, we can use $f in the printf statement to get full paths.

Most of the complexity comes from potentially problematic components in the $PATH values:

  • paths with \[?* wildcard or similar special globbing characters
  • empty paths
  • paths starting with dashes
  • files starting with . periods

Notably, these are common considerations for many manual methods.

2.2. Using find and sed

Upgrading to a bit more sophisticated POSIX tools, we can simplify the code for our task:

$ {
  IFS=:;
  set -f;
  find -L $PATH -maxdepth 1 -type f -perm -100 -print;
}
/usr/local/bin/cpan-mirrors
/usr/local/bin/cpan
/usr/bin/cpan
/bin/cpan
/usr/sbin/adduser
/sbin/adduser
[...]

In this case, *we replace most processing with a find command that follows symbolic [-L]inks and goes to a -maxdepth of 1 to avoid descending into subdirectories*. While following those rules, the command [-print]s every object of the [f]ile -type that has the 100 executable [-perm]ission set.

Finally, we can optionally pipe the list through sed to [s]ubstitute the directory paths with an empty string, leaving only the file name parts:

$ {
  IFS=:;
  set -f;
  find -L $PATH -maxdepth 1 -type f -perm -100 -print;
} | sed 's!.*/!!'
cpan-mirrors
cpan
cpan
cpan
adduser
adduser

In this case, we can pipe to sort -u for removing duplicates. Now, let’s see a more crude, but even simpler POSIX solution.

2.3. Using ls

Since $PATH can contain any directories, it’s possible that their contents include more than just executable files. As long as extra files aren’t a problem when listing, we can use ls after assigning : colon to $IFS:

$ {
  IFS=:;
  ls -H $PATH;
}
apropos
apt
apt-cache
apt-cdrom
apt-config
[...]

Of course, we add the –dereference-command-line or -H flag to follow links.

Notably, the POSIX ls command doesn’t have the –almost-all or -A flag for showing dotfiles without the special . and .. directories. Because of this, we use the –all or -a combined with the standard grep:

$ {
  IFS=:;
  ls -aH $PATH | grep --fixed-strings --line-regexp --invert-match -e . -e ..;
}

Here, we use four grep flags:

  • –fixed-strings or -F – patterns are fixed strings, not a regular expression (regex)
  • –line-regexp or -x – expect an exact, not partial, match
  • –invert-match or -v – show only non-matching lines
  • –regexp or -e – supply several patterns

Thus, we get an overview of all $PATH files. For color-coded ls output, we can more simply distinguish the executables among them.

Yet, if we turn to shell-specific solutions, we can simplify further.

3. Bash compgen

The Bash compgen command can list all available [-a]liases, [-b]uiltins, [-c]ommands, functions, and [-k]eywords:

$ compgen -A function -abck
ls
.
:
[
alias
[...]

In fact, it’s a common method to implement shell auto-completion.

In our case, the output of compgen with its -c flag should mostly overlap with the output we need:

$ compgen -c
ls
if
then
else
elif
[...]

However, some commands in the resulting list come from the Bash shell and not $PATH. On the one hand, we can exclude everything but executables with grep:

$ grep --fixed-strings --invert-match -f <(compgen -A function -abk) <(compgen -c)
cpan-mirrors
cpan
xfs_quota
xfs_scrub
[...]

Here, we again use the -F and -v flags of grep. With the –file or -f flag and process substitution, *we get all [-c]ommands from executables by excluding other objects*. Critically, doing so may exclude overlapping names like printf, echo, and similar, which exist both as builtins or aliases and executable files.

We can again sort the output if we prefer.

4. Zsh

There are two main ways in which the Zsh shell can list executables from $PATH.

4.1. Bash Compatibility

The zsh shell has a Bash compatibility mode that includes a compgen implementation:

$ autoload bashcompinit
$ bashcompinit

First, we use the Zsh autoload builtin to define the bashcompinit() function. After that, we run bashcompinit() to define all Bash-emulating functions.

One of these functions is compgen(). Let’s see the definition of compgen() to verify it’s available:

$ which compgen
compgen() {
  local opts prefix suffix job OPTARG OPTIND ret=1
  local -a name res results jids
  local -A shortopts

  # words changes behavior: words[1] -> words[0]
  emulate -L sh
  setopt kshglob noshglob braceexpand nokshautoload

  shortopts=(
    a alias b builtin c command d directory e export f file
    g group j job k keyword u user v variable
  )

  while getopts "o:A:G:C:F:P:S:W:X:abcdefgjkuv" name; do
    case $name in
      [abcdefgjkuv]) OPTARG="${shortopts[$name]}" ;&
      A)
        case $OPTARG in
      alias) results+=( "${(k)aliases[@]}" ) ;;
      arrayvar) results+=( "${(k@)parameters[(R)array*]}" ) ;;
      binding) results+=( "${(k)widgets[@]}" ) ;;
      builtin) results+=( "${(k)builtins[@]}" "${(k)dis_builtins[@]}" ) ;;
      command)
        results+=(
          "${(k)commands[@]}" "${(k)aliases[@]}" "${(k)builtins[@]}"
          "${(k)functions[@]}" "${(k)reswords[@]}"
        )
      ;;
[...]
  print -l -r -- "$prefix${^results[@]}$suffix"
}

Now, we can use compgen as we did earlier:

$ grep --fixed-strings --invert-match -f <(compgen -A function -abk) <(compgen -c)
smbd
sed
comm
seq
apt
[...]

Of course, zsh also has a designated builtin way to get the same information.

4.2. Zsh-Specific Commands

Zsh provides the whence builtin, which is like a combination between the Bash which and compgen commands.

*Let’s use whence for a [-p]ath search with a pattern [-m]atch for * any executable file*:

$ whence -pm '*'
/usr/bin/X11
/usr/bin/[
/usr/sbin/addgroup
/usr/sbin/agetty
/usr/sbin/anacron
[...]

Actually, we can also use the Zsh print builtin for the same purpose:

$ print -rC1 -- $commands
/usr/sbin/smbd
/usr/bin/comm
/usr/bin/sed
/usr/sbin/fsck.vfat
/usr/bin/seq
[...]

Here, we print all values of the special $commands associative array in 1 [-C]olumn, ignoring any [-e]scaping.

Critically, any executables from more than one $PATH component will only appear once. In all cases above, we can again use sed to remove the directory path prefixes.

However, print can manage this alone by printing the command names (keys) of the $commands associative array instead of the values, i.e., paths:

$ print -rC1 -- ${(ko)commands}
ab
addgroup
agetty
anacron
apt
[...]

In addition, the output of this command is sorted due to the associative array order.

5. Summary

In this article, we looked at many ways to output the executables within the $PATH environment variable directories.

In conclusion, both Bash and Zsh have builtin methods to provide us with the executable files from $PATH, but we can also turn to POSIX for a more universal solution.