Renaming and moving wildcard files with Bash - linux

I'm trying to do the following with this bash script but i've come unstuck..
1) Find every file with the extension .mp3 in the /usr/incoming/ directory
2) Use eyeD3 to strip all of the existing ID3 tags
3) Use eyeD3 to write a title tag "NEW NAME" back to the file
4) Use mv to rename every file with the extension .mp3 to latest.mp3 and then force move it (so it will overwrite any other file with the same name) to the usr/complete directory.
It's all working apart from the last bit (No.4).
I know i'm doing something wrong with the mv command but I'm not sure what.
Here's the code :
find /usr/incoming/ -name '*.mp3' \
-exec eyeD3 --remove-all -t 'NEW NAME' '{}' \; \
-exec mv -f '*.mp3' latest.mp3 /usr/complete \;
Can anybody show me the error of my ways? ;)

Change the *.mp3 in your later command to {} to pass in an explicit name of the file you just tagged. mv will refuse to rename multiple files to the same name in a single invocation -- when passed more than two arguments, it requires the last to be a directory -- and anyhow, anything given as an argument to find's -exec is passed as a literal argument, not through a shell, so globs aren't expanded, redirections aren't processed, etc. except for find's own special strings such as {}.
find /usr/incoming/ -name '*.mp3' \
-exec eyeD3 --remove-all -t 'NEW NAME' {} ';' \
-exec mv -f {} /usr/complete/latest.mp3 ';'

Related

How to find filename in file content with bash script?

I wrote this command in my shell but it's not doing what I wanted and I can't figure out what I did wrong. I want to get files from current directories which include their names in their content.
find -type f -exec grep -il {} +
I understand find -type f gives me standard files from the directory and -exec executes following grep command with pattern - which is filename - on given file ('+'). Am I right? Because it seems like I don't understand something since it's not finding my file specially created for this purpose.
on given file ('+')
+ executes the command on all found files. The {} is replaced by the list of files in current directory.
Debug with -exec echo grep -il {} +.
You want:
-exec grep -Fil {} {} ';'
To search the filename {} as a pattern in the file named {}. The ';' terminates the command.
I also added -F to interpret pattern literally.
File in current directory test.txt
Content of file includes "test"
find . -name 'test.txt' -exec grep -i 'test' {} \;

Move command error target is not directory UNIX

