2

I'm very new to scripting (bash shell) and I'm unable to figure out the logic of this task I'm trying to accomplish.

I have a folder with file names like these (for example):

PIC-100
PIC-1
PIC-23
PIC-5

I'd like to be able to take the number from their file name, rank them in order from lowest to highest, then rename it based on their position on the ranked list.

So for example, they'll be renamed like:

PIC-100 --> PIC-4
PIC-1   --> PIC-1
PIC-23  --> PIC-3
PIC-5   --> PIC-2

Thanks all.

  • "but be able to keep the same order as they were in the folder (i.e. they won't be sorted)" ... I don't understand this - entries in a directory aren't sorted. The application showing the entries might decide to sort them in some order, but then that depends entirely on the application. – muru Jan 29 at 4:53
  • I may have overexplained myself there. You're right that it is dependent on the application that they could be sorted in some way. What I meant to say is that I would like to keep them in the order they were loaded in. Sorry for the confusion, I'll removed that portion in the question. – szenty Jan 29 at 4:56
  • Loaded into what? An array? Do you already have a script that you're looking to modify or add to? – muru Jan 29 at 4:57
  • What I meat by loaded into was the way they were inputted into the folder. At the moment, I'm having trouble working the logic of what I'd like to do so I don't currently have anything for a script. – szenty Jan 29 at 4:59
  • I think this is an XY problem. Why do you need to preserve that order? What uses that order? Do you actually want to preserver modification times or something? – muru Jan 29 at 5:01
1

I'll show you how I'd approach this problem bit by bit. First, let's create some test data:

mkdir /tmp/blah && cd /tmp/blah
for f in 1 90 $RANDOM $RANDOM $RANDOM; do touch PIC-$f; done
ls

The files should be shown sorted by name, but we want them sorted numerically. The command sort can help with this:

ls | sort -V

-V means "Versioned sort", which is fine for your case. For other options, see man sort.

Now for the rename. There's a bunch of ways to do this, but I'll loop over the output of our sorted list. We'll need a variable and a loop:

counter=1 ls PIC-* | sort -V | while read fname; do
    echo mv -i "$fname" NEWPIC-$counter
    let counter+=1
done

A lot is happening here, so I'll explain in some detail - starting with safety. First of all, BACK UP YOUR FILES before going any further - loops can go wrong very easily and cause a bad day. Here's a command to back everything up:

mkdir backup; cp PIC* backup

If we get this loop wrong we could make a mess, so I like to see what's going to happen before I run it. Notice the echo? This will print out the intended commands instead of running them. You can either remove the echo or just copy-paste the output to run the command "for real"

Next, I've run mv -i for the rename. This will prompt if I'm going to overwrite something - at that point everything has gone pear shaped anyway, but at least I'll know.

Third, notice that I'm renaming from PIC-xxx to NEWPIC-xxx. This is really important because I could accidentally rename PIC-3 over PIC-2 in certain cases (e.g. if there is a PIC-2.5 for some reason). We can clean up the NEW part later.

Also, I start the command with counter=1 ls | sort -V | .... By setting the variable in the command, I avoid seeing "strange" behaviour when I forget to set it between test runs - where the destination filenames would start at NEWPIC-6 or something instead of NEWPIC-1. This is one of those habits you pick up after making the mistake a few times :)

Now for the loop itself.

  • while read fname says "every time you see a line of input, put the line into a variable fname and run the next part
  • do .. done surrounds the code you want to run for each line
  • echo mv -i "$fname" NEWPIC-$counter prints out the command you want to run
  • let counter+=1 increments your counter variable. If you forget this, you would end up with one file called NEWPIC-1 and all your other files would be gone. This is why we have backups :)

Lastly, here's a little trick to remove that NEW from the start of the file name:

