Take the 2-minute tour ×
Ask Different is a question and answer site for power users of Apple hardware and software. It's 100% free, no registration required.

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

share|improve this question
    
Why are you using the cut - could you not just use -exec. –  Mark Dec 12 '13 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 '13 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 '13 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. –  Mark Dec 12 '13 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 '13 at 15:41

5 Answers 5

up vote 10 down vote accepted

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 '{}' \;
share|improve this answer
3  
And to answer to the initial need: find . -maxdepth 1 -mindepth 1 -type f -exec chmod u-x '{}' \; –  daniel Azuelos Dec 13 '13 at 15:01

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 *
share|improve this answer

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).

share|improve this answer
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 '13 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 '13 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 '13 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 '13 at 15:58
    
@JJarava Just do shopt -s dotglob to enable dotted-name glob expansion. –  kojiro Dec 13 '13 at 20:05

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

share|improve this answer
    
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 '13 at 15:37

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

share|improve this answer
    
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 '13 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 '13 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 '13 at 16:34

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.