3

I would like to rename a series of files named this way

name (10).ext
name (11).ext
...

to

name_10.ext
name_11.ext
...

This one-liner works:

$ for i in name\ \(* ; do echo "$i" | sed -e 's! (\([0-9]\{2\}\))!_\1!' ; done
name_10.ext
name_11.ext
...

But this one doesn't:

$ for i in name\ \(* ; do mv "$i" "$(echo "$i" | sed -e 's! (\([0-9]\{2\}\))!_\1!')" ; done
bash: !_\1!': event not found

Why? How to avoid this?


Using

$ bash --version
GNU bash, versione 4.3.48(1)-release (x86_64-pc-linux-gnu)

on Ubuntu 18.04.


While in this similar question a simple case with ! is shown, here a ! just inside single quotes is considered and compared to a ! inside single quotes, inside double quotes. As pointed out in the comments, Bash behaves in a different way in these two cases. This is about Bash version 4.3.48(1); this problem seems instead to be no more present in 4.4.12(1) (it is however recommended to avoid this one-liner, because the Bash version may be unknown in some cases).

As suggested in the Kusalananda answer, if the sed delimiter ! is replaced with #,

$ for i in name\ \(* ; do mv "$i" "$(echo "$i" | sed -e 's# (\([0-9]\{2\}\))#_\1#')" ; done

this problem does not arise at all.

BowPark
  • 4,895
  • Bounty for any one that can explain why. I am struggling with this. I have never had a problem, until trying to reproduce this. Some how the outer double quotes disable the inner single quotes, even though the inner quotes are in a sub-shell (). Can you enlighten. – ctrl-alt-delor Nov 22 '18 at 13:11
  • 1
    Hmm... there does seem to be a difference in behavior here between bash 4.3.48 and bash 4.4.19 using a simple test echo "$(echo '!foo')". I wonder if 4.3 is erroneously applying the "single quotes lose their special meaning inside double quotes" rule even when the former are within a command substitution? – steeldriver Nov 22 '18 at 13:11
  • 1
    @steeldriver seems correct. I have not looked at the bug tracker, but version 4.4.12(1) seems to get it correct. Better to avoid for a few years, as there will me plenty of all bash around for a bit. steeldriver write-up in an answer, and send me a reminder, when I can set the bounty in a few days. – ctrl-alt-delor Nov 22 '18 at 13:18

3 Answers3

7

When used in an interactive shell, the ! may initiate history expansion (not an issue in scripts).

The solution is to use another delimiter than ! in the sed expression.

An alternative bash loop:

for name in "name ("*").ext"; do
    if [[ "$name" =~ (.*)" ("([^\)]+)").ext" ]]; then
        newname="${BASH_REMATCH[1]}_${BASH_REMATCH[2]}.ext"
        printf 'Would move %s to %s\n' "$name" "$newname"
        # mv -i "$name" "$newname"
    fi
done
Kusalananda
  • 333,661
  • Ok! Thanks. But, out of curiosity: why does the first one-liner work, then? It contains ! as well. – BowPark Nov 22 '18 at 13:01
  • I am struggling with this. I have never had a problem, until trying to reproduce this. Some how the outer double quotes disable the inner single quotes, even though the inner quotes are in a sub-shell (). Can you enlighten. – ctrl-alt-delor Nov 22 '18 at 13:09
  • 1
    @BowPark The parsing of history expansion expressions is a bit of a mystery to me, and it seems to not obey ordinary quoting rules. In the first command, the ! occurs in a single-quoted string. That's the case in the second command as well, but that is included inside a double-quoted string (not taking the fact that there should not be a an issue since the single quoted ! is in a command substitution). – Kusalananda Nov 22 '18 at 13:09
  • 1
    @ctrl-alt-delor Yeah. History expansions in bash hos always been a bit of a black box to me. It's the quotes. The parsing of the history expansion does not seem to obey normal scoping rules. – Kusalananda Nov 22 '18 at 13:11
  • Better: set +H to disable this hideous bash misfeature. Add it to your .bashrc. – R.. GitHub STOP HELPING ICE Nov 22 '18 at 14:41
3

You can use Larry Wall's rename command (rename package in Debian, prename in RedHat): it uses the (much easier IMHO) Perl regex syntax, and will iterate the file args (no need to code a loop):

rename 's/ \(\d+\)/_$1/' name\ *
xenoid
  • 8,888
  • Thank you, it seems very convenient. I wonder, however, if it is portable to other *nix systems (not only Linux). – BowPark Nov 22 '18 at 13:03
  • 1
    It is possible, just install it. It is a purl script. It has no dependencies on Linux (the kernel). However beware that there are other commands with the name rename, they are not the same. Some Unixes (including those using Linux), have another rename installed. – ctrl-alt-delor Nov 22 '18 at 13:06
  • Available wherever you have Perl, which is about any Unix system. On my Ubuntu I have prenameinstalled as part of the Perl package, and rename installed as stand-alone (not exactly the same code, even though the core is identical). On Redhat and derivatives there is already a (less powerfull, but easier) rename command, so you only have prename. – xenoid Nov 22 '18 at 14:41
1

If you know the exact range, you can rename the files like this:

for i in {10..99}; do mv "name ($i).ext" "name_$i.ext"; done

Or, if you want to be POSIX-pedantic:

for i in `seq 10 99`; do mv "name ($i).ext" "name_$i.ext"; done