Iterating over directory and replacing '+' from a file name to '_' in bash - bash

I have a directory named as assets, which in further has a set of directories.
user_images
|-content_images
|-original
|-cropped
|-resize
|-gallery_images
|-slider_images
|-logo
These folders can have folders like original, cropped, resize. And these folders further will have images. These images are named something like this – 14562345+Image.jpeg. I need to replace all the images/files that have + to _.
for f in ls -a;
do
if [[ $f == ​+​ ]]
then
cp "$f" "${f//+/_}"
fi
done
I was able to do this in the current directory. But I need to iterate this to other many other directories. How can I do that?

You can use this loop using find in a process substitution:
cd user_images
while IFS= read -r -d '' f; do
echo "$f"
mv "$f" "${f//+/_}"
done < <(find . -name '*+*' -type f -print0)

With find -exec:
find user_images -type f \
-exec bash -c '[[ $0 == *+* ]] && mv "$0" "${0//+/_}"' {} \;
Notice that this uses mv and not cp as the question states "rename", but simply replace by cp if you want to keep the original files.
The bash -c is required to be able to manipulate the file names, otherwise we could use {} directly in the -exec action.

The following will run recursively and will rename all files replacing + with a _ :
find . -name '*+*' -type f -execdir bash -c 'for f; do mv "$f" "${f//+/_}"' _ {} +
Notice the use of -execdir :
Like -exec, but the specified command is run from the subdirectory containing the matched file, which is not normally the directory in which you started find -- Quoted from man find.
Which will protect us in case of directory names matching the pattern *+* which you do not want to rename.

Related

bash doing different actions for different filetypes, for all files in directory

