Find and rename files by pattern works in Debian, but not in CentOS7 - bash

I need to find and rename files with question mark in names.
Example: "style.css?ver=111" should become "style.css"
I use this command
find . -type f -name "*\?*" -exec rename 's/\?.*//' '{}' \;
In Debian all works fine, but in CentOS7 I get and error that "rename: not enough arguments
"
Any ideas why?

For a reliable option that should work in any POSIX-compliant system, you may use
find . -type f -name "*\?*" -exec sh -c 'mv -- "$1" "${1%%\?*}"' findshell {} \;
$1 is the name of each file found and ${1%%\?*} is a construct that strips the substring starting from the question mark.
That should be enough if you have a few matching files. If you need it, a more efficient alternative is
find . -type f -name "*\?*" -exec sh -c '
for file in "$#"; do
mv -- "$file" "${file%%\?*}"
done
' findshell {} +

Related

Unix find -exec: Why do the following behaviors differ?

The following works as intended:
$ find . -name .git -exec dirname '{}' \;
./google/guava
./JetBrains/intellij-community
./zsh-users/zsh-syntax-highlighting
But the following only returns dots:
$ find . -name .git -exec echo "$(dirname '{}')" \;
.
.
.
Why is that, and how can I use $(dirname '{}') in a find -exec command?
Please note, I am asking about UNIX find (OS X and FreeBSD, specifically), not GNU.
Reason for behaviour difference
Your shell is evaluating the $(dirname) before find, leading to this command being executed :
find . -name .git -exec echo . ;
Other ways to do this
You can of course use shell expansion inside find, by calling another shell yourself (or better, calling a script using the shell you want as shebang).
In other words:
find . -name .git -exec sh -c 'dirname {}' \;
Solution without dirname (POSIX, faster, one less subprocess to call) :
find . -name .git -exec sh -c 'path={}; echo "${path%/*}" ' \;
Combing /u/tripleee's answer (upvote him not me) with the find optimization :
find . -name .git -exec sh -c 'for f; do echo "${f%/*}"; done' _ {} \+
If you're using gnu find then you can ditch dirname and use printf:
find . -name .git -printf "%h\n"
The general answer is to run -exec sh (or -exec bash if you need Bash features).
find . -name .git -exec sh -c 'for f; do dirname "$f"; done' _ {} \+
dirname can easily be replaced with something simpler, but in the general case, this is a useful pattern for when you want a shell to process the results from find.
-exec ... \+ is running the shell on as many matches as possible, instead of executing a separate shell for each match. This optimization is not available in all versions of find.
If you have completely regular file names (no newlines in the results, etc) you might be better off with something like
find . -name .git | sed 's%/[^/]*$%%'
However, assuming that file names will be regular is a huge recurring source of bugs. Don't do that for a general-purpose tool.

Shell generic equivalent of Bash Substring replacement ${foo/a/b}

Is there a shell-independent equivalence of Bash substring replacement:
foo=Hello
echo ${foo/o/a} # will output "Hella"
Most of the time I can use bash so that is not a problem, however when combined with find -exec it does not work. For instance, to rename all .cpp files to .c, I'd like to use:
# does not work
find . -name '*.cpp' -exec mv {} {/.cpp$/.c}
For now, I'm using:
# does work, but longer
while read file; do
mv "$file" "${file/.cpp$/.c}";
done <<< $(find . -name '*.cpp')
Ideally a solution that could be used in scripts is better!
Using find and -exec you can do this:
find . -name '*.cpp' -exec bash -c 'f="$1"; mv "$f" "${f/.cpp/.c}"' - '{}' \;
However this will fork bash -c for each filename so using xargs or a for loop like this is better for performance reasons:
while IFS= read -d '' -r file; do
mv "$file" "${file/.cpp/.c}"
done < <(find . -name '*.cpp' -print0)
Btw, an alternative to using bash would be to use rename. If you have the cool version of the rename command, which is shipped along with perl you can do:
find -name '*.cpp' -exec rename 's/\.cpp$/.c/' {} +
The above example assumes that you have GNU findutils, having this you don't need to pass the current directory since it is the default. If you don't have GNU findutils, you need to explicitly pass it:
find . -name '*.cpp' -exec rename 's/\.cpp$/.c/' {} +

In unix , moving a file with xargs after finding and zipping it?

