3

I want to change all filenames they have the exact length of 16 characters (digits and lower case letters). I tried [0-9a-z]{16} and [0-9a-z]\{16\} for the placeholder regX in the following snippet, but it doesn't work.

for file in <regX>
do
  mv "$file" "${file}.txt"
done
Hölderlin
  • 1,186
  • 2
    which shell are you using? https://en.wikipedia.org/wiki/Glob_(programming) is different from regex – Sundeep Apr 09 '17 at 02:58
  • 2
    if you are using bash and has extglob , you can iterate over for file in +([0-9a-z]) and then rename only if length of ${#file} is 16 – Sundeep Apr 09 '17 at 03:02
  • @Sundeep, my default shell $ echo $0 is bash. And it's not possible to specify the number of repetitions in this case? – Hölderlin Apr 09 '17 at 03:13
  • 1
    You may find success with ???????????????? in place of <regX>, though this doesn't let you care what those sixteen characters are – Fox Apr 09 '17 at 03:31
  • What about using find instead? – cylgalad Apr 09 '17 at 08:39

4 Answers4

5

Shell globbing patterns are not regular expressions. They may look similar, but they work quite differently in some respects.

The regular expression [0-9a-z]{16} matches a string that contains a run of 16 characters of the mentioned character range (the string containing the match may be longer).

The equivalent shell globbing pattern would be something like

*[0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z]*

This is because

  1. Globbing patterns do not support modifiers that says "repeat the previous N times".
  2. Globbing patterns are by default anchored to the beginning and end. This is why I inserted * at the start and end of the pattern. A * matches any number of any character. Remove these if you want to match exactly 16 characters.

You may create a string of 16 [0-9a-z] using Perl:

$ pattern="$( perl -e 'print "[0-9a-z]" x 16' )"

With bash, you may choose to iterate over all names in the current directory, and then test whether the names match your regular expression:

for name in *; do
    if [[ -f "$name" ]] && [[ "$name" =~ ^[0-9a-z]{16}$ ]]; then
        echo mv "$name" "$name.txt"
    fi
done

Remove the echo once you know that it's doing the right thing.

Notice that I've anchored the pattern so that longer names won't match. I'm also making sure, with the -f test, that the names that we get are actually those of regular files only.

Kusalananda
  • 333,661
3

If you have perl-rename (called rename on Debian-based systems, perl-rename on others), you can do:

perl-rename -n 's/^[0-9a-z]{16}$/$&.txt/' *

If that does what you want, remove the -n to make it actually rename the files. The syntax of perl-rename is a perl command. Here, we're using the substitution operator (s/from/to/) which will replace from with to. The from in this case is your regex and the to is the special variable $& which means "whatever was matched", plus the extension .txt.


To do this with a shell glob (use @Kusalananda's or @Sundeep's approach, this is just for the sake of completion) :

for f in [0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z][0-9a-z]; do
    mv -- "$f" "$f".txt
done

With GNU find:

find . -regextype posix-extended -regex '.*/[0-9a-z]{16}' -exec mv {} {}".txt" \;
terdon
  • 242,166
3

With extglob

shopt -s extglob
for file in +([0-9a-z])
do
    [[ ${#file} == 16 ]] && echo mv "$file" "${file}.txt"
done
  • +([0-9a-z]) means one or more of [0-9a-z] characters
  • ${#file} gives the length of filename
  • echo is for dry run, remove once things are fine
Sundeep
  • 12,008
1
L16=$(csh -c 'repeat 16 echo -n "?"')
find . -name "$L16" ! -name '*[!0-9a-z]*' -exec sh -c '
   mv "$1" "$1.txt"
' {} {} \;