Should be:
find . -depth ! -name . -type d -execdir sh -c '
for dir do
dir=${dir#*/} # remove the ./ prefix that some find implementations add
new_dir=$(ccase -t Kebab -- "$dir") || continue
[ "$dir" = "$new_dir" ] ||
mv -i -- "$dir" "$new_dir"
done' sh {} +
Never embed those {} in the code argument of sh/bash or any language interpreter, that would make it a command injection vulnerability. See Is it possible to use `find -exec sh -c` safely?
Some other notes:
- when you can't guarantee variable data won't start with a
-, use the -- to mark the end of options. I don't known if ccase supports --, if it doesn't you can report it as a bug to their maintainers.
- no need for
bash here, your system's sh should be enough
- in sh like in bash, parameter expansions and command substitutions must be quoted when in list context at least or otherwise they undergo split+glob! See for instance Security implications of forgetting to quote a variable in bash/POSIX shells
- Especially when doing something potentially destructive like calling
mv, it's a good idea to check that each command you run succeeds. Here we use || continue to skip dirs for which ccase fails.
- note the
+ instead of ; to pass several dirs to sh where possible which avoids having to call one shell per file. Not all find implementations that support that non-standard -execdir predicate will do it though. In some like on some BSDs or older versions of GNU find, + just does the same as ; and the loop is then redundant (though harmless).
- beware that in
mv -- SomeDir some_dir, if some_dir already exists and is a directory or a symlink to a directory, that becomes the same as mv -- SomeDir some_dir/SomeDir. With the GNU implementation of mv, that can be avoided with the -T option (or use zmv as below which will detect the clash)
To avoid that -execdir and run as few shells as possible, you could use -exec instead but then you'd need to separate out the base name and the parent directory. As an improvement, we can also record failures of ccase or mv so it be reflected in find's exit status:
find . -depth ! -name . -type d -exec sh -c '
ret=0
for dir do
base=${base##*/}
parent=${dir%/*}
new_base=$(ccase -t Kebab -- "$base") && {
[ "$base" = "$new_base" ] ||
mv -i -- "$dir" "$parent/$new_base"
} || ret=$?
done
exit "$ret"' sh {} +
Note that above I'm adding a -i to mv to at least give the user an option to avoid data loss like when two files end up having the same new name. A better approach would be to use zsh's zmv which can check everything in advance and has a dry-run mode (-n):
autoload -Uz zmv
zmv -n '(**/)(*)(#q/)' '$1$(ccase -t Kebab -- $2)'
It also omits hidden files by default. It does a depth-first traversal by default. The (#q/) is the equivalent of -type d. In this case, the exit status of ccase is ignored. When the name is unchanged, there's no rename attempt.
You could replace (*) with (^*-*) to avoid processing files that already have hyphens in their name.
You could also probably do the conversion to Kebab case in zsh:
zmv -n '(**/)(*)(#q/)' \
'$1${${${2//[_[:space:]]##/-}//(#b)([^[:upper:].-])([[:upper:]])/$match[1]-$match[2]}:l}'
Where we convert all sequences of whitespace or underscore to a single -, insert -s in between non-uppercase (except ., and -) and uppercase characters in the base name, and convert the whole result to lowercase. You'd need to adapt if you want AFileInTheUK.txt to be renamed to a-file-in-the-u-k.txt instead of afile-in-the-uk.txt.
mv camelCase (ccase -t Kebab camelCase)is fish shell syntax. In Korn/POSIX-like shells, that should be:mv camelCase "$(ccase -t Kebab camelCase)"– Stéphane Chazelas Jun 24 '23 at 09:04