Several problems:
- You can't combine
-prune
with -depth
as -depth
processes leaves first, so -prune
has no effect as it comes too late, after the whole contents of the directory has already been processed.
- Your
-prune
is misplaced. Where you put it, it would only apply to the files that match -name "*SynoResource*"
. Same for -type d
which only applies to -name "@*"
.
- you have missing double quotes around your parameter expansions and command substitutions which causes them to undergo split+glob.
echo
generally can't be used to output arbitrary data.
- You can't have a
'
inside single quoted strings. If you need to pass a '
literally to some command, you need to quote it using another shell quoting operator ("..."
or backslash) and outside of single quotes.
'
is not special to mv
, sed
nor find
, no need to escape it for them. It's only special to the shell where it's a strong quoting operator.
- You should use
$(...)
instead of that deprecated `...`
(which also adds even more complication when backslash are involved).
So, here:
LC_ALL=C Q="'" find . -depth \
! -path '*/@*' \
! -path '*#recycle*' \
! -path '*SynoResource*' \
-name "*'*" -execdir bash -c '
for file do
mv -i -- "$file" "${file//$Q/X}"
done' bash {} +
Here, since -prune
can't be used with -depth
, we're using -path
(aka -wholename
in GNU find
) to filter out the files in those excluded directories based on their full path. That means however that find
will descend into those directories which is not ideal from a performance point of view.
Using -execdir
means you're renaming only the basename which is good, but that also means you end up running at least one bash
per directory that contains files with '
s. Alternatively, you could do:
LC_ALL=C Q="'" find . -depth \
! -path '*/@*' \
! -path '*#recycle*' \
! -path '*SynoResource*' \
-name "*'*" -exec bash -c '
for file do
dir=${file%/*} base=${file##*/}
mv -i -- "$file" "$dir/${base//$Q/X}"
done' bash {} +
That makes it more efficient though less safe in the face of somebody renaming files and directories whilst the script is running.
For a dry-run, you can replace the mv
command with:
(PS4="Would run"; set -x; : mv -- "$file" "$dir/${base//$Q/X}")
That's better than using echo
as bash
will use quotes where necessary in its tracing output so as to make it unambiguous. The tracing output will look as if it was valid shell code that could be used to run the same command.
There, using -exec
instead of -execdir
also makes the tracing clear about what files would be renamed.
To use -prune
but still process the directory depth first, an alternative would be to use GNU tac -s ''
if available (along with GNU xargs
) on the output of find -print0
to reverse it:
LC_ALL=C find . -depth \
'(' -name '@*' -o \
-name '*#recycle*' -o \
-name '*SynoResource*' \
')' -type d -prune -o \
-name "*'*" -print0 |
tac -s '' |
Q="'" xargs -r0 bash -c '
for file do
dir=${file%/*} base=${file##*/}
mv -i -- "$file" "$dir/${base//$Q/X}"
done' bash {} +
Without GNU tac
, xargs
but if your bash is of version 4.4 or above, you could also do:
LC_ALL=C find . -depth \
'(' -name '@*' -o \
-name '*#recycle*' -o \
-name '*SynoResource*' \
')' -type d -prune -o \
-name "*'*" -print0 |
Q="'" bash -c '
readarray -td "" files &&
for (( i = ${#files[@]} - 1; i >= 0; i-- )); do
file=${files[i]}
dir=${file%/*} base=${file##*/}
mv -i -- "$file" "$dir/${base//$Q/X}"
done' bash {} +
(beware I've not tested any of this).