8

I'm getting a bit confused on how to handle spaces in path names when returned in a for loop.

Rationale: I'm cleaning up the permissions on folders and files that I copy over from Windows. Most of the files end up with -rwx------ or -rwxr-xr-x permissions so I like to do "chmod -x *" and then "chmod u+x <folders>" so I'm trying the following:

$ alias getdirs='find . -maxdepth 1 -mindepth 1 -type d | cut -c 3-'
$ for i in $(getdirs); do chmod u+x $i; done

which works fine, as long as the directories don't have a space in the name.

I've tried different permutations of chmod u+x "$i", chmod u+x '$i' and similar to get the behavior I wanted, but to no avail.

How to improve my bash code, that works with folder names containing space?

The purpose of this is to be able to remove the "exec" bit from plain files (hence the chmod -x * part) but then to restore it to the directories to allow getting into them (chmod u+x <dirname>). From the comments and answers so far I'm thinking that it probably will be easier to do with the proper "find" incantation

6
  • Why are you using the cut - could you not just use -exec.
    – mmmmmm
    Dec 12, 2013 at 12:34
  • 2
    Each to their own, stringing together a load of commands with pipes can act as a nice way to provide a sort of code based commentary of logical steps, whilst most things can be condensed to minimal steps within commands like find and sed and awk etc, many less experienced shell tinkerers find the flowchart style command chains more readable and understandable.
    – stuffe
    Dec 12, 2013 at 12:37
  • @Mark: I want to get rid of the ./ at the beginning of the output to get "nice" output, that's all
    – JJarava
    Dec 12, 2013 at 14:11
  • @stuffe -but then you have to deal with this string problem so I find then both equally complex - My suggestion is give up on shell and use a fuller non text based language - e.g. python, perl, tcl etc.
    – mmmmmm
    Dec 12, 2013 at 14:16
  • @Mark Actually, that's "plan b". I'm reasonably comfortable with Python (it's my scripting language of choice to develop utilities) and since moving to Mac I've developed some more, but I was trying to do it "the Unix way". From the other answers I believe "the way to go" (other than a full blown Python script) is probably to leverage "find". Will look into it and report back
    – JJarava
    Dec 12, 2013 at 15:41

5 Answers 5

10

These kind of things can be tricky in all Unix shells due to the way space is acting as a separator, running aliases as part of shell scripts just makes things even more interesting. I would probably run two passes of find to set first the directories in order, and then next the files:

find . -maxdepth 1 -mindepth 1 -type d -exec chmod u+x '{}' \;
find . -maxdepth 1 -mindepth 1 -type f -exec chmod u-x '{}' \;
1
  • 3
    And to answer to the initial need: find . -maxdepth 1 -mindepth 1 -type f -exec chmod u-x '{}' \;
    – dan
    Dec 13, 2013 at 15:01
8

In general, the 'proper' way to parse the output of find into a bash loop is to use a while read loop, rather than a for loop. In bash, for loops split using any whitespace (space, tab, newline) by default -- this can be changed, but it's easier and (in my opinion) cleaner to use read, which reads one line at a time by default.

find . -maxdepth 1 -mindepth 1 -type d | while read i; do chmod u+x "$i"; done

Note that I quoted the "$i" there -- that's just as important, because quoting variables prevents the shell from splitting their contents (it's the same problem that for has, but on the other end). Also note that you can't use single quotes: '$i' would return a literal $i, rather than the contents of the variable.

This will still break on directories with newlines in their names. There is a workaround involving find's -print0, but I've only ever seen newlines in filenames specifically made to test scripts. I don't know if this works with the version of bash in OSX (taken from greg's wiki):

find . -maxdepth 1 -mindepth 1 -type d -print0 | while IFS= read -r -d '' i; do chmod u+x "$i"; done

However, in this case it's easier to use globs: a glob ending in a / will expand to directories only, so you could just

chmod u+x */

(this will work perfectly well with spaces, newlines, anything). More generally, to loop through all directories:

for f in */; do stuff with "$f"; done

