I noticed that find ... -exec ... {} \; or xargs -i ... {} seems to evaluate variables or subshells (like $RANDOM or $(uuidgen)) only once, even the command was executed mutiple times.
For example:
$ find . -type f -name \*.txt -exec echo "$RANDOM {}" \;
28855 ./foo/bar.txt
28855 ./foo/bar1.txt
28855 ./foo/bar2.txt
28855 ./foo/bar3.txt
28855 ./foo/bar4.txt
$ grep -lr SOME_TEXT --include=\*.txt | xargs -i echo "$RANDOM {}"
6153 ./foo/bar.txt
6153 ./foo/bar1.txt
6153 ./foo/bar2.txt
6153 ./foo/bar3.txt
6153 ./foo/bar4.txt
Is there a way to get a result like below?
1543 ./foo/bar.txt
543 ./foo/bar1.txt
57224 ./foo/bar2.txt
3525 ./foo/bar3.txt
18952 ./foo/bar4.txt
Yes. The variable expansion is performed after the line has been accepted, but before it has been executed. This means that the command that ends up being executed is
'/usr/bin/find' '.' '-type' 'f' '-name' '*.txt' '-exec' 'echo' '28855 {}' ';'
Two basic ways around this:
Use another bash that will delay the execution:
find . -type f -name \*.txt -exec bash -c 'echo "$RANDOM {}"' \;
Use a loop:
for file in $(find . -type f -name \*.txt -print)
do
echo "$RANDOM $file"
done
If your files have spaces, you have to do something different to preserve them:
mapfile -d '' files < <(find . -type f -name \*.txt -print0)
for file in "${files[#]}"
do
echo "$RANDOM $file"
done
How can I use command substitution in find … -exec … to avoid using xargs in the following command?
find -L -- /path/to/directory -mindepth 2 -maxdepth 2 -type d -exec dirname '{}' \; | xargs basename -a
I tried the following using command substitution, but it output . for each result instead of the desired output:
find -L -- /path/to/directory -mindepth 2 -maxdepth 2 -type d -exec basename "$(dirname '{}')" \;
Your first command will return strange results if a path contains whitespace.
Use a small shell script:
find -L -- . -mindepth 2 -maxdepth 2 -type d -exec sh -c 'basename "$(dirname "{}")"' \;
Alternative syntax to pass one path argument to the script:
find -L -- . -mindepth 2 -maxdepth 2 -type d -exec sh -c 'basename "$(dirname "$1")"' sh {} \;
Or pass as many arguments to the script as possible:
find -L -- . -mindepth 2 -maxdepth 2 -type d -exec sh -c '
for path do
basename "$(dirname "$path")"
done
' sh {} +
With GNU utilities it's possible to output NUL-terminated strings with dirname passed to xargs -0. The basename command is not run if there are no arguments (-r):
find -L -- . -mindepth 2 -maxdepth 2 -type d -exec dirname -z {} + | xargs -r0 basename -a
I've been using "find -exec" commands as in code 1 and 2:
Code 1
find . -type d -exec chmod 775 {} +
Code 2
find . -type f -exec mv {} ./.. \;
But sometimes I see people use "bash" or "sh" just after "-exec" as in code 3 and 4:
Code 3
alias foo="find . -maxdepth 1 -mindepth 1 -type d -exec sh -c 'echo \"\$(find \"{}\" -type f | wc -l)\" {}' \; | sort -nr
Code 4
alias foo="find . -maxdepth 1 -mindepth 1 -type d -exec bash -c 'echo \"\$(find \"\${1}\" -type f | wc -l) \"\${1}\" \"' -- \"{}\" \; | sort -nr"
Question 1abc: When do we use bash or sh after -exec ? Is there something special about echo as opposed to chmod and mv? Does echo belong to bash and if so, how do I know what other commands need bash in front?
Code 4 was a proposed improvement upon Code 3 because
they said "you should pass the filename to the inner find command as an argument. Otherwise you will run into problems if one of your folders has a name with a " in it:" , which I can understand.
Question 2abc:
I don't understand how ${1} makes it safer than {}? Don't they reference the same thing?
I tried replacing the last \"{}\" part with \"\${1}\" like below but it fails strangely. I don't understand why.
Code 5
alias foo="find . -maxdepth 1 -mindepth 1 -type d -exec bash -c 'echo \"\$(find \"\${1}\" -type f | wc -l) \"\${1}\" \"' -- \"\${1}\" \; | sort -nr"
I'm sure I'm missing something but I can't figure it out. Given:
$ find -type f
./hello.txt
./wow.txt
./yay.txt
how come the next two commands render different results?
$ find -type f -exec basename {} \;
hello.txt
wow.txt
yay.txt
$ find -type f -exec echo $(basename {}) \;
./hello.txt
./wow.txt
./yay.txt
$(basename {}) is evaluated before the command runs. The result is {} so the command echo $(basename {}) becomes echo {} and basename is not run for each file.
A quick debug on that using the bash -x debugger demonstrated this,
[The example is my own, just for demonstration purposes]
bash -xc 'find -type f -name "*.sh" -exec echo $(basename {}) \;'
++ basename '{}'
+ find -type f -name '*.sh' -exec echo '{}' ';'
./1.sh
./abcd/another_file_1_not_ok.sh
./abcd/another_file_2_not_ok.sh
./abcd/another_file_3_not_ok.sh
And for just basename {}
bash -xc 'find -type f -name "*.sh" -exec basename {} \;'
+ find -type f -name '*.sh' -exec basename '{}' ';'
1.sh
another_file_1_not_ok.sh
another_file_2_not_ok.sh
another_file_3_not_ok.sh
As you can see in the first example, echo $(basename {}) gets resolved in two steps, basename {} is nothing but the basename on the actual file (which outputs the plain file name) which is then interpreted as echo {}. So it is nothing but mimic-ing the exact behaviour when you use find with exec and echo the files as
bash -xc 'find -type f -name "*.sh" -exec echo {} \;'
+ find -type f -name '*.sh' -exec echo '{}' ';'
./1.sh
./abcd/another_file_1_not_ok.sh
./abcd/another_file_2_not_ok.sh
./abcd/another_file_3_not_ok.sh
I am trying to use find -exec with multiple commands without any success. Does anybody know if commands such as the following are possible?
find *.txt -exec echo "$(tail -1 '{}'),$(ls '{}')" \;
Basically, I am trying to print the last line of each txt file in the current directory and print at the end of the line, a comma followed by the filename.
find accepts multiple -exec portions to the command. For example:
find . -name "*.txt" -exec echo {} \; -exec grep banana {} \;
Note that in this case the second command will only run if the first one returns successfully, as mentioned by #Caleb. If you want both commands to run regardless of their success or failure, you could use this construct:
find . -name "*.txt" \( -exec echo {} \; -o -exec true \; \) -exec grep banana {} \;
find . -type d -exec sh -c "echo -n {}; echo -n ' x '; echo {}" \;
One of the following:
find *.txt -exec awk 'END {print $0 "," FILENAME}' {} \;
find *.txt -exec sh -c 'echo "$(tail -n 1 "$1"),$1"' _ {} \;
find *.txt -exec sh -c 'echo "$(sed -n "\$p" "$1"),$1"' _ {} \;
Another way is like this:
multiple_cmd() {
tail -n1 $1;
ls $1
};
export -f multiple_cmd;
find *.txt -exec bash -c 'multiple_cmd "$0"' {} \;
in one line
multiple_cmd() { tail -1 $1; ls $1 }; export -f multiple_cmd; find *.txt -exec bash -c 'multiple_cmd "$0"' {} \;
"multiple_cmd()" - is a function
"export -f multiple_cmd" - will export it so any other subshell can see it
"find *.txt -exec bash -c 'multiple_cmd "$0"' {} \;" - find that will execute the function on your example
In this way multiple_cmd can be as long and as complex, as you need.
Hope this helps.
There's an easier way:
find ... | while read -r file; do
echo "look at my $file, my $file is amazing";
done
Alternatively:
while read -r file; do
echo "look at my $file, my $file is amazing";
done <<< "$(find ...)"
Extending #Tinker's answer,
In my case, I needed to make a command | command | command inside the -exec to print both the filename and the found text in files containing a certain text.
I was able to do it with:
find . -name config -type f \( -exec grep "bitbucket" {} \; -a -exec echo {} \; \)
the result is:
url = git#bitbucket.org:a/a.git
./a/.git/config
url = git#bitbucket.org:b/b.git
./b/.git/config
url = git#bitbucket.org:c/c.git
./c/.git/config
I don't know if you can do this with find, but an alternate solution would be to create a shell script and to run this with find.
lastline.sh:
echo $(tail -1 $1),$1
Make the script executable
chmod +x lastline.sh
Use find:
find . -name "*.txt" -exec ./lastline.sh {} \;
Thanks to Camilo Martin, I was able to answer a related question:
What I wanted to do was
find ... -exec zcat {} | wc -l \;
which didn't work. However,
find ... | while read -r file; do echo "$file: `zcat $file | wc -l`"; done
does work, so thank you!
1st answer of Denis is the answer to resolve the trouble. But in fact it is no more a find with several commands in only one exec like the title suggest. To answer the one exec with several commands thing we will have to look for something else to resolv. Here is a example:
Keep last 10000 lines of .log files which has been modified in the last 7 days using 1 exec command using severals {} references
1) see what the command will do on which files:
find / -name "*.log" -a -type f -a -mtime -7 -exec sh -c "echo tail -10000 {} \> fictmp; echo cat fictmp \> {} " \;
2) Do it: (note no more "\>" but only ">" this is wanted)
find / -name "*.log" -a -type f -a -mtime -7 -exec sh -c "tail -10000 {} > fictmp; cat fictmp > {} ; rm fictmp" \;
I usually embed the find in a small for loop one liner, where the find is executed in a subcommand with $().
Your command would look like this then:
for f in $(find *.txt); do echo "$(tail -1 $f), $(ls $f)"; done
The good thing is that instead of {} you just use $f and instead of the -exec … you write all your commands between do and ; done.
Not sure what you actually want to do, but maybe something like this?
for f in $(find *.txt); do echo $f; tail -1 $f; ls -l $f; echo; done
should use xargs :)
find *.txt -type f -exec tail -1 {} \; | xargs -ICONSTANT echo $(pwd),CONSTANT
another one (working on osx)
find *.txt -type f -exec echo ,$(PWD) {} + -exec tail -1 {} + | tr ' ' '/'
A find+xargs answer.
The example below finds all .html files and creates a copy with the .BAK extension appended (e.g. 1.html > 1.html.BAK).
Single command with multiple placeholders
find . -iname "*.html" -print0 | xargs -0 -I {} cp -- "{}" "{}.BAK"
Multiple commands with multiple placeholders
find . -iname "*.html" -print0 | xargs -0 -I {} echo "cp -- {} {}.BAK ; echo {} >> /tmp/log.txt" | sh
# if you need to do anything bash-specific then pipe to bash instead of sh
This command will also work with files that start with a hyphen or contain spaces such as -my file.html thanks to parameter quoting and the -- after cp which signals to cp the end of parameters and the beginning of the actual file names.
-print0 pipes the results with null-byte terminators.
for xargs the -I {} parameter defines {} as the placeholder; you can use whichever placeholder you like; -0 indicates that input items are null-separated.
I found this solution (maybe it is already said in a comment, but I could not find any answer with this)
you can execute MULTIPLE COMMANDS in a row using "bash -c"
find . <SOMETHING> -exec bash -c "EXECUTE 1 && EXECUTE 2 ; EXECUTE 3" \;
in your case
find . -name "*.txt" -exec bash -c "tail -1 '{}' && ls '{}'" \;
i tested it with a test file:
[gek#tuffoserver tmp]$ ls *.txt
casualfile.txt
[gek#tuffoserver tmp]$ find . -name "*.txt" -exec bash -c "tail -1 '{}' && ls '{}'" \;
testonline1=some TEXT
./casualfile.txt
Here is my bash script that you can use to find multiple files and then process them all using a command.
Example of usage. This command applies a file linux command to each found file:
./finder.sh file fb2 txt
Finder script:
# Find files and process them using an external command.
# Usage:
# ./finder.sh ./processing_script.sh txt fb2 fb2.zip doc docx
counter=0
find_results=()
for ext in "${#:2}"
do
# #see https://stackoverflow.com/a/54561526/10452175
readarray -d '' ext_results < <(find . -type f -name "*.${ext}" -print0)
for file in "${ext_results[#]}"
do
counter=$((counter+1))
find_results+=("${file}")
echo ${counter}") ${file}"
done
done
countOfResults=$((counter))
echo -e "Found ${countOfResults} files.\n"
echo "Processing..."
counter=0
for file in "${find_results[#]}"
do
counter=$((counter+1))
echo -n ${counter}"/${countOfResults}) "
eval "$1 '${file}'"
done
echo "All files have been processed."