So I want to change the meta title of all my movies in a directory and its subdirectories
for file in *; do
if [[ $file == *.mkv ]]
then
mkvpropedit --set "title=$file" "$file";
elif [[ $file == *.mp4 ]]
then
exiftool "-Title<Filename" *.mp4 -overwrite_original -r
else
echo "$file wrong filename"
fi
done
That's my general idea so far, but it doesn't find the files. The commands should work, but the if conditions aren't even enterd. Also just using * doesn't search subdirectories.
With bash >= 4.0:
shopt -s globstar # enable globstar
for file in **; do
case "${file##*.}" in # extract suffix
mkv) echo "do something with $file"
;;
mp4) echo "do something with $file"
;;
*) echo "unknown suffix at $file"
;;
esac
done
You can use two find commands:
find . -type f -name '*.mkv' -exec mkvpropedit --set "title={}" "{}" \;
find . -type f -name '*.mp4' -exec exiftool "-Title<Filename" "{}" -overwrite_original -r \;
The first find recursively searches the working directory for any files with names that end in .mkv, and then runs the following command (i.e., the command is everything between the -exec and the \;), replacing all {}s with the name of the file.
The second find does essentially the same thing, but for files with names that end in .mp4.
For more info on find, see its Linux man page or the GNU manual.
If you want to get just the filename to set the title, you can use the following:
find . -type f -name '*.mkv' -exec sh -c 'mkvpropedit --set "title=$(basename "$1")" "$1"' sh '{}' \;
basename takes a path as an argument and returns just the last part of the path (i.e., the directory name or filename).
sh -c 'mkvpropedit --set "title=$(basename "$1")" "$1"' sh '{}' runs sh with the command mkvpropedit --set "title=$(basename "$1")" "$1", where the argument ($1) to the command is one of the filenames found by find. (The filename is passed as an argument this way to avoid command injections.)
This script will attempt to edit all relevant files in the specified directory, recursively:
#!/bin/sh
target_dir=${1:?no target dir provided}
cd "$target_dir" || exit 1
# pass the mkv file paths to a shell loop, to strip the leading path
find . -mindepth 1 -type f -iname '*.mkv' \
-exec sh -c '
for i; do
name=${i##*/}
mkvpropedit --set title="$name" "$i"
done
' _ {} +
# exiftool can target specific extensions, and operate recursively
# use -r. to include hidden sub-directories
exiftool -r -ext mp4 -overwrite_original -Title'<Filename' .
Use like /path/to/myscript /path/to/media-dir.
File name extensions are case insensitive for both commands.
Remember that you can use . to target the current directory. If you want, you could change the script to use the current directory by default, if no argument is given.
I used sh. Writing for bash would be identical.

Rename files in several subdirectories

I want to rename a file present in several subdirectories using bash script.
my files are in folders:
./FolderA/ABCD/ABCD_Something.ctl
./FolderA/EFGH/EFGH_Something.ctl
./FolderA/WXYZ/WXYZ_Something.ctl
I want to rename all of the .ctl file with the same name (name.ctl).
I tried several command using mv or rename but didnt work.
Working from FolderA:
find . -name '*.ctl' -exec rename *.ctl name.ctl '{}' \;
or
for f in ./*/*.ctl; do mv "$f" "${f/*.ctl/name .ctl}"; done
or
for f in $(find . -type f -name '*.ctl'); do mv $f $(echo "$f" | sed 's/*.ctl/name.ctl/'); done
Can you help me using bash?
thanks
You can do this with one line with:
find . -name *.ctl -exec sh -c 'mv "$1" `dirname "$1"`/name.ctl' x {} \;
The x just allows the filename to be positional character 1 rather than 0 which (in my opinion) wrong to use as a parameter.
Try this:
find . -name '*.ctl' | while read f; do
dn=$(dirname "${f}")
# remove the echo after you sanity check the output
echo mv "${f}" "${dn}/name.ctl"
done
find should get all the files you want, dirname will get just the directory name, and mv will perform the rename. You can remove the quotes if you're sure that you'll never have spaces in the names.

Search recursively for a list of filenames in shell

I have a list of file names inside filenames.txt like the following:
1.sql
2.sql
3.sql
..
..
500.sql
I want to search for the file names in a directory and its sub-directories like the following:
Dir1/1.sql
Dir1/2.sql
Dir2/3.sql
Dir3/4.sql
Dir4/5.sql
Dir4/6.sql
..
..
etc
and copy the founded files to another directory.
I tried:
$ for i in `cat filenames.txt`; do `find ./* -type f -printf "%f\n"|grep -ie "$i" && cp -t "$i" /home/user/other_directory/"$i"`; done
but this doesn't work.
because the first argument of cp is "$i" (the matching pattern) and not the filename
find . -type f -iname "$i" -exec cp -t "{}" /home/user/other_directory \;

Bash script find behaviour

for subj in `cat dti_list.txt`; do
echo $subj
find . -type f -iname '*306.nii' -execdir bash -c 'rename.ul "$subj" DTI_MAIN_AP.nii *.nii' \+
done
I have some trouble with a small bash script, which adds the name instead of replacing when I use the rename.ul function.
Currently, the code adds DTI_MAIN_AP.nii in front of the old name.
My goal is to replace the name from the subj list and using the find to search up any directory with a *306.nii file, and then using execdir to execute the rename.ul function to rename the file from the dti_list.txt.
Any solution, or correction to get the code working, will be appreciated.
If you just want to rename the first file matching *306.nii in each directory to DTI_MAIN_AP.nii, that might look like:
find . -type f -iname '*306.nii' \
-execdir sh -c '[[ -e DTI_MAIN_AP.nii ]] || mv "$1" DTI_MAIN_AP.nii' _ {} +
If instead of matching on *306.nii you want to iterate over names from dti_list.txt, that might instead look like:
while IFS= read -r -d '' filename <&3; do
find . -type f -name "$filename" \
-execdir sh -c '[[ -e DTI_MAIN_AP.nii ]] || mv "$1" DTI_MAIN_AP.nii' _ {} +
done <dti_list.txt
References of note:
BashFAQ #1 (on reading files line-by-line)
Using Find

Bash - Rename ".tmp" files recursively

A bunch of Word & Excel documents were being moved on the server when the process terminated before it was complete. As a result, we're left with several perfectly fine files that have a .tmp extension, and we need to rename these files back to the appropriate .xlsx or .docx extension.
Here's my current code to do this in Bash:
#!/bin/sh
for i in "$(find . -type f -name *.tmp)"; do
ft="$(file "$i")"
case "$(file "$i")" in
"$i: Microsoft Word 2007+")
mv "$i" "${i%.tmp}.docx"
;;
"$i: Microsoft Excel 2007+")
mv "$i" "${i%.tmp}.xlsx"
;;
esac
done
It seems that while this does search recursively, it only does 1 file. If it finds an initial match, it doesn't go on to rename the rest of the files. How can I get this to loop correctly through the directories recursively without it doing just 1 file at a time?
Try find command like this:
while IFS= read -r -d '' i; do
ft="$(file "$i")"
case "$ft" in
"$i: Microsoft Word 2007+")
mv "$i" "${i%.tmp}.docx"
;;
"$i: Microsoft Excel 2007+")
mv "$i" "${i%.tmp}.xlsx"
;;
esac
done < <(find . -type f -name '*.tmp' -print0)
Using <(...) is called process substitution to run find command here
Quote filename pattern in find
Use -print0 to get find output delimited by a null character to allow space/newline characters in file names
Use IFS= and -d '' to read null separated filenames
I too would recommend using find. I would do this in two passes of find:
find . -type f -name \*.tmp \
-exec sh -c 'file "{}" | grep -q "Microsoft Word 2007"' \; \
-exec sh -c 'f="{}"; echo mv "$f" "${f%.tmp}.docx"' \;
find . -type f -name \*.tmp \
-exec sh -c 'file "{}" | grep -q "Microsoft Excel 2007"' \; \
-exec sh -c 'f="{}"; echo mv "$f" "${f%.tmp}.xlsx"' \;
Lines are split for readability.
Each instance of find will search for tmp files, then use -exec to test the output of find. This is similar to how you're doing it within the while loop in your shell script, only it's launched from within find itself. We're using the pipe to grep instead of your case statement.
The second -exec only gets run if the first one returned "true" (i.e. grep -q ... found something), and executes the rename in a tiny shell instance.
I haven't profiled this to see whether it would be faster or slower than a loop in a shell script. Just another way to handle things.

Resources