How do I rename files found with the find command - bash

I have a series of music folders. Some of the file names contain an underscore which I would like to get rid of.
With
find /Users/Chris/CDs -type f -name "*_*"
I find all of the files with underscores.
it appears that I can add -execdir mv {} to the command but do not know what to add from there.
I think {} provides the full path and file name as a string of the file with underscores but I do not know how to use something like sed 's/_//g' to remove the _ on the new file name.
Any help would be greatly appreciated.

Try:
find /Users/Chris/CDs -type f -name "*_*" -execdir bash -c 'mv -i -- "$1" "${1//_/}"' Mover {} \;
How it works:
-execdir bash -c '...' Mover {} \;
This starts up bash and tells it to run the command in the single quotes with Mover assigned to $0 and the file name assigned to $1.
mv -i -- "$1" "${1//_/}"
This renames file $1. This uses bash's parameter expansion feature, ${1//_/}, to create the target name from $1 by removing all underlines.
The option -i tells mv to ask interactively before overwriting a file.
The option -- tells mv that there are no more options. This is needed so that files whose names begin with - will be processed correctly.
Example
Let's start with a directory with these files:
$ ls
1_2_3_4 a_b c_d
Next we run our command:
$ find . -type f -name "*_*" -execdir bash -c 'mv -i -- "$1" "${1//_}"' Mover {} \;
After the command completes, the files are:
$ ls
1234 ab cd
The purpose of $0
Observe this command where we have added an error:
$ find . -type f -name "*_*" -execdir bash -c 'foobar -i -- "$1" "${1//_}"' Mover {} \;
Mover: foobar: command not found
Note that Mover appears at the beginning of the error message. This signals that the error comes from within the bash -c command.
If we replace Mover with -, we would see:
$ find . -type f -name "*_*" -execdir bash -c 'foobar -i -- "$1" "${1//_}"' - {} \;
-: foobar: command not found
When running a single command in a terminal, the source of the error may still be obvious anyway. If this find command were buried inside a long script, however, the use of a more descriptive $0, like Mover or whatever, could be a big help.

Related

I used the'-print0' option to handle filenames with spaces, but I get an error

#!/bin/bash
find /home/data -name '*QQ*' -print0 -exec bash -c ' mv $1 ${0/\-QQ/-TT}' {} \;
I used the'-print0' option to handle filenames with spaces, but I get an error
/home/data/gone to sea.1080p-QQ.mp4mv: target 'sea.1080p-TT.mp4' is not a directory
Which part is wrong?
Thanks
You don't need -print0, since you're not piping the output to another program.
You just need to quote properly in the bash command.
find /home/data -name '*-QQ*' -exec bash -c 'mv "$1" "${1/\-QQ/-TT}"' {} {} \;
This should work as long as the filenames don't contain double quote or $ characters.
You could also avoid bash -c by using the rename command:
find /home/data -name '*-QQ*' -exec rename 's/-QQ/-TT/' {} +

Rename all files in directory and (deeply nested) sub-directories

What is the shell command for renaming all files in a directory and sub-directory (recursively)?
I would like to add an underscore to all the files ending with *scss from filename.scss to _filename.scss in all the directories and sub-directories.
I have found answers relating to this but most if not all require you to know the filename itself, and I do not want this because the filenames differ and are a lot to know by heart or even type them manually and some of them are deeply nested in directories.
Edit: I was under the impression that the bash -c bit was somehow necessary for multiple expansion of the found element; anubhava's answer proved me wrong. I am leaving that bit in the answer for now as it worked for the OP.
find . -type f -name *scss -exec bash -c 'mv $1 _$1' -- {} \;
find . -- find in current directory (recursively)
-type f -- files
-name *scss -- matching the pattern *scss
-exec -- execute for each element found
bash -c '...' -- execute command in a subshell
-- -- end option parsing
{} -- expands to the name of the element found (which becomes the positional parameter for the bash -c command)
\; -- end the -exec command
You can use -execdir option here:
find ./src/components -iname "*.scss" -execdir mv {} _{} \;
You are close to a solution:
find ./src/components -iname "*.scss" -print0 | xargs -0 -n 1 -I{} mv {} _{}
In this approach, the "loop" is executed by xargs. I prefer this solution overt the usage of the -exec in find. The syntax is clear to me.
Also, if you want to repeat the command and avoid double-adding the underscore to the already processed files, use a regexp to get only the files not yet processed:
find ./src/components -iregex ".*/[^_][^/]*\.scss" -print0 | xargs -0 -n 1 -I{} mv {} _{}
By adding the -print0/-0 options, you also avoid problems with whitespaces.
#!/bin/sh
EXTENSION='.scss'
cd YOURDIR
find . -type f | while read -r LINE; do
FILE="$( basename "$LINE" )"
case "$LINE" in
*"$EXTENSION")
DIRNAME="$( dirname "$LINE" )"
mv -v "$DIRNAME/$FILE" "$DIRNAME/_$FILE"
;;
esac
done

