My approach:
find . -depth -name "* *" -execdir bash -c 'pwd; for f in "$@"; do mv -nv "$f" "${f// /_}"; done' dummy {} +Multi-line version for readability:
find . -depth -name "* *" -execdir \ bash -c ' pwd for f in "$@"; do mv -nv "$f" "${f// /_}" done' dummy {} +Explanation:
find . -name "* *"finds objects that need to be renamed. Notefindis very flexible with its tests, so if you want (e.g.) to rename directories only, start withfind . -depth -type d -name "* *".-execdirexecutes the given process (bash) in a directory where the object is, so any path passed by{}is always like./bar, not./foo/bar. This means we don't need to care about the whole path. The downside ismv -vwon't show you the path, so I addedpwdjust for information (you can omit it if you want).bashlets us use the"${baz// /_}"syntax.-depthensures the following won't happen:findrenames a directory (if applicable) and then tries to process its content by its old path.{} +is able to feedbashwith multiple objects (contrary to{} \;syntax). We iterate over them withfor f in "$@". The point is not to run a separatebashprocess for every object since creating a new process is costly. I think we cannot easily avoid running separatemv-s; still, reducing the number ofbashinvocations seems a good optimization (pwdis a builtin inbashand doesn't cost us a process). However-execdir ... {} +won't pass files from different directories together. By using-exec ... {} +instead of-execdir ... {} +we may further reduce the number of processes but then we need to care about the paths, not just filenames (compare this other answer, it seems to do a decent job butwhile readslows it down). This is a matter of speed versus (relative) simplicity. My solution with-execis down below.dummyjust before{}becomes$0inside ourbash. We need this dummy argument because"$@"is equivalent to"$1" "$2" ...(not"$0" "$1" ...). This way everything passed by{}is available later as"$@".
More complex, slightly optimized version (various ${...} tricks taken from another answer):
find . -depth -name "* *" -exec \ bash -c ' for f in "$@"; do n="${f##*/}" mv -nv "$f" "${f%/*}/${n// /_}" done' dummy {} +Another (experimental!) approach involves vidir. The trick is vidir uses $EDITOR which may be a non-interactive program:
find . -name "* *" | EDITOR='sed -i s/\d32/_/g' vidir -Caveats:
- This will fail for file/directory names with special characters (e.g. newlines).
- We can't use
s/ /_/gdirectly,\d32is a workaround. - Because of how
vidirworks, the approach would get tricky if you would like to replace a digit or a tab. - Here
vidirworks with paths, not only filenames (base names), thus renaming files only (i.e. not directories) may be hard.
Nevertheless if you know what you're doing then this may do the job even faster. I don't recommend such (ab)use of vidir in general case though. I included it in my answer because I found this experimental approach interesting.