for fname in NEWPIC-*; do echo mv $fname ${fname#NEW}; done

As before, remove the echo when you are happy with the output.

| improve this answer | |
  • Hey, thanks for you answer and detailed explanation! Great tips as well – szenty Jan 29 at 6:54
  • 1
    This seems to assume that there are no other files in the current directory, and will rename all files into NEWPIC-n (where n is an integer, except for the first one, where it's empty, because...). You also set counter uselessly in the environment of ls to 1 (this means that the first file will be called NEWPIC- with no number). You should use counter=1; ls ... to fix this. Any file whose name starts with a dash may upset the mv command. You should do mv -i -- "$fname" ... to fix this. Old files called NEWPIC-n may be overwritten. – Kusalananda Jan 29 at 7:50
  • Thanks for the feedback @Kusalananda, I have updated the post – dwurf Jan 30 at 22:31
1

This annotated shell script could do what you might want. The semicolons means execute commands sequentially, the vertical bars means pipeline, the hash sign starts comments, the "do-done" pair is the loop enclosed in "while"

n=1            ; # initialize counter
ls             | # list current working directory
sort -V        | # sort (thanks to dwurf for the flag, but this isn't in POSIX standard yet)
while read f   ; do
mv "$f" PIC-$n ; # rename file 
n=$((n+1))     ; # counter increment. 
done
| improve this answer | |
  • A couple of bugs: this will create files named PIC-PIC-1 given the existing files already start with PIC-. The sort -n also won't work because the filenames don't start with numbers. – dwurf Jan 29 at 6:21
  • Noted, this is inferior to your answer, shame on me... – DannyNiu Jan 29 at 6:22
  • I noted the -n and -V flags, but double-prefix PIC-PIC wouldn't happen because I didn't name the second argument to mv as "$f-$n". Also, after sorting, source ranking index would always be no less than the target naming index if my maths are correct. – DannyNiu Jan 29 at 6:29
  • Great answer! I was researching on sort and could sort work if we specify it like this: sort -n -t- -k2, since all my files are named like PIC-[number] – szenty Jan 29 at 6:55
1

Using the zsh shell:

tmpdir=$(mktemp -d)

n=1
for name in PIC-*(.n); do
    mv $name $tmpdir/PIC-$(( n++ ))
done

mv $tmpdir/PIC-* .
rmdir $tmpdir

This moves+renames all regular files whose names matches PIC-* in the current directory into a temporary directory. The files are moved in sorted numerical order based on the number in the filename (this is what the n in the (.n) glob qualifier does; the . selects only regular files). For each file, the file is renamed into PIC-n where n is a counter.

Once all files have been moved and renamed, they are moved back into the current directory and the temporary directory is deleted.

The use of a temporary directory as a staging area is to avoid name collisions.

If there are several thousands of files, then the second mv may need to be executed in a loop instead to avoid an "argument list too long" error:

# first loop as before, then...

for name in $tmpdir/PIC-*; do
    mv $name .
done

rmdir $tmpdir

Doing the operations in "reverse order" will additionally give you a backup of the files with their original names in the temporary directory if you cp the file instead of mv them at the end:

tmpdir=$(mktemp -d)

mv PIC-*(.) $tmpdir

n=1
for name in $tmpdir/PIC-*(n); do
    # change "mv" to "cp" (and remove "rmdir" below)
    # to leave a copy in the temporary directory
    mv $name PIC-$(( n++ ))
done

rmdir $tmpdir
| improve this answer | |
  • The question had been (explicitly?) tagged with "bash", but zsh features such as ones used here may be applicable to bash since I'm not familiar with many "non-POSIX" features. – DannyNiu Jan 29 at 9:30
  • 2
    @DannyNiu The question was tagged with bash, but since a bash solution would use a number of external tools, such as sort and ls, you might as well use the zsh utility, which has much of the functionality built-in. It's not different from making use of e.g. awk. I'm also noting that the question itself does not contain any code, so there's no bash script to look at, comment on, or correct. – Kusalananda Jan 29 at 10:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.