61

I have a folder with about 1,700 files. They are all named like 1.txt or 1497.txt, etc. I would like to rename all the files so that all the filenames are four digits long.

I.e., 23.txt becomes 0023.txt.

What is a shell script that will do this? Or a related question: How do I use grep to only match lines that contain \d.txt (i.e., one digit, then a period, then the letters txt)?

Here's what I have so far:

for a in [command i need help with]
do
  mv $a 000$a
done

Basically, run that three times, with commands there to find one digit, two digits, and three digit filenames (with the number of initial zeros changed).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
David Oneill
  • 12,502
  • 16
  • 58
  • 70
  • Possible duplicate of [Bash script to pad file names](http://stackoverflow.com/questions/55754/bash-script-to-pad-file-names) – Ciro Santilli OurBigBook.com Oct 01 '15 at 07:41
  • 1
    @CiroSantilli, incidentally, I had someone recently push back on a dupe being closed with that as the logic to pad leading numbers and the logic to pad numbers later in a name differs somewhat. (Consequently, https://stackoverflow.com/questions/46993470/padding-filenames-with-zeros-in-bash is presently flagged a dupe of this answer, and *not* a dupe of that one). – Charles Duffy Oct 28 '17 at 20:04

10 Answers10

54

Try:

for a in [0-9]*.txt; do
    mv $a `printf %04d.%s ${a%.*} ${a##*.}`
done

Change the filename pattern ([0-9]*.txt) as necessary.


A general-purpose enumerated rename that makes no assumptions about the initial set of filenames:

X=1;
for i in *.txt; do
  mv $i $(printf %04d.%s ${X%.*} ${i##*.})
  let X="$X+1"
done

On the same topic:

Community
  • 1
  • 1
Colin Hebert
  • 91,525
  • 15
  • 160
  • 151
  • This outputs a bunch of errors for each filename that is already 4 digits. Adding the foo at the start would get around this, but I didn't want that there. Thanks for those links. – David Oneill Sep 08 '10 at 22:30
  • 1
    You solution is not robust. If an input file is named `050.txt`, it will be renamed to `0040.txt`. Why ? Because in C `050` is an octal constant whose value is `40`! –  May 06 '18 at 19:46
  • @AntoineMathys: good catch!! Worse, repeating the command renames that file, again, at each execution. – Victoria Stuart Mar 28 '19 at 22:40
  • I would suggest `$(ls -1 *.txt | sort -V)` instead of `*.txt` as this will preserve ordering. – Mr Squid Feb 09 '21 at 19:19
  • Modifying `${a%.*}` to `$((10#${a%.*}))` in the printf statement should fix the issue with leading zeros causing an octal read failure. This uses the numerical evaluation syntax to convert to base 10. – heuristicus Mar 24 '22 at 16:26
17

Using the rename (prename in some cases) script that is sometimes installed with Perl, you can use Perl expressions to do the renaming. The script skips renaming if there's a name collision.

The command below renames only files that have four or fewer digits followed by a ".txt" extension. It does not rename files that do not strictly conform to that pattern. It does not truncate names that consist of more than four digits.

rename 'unless (/0+[0-9]{4}.txt/) {s/^([0-9]{1,3}\.txt)$/000$1/g;s/0*([0-9]{4}\..*)/$1/}' *

A few examples:

Original    Becomes
1.txt       0001.txt
02.txt      0002.txt
123.txt     0123.txt
00000.txt   00000.txt
1.23.txt    1.23.txt

Other answers given so far will attempt to rename files that don't conform to the pattern, produce errors for filenames that contain non-digit characters, perform renames that produce name collisions, try and fail to rename files that have spaces in their names and possibly other problems.

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • Yagni. Seriously. Who cares about filenames that contain spaces or non-numeric characters if that's not in the scope of the question? If the OP wanted to write a general-use script, he would have said so in the question, and the answers would have accommodated accordingly. – C. K. Young Sep 15 '10 at 19:41
  • 17
    @Chris: My answers often do two things. One, they attempt to answer an OP's question. Two, they attempt to provide additional information that may be useful to future viewers of the question. – Dennis Williamson Sep 16 '10 at 00:17
  • 2
    @Dennis: Thanks for providing this, the top rated answer did not solve my particular issue but as you noted, you provided a more general and broader answer which helped. Thanks. – Ne Ma Apr 26 '13 at 08:48
14
for a in *.txt; do
  b=$(printf %04d.txt ${a%.txt})
  if [ $a != $b ]; then
    mv $a $b
  fi
done
C. K. Young
  • 219,335
  • 46
  • 382
  • 435
9

One-liner:

ls | awk '/^([0-9]+)\.txt$/ { printf("%s %04d.txt\n", $0, $1) }' | xargs -n2 mv

How do I use grep to only match lines that contain \d.txt (IE 1 digit, then a period, then the letters txt)?

grep -E '^[0-9]\.txt$'
rkhayrov
  • 10,040
  • 2
  • 35
  • 40
6

Let's assume you have files with datatype .dat in your folder. Just copy this code to a file named run.sh, make it executable by running chmode +x run.sh and then execute using ./run.sh:

#!/bin/bash
num=0
for i in *.dat
do

  a=`printf "%05d" $num`
  mv "$i" "filename_$a.dat"
  let "num = $(($num + 1))"
done

This will convert all files in your folder to filename_00000.dat, filename_00001.dat, etc.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
sinner
  • 803
  • 2
  • 11
  • 24
1

This version also supports handling strings before(after) the number. But basically you can do any regex matching+printf as long as your awk supports it. And it supports whitespace characters (except newlines) in filenames too.

for f in *.txt ;do
    mv "$f" "$( 
        awk -v f="$f" '{
            if ( match(f, /^([a-zA-Z_-]*)([0-9]+)(\..+)/, a)) {
                printf("%s%04d%s", a[1], a[2], a[3])
            } else {
                print(f)
            }
        }' <<<''
    )"
done
rindeal
  • 993
  • 11
  • 16
  • I just tried the above and it blew away all the files in the directory and left a single file "0000". – Ryan Aug 28 '17 at 20:49
  • @Ryan That means the regex didn't match any files in your dir. You have to customize the regex + printf to get anything done. The snippet above is just an example. – rindeal Aug 31 '17 at 20:50
  • rindeal is correct. When I corrected the regex for my filenames (mine had spaces) it worked. Filenames that don't match the regex still get moved to '0000' which maybe a little surprising. If you are doing a bulk rename and going to try random scripts you find on SO you better have a backup. – Ryan Sep 01 '17 at 20:24
  • @Ryan ok, I modified the snippet to preserve unmatched files – rindeal Sep 03 '17 at 21:00
0

One-liner hint:

while [ -f ./result/result`printf "%03d" $a`.txt ]; do a=$((a+1));done
RESULT=result/result`printf "%03d" $a`.txt
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
kayn
  • 673
  • 5
  • 14
0

To only match single digit text files, you can do...

$ ls | grep '[0-9]\.txt'
LukeN
  • 5,590
  • 1
  • 25
  • 33
0

To provide a solution that's cautiously written to be correct even in the presence of filenames with spaces:

#!/usr/bin/env bash

pattern='%04d'  # pad with four digits: change this to taste

# enable extglob syntax: +([[:digit:]]) means "one or more digits"
# enable the nullglob flag: If no matches exist, a glob returns nothing (not itself).
shopt -s extglob nullglob

for f in [[:digit:]]*; do               # iterate over filenames that start with digits
  suffix=${f##+([[:digit:]])}           # find the suffix (everything after the last digit)
  number=${f%"$suffix"}                 # find the number (everything before the suffix)
  printf -v new "$pattern" "$number" "$suffix"  # pad the number, then append the suffix
  if [[ $f != "$new" ]]; then                   # if the result differs from the old name
    mv -- "$f" "$new"                           # ...then rename the file.
  fi
done
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
0

There is a rename.ul command installed from util-linux package (at least in Ubuntu) by default installed.

It's use is (do a man rename.ul):

rename [options] expression replacement file...

The command will replace the first occurrence of expression with the given replacement for the provided files.

While forming the command you can use:

rename.ul -nv replace-me with-this in-all?-these-files*

for not doing any changes but reading what changes that command would make. When sure just reexecute the command without the -v (verbose) and -n (no-act) options

for your case the commands are:

rename.ul "" 000 ?.txt
rename.ul "" 00 ??.txt
rename.ul "" 0 ???.txt
Nico Rodsevich
  • 2,393
  • 2
  • 22
  • 32