4

I have a lot of files that are named like:

data1_1.txt
data1_2.txt
data1_3.txt
data2_1.txt
data2_2.txt
...

However these were downloaded and named in the reverse ordering. How could I rename all of these in a batch so that the result would be:

data1_3.txt
data1_2.txt
data1_1.txt
data2_2.txt
data2_1.txt
...

My first thought was just a bash / zsh script but if there's another tool that would work better please let me know.

3
  • 3
    I would write a "script that writes a script": write a script (in any language) that produces in its output the commands that you would need for the renaming but does not execute them. Then check the output carefully to make sure that it does exactly what you want. When you are sure you've got it right, save the output to a file, make it executable and then run it. Do not fall into the trap of e.g. in the case of three files named one, two and three, doing mv three one; mv two two; mv one three, or you will live to regret it :-) And do it all on a copy of all the files. Commented Dec 14, 2019 at 3:21
  • Welcome! What is exactly "reverse ordering"? Can you give an example of the result you are aiming for? Commented Dec 14, 2019 at 3:47
  • do you have more than a single digit for each of the sequences (i.e. could there be a data1_13.txt? Commented Dec 14, 2019 at 13:40

6 Answers 6

2

With zsh:

autoload zmv # best in ~/.zshrc

typeset -A c=()
zmv -n '(*)_<->.txt(#qnOn)' '$1_$((++c[${(b)1}])).txt-renamed' &&
  : zmv '(*)-renamed' '$1'

(remove the -n (dry-run) and :, if happy (and remember to re-initialize c=() before running again without dry run)).

  • <->: is like <1-12> to match decimal numbers in a range, but here with no bound specified, so matches any sequence of one or more decimal digits. Could also be written [0-9]## where ## is zsh's equivalent of ERE +.
  • (#q...) is the explicit syntax for specifying glob qualifiers.
  • n: sorts numerically
  • On: sorts by name in reverse. So with n above, that sorts the list of matching files numerically in reverse.
  • For the replacement, $1 contains what's captured in (*), so the part before _<digits>.txt.
  • We append $((++c[${(b)1}])), where $c is the associative array declared earlier.
  • ${(b)1} is $1 with glob characters escaped (without it, it wouldn't work properly if $1 contained ]).
  • we do it in 2 stages (append a -renamed suffix which is stripped in the second stage), to avoid overwriting files in the process.

On your sample, that gives:

mv -- data2_2.txt data2_1.txt-renamed
mv -- data2_1.txt data2_2.txt-renamed
mv -- data1_3.txt data1_1.txt-renamed
mv -- data1_2.txt data1_2.txt-renamed
mv -- data1_1.txt data1_3.txt-renamed

mv -- data1_1.txt-renamed data1_1.txt
mv -- data1_2.txt-renamed data1_2.txt
mv -- data1_3.txt-renamed data1_3.txt
mv -- data2_1.txt-renamed data2_1.txt
mv -- data2_2.txt-renamed data2_2.txt

Note that technically, it doesn't reverse the order, or only does it in the case where the numbers are incrementing by one and start at 1 like in your sample. It will turn all of [1, 2, 3], [4, 5, 6], [0, 10, 20] to [3, 2, 1].

To reverse the list, it would be a bit more involved. It could be something like:

all_files=(*_<->.txt(n))
prefixes=(${all_files%_*})

for prefix (${(u)prefixes}) {
  files=(${(M)all_files:#${prefix}_<->.txt})
  new_files=(${(Oa)^files}-renamed)
  for old new (${files:^new_files})
    echo mv -i -- $old $new-renamed
}

(remove echo when happy).

And run the zmv '(*)-renamed' '$1' again as the second phase.

On a different sample with a additional [0, 3, 10, 20] list as a third example, that gives:

mv -i -- data1_1.txt data1_3.txt-renamed
mv -i -- data1_2.txt data1_2.txt-renamed
mv -i -- data1_3.txt data1_1.txt-renamed
mv -i -- data2_1.txt data2_2.txt-renamed
mv -i -- data2_2.txt data2_1.txt-renamed
mv -i -- data3_0.txt data3_20.txt-renamed
mv -i -- data3_3.txt data3_10.txt-renamed
mv -i -- data3_10.txt data3_3.txt-renamed
mv -i -- data3_20.txt data3_0.txt-renamed

Those solutions make no assumption on what character (or non-character) the file names may contain, won't rename files unless they end in _<digits>.txt. The zmv-based approach will guard against overwriting files named with a -renamed suffix that would have been there beforehand, not the latter approach (though -i will cause mv to prompt you before that happens). Alternatively, instead of adding a -renamed suffix, you could move the renamed file into a renamed directory.

1

Here's a bash snippet to do the job assuming files are indeed named the way you've depicted them (data<one-digit>_<digits>.txt).

shopt -s extglob

#gather files into array
files=( data[[:digit:]]_+([[:digit:]]).txt )

#zip original files with their target file names and feed to mv
paste <(printf '%s\n' "${files[@]}" | sort -k1.5,1n -k2n -t'_') \
    <(printf '%s.ren\n' "${files[@]}" | sort -k1.5,1n -k2nr -t'_') | 
    xargs -n 2 mv --

#strip the temporary .ren suffix
for f in data*.ren; do mv -- "$f" "${f%.ren}"; done
1

First, rename all files to prefix "old-":

for i in *
do
    mv "$i" "old-$i"
done

Then run this command and eyeball the output to make sure it looks good:

ls -v | tac | sort -s -t _ -k1,1 | sed -e 's/^old-//' | paste <(ls -v) - | sed -e 's/^/mv /'

If it does, pipe the output to sh.

Here's what's happening.

  • ls -v produces them in sorted order (the -v says to sort 11 after 9, for example)
  • tac reverses the whole input (the entire file; bear with me!)
  • the sort says do a stable sort on only the characters before the first _. The -k1,1 and -s are both important to make sure you get the right output. Without the -k1,1 the rest of the line is used to resolve duplicates, which we don't want, and without -s duplicates are ordered arbitrarily.

The rest is easy enough.

1
  • 1
    Cool solution. I had to add --color=never, because my colored ls output disturbed the command. Here is a final oneliner: for i in *; do mv "$i" "old-$i"; done; ls --color=never -v | tac | sort -s -t _ -k1,1 | sed -e 's/^old-//' | paste <(ls --color=never -v) - | sed -e 's/^/mv /' | bash Commented Apr 8, 2021 at 8:54
0

for a start you may try like this:

mkdir x
for base in $(ls data* | sed 's/_[0-9][0-9]*.txt//' | sort -u ) ;do
  ls ${base}_*| tac | for i in data1*; do
    read x;cp $i x/$x
  done
done

It works well only for special cases, you may have unexpected results with unpadded numbers like 1 2 11 111 or filenames like data1_somethingelse1.txt, not to mentions space or special characters in filenames.

0

Here is a simple solution and I believe it would work for your situation very well. What we are doing here is, first checking how many file types we got by prefix. Here by file type prefix I meant data1_, data2_ and so on. Then for each prefix type, we are then getting the total number of available files and storing them into an array called totalFilesForEachPrefix.

Then, in step 1 we are renaming the file to a temporary file. The reason for moving to otemp extension is to avoid name collision and overwrite the existing file. Here, I assumed that you have files like 1_1 1_2 1_3 1_4 and so forth.

Then, in step 2, we are just getting rid of .otemp extension.

#!/usr/bin/env bash
totalFileTypeByPrefix=$( for file in data*_1.txt; do echo $file; done | wc -l )
totalFilesForEachPrefix=()
for (( prefix = 1; prefix <= totalFileTypeByPrefix; prefix++ )); do
  totalFilesForEachPrefix+=( $( for file in data${prefix}_*.txt; do echo $file; done | wc -l) )
done
### Step 1
prefix=1; type=0;
while (( prefix <= totalFileTypeByPrefix )); do
  suffix=${totalFilesForEachPrefix[$type]}
  for file in data${prefix}_*.txt; do
    mv $file data${prefix}_${suffix}.txt.otemp; echo "$file renamed temporary --> data${prefix}_${suffix}.txt.otemp"
    suffix=$((suffix -1))
  done
  type=$((type+1))
  prefix=$((prefix+1))
done
### Step 2
echo "....Finally changing the temporary files"....
for tempfile in *.otemp; do
  file=${tempfile::-6};
  mv $tempfile $file; echo "$tempfile renamed final --> $file";
done

Here is the output with an explanation of whats happening:

data1_1.txt renamed temporary --> data1_3.txt.otemp
data1_2.txt renamed temporary --> data1_2.txt.otemp
data1_3.txt renamed temporary --> data1_1.txt.otemp
data2_1.txt renamed temporary --> data2_2.txt.otemp
data2_2.txt renamed temporary --> data2_1.txt.otemp
....Finally changing the temporary files....
data1_1.txt.otemp renamed final --> data1_1.txt
data1_2.txt.otemp renamed final --> data1_2.txt
data1_3.txt.otemp renamed final --> data1_3.txt
data2_1.txt.otemp renamed final --> data2_1.txt
data2_2.txt.otemp renamed final --> data2_2.txt
0
-1

A stupid, but always functional method (following The KISS principle) is this:

/bin/ls data*.txt >rename.sh
# edit the file so that instead of
# data1_1.txt
# each line reads
# mv data1_1.txt data1_3.txt
# (you can use column selection in your editor)
sh rename.sh

Of course, it is obvious, but usually doing this is a way faster than fiddling with complicated (and one-of anyway) scripts.

(edited later) Comments are right, that in this case using this technique can lead to the loss of data (data1_3.txt can be wiped out). Therefore it is safe to make move in two steps: first change names, but move the file to some safe location (either add arbitrary false extension, or create a temporary subdirectory, where all files are moved), and then in the second step move all files to their final destination.

2
  • This is prone to fall into the trap I mentioned in my comment: you really have to be very careful to avoid trashing one or more of the files. Commented Dec 14, 2019 at 21:40
  • This would overwrite an existing file. More info at: unix.stackexchange.com/questions/557157/… Commented Dec 15, 2019 at 3:47

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.