2

I'm making a function in my shell script that looks like this:

getcmds()
{
    # find all executable files/symlinks in $(searchdirs) that start with 'upvoter-'
    searchdirs | xargs -i find {} -name 'upvoter-*' -type f -or -type l -maxdepth 1 -perm +111
}

When I ran this function from another location in my script, I was getting a whole bunch of output that did not start with upvoter-. I eventually narrowed it down to the fact that find was interpreting my query like this:

find everything that's a file and starts with upvoter-, OR is an executable symlink at the top-level directory

I looked on the find man page to try to find a remedy for my problem. I noticed find supported parentheses, so I tried this:

find {} -name 'upvoter-*' -type f -or \( -type l \) -maxdepth 1 -perm +111

and this:

find {} -name 'upvoter-*' \( -type f -or -type l \) -maxdepth 1 -perm +111

Unfortunately, neither of them worked. What can I do to fix this?

Thanks.

mklement0
  • 382,024
  • 64
  • 607
  • 775
James Ko
  • 32,215
  • 30
  • 128
  • 239
  • 1
    The last command works as expected for me. – muru Feb 20 '16 at 06:32
  • 1
    As an aside: The description of the broken logic - "OR is an executable symlink _at the top-level directory_" - is not quite correct, because the top-level test (`-maxdepth 1`) is technically an _option_, which means it is not positional and invariably applies to the _entire_ command. – mklement0 Feb 20 '16 at 17:25

2 Answers2

5

To complement Jonathan Leffler's helpful answer and @muru's helfpul comments:

  • The last command command in your question should work in terms of precedence:

    • find implicitly combines tests such as -type and actions such as -print with -and (logical AND; the POSIX-compliant form is -a ).
      • By contrast, -maxdepth is an option, which is not positional and always applies to the whole command.
      • For an overview of find terminology and concepts, see this answer of mine.
    • Your last solution attempt correctly uses parentheses to change the implied evaluation precedence to achieve the desired logic (-or has lower precedence than -and).
      • By contrast, the parentheses in your first solution attempt, -or \( -type l \), have no effect on precedence at all, because they enclose a single test only (whereas precedence by definition only matters for multiple operands).
      • Note how ( and ) are quoted as \( and \) to protect them from interpretation by the shell; '(' and ')' would work too.
  • However, the -perm test likely does NOT do what you want it to: as written, it tests symlinks themselves for being executable, as opposed to their target. Given that symlinks are always marked as executable, irrespective of whether their target is, you'll end up matching any symlink, even if it doesn't refer to an executable file.

    • To fix that, use the -L option, which makes find apply tests such as -perm to the target of a symlink.
      • Note, however, that there's a side effect: with -L, when find encounters a symlink to a directory, it descends into that directory (i.e., it processes the symlink's target directory as well), which by default does not happen.

With the above in mind, somewhat ironically, the need for parentheses goes away altogether, because \( -type f -or -type l \) can be replaced with just -type f, given that -L now ensures that a symlink target's type is tested:

Note:
- I've removed the {} from the following commands to focus on the find command only. Without a filename argument, GNU find implicitly operates on the current directory (. is implied); BSD find, by contrast requires a filename argument.
- Also, /111 rather than +111 is used as the permissions mask, because the + syntax was deprecated a while ago and was actually removed in GNU find 4.5.12.
- Finally, -maxdepth 1 was placed before the first positional element (test -name) not only for greater conceptual clarity (as stated -maxdepth, because it is an option, always applies to the entire command), but also to suppress a warning that recent versions of GNU find would otherwise issue.

find -L -maxdepth 1 -name 'upvoter-*' -type f -perm /111

Additional thoughts about your command:

  • -perm /111 (-perm +111) applies any-specified-bit-set logic i.e., it tests whether the executable bit is set for any security principal; the symbolic-mode equivalent is -perm /a=x.
    • As stated, the + syntax was deprecated a while ago and removed in GNU find v4.5.12.
    • BSD find, by contrast, continues to only support +, which unfortunately means that no single command with this feature will work with both implementations with GNU find 4.5.12 or higher.
    • In any event, this feature is nonstandard (not POSIX-compliant).
  • If instead you wanted to match only files where all execution bits are set, use prefix -: -perm -111 or -perm -a=x
  • Alternatively, if you wanted to test whether files are executable by you, use the -executable test (a GNU find extension).
    • Also, -executable "takes into account access control lists and other permissions artefacts which the -perm test ignores." (from man find).

Finally, GNU xargs's -i option is deprecated; the manual recommends using -I {} instead.

Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
4

The terms in the condition of (GNU) find are (implicitly) connected by -and unless you override it with -or. So, your last find should work:

… |
xargs -i find {} -name 'upvoter-*' \( -type f -or -type l \) -maxdepth 1 -perm +111

Under the named directories, it will look for names starting upvoter- and files of type f or l (file or symlink) and to a maximum depth of 1 and with at least universal execute permission. It won't find files with 750 or 455 or some other oddball permissions, but it will find 755 or 711 or 511 or other similar permissions. With a symlink, it would be surprising if it did not look for the permissions on the file at the end of the symlink rather than the permissions of the symlink itself. There is an implicit -print at the end when no other action is specified.

Note that POSIX find only supports -a and -o for and and or respectively.

mklement0
  • 382,024
  • 64
  • 607
  • 775
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • 2
    I doubt `-perm` tests the symlink target's permissions without `-L`. (And of course, if `-L` were used, `-type l` would be unnecessary.) – muru Feb 20 '16 at 06:52
  • 2
    @muru is correct: without `-L`, `find` tests the permissions of _symlinks themselves_ rather than their targets'. Given that symlinks to _any_ target _themselves_ always have executable permissions, the OP's command will match _any_ symlink. I suspect what happened was: The inclusion of unwanted matches led the OP to the mistaken conclusion that he still had an _evaluation precedence_ problem, even though his last solution attempt did fix _that_ aspect. As is implied by muru's comment: Adding `-L` fixes the remaining problem (and obviates the need for `-or -type l` and therefore parentheses). – mklement0 Feb 20 '16 at 17:03