So in a bashscript i've the following very simple line , but how can i chain it further to move the file ?
find . -type f -ctime -$2 -name "mylog*.log" | xargs bzip2
This works fine but i'd also like to move the file to a new directory once I am done with the bzip2.
One standard trick is to use a new script that does whatever you need. Here, I assume that ${OTHER_DIRECTORY} is an environment variable that says where to put the compressed files, but there are plenty of other (better!) ways to get that information to the script (such as specifying the directory as the first argument — as the last argument is a bad idea).
#!/bin/bash
for file in "$#"
do
bzip2 "$file"
mv "$file.bz2" "${OTHER_DIRECTORY:-/tmp}"
done
You then run that script with find:
find . -type f ctime -$2 -name "mylog*.log" -exec tinyscript.sh {} +
This is pretty effective. If you only want one mv command, you can consider something along the lines of:
bzip2 "$#"
bz2=()
for file in "$#"; do bz2+=( "$file.bz2" ) done
mv "${bz3[#]}" "${OTHER_DIRECTORY:-/tmp}"
This code works even if the path names contain spaces and other untoward characters.
One option might be something like this:
find . -type f -ctime -$2 -name "mylog*.log" -exec bzip2 {} \; -exec mv {} /path/to/new_dir/ \;

How to add .txt to all files in a directory using terminal

I have many files without file extention. Now I want to add .txt to all files. I tried the following but it gives an error, mv: rename . to ..txt: Invalid argument.
How can I achieve this?
find . -iname "*.*" -exec bash -c 'mv "$0" "$0.txt"' {} \;
You're nearly there!
Just add -type f to only deal with files:
find . -type f -exec bash -c 'mv "$0" "$0.txt"' {} \;
If your mv handles the -n option, you might want to use it (that's the option to not overwrite existing files).
The error your having is because . is one of the first found by found, and your system complains (rightly) when you want to rename .! with the -type f you're sure this won't happen. Now if you wanted to act on everything inside your directory, you would, e.g., add -mindepth 1 at the beginning of the find command (as . is considered depth 0).
It is not very clear in your question, but what if you want to add the extension .txt to all files that don't have an extension? (we'll agree that to have an extension means to have a period in the name). In this case, you'll use the negation of -name '*.*' as follows:
find . -type f \! -name '*.*' -exec bash -c 'mv "$0" "$0.txt"' {} \;

recursively rename directories in bash

I'd like to recursively rename all directories containing the string foo by replacing that part of the string with Bar. I've got something like this so far, but it doesn't quite work. I'd also like foo to be searched case-insensitive.
find . -type d -exec bash -c 'mv "$1" "${1//foo/Bar}"' -- {} \;
Are there any elegant one-liners that might be better than this attempt? I've actually tried a few but thought I'd defer to the experts. Note: i'm doing this on a Mac OS X system, and don't have tools like rename installed.
Try the following code using parameter expansion
find . -type d -iname '*foo*' -depth -exec bash -c '
echo mv "$1" "${1//[Ff][Oo][Oo]/BAr}"
' -- {} \;
But your best bet will be the prename command (sometimes named rename or file-rename)
find . -type d -iname '*foo*' -depth -exec rename 's#Foo#Bar#gi' {} +
And if you are using bash4 or zsh (** mean recursive):
shopt -s globstar
rename -n 's#Foo#Bar#gi' **/*foo*/
If it fit your needs, remove the -n (dry run) switch to rename for real.
SOME DOC
rename was originally written by Perl's dad, Larry Wall himself.
I suspect the problem is getting it to work with mkdir -p foo/foo/foo.
In this regard, I think a solution based on find will likely not work because the list of paths is probably predetermined.
The following is in no way elegant, and stretches the definition of a one-liner, but works for the above test.
$ mkdir -p foo/foo/foo
$ (shopt -s nullglob && _() { for P in "$1"*/; do Q="${P//[Ff][Oo][Oo]/bar}"; mv -- "$P" "$Q"; _ "$Q"; done } && _ ./)
$ find
.
./bar
./bar/bar
./bar/bar/bar
find . -type d -iname '*foo*' -exec bash -O nocasematch -c \
'[[ $1 =~ (foo) ]] && mv "$1" "${1//${BASH_REMATCH[1]}/Bar}"' -- {} \;
Pro: Avoids sed.
Con: Will not find all matches if there are multiple in different cases.
Con: Is ridiculous.
Thanks #Gilles Quenot and Wenli: The following worked for me. I based it on both of your solutions.
find . -depth -type d -name 'ReplaceMe*' -execdir bash -c 'mv "$1" "${1/ReplaceMe/ReplaceWith}"' -- {} \;
The -execdir seems to be key on linux Red hat 7.6
I've been searching similar answers and this one worked:
find . -depth -name 'foo' -execdir bash -c 'mv "$0" ${0//foo/Bar}"' {} \;

Resources