I am new to unix and I am trying to write a bash that moves all files that ends to .c to another folder. When I'm executing command:
find ~/testfiles -name '*.c' -exec mv -i ~/destination {} + I got an error: mv: target '/home/user/testfiles/dir3/sourcefile1.c' is not a directory.
The testfiles folder includes files and folders.
I am using ubuntu 20.04 if that helps..Thanks for your time.
In find -exec the {} is replaced by the file/directory found.
The mv command used as in your example is trying to move the first argument to the second argument so your arguments are in the wrong order.
This would be correct:
[[ -d ~/destination ]] && find ~/testfiles -name '*.c' -exec mv -i '{}' ~/destination \;
A slightly more effetive way would be to use mvs -t (target) option which means that you can use the + in find (to supply as many arguments to mv in one go as possible):
[[ -d ~/destination ]] && find ~/testfiles -name '*.c' -exec mv -i -t ~/destination '{}' +
I added [[ -d ~/destination ]] && before the find command since one common mistake is to move a lot of files to the same destination and if it doesn't exist (or is a file and not a directory) it'll create a file named destination and overwrite that with all the files making you loose all your files (except the last one moved).
Explanation for the shift from + to \; in -exec taken directly from my man find page:
-exec command ;
All following arguments to find are taken to be arguments to the command until an argument consisting of ; is encountered. The string
{} is replaced by the current file name being processed everywhere it occurs in the arguments to the command, ... The specified command is run once for each matched file. ...
-exec command {} +
This variant of the -exec action runs the specified command on the selected files, but the command line is built by appending each selected file name at the end; The command line is built in much the same way that xargs builds its command lines. Only one instance of {} is allowed within the command, ...
Note that the syntax differs and that {} + must be at the end (where it's appending files) for the second version.

Remove common strings in multiple files and copy them

I would like to copy and rename all files in a folder that are matching this pattern:
test.mp4.small.mp4
test2.mp4.small.mp4
test3.mp4.small.mp4
All files have in common that they end with .mp4.small.mp4.
I would like to copy them to a new folder and rename them - the result should be:
test.mp4
test2.mp4
test3.mp4
Attempt:
find . -name '*.mp4.small.mp4' -exec bash -c 'echo cp $0 ${0/PATH/}' {} \;
But this does not rename it.
Use the parameter expansion syntax for removing the shortest string from the beginning upto delimiter . of syntax ${word%string} as
find . -name '*.mp4.small.mp4' -exec bash -c 'echo cp $0 ${0%.mp4.small.mp4}.mp4' {} \;
Also I assume, you knowingly have added the echo to just troubleshoot how the renamed string looks like. To actually copy the files, drop it in the final command.
rename 's/.mp4.small//' *mp4.small.mp4
will not overwrite by default.
to test just to make sure:
ls *mp4.small.mp4 | sed 's/.mp4.small//' #list resulting file names

Creating a file in a directory other than root using bash

I am currently working on an auto grading script for a class project. It has to be able to search any number of given directories lets say
for example
usr/autograder/jdoe/
jdoe contains two files house.c and readme.txt.
I need to create a file in jdoe called jdoe.pdf
Currently i'm using this line of code below to get the path to where i need to create the file. Where $1 is user input of the path containing the projects the auto grader will grade.
find $1 -name "*.txt" -exec sh -c "dirname {}"
When I try adding /somename.pdf to the end of this statement I get readme.txt/somename.pdf
along with another -exec to get the name for the file.
\; -exec sh -c "dirname {} xargs -n 1 basename" \;
I'm having problems combining these two into one working statement.
I'm new to unix programming and would appreciate any advice or help even if it means re-writing the code using different unix tools.
The main question here is how do I create files in a path other than the directory I call my script from. Thanks in advance.
How about this?
find "$1" -name "*.txt" -exec bash -c 'd=$(dirname "$1"); touch $d"/"$(basename "$d").pdf' - {} \;
You can create files in another path using change directory command (cd).
If you start your script in usr/autograder/script and want to change to usr/autograder/jdoe you can change directory with shell command cd ../jdoe (relative) or cd usr/autograder/jdoe (absolute).
Now you are in the directory of usr/autograder/jdoe and you are able to create files in this directory, for example gedit readme.txt will open gedit and creates the file in usr/autograder/jdoe.
The simplest way is to loop over the files returned by find and then do whatever you need to do.
For example:
find "$1" -type f -name "*.txt" -print0 | while IFS= read -r -d $'\0' filename; do
dir=$(dirname "$filename")
# create pdf file
touch "$dir/${dir##*/}.pdf"
done
(Note the use of find -print0 to correctly handle filenames containing whitespace and newline characters.)
Is this what you are looking for?
function process_file {
dir=$(dirname "$1")
name=$(basename "$1")
echo name is $name and dir is $dir;
cd "$dir"
touch "${dir##*/}.pdf" # or anything else
}
# export the function, so that it is known in the child processes
export -f process_file
find . -name '*.txt' -exec bash -c "process_file '{}'" \;

Find multiple files and rename them in Linux

I am having files like a_dbg.txt, b_dbg.txt ... in a Suse 10 system. I want to write a bash shell script which should rename these files by removing "_dbg" from them.
Google suggested me to use rename command. So I executed the command rename _dbg.txt .txt *dbg* on the CURRENT_FOLDER
My actual CURRENT_FOLDER contains the below files.
CURRENT_FOLDER/a_dbg.txt
CURRENT_FOLDER/b_dbg.txt
CURRENT_FOLDER/XX/c_dbg.txt
CURRENT_FOLDER/YY/d_dbg.txt
After executing the rename command,
CURRENT_FOLDER/a.txt
CURRENT_FOLDER/b.txt
CURRENT_FOLDER/XX/c_dbg.txt
CURRENT_FOLDER/YY/d_dbg.txt
Its not doing recursively, how to make this command to rename files in all subdirectories. Like XX and YY I will be having so many subdirectories which name is unpredictable. And also my CURRENT_FOLDER will be having some other files also.
You can use find to find all matching files recursively:
find . -iname "*dbg*" -exec rename _dbg.txt .txt '{}' \;
EDIT: what the '{}' and \; are?
The -exec argument makes find execute rename for every matching file found. '{}' will be replaced with the path name of the file. The last token, \; is there only to mark the end of the exec expression.
All that is described nicely in the man page for find:
-exec utility [argument ...] ;
True if the program named utility returns a zero value as its
exit status. Optional arguments may be passed to the utility.
The expression must be terminated by a semicolon (``;''). If you
invoke find from a shell you may need to quote the semicolon if
the shell would otherwise treat it as a control operator. If the
string ``{}'' appears anywhere in the utility name or the argu-
ments it is replaced by the pathname of the current file.
Utility will be executed from the directory from which find was
executed. Utility and arguments are not subject to the further
expansion of shell patterns and constructs.
For renaming recursively I use the following commands:
find -iname \*.* | rename -v "s/ /-/g"
small script i wrote to replace all files with .txt extension to .cpp extension under /tmp and sub directories recursively
#!/bin/bash
for file in $(find /tmp -name '*.txt')
do
mv $file $(echo "$file" | sed -r 's|.txt|.cpp|g')
done
with bash:
shopt -s globstar nullglob
rename _dbg.txt .txt **/*dbg*
find -execdir rename also works for non-suffix replacements on basenames
https://stackoverflow.com/a/16541670/895245 works directly only for suffixes, but this will work for arbitrary regex replacements on basenames:
PATH=/usr/bin find . -depth -execdir rename 's/_dbg.txt$/_.txt' '{}' \;
or to affect files only:
PATH=/usr/bin find . -type f -execdir rename 's/_dbg.txt$/_.txt' '{}' \;
-execdir first cds into the directory before executing only on the basename.
Tested on Ubuntu 20.04, find 4.7.0, rename 1.10.
Convenient and safer helper for it
find-rename-regex() (
set -eu
find_and_replace="$1"
PATH="$(echo "$PATH" | sed -E 's/(^|:)[^\/][^:]*//g')" \
find . -depth -execdir rename "${2:--n}" "s/${find_and_replace}" '{}' \;
)
GitHub upstream.
Sample usage to replace spaces ' ' with hyphens '-'.
Dry run that shows what would be renamed to what without actually doing it:
find-rename-regex ' /-/g'
Do the replace:
find-rename-regex ' /-/g' -v
Command explanation
The awesome -execdir option does a cd into the directory before executing the rename command, unlike -exec.
-depth ensure that the renaming happens first on children, and then on parents, to prevent potential problems with missing parent directories.
-execdir is required because rename does not play well with non-basename input paths, e.g. the following fails:
rename 's/findme/replaceme/g' acc/acc
The PATH hacking is required because -execdir has one very annoying drawback: find is extremely opinionated and refuses to do anything with -execdir if you have any relative paths in your PATH environment variable, e.g. ./node_modules/.bin, failing with:
find: The relative path ‘./node_modules/.bin’ is included in the PATH environment variable, which is insecure in combination with the -execdir action of find. Please remove that entry from $PATH
See also: https://askubuntu.com/questions/621132/why-using-the-execdir-action-is-insecure-for-directory-which-is-in-the-path/1109378#1109378
-execdir is a GNU find extension to POSIX. rename is Perl based and comes from the rename package.
Rename lookahead workaround
If your input paths don't come from find, or if you've had enough of the relative path annoyance, we can use some Perl lookahead to safely rename directories as in:
git ls-files | sort -r | xargs rename 's/findme(?!.*\/)\/?$/replaceme/g' '{}'
I haven't found a convenient analogue for -execdir with xargs: https://superuser.com/questions/893890/xargs-change-working-directory-to-file-path-before-executing/915686
The sort -r is required to ensure that files come after their respective directories, since longer paths come after shorter ones with the same prefix.
Tested in Ubuntu 18.10.
Script above can be written in one line:
find /tmp -name "*.txt" -exec bash -c 'mv $0 $(echo "$0" | sed -r \"s|.txt|.cpp|g\")' '{}' \;
If you just want to rename and don't mind using an external tool, then you can use rnm. The command would be:
#on current folder
rnm -dp -1 -fo -ssf '_dbg' -rs '/_dbg//' *
-dp -1 will make it recursive to all subdirectories.
-fo implies file only mode.
-ssf '_dbg' searches for files with _dbg in the filename.
-rs '/_dbg//' replaces _dbg with empty string.
You can run the above command with the path of the CURRENT_FOLDER too:
rnm -dp -1 -fo -ssf '_dbg' -rs '/_dbg//' /path/to/the/directory
You can use this below.
rename --no-act 's/\.html$/\.php/' *.html */*.html
This command worked for me. Remember first to install the perl rename package:
find -iname \*.* | grep oldname | rename -v "s/oldname/newname/g
To expand on the excellent answer #CiroSantilliПутлерКапут六四事 : do not match files in the find that we don't have to rename.
I have found this to improve performance significantly on Cygwin.
Please feel free to correct my ineffective bash coding.
FIND_STRING="ZZZZ"
REPLACE_STRING="YYYY"
FIND_PARAMS="-type d"
find-rename-regex() (
set -eu
find_and_replace="${1}/${2}/g"
echo "${find_and_replace}"
find_params="${3}"
mode="${4}"
if [ "${mode}" = 'real' ]; then
PATH="$(echo "$PATH" | sed -E 's/(^|:)[^\/][^:]*//g')" \
find . -depth -name "*${1}*" ${find_params} -execdir rename -v "s/${find_and_replace}" '{}' \;
elif [ "${mode}" = 'dryrun' ]; then
echo "${mode}"
PATH="$(echo "$PATH" | sed -E 's/(^|:)[^\/][^:]*//g')" \
find . -depth -name "*${1}*" ${find_params} -execdir rename -n "s/${find_and_replace}" '{}' \;
fi
)
find-rename-regex "${FIND_STRING}" "${REPLACE_STRING}" "${FIND_PARAMS}" "dryrun"
# find-rename-regex "${FIND_STRING}" "${REPLACE_STRING}" "${FIND_PARAMS}" "real"
In case anyone is comfortable with fd and rnr, the command is:
fd -t f -x rnr '_dbg.txt' '.txt'
rnr only command is:
rnr -f -r '_dbg.txt' '.txt' *
rnr has the benefit of being able to undo the command.
On Ubuntu (after installing rename), this simpler solution worked the best for me. This replaces space with underscore, but can be modified as needed.
find . -depth | rename -d -v -n "s/ /_/g"
The -depth flag is telling find to traverse the depth of a directory first, which is good because I want to rename the leaf nodes first.
The -d flag on rename tells it to only rename the filename component of the path. I don't know how general the behavior is but on my installation (Ubuntu 20.04), it could be the file or the directory as long as it is the leaf node of the path.
I recommend the -n (no action) flag first along with -v, so you can see what would get renamed and how.
Using the two flags together, it renames all the files in a directory first and then the directory itself. Working backwards. Which is exactly what I needed.
classic solution:
for f in $(find . -name "*dbg*"); do mv $f $(echo $f | sed 's/_dbg//'); done

Resources