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
How do I properly escape the path to come out of find to a new command argument?
#!/bin/bash
for f in $(find . -type f -name '*.flac')
do
if flac -cd "$f" | lame -bh 320 - "${f%.*}".mp3; then
rm -f "$f"
echo "removed $f"
fi
done
returns
lame: excess arg Island of the Gods - 3.mp3
Using a Bash for loop is not ideal for the results of find or ls. There are other ways to do it.
You may want to use -print0 and xargs to avoid word splitting issues.
$ find [path] -type f -name *.flac -print0 | xargs -0 [command line {xargs puts in fn}]
Or use -exec primary in find:
$ find [path] -type f -name *.flac -exec [process {find puts in fn}] \;
Alternative, you can use a while loop:
find [path] -type f -name *.flac | while IFS= read -r fn; do # fn not quoted here...
echo "$fn" # QUOTE fn here!
# body of your loop
done
count=0; #count for counting
IFS='
'
for x in `ls -l $input`; #for loop using ls command
do
a=$(ls -ls | awk '{print $6}') #print[6] is sizes of file
echo $a
b=`echo $a | awk '{split($0,numbers," "); print numbers[1]}'`
echo $b
if [ $b -eq 0 ] # b is only size of a file
then
count=`expr $count + 1` #if b is zero , the count will increase one by one
fi
echo $count
done
I want to find 0 size files . I do that using find command. The second thing is I want to count number of has 0 size of files using ls command and awk. But It doesn't true code . What is my mistake ?
The -s test is true if a file has non-zero size. If that test fails for file, increment your empty-file count.
empty_files=0
for f in "$input"/*; do
[ -s "$f" ] || : $(( empty_files++ ))
done
Your main mistake is that you're parsing ls!
If you want to find (regular) files that are empty, and if you have a version of find that supports the -empty predicate, use it:
find . -type f -empty
Note that this will recurse in subfolders too; if you don't want that, use:
find . -maxdepth 1 -type f -empty
(assuming that your find also supports -maxdepth).
If you only want to count how many empty (regular) files you have:
find . -maxdepth 1 -type f -empty -printf x | wc -m
and if you want to perform both operations at the same time, i.e., print out the name or save them in an array for future use, and count them:
empty_files=()
while IFS= read -r -d '' f; do
empty_files+=( "$f" )
done < <(find . -maxdepth 1 -type f -empty -print0)
printf 'There are %d empty files:\n' "${#empty_files[#]}"
printf ' %s\n' "${empty_files[#]}"
With Bash≥4.4, you could use mapfile instead of the while-read loop:
mapfile -t -d '' empty_files < <(find . -maxdepth 1 -type f -empty -print0)
printf 'There are %d empty files:\n' "${#empty_files[#]}"
printf ' %s\n' "${empty_files[#]}"
For a POSIX-compliant way, use test with the -s option:
find . -type f \! -exec test -s {} \; -print
and if you don't want to recurse into subdirectories, you'll have to -prune them:
find . \! -name . -prune -type f \! -exec test -s {} \; -print
and if you want to count them:
find . \! -name . -prune -type f \! -exec test -s {} \; -exec printf x | wc -m
and here, if you want to perform both operations (count them and save them in an array for later use), use the previous while-read loop (or mapfile if you live in the future) with this find:
find . \! -name . -prune -type f \! -exec test -s {} \; -exec printf '%s\0' {} \;
Also see chepner's answer for a pure shell solution (needs minor tweaking to be POSIX compliant).
Regarding your comment
I want to count and delete [empty files]. How can I do that at the same time?
If you have GNU find (or a find that supports all the goodies):
find . -maxdepth 1 -type f -empty -printf x -delete | wc -m
if not,
find . \! -name . -prune -type f \! -exec test -s {} \; -printf x -exec rm {} \; | wc -m
Make sure that the -delete (or -exec rm {} \;) predicate is at the end! do not exchange the order of the predicates!
for example:
find /usr/lib -maxdepth 1 -type l -iname "*libblas*"|xargs -I{} echo "{} =>" $(realpath {})
I would like it to output:
/usr/lib/libblas.so.3gf=>/usr/lib/libblas/libblas.so.3gf.0
/usr/lib/libblas.so=>/usr/lib/libblas/libblas.so.3gf.0
/usr/lib/libblas.a=>/usr/lib/libblas/libblas.a
This will not work because the value in $() is expanded before the script actual running.
Is there any way I can achieve this result? without loop in bash?
Alternatively:
find /usr/lib -maxdepth 1 -type l \
-exec echo -n '{} =>' \; \
-exec realpath '{}' \;
Have xargs call the shell:
find /usr/lib -maxdepth 1 -type l -iname "*libblas*"|xargs -I{} sh -c 'echo "{} =>" $(realpath {})'
You need the command substitution to happen after the file name is known. So you need xargs to call a shell and do the substitution there.
Since you're running the command on a single file at a time, using xargs is a useless complication (and it also mangles some file names). Use -exec!
find /usr/lib -maxdepth 1 -type l -iname "*libblas*" -exec sh -c 'echo "$0 => $(realpath "$0")' {} \;
You could make this slightly faster and not less clear by not using a command substitution:
find /usr/lib -maxdepth 1 -type l -iname "*libblas*" -exec sh -c 'echo -n "$0 => "; realpath "$0"' {} \;
To make things a little faster, don't invoke a new shell process for every file:
find /usr/lib -maxdepth 1 -type l -iname "*libblas*" -exec sh -c 'for x; do echo -n "$x => "; realpath "$x"; done' _ {} +
(You can do the same with xargs, but just drop xargs and stick to the simpler, faster, more robust -exec.)
Try to convert each filename separately using line-by-line "while" loop:
find ... | while read f; do echo "$f" '=>' "$(realpath $f)" ; done
The shortest seems to be using GNU Parallel:
find /usr/lib -maxdepth 1 -type l -iname "*libblas*"|parallel echo {} '=\> $(readlink -f {})'
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."