1

I have this script which finds files with incorrect permissions. If any are found, it asks the user if they want to fix them or show them. The find results are stored in a variable in order to avoid running the same command multiple times:

#!/usr/bin/env sh

results=$(find "$0" -type f -not -perm 644)

if [ -z "$results" ]; then echo 'No files with incorrect permissions found' else while true; do read -p 'Fix files with incorrect permissions? (yes/no/show) ' ans case "$ans" in Y | y | yes) echo 'Changing file permissions...' chmod 644 "$results" break;; N | n | no) break;; S | s | show) echo "$results";; *) echo 'Please answer yes or no';; esac done fi

The problem is chmod throws an error due to the newlines:

chmod: cannot access 'test/foo'$'\n''test/bar'$'\n''test/foo bar': No such file or directory

If I remove the quotes around "$results", it works a little better, but then of course file names containing spaces are problematic.

I've been messing around with IFS=$'\n' but am not sure where I should set that. This doesn't seem to work:

IFS=$'\n' chmod 644 $results

However, this does:

IFS=$'\n'
chmod 644 $results
unset IFS

I guess I'm just wondering if this is correct or if there's a better way.

  • Use an array. It makes everything simpler and safer. A naîve replacement would look like results=($(find "$0" -type f -not -perm 644)). A much better + safer option would be readarray -t results < <(find "$0" -type f -not -perm 644). Then you can check for empty results (if ((! ${#results[@]})); then ...; fi) and loop over the results (for result in "${results[@]}"; do ...; done). – Andrej Podzimek Oct 25 '21 at 00:44
  • @AndrejPodzimek why is your first example a naîve replacement? – Big McLargeHuge Oct 29 '21 at 21:33
  • Because it will fall apart if the file names contain spaces. – Andrej Podzimek Oct 29 '21 at 23:32

2 Answers2

3

Setting IFS to just the newlines helps, but it still leaves the problems of 1) filenames with newlines (obviously), and 2) filenames with glob characters. E.g. a file called * would expand to all filenames in the directory.

In Bash, use mapfile/readarray to populate an array instead:

mapfile -d '' files < <(find . -type f ! -perm 0644 -print0)
printf "%d matching files found\n" "${#files[@]}"
printf "they are:\n"
printf "  %q\n" "${files[@]}"

See also:

ilkkachu
  • 138,973
  • Even though I'm using bash in this case, I usually try to avoid bashisms. But in this case, it might be worth it. Neat feature. – Big McLargeHuge Oct 24 '21 at 20:31
  • @BigMcLargeHuge, arrays are just hugely useful for handling lists of arbitrary strings. In a pure POSIX shell you could use the positional parameters as a substitute up to an extent and as long as you need only one array/list. But I don't think you can deal with the NUL-separated input in pure POSIX sh, so you'd be limited to set -f; IFS='<newline>'; set -- $(find ... -print), and ignoring filenames with newlines. Or just run find repeatedly. – ilkkachu Oct 24 '21 at 20:35
  • From man zshmodules: "This associative array takes as keys the names of files; the resulting value is the content of the file... The value may also be assigned to, in which case the file in question is written (whether or not it originally existed); or an element may be unset, which will delete the file in question." That doesn't sound safe at all! – Big McLargeHuge Oct 29 '21 at 21:39
  • @BigMcLargeHuge, that's the zsh/mapfile module if I read the manpage correctly. Given it's a module, I'm pretty sure you don't need to use it, if you think it's too dangerous, or you otherwise don't want to. Bash's mapfile is not the same as that module, since Bash isn't zsh. – ilkkachu Oct 29 '21 at 23:54
  • Gotcha. I'll remove the "bash" tag then. Sorry for the confusion. I still appreciate your answer though. – Big McLargeHuge Oct 30 '21 at 00:07
  • @BigMcLargeHuge, right, you also had #!/usr/bin/env bash, so I assumed you were using Bash here. I'm actually not sure what the best equivalent to mapfile would be with zsh, sorry. – ilkkachu Oct 30 '21 at 00:09
  • Good catch about the shebang. I meant to remove that too. My bad. – Big McLargeHuge Oct 30 '21 at 00:12
0

Instead of setting IFS before chmod and unsetting it immediately after, it appears to work equally well if I set/unset it before/after find and wrap the subshell in an array as suggested in the comments:

IFS=$'\n'
results=($(find "$0" -type f -not -perm 644))
unset IFS

This way, the array has the correct number of items, and chmod 644 "${results[@]}" works as expected, as long as there no no filenames containing newline characters (though I can't imagine why anyone would do such a thing on purpose).