'(How) is it possible to add the x permission only to those who already have the r permission using standard shell commands and/or Unix utils?

I want to write a shell script that will operate on multiple files, and (among other things) will make them executable.

The problem: the files have different permissions – in particular, some are readable by all users and some are only readable by the owner – and I want to add the execution permission exactly for those who've got the read permission (that is, if all can read the file, all should be able to execute it, but if only the owner can read it, then only the owner should be able to execute it).

I know how to achieve this by writing a small program in any number of programming languages, and using a few different approaches (from a naive, straight-forward condition on the permissions for each agent to a tricky use of masking and shifting of the permission bits), but I'd like to achieve this in a "pure" shell script, using nothing but standard shell commands and Unix utils.

UPDATE: There are currently two answers, both of which suggesting interesting techniques (using zsh's glob qualifiers, and using find) which seem closely related to what I need, but slightly missing the point of the question, as the challenge I'm facing is not finding which files to work on: the user of the script will tell it which files to work on. Rather, the challenge is, given a specific file, to decide, for example, whether to call chmod o+x on it or not, based on whether the o+r permission is already set.



Solution 1:[1]

The answers provided so far present useful ideas, but, as none of them answers the question as asked, I am hereby posting an answer to my own question.

This answer presents three different solutions, each based on a different approach suggested in the previously posted answers; there are other ways too which are not covered (I'll only mention, without showing the actual code, the trick of getting the permissions as an octal number, and then – using some mechanism of performing arithmetic/bit-wise calculations – anding it with 444 to get the r bits, shifting right 2 bits to get the corresponding x bits, oring with the original permissions, and setting the result as the new permissions).

All of the solutions below assume working on the file "$FILE".

Solution 1 – ifing on the pattern of the current permissions as text

This is perhaps the most straightforward approach:

permissions=$(stat -f %SA "$FILE")
if [[ $permissions =~ .r........ ]]; then chmod u+x "$FILE"; fi
if [[ $permissions =~ ....r..... ]]; then chmod g+x "$FILE"; fi
if [[ $permissions =~ .......r.. ]]; then chmod o+x "$FILE"; fi

Solution 2 – using find, taking advantage of the -perm and -exec options

This approach uses find not to find multiple files, but rather to either find the given file (in case it has a certain permission) or to not find anything (otherwise), and to set the matching desired permission in case the file is found (and do nothing otherwise):

find "$FILE" -perm -u=r -exec chmod u+x {} \;
find "$FILE" -perm -g=r -exec chmod g+x {} \;
find "$FILE" -perm -o=r -exec chmod o+x {} \;

The three cases can easily be combined into a single for loop:

for w in u g o; find "$FILE" -perm -$w=r -exec chmod $w+x {} \;; done

Solution 3 (zsh only) – using glob qualifiers

This approach runs each of the relevant chmod commands on "$FILE" only in case it already matches the corresponding permission. Note || : is added to avoid errors in case there's no match, i.e. if the file does not have the corresponding permission (also note zsh still emits a no matches found warning in this case):

chmod u+x "$FILE"(r) || :
chmod g+x "$FILE"(A) || :
chmod o+x "$FILE"(R) || :

Solution 2:[2]

If using zsh as your shell, you can take advantage of glob qualifiers to limit matches to files with and without specific permission bits:

# chmod all plain files (.) that are world-readable (R) but not world-executable (^X)
zsh$ chmod o+x *(.R^X)
# files that are owner-readable but not owner-executable
zsh$ chmod u+x *(.r^x)
# And groups
zsh$ chmod g+x *(.A^E)

You can also do things like *(.X^R) to get files that are world-executable but not world-readable, or other combinations of having a permission (Before the ^) and not having it (After the ^). There's also a ffspec qualifier for even more control over permission bit matching; see the documentation for details.

To get all files in a directory tree instead of just the current directory, use **/* instead of *.

Solution 3:[3]

This is probably not the most efficient way to do it, but I really like the find command for its intuitive API.

The following command finds all files and directories in the current directory (the .) that are readable by the group or by all users:

find . -perm /go=r

And this command finds all files and directories that are readable by the owner but NOT readable by the group or by all users:

find . -perm /u=r ! -perm /go=r

Once you find the files you want to process, you can add an -exec argument to execute a command on each of the matches. For example, the following command would set owner's executable bit on all files and directories that are readable by the owner only:

find . -perm /u=r ! -perm /go=r -exec chmod u+x {} \;

The end of the line is quite weird but enter it exactly as shown and it should work.

And this command would set the executable bits for "group" and "others" on all files and directories that are readable by group and others:

find . -perm /go=r -exec chmod go+x {} \;

All these commands search recursively in the current directory. You can specify an arbitrary other directory by replacing . with another path.

You can also specify how deep the searching should go with -maxdepth N.

Edit: In case you want to process a single specified file, you can use the following script. It checks file's permissions by using the stat command which returns the permissions in format similar to what ls -l displays, for example -rw-r--r--.

myfile="/tmp/abc"

permissions="$(stat -c %A $myfile)"
if [[ "$permissions" =~ ^....r.....$ ]] || [[ "$permissions" =~ ^.......r..$ ]]; then
    # file is readable by group and/or others
    # set executable bit for owner, group and others
    chmod a+x "$myfile"
elif [[ "$permissions" =~ ^.r........$ ]]; then
    # file is note readable by group or others
    # Set executable bit for owner only
    chmod u+x "$myfile"
fi

This script works in bash and zsh.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Tom
Solution 2 Shawn
Solution 3