OSX Bash for loop - issues with spaces in folder names?
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
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 '{}' \;
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).
I have two suggestions:
- Use
sed
to put quotes around all the directory names. - Pipe to
xargs
with argument-L 1
. This will execute a command on each line of stdin, obviating thefor
loop.
Try this pipeline:
find . -maxdepth 1 -mindepth 1 -type d | cut -c 3- | sed 's/.*/"&"/' | xargs -L 1 chmod u+x
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 *