Wednesday, November 28, 2012

Renaming files with bash (with a brief bash introduction)

Here is a one-line script to rename .xlsx files in a directory. It truncates a file name from something like s100-A1-Laura.xlsx to s100-A1.xlsx. In the case that there are two files that start with s100-A1, the second file would be named s100-A1_2.xlsx.
       


for file in $(ls *.xlsx); do echo $file; newfilename=${file%-*.xlsx};   if [ -e $newfilename".xlsx" ]; then newfilename=$newfilename"_2"; fi; echo $newfilename; mv $file $newfilename".xlsx"; done



The text below explains a bit of how this line of code functions.



A shell in unix is a command-line interpreter. It allows a user to interact with the system on a more basic level than other applications. The Linux Documentation Project has a nice introduction to BASH

There are different ways to run commands in the shell. One is to interact with the shell by typing commands one at a time. On a mac, the Terminal program allows you to do this. Another way is to write self-contained scripts -- just like you would for a program in C. Scripts, by the way, are programs that automate "tasks which could alternatively be executed one-by-one by a human operator." i.e. Scripts don't need to be compiled before executing.

Script file vs. interactive command line
Below is an example of a script file. It prints out a one number on each line. The sequence function starts at 1 and ends at 3.

       

#!/bin/sh

for num in $(seq 1 3 );
do
  echo $num;
done 


A bit of bash info:

  • Variables (like num in the example below) don't need to be explicitly declared, but when they are called for their value, their name must be preceded by a dollar sign ($num). 
  • echo is an easy way to print in bash. The statement "echo $num" prints the value of the num variable and newline (so each echo statement is on its own line unless you add extra flags)
  • You can call bash functions in a bash script. Often for this to work properly, you need to put these functions inside of parentheses preceded by a $. e.g. $(seq 1 3). In other words, the command seq 1 3 can be typed into the bash prompt and will give the same output.
If you're in a hurry, you can do this all on one line in the terminal and obtain the same output:

      


for num in $(seq 1 3); do echo $num; done



The output looks like this:

      


ifernando:~ ltucker$ for num in $(seq 1 2 10); do echo $num; done 
1
2
3
ifernando:~ ltucker$



Example: Changing the names of all files in a directory
A shell script is an efficient way to change the names of all the files in a directory.

First, let's create a bunch of files (since I don't have any that need name changing at the moment):

      


#!/bin/sh

for num in $(seq 1 3);
do
  echo $num > s100-A$num-Laura.xlsx;
done 



In this script, instead of printing the numbers to the screen, the > redirects the output of each echo statement to a file. Since each filename contains the variable $num, this script creates three different files: s100-A1-Laura.xlsx, s100-A2-Laura.xlsx, and s100-A3-Laura.xlsx. This bash script would have no output since the output that printed before to the screen has been redirected to the files s100-A<number>-Laura.xlsx. You can check with the command ls in bash that you have created new files. The ls command lists the contents of the current directory.

In one line, also showing the output from ls:

       


ifernando:bash_demo ltucker$ for num in $(seq 1 3); do echo $num > s100-A$num"-Laura.xlsx"; done 
ifernando:bash_demo ltucker$ ls
s100-A1-Laura.xlsx s100-A2-Laura.xlsx s100-A3-Laura.xlsx
ifernando:bash_demo ltucker$




Now, instead of looping over numbers in a sequence, we want to loop over a set of files so we can change their names. Here, we make use of the wildcard *

       


#!/bin/sh

for file in $(ls s100*.xlsx);
do
  echo $file;
done


In one line, with output:
       


ifernando:bash_demo ltucker$ for file in $(ls s100*.xlsx); do echo $file; done
s100-A1-Laura.xlsx
s100-A2-Laura.xlsx
s100-A3-Laura.xlsx
ifernando:bash_demo ltucker$ 



Now we are ready to change the names of the files. Because there is potential in bash to alter or delete files by typing single, short commands, it's smart to make a copy of the data and work on that copy rather than the original data whenever possible. It's safest if you copy the entire directory so that you are not in the directory with the original data.

Once in the directory with the copied data, we can change file names with the mv command. We'll use a loop that treats the file names as strings and will manipulate those strings to get the desired name for each file. This page on manipulating strings is very helpful to know how to parse substrings.

In this example, we want to get rid of the -Laura part of the filename. Some substring deletion in the filename will accomplish this.

       


#!/bin/sh
for file in $(ls s100*.xlsx);
do 
  echo $file;
  newfilename=${file%-Laura.xlsx};
  echo $newfilename
  mv $file $newfilename".xlsx"
done





In one line, with output:
       


ifernando:bash_demo ltucker$ for file in $(ls s100*.xlsx); do echo $file; newfilename=${file%-Laura.xlsx}; echo $newfilename; mv $file $newfilename".xlsx"; done
s100-A1-Laura.xlsx
s100-A1
s100-A2-Laura.xlsx
s100-A2
s100-A3-Laura.xlsx
s100-A3
ifernando:bash_demo ltucker$ ls
s100-A1.xlsx s100-A2.xlsx s100-A3.xlsx



In the case that there are multiple coders, we can add a _2 for the second coding of a student. To do this, we'll check if the first coded file exists before moving the file. If it does, we add a "_2" in the filename. This can be easily extended for more than two coders.
       


#!/bin/sh
for file in $(ls s100*.xlsx);
do 
  echo $file;
  newfilename=${file%-*.xlsx};
  if [ -e $newfilename".xlsx" ];
  then
    newfilename=$newfilename"_2";
  fi
  echo $newfilename
  mv $file $newfilename".xlsx"
done




       


for file in $(ls s100*.xlsx); do echo $file; newfilename=${file%-*.xlsx};   if [ -e $newfilename".xlsx" ]; then newfilename=$newfilename"_2"; fi; echo $newfilename; mv $file $newfilename".xlsx"; done



To check if this works, let's create a set of 3 files for Laura and two for Jason:
       


ifernando:bash_demo ltucker$ for num in $(seq 1 2); do echo $num > s100-A$num"-Jason.xlsx"; done 
ifernando:bash_demo ltucker$ ls
s100-A1-Jason.xlsx s100-A2-Jason.xlsx s100-A3-Laura.xlsx
s100-A1-Laura.xlsx s100-A2-Laura.xlsx
ifernando:bash_demo ltucker$



It works! Here is the final one-line script with sample output:
       


ifernando:bash_demo ltucker$ ls
s100-A1-Jason.xlsx s100-A2-Jason.xlsx s100-A3-Laura.xlsx
s100-A1-Laura.xlsx s100-A2-Laura.xlsx
ifernando:bash_demo ltucker$ for file in $(ls s100*.xlsx); do echo $file; newfilename=${file%-*.xlsx};   if [ -e $newfilename".xlsx" ]; then newfilename=$newfilename"_2"; fi; echo $newfilename; mv $file $newfilename".xlsx"; done
s100-A1-Jason.xlsx
s100-A1
s100-A1-Laura.xlsx
s100-A1_2
s100-A2-Jason.xlsx
s100-A2
s100-A2-Laura.xlsx
s100-A2_2
s100-A3-Laura.xlsx
s100-A3
ifernando:bash_demo ltucker$ ls
s100-A1.xlsx   s100-A1_2.xlsx s100-A2.xlsx   s100-A2_2.xlsx s100-A3.xlsx
ifernando:bash_demo ltucker$ 




Shortcuts
There are lots of helpful shortcuts, like ctrl+a moves the shell cursor to the beginning of the line, and ctrl+e moves the shell cursor to the end of the line

No comments:

Post a Comment