Strip ./ from filename in find -execdir

Whole story: I am writing the script that will link all files from one directory to another. New file name will contain an original directory name. I use find at this moment with -execdir option.
This is how I want to use it:
./linkPictures.sh 2017_wien 2017/10
And it will create a symbolic link 2017_wien_picture.jpg in 2017/10 pointing to a file 2017_wien/picture.jpg.
This is my current script:
#!/bin/bash
UPLOAD="/var/www/wordpress/wp-content/uploads"
SOURCE="$UPLOAD/photo-gallery/$1/"
DEST="$UPLOAD/$2/"
find $SOURCE -type f -execdir echo ln -s {} $DEST/"$1"_{} ";"
It prints:
ln -s ./DSC03278.JPG /var/www/wordpress/wp-content/uploads/2017/10/pokus_./DSC03278.JPG
This is what I want:
ln -s ./DSC03278.JPG /var/www/wordpress/wp-content/uploads/2017/10/pokus_DSC03278.JPG
How to implement it? I do not know how to incorporate basename into to strip ./.
To run basename on {} you would need to execute a command through sh:
find "$SOURCE" -type f -execdir sh -c "echo ln -s '{}' \"$DEST/${1}_\$(basename \"{}\")\"" ";"
This won't win any speed contests (because of the sh for every file), but it will work.
All the quoting may look a bit crazy, but it's necessary to make it safe for files that may contain spaces.
You can use this find with bash -c:
find $SOURCE -type f -execdir bash -c 'echo ln -s "$2" "/$DEST/$1"_${2#./}' - "$1" '{}' \;
${2#./} will strip starting ./ from each entry of find command's output.
$1 will be passed as is to bash -c command line.
If you have large number of files to process I suggest using this while loop using a process substitution for faster execution since it doesn't spawn a new bash for every file. Moreover it will also handle filenames with white-spaces and other special characters:
while IFS= read -r file; do
echo ln -s "$file" "/$DEST/${1}_${file#./}"
done < <(find "$SOURCE" -type f -print0)

Interpret bash commands

I checked some resources, but still hard to find a clue to interpret the codes.
$ find . -iname "*.dwp" -exec bash -c 'mv "$0" "${0%\.dwp}.html"' {} \;
$ find . -name ".DS_Store" -exec rm {} \;
To be more specific, what's the difference between -iname and -name? And what does "-c" and "%" symbolize?
Can you interpret the two commands a bit for me?
The first one:
-iname "*.dwp", indicate to the find command to find files whose name matches the pattern *.dwp, ignore case, e.g.: ./a.dwp
-exec expression {} \; part, execute the command bash -c 'mv "$0" "${0%\.dwp}.html"' {}. {} will be replaced by the path of each file. The expression is terminated by a semicolon. If there is a file a.dwp in the current directory, bash -c 'mv "$0" "${0%\.dwp}.html"' a.dwp will execute.
bash -c 'mv "$0" "${0%\.dwp}.html"' {}:
-c means read command from string, do not start an interactive shell.
$0 is the argument of the command, a.dwp in this example.
${0%\.dwp}.html is string manipulation, % removes the shortest match from the end, so for a.dwp, remove .dwp from end to get the file name a without extension.
So the command is mv a.dwp a.html.
The second one is very simple if you understand first one.

Rename files with dynamic name in bash

I am trying to rename all files which contains 'Name' with dynamic name.
So this file 'NameSomething' should look like this 'SearchSomething'. But script below just removes 'Name' from file name.
name='search'
Name='Search'
find ../../$name-module -name 'Name*' -type f -exec bash -c 'mv "$1" "${1/Name/$Name}"' -- {} \;
You have to export Name, because otherwise the command find will not inherit the variable $Name into its environment. So:
name='search'
export Name='Search'
find ../../$name-module -name 'Name*' -type f -exec bash -c 'mv "$1" "${1/Name/$Name}"' -- {} \;
\You can escape quotes, no need for an export:
find ../../$name-module -name 'Name*' -type f -exec bash -c "mv \"\$1\" \"\${1/Name/$Name}\"" -- {} \;
You might use find to find a list of the files you want to rename and pipe that to another program to do the renaming.
For example, you could do:
find ../../$name-module -print0 -name 'Name*' -type f | \
xargs -0 rename "s/Name/$Name/"
No need for exporting (ugly + might affect other commands)
Rename might get run only once (if all the found files fit on one command line) (with -exec, you're running two programs -- bash and mv -- per each file. That might be inefficient.
(You could get further performance increase with this approach by using Gnu parallel instead of xargs, if you're on a multicore computer)
You could use the Perl-based rename command:
Name=Search
rename "s/^Name/$Name/" *

Resources