1

I'm having trouble quoting a filename that's eventually passed to chmod. The script was originally written for SSH's StrictModes and authorized_keys, but I had to expand it due to a bad UMASK that caused some filesystem discomfort. The script is below, and its producing boatloads of:

chmod: /Users/<user>/Library/Application: No such file or directory
chmod: Support/TextMate/Managed/Bundles/HTML.tmbundle/Macros/Delete: No such file or directory
chmod: whitespace: No such file or directory
chmod: between: No such file or directory
chmod: tags.plist: No such file or directory
...
chmod: /Users/<user>/Library/Caches/com.operasoftware.Opera/Media: No such file or directory
chmod: Cache/index: No such file or directory
cut: stdin: Illegal byte sequence
...

I've tried a few workarounds, but none of them have helped. I tried double quoting like ""$file"", but the issue persisted. I also tried the back ticks (that are discouraged in this case), but the issue persisted.

The closest I got to actually quoting was the dysfunctional "\"""$file""\"". But then chmod complained the filename (with quotes) was not a real file:

$ sudo ~/fix-perms.sh
chmod: "/Users/Shared/.localized": No such file or directory
chmod: "/Users/<user>/.CFUserTextEncoding": No such file or directory
chmod: "/Users/<user>/.lesshst": No such file or directory

How do I quote the filename that comes out of find that gets passed onto chmod? Or how do I get chmod to take the filename as a single argument?


$ cat ~/fix-perms.sh 
#!/bin/bash

# Directories
find /Users/* -type d -exec chmod 0700 {} \;
find /Users/Shared -type d -exec chmod 0777 {} \;

# Files
for file in `find /Users/* -type f`;
do
    if [ `file "$file" | cut -d":" -f 2 | grep -i -c executable` -eq 0 ];
    then
        `chmod 0600 "$file"`
    else
        `chmod 0700 "$file"`
    fi
done

for user in `ls -A /Users`;
do
    if [ -e "/Users/$user/.ssh/authorized_keys" ];
    then
        chmod 0600 "/Users/${user}/.ssh/authorized_keys"
    fi
done
  • 1
    Are you invested in bash scripting or are there other options you would accept? If you're bash, I might be able to rework things to be find | xargs sudo chmod instead of unrolling everything in a for loop where spaces are causing you grief to iterate over the variable. – bmike Aug 26 '15 at 14:17
3

fwiw, find [options] -print0, when used in conjunction with (piped to) xargs -0 [options], handles filenames with spaces, without messing with for loops or IFS:

find /Users ! -path '/Users/Shared*' -type f -print0 | xargs -0 -I {} \
    bash -c 'if [[ "$(file -b --mime-type -- "{}")" = "application/x-mach-binary" ]]; then
                 chmod 700 "{}"
             else
                 chmod 600 "{}"
             fi'
  • 1
    thanks @patrix, that's better than what I would have come up with. – mtklr Aug 30 '15 at 18:27
  • Yeah, there's so many ways to do things with the shell.... (I find it frustrating at times). – user83961 Aug 30 '15 at 20:59
  • Slight alternative: find /path -type d -maxdepth 1 ! -perm 0700 -exec chmod 0755 {} \; The -maxdepth # is arbitrary, sometimes you don't need or want to go too far down tree. – user76598 Aug 30 '15 at 21:24
1

I was able to resolve the issue by changing IFS to IFS=$(echo -en "\n\b") and not quoting the filename. IFS is Internal Field Separator, it is used (among others) for word splitting after shell expansions, and it includes a space by default.

I found the IFS trick at nixCraft's BASH Shell: For Loop File Names With Spaces.

$ cat fix-perms.sh
#!/bin/bash

SAVED_IFS=$IFS
IFS=$(echo -en "\n\b")

# Directories
chmod 0755 /Users
find /Users/* -type d -exec chmod 0700 {} \;
find /Users/Shared -type d -exec chmod 0777 {} \;

# Files
for file in `find /Users/* -type f`;
do
    if [ `file "$file" | cut -d":" -f 2 | grep -i -c executable` -eq 0 ];
    then
        chmod 0600 "$file"
    else
        chmod 0700 "$file"
    fi
done

for user in `ls -A /Users`;
do
    if [ -e "/Users/$user/.ssh/authorized_keys" ];
    then
        chmod 0600 "/Users/${user}/.ssh/authorized_keys"
    fi
done

IFS=$SAVED_IFS
  • I can't really recommend this method, since it'll fail for filenames with linefeeds (or bells? why's that there...). Suppose a user created a file named a\n*\nb -- it'll be parsed into three words: "/Users/something/a", *, and "b"; then the * will get expanded into a list of files and subdirectories in the directory you're in, and the script will then change their permissions (maybe wrong, since the subdirectories aren't executable). But your users probably won't do this. Probably... – Gordon Davisson Aug 31 '15 at 2:24
  • @Gordon - I did not look up the shell meta characters, but I meant to. Thanks for telling me about the bell... Why in the world is \b there, but not \t? Naively, I thought \b was either the tab or backspace. I kinda understood the backspace, but the bell makes no sense to me. – user83961 Aug 31 '15 at 5:38
  • @Gordon - apparently, the Bash page uses an incorrect assumption for OS X. On OS X under HFS+, filenames can contain \0; see Mac OS X Lion: What are invalid characters for a file name? on Super User. A leading \0 is probably a good way for a virus to hide itself... – user83961 Aug 31 '15 at 5:57
0

find can handle spaces in file names. Your for loops are causing the problems. You can put your logic in multiple find commands. Not every directory in a user's home folder should be accessible to only them self. Public and Sites come to mind. You can fix them separately in another find command.

find /Users/* ! -path '/Users/Shared*'  -type d ! \( -name Public -o -name Sites \) -exec chmod 0700 {} +

The command excludes the Shared directory along with Public and Sites directories then changes the permissions on the remaining directories.

You can use + in your find statements to have find act as if it were xargs.

find /Users/Shared -type d -exec chmod 0777 {} +

Assuming that the executable files in a user's home folder are application bundles then you could do this.

find /Users/* ! -path '/Users/Shared*' type f ! perm -755 -exec chmod 0600 {} +
  • Thanks fd0. "find can handle spaces in file names. Your for loops are causing the problems." - OK, good, I'll take your word for it. Now what? You don't tell really me how to fix it. (Don't worry about Shared or $HOME/Public. That's icing on the cake. The primary concern is permissions on home directories). – user83961 Aug 26 '15 at 23:05
0

mtklr's (and Patrix') solution will work, but I find it's simpler to use a while read loop when dealing with a file list from find ... -print0:

find /Users '!' -path '/Users/Shared*' -type f -print0 |
    while IFS= read -d '' -r file; do
        if file "$file" | grep -iq ": .*executable"; then
            chmod 700 "$file"
        else
            chmod 600 "$file"
        fi
    done

read -d '' uses nulls as a delimiter. I also used -r to prevent it from trying to parse escapes, and IFS= to prevent it trimming whitespace at the end of the filename (and since it's a prefix to the read command, it only applies to that one and you don't have to set it back afterward).

BTW, I also used if ... | grep -q ... -- the -q tells grep not to output anything, but its exit status will only succeed if it found at least one match. And since if just tests the exit status of the command in it, that's all that's needed.

BTW, this is mostly based on BashFAQ #20: How can I find and safely handle file names containing newlines, spaces or both?. I recommend giving it a look...

You must log in to answer this question.