Is there any way to apply Bash variable substitution on find's output? I know I've seen someone do it on Stack Overflow but I can't seem to find that particular post anymore.
As an example, let's say I want to rename files ending in *.png to *_copy.png. I know I can do this using rename but it's just a thought experiment.
Now I'd like to be able to do something like this:
find . -name "*png" -exec mv "{}" "${{}%.*}_copy.png" \;
Which results in an invalid substitution. Of course, I could first assign the output to a variable and then apply substitution in a sub-shell, but is this really the only way?
find . -name "*.png" -exec bash -c 'var="{}"; mv "{}" "${var%.*}_copy.png"' \;
Or is there any way this can be achieved directly from {}?
Consensus
As Etan Reisner remarked, a better and safer way to handle the output of find would be to pass it as a positional argument:
find . -name "*.png" -exec bash -c 'mv "$0" "${0%.*}_copy.png"' "{}" \;
It took me a while to get the question. Basically you are asking if something like:
echo "${test.png%.png}"
could be used to get the word test.
The answer is no. The string manipulation operators starting with ${ are a subset of the parameter substitution operators. They work only on variables, not with string literals, meaning you need to store the string in a variable before. Like this:
img="test.png"
echo "${img%.png}"
Just for travellers from Google, I would use rename for this particular task. (As the OP already mentioned in his question). The command could look like this:
find -name '*png' -execdir rename 's/\.png/_copy.png/' {} +
Related
I try to use find to create a very simple way to add newline to a file. I know there are tons of other ways to do this but it bugs the hell out of me that I cannot get this way to work.
So - I'm NOT asking how to add newline to a file - I'm asking why find is weird.
find . -type f -iname 'file' -exec echo >> {} \;
results in a new file created named "{}" with the newline in while (to check that find works on my computer):
find . -type f -iname 'file' -exec echo {} \;
prints "./file".
So the >> makes find confused. The question is why and how do I solve that?
I'm asking why find is weird.
It isn't. This has nothing to do with find. In fact, when the file is created, find hasn't even started to run.
>> roughly means "redirect stdout to the end of this file, create a new file when necessary". Note how nothing of this has anything to do with whatever is left of the >>.
Redirection is a feature of the shell, find knows nothing about the redirection and the shell knows nothing about find. >> doesn't magically change its meaning just because you happened to call find. It still means the exact same thing.
If you want to use a shell feature whithin -exec, you need to use a shell within -exec:
find . -type f -iname 'file' -exec sh -c 'echo >> "{}"' \;
While the question itself has already been answered, I'd like to point out that you don't strictly need to make find do everything, rather you can use other available facilities to work together with it, for example:
find . -type f -iname 'file' | while read file; do echo >>"$file"; done
This approach also has the advantage of not executing a new process for every match, which is irrelevant in this case but potentially important if there are thousands of matches and the exec is relatively heavy.
I am figuring out a command to copy files that are modified on a Saturday.
find -type f -printf '%Ta\t%p\n'
This way the line starts with the weekday.
When I combine this with a 'egrep' command using a regular expression (starts with "za") it shows only the files which start with "za".
find -type f -printf '%Ta\t%p\n' | egrep "^(za)"
("za" is a Dutch abbreviation for "zaterdag", which means Saturday,
This works just fine.
Now I want to copy the files with this command:
find -type f -printf '%Ta\t%p\n' -exec cp 'egrep "^(za)" *' /home/richard/test/ \;
Unfortunately it doesn't work.
Any suggestions?
The immediate problem is that -printf and -exec are independent of each other. You want to process the result of -printf to decide whether or not to actually run the -exec part. Also, of course, passing an expression in single quotes simply passes a static string, and does not evaluate the expression in any way.
The immediate fix to the evaluation problem is to use a command substitution instead of single quotes, but the problem that the -printf function's result is not available to the command substitution still remains (and anyway, the command substitution would happen before find runs, not while it runs).
A common workaround would be to pass a shell script snippet to -exec, but that still doesn't expose the -printf function to the -exec part.
find whatever -printf whatever -exec sh -c '
case $something in za*) cp "$1" "$0"; esac' "$DEST_DIR" {} \;
so we have to figure out a different way to pass the $something here.
(The above uses a cheap trick to pass the value of $DEST_DIR into the subshell so we don't have to export it. The first argument to sh -c ... ends up in $0.)
Here is a somewhat roundabout way to accomplish this. We create a format string which can be passed to sh for evaluation. In order to avoid pesky file names, we print the inode numbers of matching files, then pass those to a second instance of find for performing the actual copying.
find \( -false $(find -type f \
-printf 'case %Ta in za*) printf "%%s\\n" "-o -inum %i";; esac\n' |
sh) \) -exec cp -t "$DEST_DIR" \+
Using the inode number means any file name can be processed correctly (including one containing newlines, single or double quotes, etc) but may increase running time significantly, because we need two runs of find. If you have a large directory tree, you will probably want to refactor this for your particular scenario (maybe run only in the current directory, and create a wrapper to run it in every directory you want to examine ... thinking out loud here; not sure it helps actually).
This uses features of GNU find which are not available e.g. in *BSD (including OSX). If you are not on Linux, maybe consider installing the GNU tools.
What you can do is a shell expansion. Something like
cp $(find -type f -printf '%Ta\t%p\n' | egrep "^(za)") $DEST_DIR
Assuming that the result of your find and grep is just the filenames (and full paths, at that), this will copy all the files that match your criteria to whatever you set $DEST_DIR to.
EDIT As mentioned in the comments, this won't work if your filenames contain spaces. If that's the case, you can do something like this:
find -type f -printf '%Ta\t%p\n' | egrep "^(za)" | while read file; do cp "$file" $DEST_DIR; done
This question already has answers here:
Unix find command, what are the {} and \; for?
(5 answers)
Closed 6 years ago.
I've been googling this, but although I see people using them, I see no explanation for it. I've also tried "find and sed" searches, but again no explanation on these. The sed man and other sed guides out there also don't include it.
Now, I'm using the find command to find several files and then using sed to replace strings whithin the files. I believe that these "{}" and "\;" at the end of the sed are what allows sed to take each file name from find and search through its text. But I'd rather not guess and know for sure what they are and why they're there. Here's my current command:
output=$(find . -type f -name '*.h' -o -name '*.C' -o -name '*.cpp' -o -name "*.cc" -exec sed -n -i "s/ARG2/ARG3/g" {} \;)
I'm also concerned that the ";" at the end may not be necessary since I'm wrapping it also and throwing it into a variable. Could someone clarify what those curlies and backslash are doing and whether I need them?
EDIT: It turns out that they're properties of find -exec, not sed. So I've been looking in the wrong place. Thanks!
find has two versions of the -exec action:
-exec ... {} +
...runs the command ..., with as many filenames from the results as possible added at the end.
-exec ... {} ';'
...runs the command ... once per file found, with the filename substituted in place of the {} sigil; it's not mandatory that the sigil be at the end of the line in this usage.
Thus, either a + or ; needs to be passed as a literal argument to find for it to accept the action as valid.
I am trying to make copies of certain file and let them have a prefix.
in order to do it I thought of using find. for our use, let's call them kuku files and I want them to have a "foo" prefix:
find . -maxdepth 1 -name "kuku*" -exec cp '{}' foo_'{}' \;
but it doesn't work because the find always starts the results with ./ so i get a lot of error messages saying "cp: cannot create regular file `foo_./kuku...`: No such file or directory".
the problem is solvable by using foreach f (`ls`) and than using grep and the status var, but it is cumbersome and I want to learn a better solution (and improve my knowledge of the find command along the way...).
update foreach solution (which I don't like and want your help in finding a replacement):
foreach f (`ls`)
echo $f | grep -lq kuku
if (! $status) then
cp $f foo_$f
endif
end
but this is UGLY! (end of update)
as the header says, I'm using csh - not because I love it, just because that's what we use at work...
update
trying to use basename as a solution, because find -exec basename '{}' \; removes the ./ prefix, but i failed using the basename inside the find with backticks (`), meaning that
find -name "kuku*" -exec cp '{}' foo_`basename '{}` \;
simply doesn't work.
Here you go.. I have tested in my linux box
find . -name "kuku*" -exec sh -c 'cp {} foo_`basename {}`' \;
I am trying to use the -exec option with the find command to find specific files in my massive panoramas directory and move them to a specified location. The command I am using below passes an error argument not found for -exec. Can somebody point out my error in parsing the command? Or would I have to create a pipe of some sorts instead?
$ find -name ~/path_to_directory_of_photos/specific_photo_names* -exec mv {} ~/path_to_new_directory/
You need to terminate your exec'ed command with an escaped semicolon (\;).
You should quote the name pattern otherwise the shell will expand any wildcards in it, before running find. You also need to have a semicolon (backslashed to avoid the shell interpreting it as a command separator) to indicate the end of the mv command.
The correct command would be:
find ~/path_to_directory_of_photos -name "specific_photo_names*" -exec mv {} ~/path_to_new_directory \;
I know this post is old, but here's my answer in case it helps anyone else. See the background from this post. If you end the command with + instead of \; you can run it much more efficiently. \; will cause "mv" to be executed once per file, while + will cause "mv" to be executed with the maximum number of arguments. E.g.
mv source1 destination/
mv source2 destination/
mv source3 destination/
vs.
mv source1 source2 source3 destination/
The latter is much more efficient. To use +, you should also use --target-directory. E.g.
find ~/path_to_directory_of_photos -name "specific_photo_names*" -exec mv --target-directory="~/path_to_new_directory" {} +