5

My Qnap NAS is cursed with a find command that lacks the -exec parameter, so I have to pipe to something. The shell is: GNU bash, version 3.2.57(2)-release-(arm-unknown-linux-gnueabihf)

I'm trying to set the setgid bit on all subdirectories (not files) of the current directory.

This does not work:

find . -type d | xargs chmod g+s $1

Using "$1", "$(1)", $("1") etc. will not work either. They all indicate that chmod is getting passed a directory name containing spaces as two or more parameters (it spits out its standard help message about what parameters are supported).

I don't care to use xargs if I don't have to; I think it chokes on long names anyway, doesn't it?

These and variants of them do not work:

find . -type d | chmod g+s

find . -type d | chmod g+s "$1"

I've thought of using awk or sed to inject quotation marks but I have to think there's an easier way to do this. What did people do before -exec? (The sad thing is that I probably knew, back in 1995 or so, but have long since forgotten.)

PS: Various of these directory names will contain Unicode characters, the ? symbol, etc. They're originally from macOS which is rather permissive. That said, I should probably replace all the ? instances with something like the Unicode character so Windows doesn't choke on them. But that's also going to require a similar find operation with this crippleware version of find.

  • Can you mount the NAS on a different machine that has a proper `find`, and execute there instead? – Scot May 23 '21 at 19:02
  • @Scot, that was going to be my last-resort approach. The problem with it is that it would be specific to a particular system and have to be done manually again and again, instead of being a cleanup script I can run after permissions-unruly backup jobs and other events on the NAS itself. – S. McCandlish May 24 '21 at 00:41

3 Answers3

12

The output of find emits file names separated by newlines1. This is not the format that xargs wants and find has no way to produce the format that xargs wants: it parses its input as whitespace-separated items, with \'" used for quoting. Some versions of xargs can take newline-separated input, but if your find lacks standard options, chances are that your xargs does too.

find . -type d | xargs chmod g+s works as long as your directory names don't contain whitespace or \'". Note that there's no $1: that's meaningful to a shell, but no shell is involved in parsing the output of find and feeding it to chmod, only xargs.

If your find has -print0 and your xargs has -0, you can use these options to pass null-delimited file names, which works with arbitrary file names.

find . -type d -print0 | xargs -0 chmod g+s

If your xargs supports the standard option -I, you can use it to instruct it to process each line as an item, instead of blank-separated quoted strings. This copes with spaces, but not with \"' or newlines.

find . -type d | xargs -I {} chmod g+s {}

You can use the shell to loop over lines instead of xargs. This works for any file name that doesn't contain newline characters.

find . -type d | while IFS= read -r line; do chmod g+s "$line"; done

Both of these solutions work only on file names that don't contain newline characters. The output of find with filenames containing newlines is ambiguous except in one case which is painful to parse: find won't spontaneously emit multiple slashes, so if you put // in the path to the directory to traverse, you can recognize this in the output. Here's some minimally tested code using that uses this fact to convert the output from find into the input format of xargs.

