2

I'm looking to find all executable files that are NOT in my $PATH.

Currently I'm doing this

find / \( -path "/opt" -prune -o -path "/var" -prune -o -path "/bin" -prune -o -path "/sbin" -prune -o -path "/usr" -prune -o -path "/opt" \) -o -type f -executable -exec file {} \;

I feel like there is a better way, I tried using a for loop with IFS=: to separate out the different parts of PATH but couldn't get it to work.

Edit: I should have specified I don't want to use a script for this.

3
6

Assuming GNU find and the bash shell (as is used in the question), this is a short script that would accomplish what you're trying to do:

#!/bin/bash

IFS=:
set -f

args=( -false )
for dirpath in $PATH; do
        args+=( -o -path "$dirpath" )
done

find / \( \( "${args[@]}" \) -o \
          \( -type d \( ! -executable -o ! -readable \) \) \) -prune -o \
    -type f -executable -exec file {} +

This first creates the array args, consisting of dynamically constructed arguments to find. It does this by splitting the value of $PATH on colons, the value that we've given to the IFS variable. The splitting is happening when we use $PATH unquoted in the loop header.

Ordinarily, the shell would invoke filename globbing on each of the words generated from the splitting of $PATH, but I'm using set -f to turn off filename globbing, just in case any of the directory paths in $PATH contains globbing characters (these would still be problematic as the -path operand of find would interpret them as patterns).

If my PATH variable contains the string

/usr/bin:/bin:/usr/sbin:/sbin:/usr/X11R6/bin:/usr/local/bin:/usr/local/sbin

then args will be the following list (each line here is a separate element in the array, this is not really a set of strings with newline characters in-between them):

-false
-o
-path
/usr/bin
-o
-path
/bin
-o
-path
/usr/sbin
-o
-path
/sbin
-o
-path
/usr/X11R6/bin
-o
-path
/usr/local/bin
-o
-path
/usr/local/sbin

This list is slotted into the find command invocation, in parentheses. There is no need to repeat -prune for each and every directory, as you could just use it once as I have above.

I've opted for pruning any non-executable or non-readable directory. This ought to get rid of a lot of permission errors for directories that you can't access or list the contents of. Would you want to simplify the find command by removing this bit, use

find / \( "${args[@]}" \) -prune -o \
    -type f -executable -exec file {} +

Also, I'm running file on the found pathnames in batches, rather than once per pathname.

12
  • Note that executable but not readable files would not be reported. – Stéphane Chazelas Feb 24 at 17:16
  • Note that it assumes that $PATH components don't contain wildcard characters (unlikely to be a problem in practice). – Stéphane Chazelas Feb 24 at 17:17
  • @StéphaneChazelas This is why I used set -f. Also, I should add the assumption about not wanting to see non-readable executables. – Kusalananda Feb 24 at 17:18
  • With or without -f, with a PATH='*'; -path '*' would match any path. – Stéphane Chazelas Feb 24 at 17:18
  • Note that the second -executable is redundant. – Stéphane Chazelas Feb 24 at 17:19
4

With zsh, you could do:

set -o extendedglob
LC_ALL=C find / -regextype egrep \
  -regex ${(j[|])${(u)path:P}//(#m)[][.\$^*()+{}\\|.]/\\$MATCH} -prune -o \
  -type f -executable -exec file {} +
  • $path is a special array tied to the $PATH env var.
  • $path:P gets the realpath (absolute, canonical, without symlinks) of each member
  • ${(u)array} removes duplicate (here after canonicalisation)
  • ${array//(#m)pattern/\\$MATCH} prepends a \ before anything that matches the pattern (here all the egrep regexp operators)
  • ${(j[|])array} joins the elements of the array with | to obtain a /dir|/dir\.2|... regexp.

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.