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,811
  • 12
  • 47
  • 74
  • 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
  • 320,670
  • 36
  • 633
  • 936
  • 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,648
  • 1
  • 24
  • 47
  • 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 `prename`installed 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
Martin Frodl
  • 188
  • 4