chars=$(printf '\t "'\\\')
{ find .//. -type d; echo .// } | LC_ALL=C sed -n '
s/['"$chars"']/\\&/g
/^\.\/\// {
x
s/\n/\\&/g
p
b
}
H' | LC_ALL=C xargs chmod g+s

1 More precisely: terminated by newlines (there's a newline after the last name).

Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
  • Xargs accepts input separated by newlines, this is the default. Where did you find an xargs program that doesn't? More precisely, xargs accepts blank separated input; a newline counts as a blank. This causes problems with file names containing spaces, so the `print0` / `-0` combination is a safer bet. – Johan Myréen May 23 '21 at 10:29
  • 1
    @JohanMyréen That's the point: xargs (without options) takes _blank_-separated input, not _newline_-separated input. A single line containing `foo bar` is two items. If it took newline-separated input, that would be a single item. – Gilles 'SO- stop being evil' May 23 '21 at 10:34
  • Ok, my bad. Btw. I didn't know there are versions of xargs that take newline separated input. On the other hand, that doesn't work either, if the file names contain newlines. – Johan Myréen May 23 '21 at 11:30
  • @StéphaneChazelas Thanks. I've replaced the awk code by some sed code (more cryptic but more likely to work on a limited system) which leverages xargs's quoting. – Gilles 'SO- stop being evil' May 23 '21 at 18:30
  • This `find` does lack `-print0`, unfortunately. That would've been pretty convenient. And the `xargs` lacks `-I`. It's as if this stuff was disabled with the specific intent of preventing what I'm trying to do. [sigh] The `while` version DID work. I didn't try that approach before asking here, because people vent all the time, and even write big tutorials, about how allegedly terrible it is to use `while` loops to process files and directories in the shell. LOL. I didn't test the fancy script, not having names to deal with containing newlines. Thanks for the thorough answer! – S. McCandlish May 24 '21 at 00:39
  • Is there some reason to favour `-I {}` over `-d '\n'` to just set the delimiter to a newline (without disabling putting more than one arg on each chmod fork/exec)? Or is that only available in cases where `-0` would be even better? (The OP comments that their xargs lacks `-d`, and their find lacks `-print0`, though.) – Peter Cordes May 24 '21 at 02:34
  • 1
    @S.McCandlish: Could you convince `find` to print a 0 byte with `find -printf '...\0'`? Or does it also lack `-printf`? Does your Qnap have busybox or something, or was old GNU findutils really that limited? – Peter Cordes May 24 '21 at 02:38
  • It lacks -printf, too. The only options is supports are `-follow -name -print -type -perm -mtime`. Fortunately, two solutions already provided actually work. – S. McCandlish May 24 '21 at 11:58
  • 2
    @PeterCordes POSIX `xargs` has `-I` but not `-d`. BSD doesn't have `-d` either. `-d` is a GNU extension. – Gilles 'SO- stop being evil' May 24 '21 at 12:34
4

You could do the recursion yourself with bash. Something like:

shopt -s nullglob dotglob
recurse_chmod () (
  cd "$1"
  for d in ./*/
  do
    if [ -L "$d" ]; then continue; fi
    chmod g+s "$d"
    recurse_chmod "$d"
  done
)
recurse_chmod .
Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
muru
  • 69,900
  • 13
  • 192
  • 292
  • 2
    `dotglob` and `nullglob` were already in bash 2 (before [`CHANGES`](https://github.com/bminor/bash/blob/master/CHANGES) started). (And if anyone is tempted to recurse with `globstar`, it only appeared in bash 4, and it traversed symlinks in bash 4.0 to 4.2.) – Gilles 'SO- stop being evil' May 23 '21 at 21:52
  • I can confirm that this worked. However, I didn't go for this semi-obvious approach from the start becasuse of all the criticism there often is about using shell loops to process files and directories. (Not something I've studied, but people seem to complain about it any time people suggest such an approach.) The other solution, using `while IFS=`, also worked, with less code. Both (with different `chmod` values) let me fix the addl. problems that the Qnap makes all files executable and world-writable by default (seems to control permissions only at the dir level). Next time get Synology? – S. McCandlish May 24 '21 at 00:59
  • @S.McCandlish The criticism with shell loops is mainly for when people do things like `for i in $(ls)`, `ls | while read i`, etc. where it's not possible to safely get filenames. Globs are safe. Also, does your Qnap allow running Linux containers? (The one I administered did, and I could mount the relevant filesystems in the container and use any modern tool I wanted) – muru May 24 '21 at 03:12
  • 1
    Ha! I never thought of that container trick. And yes, it does. I think that just solved all future problems of this sort that may arise, since I can just put Ubuntu or whatever in one of them. While it's still some mounting work to do, it'll only need doing once, in a container I keep on the NAS, while the "mount it from your Mac" kind of thing would be dependent on an external machine which might go away. (More like WILL go away, since my Mac is 11 years old.) – S. McCandlish May 24 '21 at 12:01
1

You can tell xargs to use \0 as delimiter use find . -type d -print0 | xargs -0 chmod g+s

or

find . -type d | xargs -I{} -d '\n' chmod g+s "{}". It will use \n as delimiter.

Prvt_Yadav
  • 5,732
  • 7
  • 32
  • 48