4

I have a bunch of files and directories containing white spaces (several of them in each file name) and I'm trying to remove them. I am plagged with the 'dumb' rename that does not understand regex. So far I end up with something like

find . -maxdepth 1 -type d -print0 | perl -pe 'print $_; s/\s+/_/g' | xargs -n 2 -0 mv

trying to combine the power of find (for now I wanted to rename only the first level of directories; I tried to circumvent the issue of xargs using spaces as separator with -print0) and of Perl regex.

It is going in the right direction (if I change mv by echo in the end I kinda get what I want) but it does not work completely.

Any generic suggestion would be appreciated!

3
  • 2
    perl -0 might help, you need the whole pipeline to produce and consume NUL separators. Look at the output from perl with e.g. od -c.
    – ilkkachu
    Commented Apr 4, 2023 at 13:06
  • 1
    do you only have bash or would you also have zsh? Commented Apr 4, 2023 at 13:11
  • perl -0 did the trick!
    – MBR
    Commented Apr 4, 2023 at 13:18

5 Answers 5

7

Since you are using perl already to generate the new names, just do the rename in perl:

find . -maxdepth 1 -type d -print0 | 
 perl -0nE '$o=$_; s/\s+/_/g; rename $o, $_'

You could also use the new-fangled /r and do:

perl -0nE 'rename $_, s/\s+/_/gr or die "$_: $!\n"'
4
  • @roaima That is a good suggestion, but in this case the -maxdepth 1 makes it unnecessary. Commented Apr 4, 2023 at 14:25
  • Good point. I should read more carefully Commented Apr 4, 2023 at 14:42
  • Yeah, but you probably want -mindepth 1 as well, or maybe -name '* *', or something to avoid matching . . What is this newfangled /r thing by the way?
    – terdon
    Commented Apr 4, 2023 at 14:42
  • @terdon, /r has the substitution return the resulting value, instead of modifying the original. (doc) perl -le '$a = "abc"; $b = ($a =~ s/.../xyz/r); print "$a $b"' gives abc xyz
    – ilkkachu
    Commented Apr 6, 2023 at 21:02
3

You need the Perl's version of rename. Seems like you use rename.ui:

If you run:

and you have:

$ file -- "$(type -p rename)"
...ELF 64-bit

you have rename.ui.

If you have:

$ file -- "$(type -p rename)"
.../rename: Perl script text executable

it's Perl's rename.

Check in depth explanations about Perl's rename:

Download or copy paste standalone rename

3
  • 2
    Yes, I know about Perl rename and would definitely use it if available; as I said, I'm stuck with a distro where it is not available (and have no priviledged access to install it).
    – MBR
    Commented Apr 4, 2023 at 13:05
  • 2
    If you read my link, there's a standalone script that you can wget or just copy/paste. raw.githubusercontent.com/sputnick-dev/perl-rename/master/… Commented Apr 4, 2023 at 13:18
  • 1
    I may be tired: I did read your link but did not see the standalone rename. Thank you for pointing to the right script.
    – MBR
    Commented Apr 4, 2023 at 13:46
2

Since you're using Perl anyway, why not do all of it in Perl?

perl -w -Mstrict -MFile::Find -E 'finddepth(sub { rename $_, s/\s+/_/gr }, ".")' 

(The -w and -Mstrict options aren't necessary, but they're good practice as they enable warnings and strict mode.)

Note that the code above will happily rename files to a name that already exists, deleting the previous file of that name. To avoid that, you can do e.g.:

perl -w -Mstrict -MFile::Find -E 'finddepth(sub { my $n = s/\s+/_/gr; rename $_, $n unless -e $n }, ".")' 

Also, any errors from the rename will be silently ignored. That's not particularly hard to fix either, but the code gets a bit verbose for a one-liner. You could always write an actual Perl script, though, e.g. like this:

#!/usr/bin/perl

use warnings;
use strict;
use File::Find qw(finddepth);
use Errno qw(EEXIST);

sub safe_rename {
  my($old, $new) = @_;
  $! = EEXIST and return if -e $new;
  rename $old, $new;
}

finddepth(sub {
  my $new = s/\s+/_/gr;
  return if $_ eq $new;
  safe_rename $_, $new or warn "Error renaming $_ to $new in $File::Find::dir: $!\n";
}, @ARGV);

If you save this script e.g. as rename_space.pl, you can run it with perl rename_space.pl (or just ./rename_space.pl if you chmod it to make it executable) and pass the directories you want to process (e.g. .) as command line arguments.

2

Just use zsh which has a zmv autoloadable function which can do batch renaming more safely and easily:

autoload -Uz zmv
zmv -n '*[[:space:]]*(#q/)' '${f//[[:space:]]##/_}'

Or recursively:

zmv -n '(**/)(*[[:space:]]*)(#q/)' '$1${2//[[:space:]]##/_}'

(remove -n (dry-run) if happy).

zmv does a few sanity checks including checking that two files are not renamed to the same file or that the destination files don't already exist before doing any renaming. It enables depth traversal by default.

It also skips hidden files by default. If you want to rename those as well, change the (#q/) (that selects files of type directory) to (#qD/) to enable Dotglob for that one glob expansion.

1

Rather than thinking in terms of find | perl | xargs, it is often easier, cleaner and safer to loop over each file. The already renamed directory is a pitfall here, which is why the loop body is more than one line. This could be done in any language, but here is one in pure bash (save for the mv command):

#!/usr/bin/env bash
set -eu
shopt -s nullglob globstar

for f in ./**/*' '*; do
    directory="${f%/*}"
    filename="${f##*/}"

    # The direcory would be renamed in a previous iteration
    # (because bash expands globs alphabetically).
    src="${directory// /_}/$filename"
    dst="${f// /_}"
    echo mv --no-clobber --no-target-directory -- "$src" "$dst"
done

Hint: Remove the echo prefix once you are happy with what it would do.

This script only replaces spaces. To replace other whitespace, adjust accordingly. As commented, this could be augmented with extglob, but I'll stop here.

4
  • 2
    Note that the OP wants to replace all sequences of whitespace with _, so that would be ${filename//+([[:space:]])/_} (for which you also need shopt -s extglob) Commented Apr 5, 2023 at 12:21
  • As you make the assumption that directory components have been successfully renamed beforehand, you probably want to add a || break to abort the loop upon error so as not to end up moving files to separate directories. Commented Apr 5, 2023 at 14:05
  • 1
    Ah, set -e would handle that sorry. Commented Apr 5, 2023 at 14:08
  • 1
    Thanks for this elegant solution in pure bash; that is indeed a potentially better alternative of what I had in mind initially.
    – MBR
    Commented Apr 6, 2023 at 7:49

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .