Need help for batch files renaming - bash

I have a folder X with lot of files:
file01x01.txt
file02x01.txt
file03x01.txt
file04 01.txt
file05 01.txt
I would like to put a "x" between the 0401 and 0501 like the first 3 files. So far I was doing something like:
for f in *.txt ; do mv "$f" "${f// /x}" ; done
This works but it applies to all the *.txt files and I would like to avoid that. How can I do it only for file04 01.txt and file05 01.txt in my example?
I was thinking maybe something like this:
for f in file[04-05].txt ; do mv "$f" "${f// /x}" ; done
but I can't get the proper syntax.

You can use * wildcards plus a quoted space ' ' to find files that have a space in their name.
for f in file*' '*.txt; do
mv "$f" "${f// /x}"
done

You can do it in this way too:
find . -name "file*[[:space:]]*.txt" -exec bash -c 'f="{}"; mv "$f" "${f// /x}"' \;

you can use the rename tool.
if your filenames have only one space, this would be enough:
$ rename -n 's/ /x/' *txt
rename(file04 01.txt, file04x01.txt)
rename(file05 01.txt, file05x01.txt)
(-n is for "dry run", remove it to perform the actual renaming)
if your filenames have more than one space character, you can use something like this:
$ rename -n 's/ (\d*.txt)$/x$1/' *txt
rename(file04 01.txt, file04x01.txt)
rename(file05 01.txt, file05x01.txt)
rename(file with spaces 06 01.txt, file with spaces 06x01.txt)

Related

Issues renaming files using bash script with input from .txt file with find -exec rename command

Update 01/12/2022
With triplee's helpful suggestions, I resolved it to take both files & directories by adding a comma in between f and d, the final code now looks like this:
while read -r old new;
do echo "replacing ${old} by ${new}" >&2
find '/path/to/dir' -depth -type d,f -name "$old" -exec rename
"s/${old}/${new}/" {} ';'
done <input.txt
Thank you!
Original request:
I am trying to rename a list of files (from $old to $new), all present in $homedir or in subdirectories in $homedir.
In the command line this line works to rename files in the subfolders:
find ${homedir}/ -name ${old} -exec rename "s/${old}/${new}/" */${old} ';'
However, when I want to implement this line in a simple bash script getting the $old and $new filenames from input.txt, it doesn't work anymore...
input.txt looks like this:
name_old name_new
name_old2 name_new2
etc...
the script looks like this:
#!/bin/bash
homedir='/path/to/dir'
cat input.txt | while read old new;
do
echo 'replacing' ${old} 'by' ${new}
find ${homedir}/ -name ${old} -exec rename "s/${old}/${new}/" */${old} ';'
done
After running the script, the text line from echo with $old and $new filenames being replaced is printed for the entire loop, but no files are renamed. No error is printed either. What am I missing? Your help would be greatly appreaciated!
I checked whether the $old and $new variables were correctly passed to the find -exec rename command, but because they are printed by echo that doesn't seem to be the issue.
If you add an echo, like -exec echo rename ..., you'll see what actually gets executed. I'd say that both the path to $old is wrong (you're not using the result of find in the -exec clause), and */$old isn't quoted and might be expanded by the shell before find ever gets to see it.
You're also having most other expansions unquoted, which can lead to all sorts of trouble.
You could do it in pure Bash (drop echo when output looks good):
shopt -s globstar
for f in **/"$old"; do echo mv "$f" "${f/%*/$new}"; done
Or with rename directly, though this would run into trouble if too many files match (drop -n when output looks good):
rename -n "s/$old\$/$new/" **/"$old"
Or with GNU find, using -execdir to run in the same directory as the matching file (drop echo when output looks good):
find -type f -name "$old" -execdir echo mv "$old" "$new" \;
And finally, a version with find that spawns just a single subshell (drop echo when output looks right):
find -type f -name "$old" -exec bash -c '
new=$1
shift
for f; do
echo mv "$f" "${f/%*/$new}"
done
' bash "$new" {} +
The argument to rename should be the file itself, not */${old}. You also have a number of quoting errors, and a useless cat).
#!/bin/bash
while read -r old new;
do
echo "replacing ${old} by ${new}" >&2
find /path/to/dir -name "$old" -exec rename "s/${old}/${new}/" {} ';'
done <input.txt
Running find multiple times on the same directory is hugely inefficient, though. Probably a better solution is to find all files in one go, and abort if it's not one of the files on the list.
find /path/to/dir -type f -exec sh -c '
for f in "$#"; do
awk -v f="$f" "f==\$1 { print \"s/\" \$1 \"/\" \$2 \"/\" }" "$0" |
xargs -I _ -r rename _ "$f"
done' input.txt {} +
(Untested; probably try with echo before you run this live.)

How to use bash string formatting to reverse date format?

I have a lot of files that are named as: MM-DD-YYYY.pdf. I want to rename them as YYYY-MM-DD.pdf I’m sure there is some bash magic to do this. What is it?
For files in the current directory:
for name in ./??-??-????.pdf; do
if [[ "$name" =~ (.*)/([0-9]{2})-([0-9]{2})-([0-9]{4})\.pdf ]]; then
echo mv "$name" "${BASH_REMATCH[1]}/${BASH_REMATCH[4]}-${BASH_REMATCH[3]}-${BASH_REMATCH[2]}.pdf"
fi
done
Recursively, in or under the current directory:
find . -type f -name '??-??-????.pdf' -exec bash -c '
for name do
if [[ "$name" =~ (.*)/([0-9]{2})-([0-9]{2})-([0-9]{4})\.pdf ]]; then
echo mv "$name" "${BASH_REMATCH[1]}/${BASH_REMATCH[4]}-${BASH_REMATCH[3]}-${BASH_REMATCH[2]}.pdf"
fi
done' bash {} +
Enabling the globstar shell option in bash lets us do the following (will also, like the above solution, handle all files in or below the current directory):
shopt -s globstar
for name in **/??-??-????.pdf; do
if [[ "$name" =~ (.*)/([0-9]{2})-([0-9]{2})-([0-9]{4})\.pdf ]]; then
echo mv "$name" "${BASH_REMATCH[1]}/${BASH_REMATCH[4]}-${BASH_REMATCH[3]}-${BASH_REMATCH[2]}.pdf"
fi
done
All three of these solutions uses a regular expression to pick out the relevant parts of the filenames, and then rearranges these parts into the new name. The only difference between them is how the list of pathnames is generated.
The code prefixes mv with echo for safety. To actually rename files, remove the echo (but run at least once with echo to see that it does what you want).
A direct approach example from the command line:
$ ls
10-01-2018.pdf 11-01-2018.pdf 12-01-2018.pdf
$ ls [0-9]*-[0-9]*-[0-9]*.pdf|sed -r 'p;s/([0-9]{2})-([0-9]{2})-([0-9]{4})/\3-\1-\2/'|xargs -n2 mv
$ ls
2018-10-01.pdf 2018-11-01.pdf 2018-12-01.pdf
The ls output is piped to sed , then we use the p flag to print the argument without modifications, in other words, the original name of the file, and s to perform and output the conversion.
The ls + sed result is a combined output that consist of a sequence of old_file_name and new_file_name.
Finally we pipe the resulting feed through xargs to get the effective rename of the files.
From xargs man:
-n number Execute command using as many standard input arguments as possible, up to number arguments maximum.
You can use the following command very close to the one of klashxx:
for f in *.pdf; do echo "$f"; mv "$f" "$(echo "$f" | sed 's#\(..\)-\(..\)-\(....\)#\3-\2-\1#')"; done
before:
ls *.pdf
12-01-1998.pdf 12-03-2018.pdf
after:
ls *.pdf
1998-01-12.pdf 2018-03-12.pdf
Also if you have other pdf files that does not respect this format in your folder, what you can do is to select only the files that respect the format: MM-DD-YYYY.pdf to do so use the following command:
for f in `find . -maxdepth 1 -type f -regextype sed -regex './[0-9]\{2\}-[0-9]\{2\}-[0-9]\{4\}.pdf' | xargs -n1 basename`; do echo "$f"; mv "$f" "$(echo "$f" | sed 's#\(..\)-\(..\)-\(....\)#\3-\2-\1#')"; done
Explanations:
find . -maxdepth 1 -type f -regextype sed -regex './[0-9]\{2\}-[0-9]\{2\}-[0-9]\{4\}.pdf this find command will look only for files in the current working directory that respect your syntax and extract their basename (remove the ./ at the beginning, folders and other type of files that would have the same name are not taken into account, other *.pdf files are also ignored.
for each file you do a move and the resulting file name is computed using sed and back reference to the 3 groups for MM,DD and YYYY
For these simple filenames, using a more verbose pattern, you can simplify the body of the loop a bit:
twodigit=[[:digit:]][[:digit:]]
fourdigit="$twodigit$twodigit"
for f in $twodigit-$twodigit-$fourdigit.pdf; do
IFS=- read month day year <<< "${f%.pdf}"
mv "$f" "$year-$month-$day.pdf"
done
This is basically #Kusalananda's answer, but without the verbosity of regular-expression matching.

File renaming using regex

I have a bunch of files in a folder. Some of them are of the format:
IMG_YYYYMMDD_junk.ext
I would like to rename such files into
YYYY-MM-DD junk.ext
Example: IMG_20170214_3939233.jpg becomes 2017-02-14 3939233.jpg
So far I was successful in filtering files I need:
find *.jpg *.jpeg *.png | egrep '^IMG_[0-9]{1,8}'
and I know I need to use sed but I am getting no where specifying and referencing match-groups in my regex for further filename transformation. I know I may have to use xarg later on in the pipe but so far I wasn't successful in transforming each file name just to print it out.
Perhaps, sed is not the best option here.
With Perl‘s standalone rename command and bash‘s option nullglob:
shopt -s nullglob
rename -n 's/.*_(....)(..)(..)_([0-9]+.*)/$1-$2-$3 $4/' *.jpg *.jpeg *.png
If everything looks fine remove option -n.
A logic in bash with NO external tools!
You can run the below script from inside the folder containing these images.
#!/bin/bash
for file in *.{jpg,jpeg,png}; do
IFS="_" read -ra fileNameList <<<"$file"
year="${fileNameList[1]:0:4}"
month="${fileNameList[1]:4:2}"
day="${fileNameList[1]:6:2}"
targetFileName="${year}-${month}-${day} ${fileNameList[2]}"
# Remove this line and uncomment the line with 'mv' if things look OK
echo "$file" "$targetFileName"
#mv -v "$file" "$targetFileName"
done
The idea is tot split the file name on _ and store them in array. Then parse the individual digits from the number and form the final name from the combined elements.
With GNU Parallel it looks like this:
find *.jpg *.jpeg *.png |
parallel mv {} '{= s/IMG_(....)(..)(..)_/$1-$2-$3 / =}'
Or:
parallel mv {} '{= s/IMG_(....)(..)(..)_/$1-$2-$3 / =}' ::: *.jpg *.jpeg *.png
I assume you're not really interested in the find command, but in nailing down the sed regex:
find ~ -type f -maxdepth 1 -name IMG*.jpg | sed -e 's/\(IMG_\)\([0-9]\{4\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)_/\2-\3-\4 /g'
Your example:
echo "IMG_20170214_3939233.jpg" | sed -e 's/\(IMG_\)\([0-9]\{4\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)_/\2-\3-\4 /g'
output:
2017-02-14 3939233.jpg
A mostly-bash solution using its regex-matching operator, =~, which supports capture groups that can be accessed via the built-in "${BASH_REMATCH[#]}" array variable:
for file in *; do
[[ $file =~ ^IMG_([0-9]{4})([0-9]{2})([0-9]{2})_(.+\.(jpg|jpeg|png))$ ]] || continue
mv "$file" "${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]} ${BASH_REMATCH[4]}"
done

Batch renaming using shell script

I have a folder with files named as
input (1).txt
input (2).txt
input (3).txt
...
input (207).txt
How do I rename them to
input_1.in
input_2.in
input_3.in
...
input_207.in
I am trying this
for f in *.txt ; do mv $f `echo $f | sed -e 's/input\ (\(\d*\))\.txt/input_\1.in/'` ; done
But it gives me
mv: target `(100).txt' is not a directory
mv: target `(101).txt' is not a directory
mv: target `(102).txt' is not a directory
...
Where did I go wrong?
I have put in the quotes now, but I get this now
mv: `input (90).txt' and `input (90).txt' are the same file
It is somehow trying to rename the file to the same name. How is that happening?
That is because bash for split the element with space ' ' so you are commanding it to move 'input' to '(1)'.
The way to solve this is to tell bash to split by new line using IFS variable.
Like this:
IFS=$'\n'
Then do your command.
However, I suggest you to use find to do this instead using -exec command.
For example:
find *.txt -exec mv "{}" `echo "{}" | sed -e 's/input\ (\([0-9]*\))\.txt/input_\1.in/'` \;
NOTE: I write this from memory and I did test this so let try and adjust it.
Hope this helps.
You're forgetting to quote your arguments.
... mv "$f" "$(echo "$f" | ... )" ; done
no need to call external commands
#!/bin/bash
shopt -s nullglob
shopt -s extglob
for file in *.txt
do
newfile="${file//[)]/}"
newfile="${file// [(]/_}"
mv "$file" "${newfile%.txt}.in"
done
As you've already fixed, you need to quote the $f argument to mv.
As to your second problem, sed doesn't support \d. You could use [0-9] instead.
for f in *.txt ; do mv "$f" `echo $f | sed -e 's/input\ (\(\d*\))\.txt/input_\1.in/'` ; done
If you have GNU Parallel http://www.gnu.org/software/parallel/ installed you can do this:
seq 1 207 | parallel -q mv 'input ({}).txt' input_{}.in
Watch the intro video for GNU Parallel to learn more:
http://www.youtube.com/watch?v=OpaiGYxkSuQ

Bash command to remove leading zeros from all file names

I have a directory with a bunch of files with names like:
001234.jpg
001235.jpg
004729342.jpg
I want to remove the leading zeros from all file names, so I'd be left with:
1234.jpg
1235.jpg
4729342.jpg
I've been trying different configurations of sed, but I can't find the proper syntax. Is there an easy way to list all files in the directory, pipe it through sed, and either move or copy them to the new file name without the leading zeros?
for FILE in `ls`; do mv $FILE `echo $FILE | sed -e 's:^0*::'`; done
sed by itself is the wrong tool for this: you need to use some shell scripting as well.
Check Rename multiple files with Linux page for some ideas. One of the ideas suggested is to use the rename perl script:
rename 's/^0*//' *.jpg
In Bash, which is likely to be your default login shell, no external commands are necessary.
shopt -s extglob
for i in 0*[^0]; do mv "$i" "${i##*(0)}"; done
Maybe not the most elegant but it will work.
for i in 0*
do
mv "${i}" "`expr "${i}" : '0*\(.*\)'`"
done
Try using sed, e.g.:
sed -e 's:^0*::'
Complete loop:
for f in `ls`; do
mv $f $(echo $f | sed -e 's:^0*::')
done
I dont know sed at all but you can get a listing by using find:
find -type f -name *.jpg
so with the other answer it might look like
find . -type f -name *.jpg | sed -e 's:^0*::'
but i dont know if that sed command holds up or not.
Here's one that doesn't require sed:
for x in *.jpg ; do let num="10#${x%%.jpg}"; mv $x ${num}.jpg ; done
Note that this ONLY works when the filenames are all numbers. You could also remove the leading zeros using the shell:
for a in *.jpg ; do dest=${a/*(0)/} ; mv $a $dest ; done
In Bash shell you can do:
shopt -s nullglob
for file in 0*.jpg
do
echo mv "$file" "${file##*0}"
done

Resources