Unfortunately, there is no way to select files only with globs in any version of bash (this is one of the reasons I prefer zsh, where the globs are powerful enough that you never have to bother with find).

5
  • 3
    I would argue that parsing the output of find is never proper. There is no scenario where parsing the output of find is more effective than -exec or -execdir, or, in very limited circumstances, find … -print0 | xargs -0 …
    – kojiro
    Dec 12, 2013 at 20:15
  • @kojiro I disagree. Your comment is overly naive. For repetitive operations on large results lists or find lists gleaned from large file systems that take a long time to search, it's advantageous to store the output of find and process it in separate routines so you don't have to repeat the find command or push the limits of single-line scripting. And that's only one such example where your argument falls apart. There are more.
    – Ian C.
    Dec 12, 2013 at 22:06
  • 1
    @IanC. So cache the output of find … -print0. Also, I contend that caching a list of unsanitized filenames is pushing the limits of single-line scripting. The cache-invalidation problem is not insignificant, too.
    – kojiro
    Dec 12, 2013 at 22:25
  • Thanks for the suggestion about using a while loop - it does indeed work better (i.e., it works). The problem with using glob expansion, is that it won't work with "hidden" directories (such as .Testdir) while FIND will.
    – JJarava
    Dec 13, 2013 at 15:58
  • @JJarava Just do shopt -s dotglob to enable dotted-name glob expansion.
    – kojiro
    Dec 13, 2013 at 20:05
3

I have two suggestions:

  1. Use sed to put quotes around all the directory names.
  2. Pipe to xargs with argument -L 1. This will execute a command on each line of stdin, obviating the for loop.

Try this pipeline:

find . -maxdepth 1 -mindepth 1 -type d | cut -c 3- | sed 's/.*/"&"/' | xargs -L 1 chmod u+x

1
  • Actually, I'm looking into leveraging find directly to do the change of permissions directly and bypass complex loops and pipes -- will post here if I manage to do it
    – JJarava
    Dec 12, 2013 at 15:37
2

The crux of the issue is that word-splitting happens for command substitution, so that names with spaces in them are indistinguishable from separate names entirely. Globbing does not suffer from this difficulty, so if there's a way to identify directories with a glob and avoid the command substitution entirely, you're home free. And there is:

chmod -x *
chmod u+x */

But even this is too much work, because chmod has the X (capital X) symbolic flag, which applies the executable bit only to directories and files that are already executable. So you can really just do:

chmod -x *
chmod u+X *
1

Either your alias is only pulling the first bit before the space, or your for loop is only reading the first bit

You can test this by adding some test commentary into your commands, I've limited the alias to only pull the last found directory to isolate the iterations to 1, printed out what the alias thinks it's receiving, and then echoed the variable contents to ensure they match:

jaravj$ alias getdirs='find . -maxdepth 1 -mindepth 1 -type d | tail -1|  cut -c 3-'
jaravj$ getdirs
jaravj$ for i in $(getdirs); do echo "Filename is "$i; chmod u+x $i; done

EDIT: changed do; echo ... to do echo ... as it was preventing the line from executing

3
  • Tried your suggestion and the output is as expected: I have a Folder named "ZZPRODUCT data folder" so the output of the FOR is: Filename is ZZPRODUCT chmod: ZZPRODUCT: No such file or directory Filename is data chmod: data: No such file or directory Filename is folder chmod: folder: No such file or directory
    – JJarava
    Dec 12, 2013 at 13:44
  • @JJarava the output ISN'T as you expect if your folder is named ZZPRODUCT data folder! If it was as expected the output would have been Filename is ZZPRODUCT data folder not Filename is ZZPRODUCT as you indicate. Stuffe has found the problem.
    – Ian C.
    Dec 12, 2013 at 16:13
  • If the output from the echo is truncated, and the output from getdirs isn't, the issue is in thap loop, if getdirs is truncated, the issue is with your alias. Having said that, I like @patrix's solution, have you tried it?
    – stuffe
    Dec 12, 2013 at 16:34

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .