find -exec basename {} vs find -exec echo $(basename {}) - bash

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

Related

Bash - dirname not working with substitution

When using find with $(dirname {}), it always just outputs "." as the dirname. E.g. for:
find . -name \*.h -exec echo {} \; -exec echo $(dirname {}) \;
outputs:
./folder/folder.h
.
./folder/sub_folder/sub_folder.h
.
./test.h
.
But I would expect this:
./folder/folder.h
./folder
./folder/sub_folder/sub_folder.h
./folder/sub_folder
./test.h
.
Interestingly, creating a new shell for each find generates the correct output:
find . -name \*.h -exec sh -c 'echo $0; echo $(dirname $0);' {} \;
After having tested the above command through a script, it has been observed that $(dirname {}) is expanded to current directory .'.
mkdir -p test1/test2
touch test1/test2/tests.h
find . -name \*.h -exec echo {} \; -exec echo $(dirname {}) \;
./test1/test2/tests.h
.
echo "find . -name \*.h -exec echo {} \; -exec echo $(dirname {}) \;" > checkh.sh
bash -vx checkh.sh
find . -name \*.h -exec echo {} \; -exec echo . \;
+ find . -name '*.h' -exec echo '{}' ';' -exec echo . ';'
./test1/test2/tests.h
.
.
That's why the output is always displayed as only . current directory.
So, use your mini-script sh -c style or Kent's solution.
A slight modification from your command will also work, i.e., put echo inside command substitution:
find . -name \*.h -exec echo {} \; -exec $(echo dirname {}) \;
./test1/test2/tests.h
./test1/test2
Test case on the modification is as follows:
echo "find . -name \*.h -exec echo {} \; -exec $(echo dirname {}) \;" > checkh2.sh
bash -vx checkh2.sh
find . -name \*.h -exec echo {} \; -exec dirname {} \;
+ find . -name '*.h' -exec echo '{}' ';' -exec dirname '{}' ';'
./test1/test2/tests.h
./test1/test2
This line gives what you want:
find . -name *.h -print -exec dirname {} \;
You might actually want to use this:
find . -name '*.h' -type f -printf "%p\n%h\n"
When you look at man find under the printf format section, you will notice that there are a plethora of useful flags.

How can I properly execute a command with lists using `sh -c`?

I have this command to recursively find directories that contain mustExist.js but not cannotExist.js:
comm -13 <(find . -type f -name cannotExist.js -exec dirname {} \; | sort -u) <(find . -type f -name mustExist.js -exec dirname {} \; | sort -u)
It works fine.
Now, I must pass it as a string to a node.js automation script. The script picks up the string and runs it as sh -c <string>. I cannot change that part.
So I pass this string:
'comm -13 <(find . -type f -name cannotExist.js -exec dirname {} \\; | sort -u) <(find . -type f -name mustExist.js -exec dirname {} \\; | sort -u)'
However, I always encounter this error:
Warning: Command failed: /bin/sh -c comm -13 <(find . -type f -name cannotExist.js -exec dirname {} \; | sort -u) <(find . -type f -name mustExist.js -exec dirname {} \; | sort -u)
/bin/sh: 1: Syntax error: "(" unexpected
When I pass this string:
'"comm -13 <(find . -type f -name cannotExist.js -exec dirname {} \\; | sort -u) <(find . -type f -name mustExist.js -exec dirname {} \\; | sort -u)"'
I get:
Warning: Command failed: /bin/sh -c "comm -13 <(find . -type f -name cannotExist.js -exec dirname {} \; | sort -u) <(find . -type f -name mustExist.js -exec dirname {} \; | sort -u)"
/bin/sh: 1: comm -13 <(find . -type f -name cannotExist.js -exec dirname {} \; | sort -u) <(find . -type f -name mustExist.js -exec dirname {} \; | sort -u): not found
When I manually try using sh to immitate the automation script:
sh -c comm -13 <(find . -type f -name cannotExist.js -exec dirname {} \; | sort -u) <(find . -type f -name mustExist.js -exec dirname {} \; | sort -u)
I get a different error:
comm: missing operand
Or with quotes:
sh -c "comm -13 <(find . -type f -name cannotExist.js -exec dirname {} \; | sort -u) <(find . -type f -name mustExist.js -exec dirname {} \; | sort -u)"
I get:
/bin/sh: 1: Syntax error: "(" unexpected
Other quotes:
sh -c 'comm -13 <(find . -type f -name cannotExist.js -exec dirname {} \; | sort -u) <(find . -type f -name mustExist.js -exec dirname {} \; | sort -u)'
/bin/sh: 1: Syntax error: "(" unexpected
Is it possible to do this somehow?
There is a much simpler and more efficient command you can use:
find . -type d -exec sh -c 'test -e "$1"/mustExist.js && ! test -e "$1"/cannotExist.js' _ {} \; -print
This iterates over the directories, and checks each one for the required file as well as making sure the forbidden file is not present.
An example, where only foo/bar3 contains yes.js without also containing no.js.
$ mkdir -p foo/bar1 foo/bar2 foo/bar3
$ touch foo/bar1/no.js foo/bar1/yes.js foo/bar2/no.js foo/bar3/yes.js
$ find foo -type d -exec sh -c 'test -e "$1"/yes.js && ! test -e "$1"/no.js' _ {} \; -print
foo/bar3
Passing this to your script requires some creative quoting, though:
somescript "find foo -type d -exec sh -c 'test -e \"\$1\"/yes.js && ! test -e \"\$1\"/no.js' _ {} \; -print"
If you are using bash, you can simplify it a little:
somescript $'find foo -type d -exec sh -c \'test -e "$1"/yes.js && ! test -e "$1"/no.js\' _ {} \; -print'
If you are willing to use the obsolete and possibly unsupported operator -a, you can reduce this to a single invocation of test.
find . -type d -exec test -e {}/mustExist.js -a ! -e {}/cannotExist.js \; -print
This is also a little simpler to pass to your script, since it does not itself contain any quotes:
somescript 'find . -type d -exec test -e {}/mustExist.js -a ! -e {}/cannotExist.js \; -print'
You can simplify it using multiple -exec primaries as well:
somescript 'find . -type d -exec test -e {}/mustExist.js \; ! -exec test -e {}/cannotExist.js \; -print'
which is a little less efficient (it runs test twice instead of once) but is more portable, while being easier to quote than the version that passes the string to sh -c.

find: How to use found paths in the -exec directive?

I have a dozen files named
~/DOMAIN1.de/bin/dbdeploy.php
~/DOMAIN2.de/bin/dbdeploy.php
~/DOMAIN3.de/bin/dbdeploy.php
I want to run them all with the same arguments.
My bash script reads:
cd ~
find . -maxdepth 1 -type d -name "*\.de" -exec php56 bin/dbdeploy.php "$1" "$2" \;
However, the path given to exec seems not to be relative to the found subdirectory but rather to my PWD:
$ bash -x ./.dbpush "some argument"
+ cd ~
+ find . -maxdepth 1 -type d -name '*\.de' -exec php56 bin/dbdeploy.php 'some argument' ';'
Could not open input file: bin/dbdeploy.php
Could not open input file: bin/dbdeploy.php
Could not open input file: bin/dbdeploy.php
How can I use the found path in the -exec directive?
Ok, actually I found the answer myself:
The "find"-results are stored in {}, so the line reads
find . -maxdepth 1 -type d -name "*\.de" -exec php56 {}/bin/dbdeploy.php "$1" "$2" \;
Alternativly
find . -type f -wholename "*\.de/bin/dbdeploy.php" -exec php56 {} "$1" "$2" \;

"find -exec print" does not work

> find . -type f -exec print {} \;
find: cannot execute print:: No such file or directory
> find . -type f -exec echo {} \;
f1.txt
f2.txt
...
Why "find -exec print" does not work?
Shell - ksh.
I think print is a shell builtin, not an executable.

bash: How to delimit strings to find files

What syntax should I use in a bash script to list files based on 3 dynamic values:
- older than X days
- in a specified directory
- whose name contains a specified string?
FILEAGE=7
FILEDIR='"/home/ecom/tmp"'
FILESTRING='"search-results-*"'
FILES_FOR_REMOVAL=$("/usr/bin/find "${FILEDIR}" -maxdepth 1 -type f -mtime +${FILEAGE} -name "${FILESTRING}" -exec ls -lth {} \;")
echo ${FILES_FOR_REMOVAL}
If I try the above I get:
-bash: /usr/bin/find "/home/ecom/tmp" -maxdepth 1 -type f -mtime +7 -name "search-results-*" -exec ls -lth {} \;: No such file or directory
Remove superfluous quotes:
FILEAGE=7
FILEDIR='/home/ecom/tmp'
FILESTRING='search-results-*'
FILES_FOR_REMOVAL=$(/usr/bin/find "${FILEDIR}" -maxdepth 1 -type f -mtime +${FILEAGE} -name "${FILESTRING}" -exec ls -lth {} \;)
Your syntax for 'find' looks ok. Try removing the quotes around the command string, i.e.
FILES_FOR_REMOVAL=$(/usr/bin/find "${FILEDIR}" -maxdepth 1 -type f -mtime +${FILEAGE} -name "${FILESTRING}" -exec ls -lth {} \;)
FILEAGE=7
FILEDIR='/home/ecom/tmp'
FILESTRING='search-results-*'
/usr/bin/find "${FILEDIR}" -maxdepth 1 -type f -mtime +${FILEAGE} -name "${FILESTRING}" -exec /bin/ls -lth '{}' \;
There were some extra quotes that created the error. Also specify full path to /bin/ls to avoid problems with potential aliasing of ls(1). And to get filenames on a separate line, I dropped the $FILES_FOR_REMOVAL variable. You can also use
/usr/bin/find "${FILEDIR}" -maxdepth 1 -type f -mtime +${FILEAGE} -name "${FILESTRING}" -ls
(I can't add comments, but ... )
To reliably handle file names with spaces, you may want to consider storing the file list in a temp text file instead of a variable and loop through it using a while construct (instead of a for)
For example:
FILEAGE=7
FILEDIR='/home/ecom/tmp'
FILESTRING='search-results-*'
TEMPFILE=".temp${RANDOM}"
CMD="find \"${FILEDIR}\" -maxdepth 1 -type f -mtime +${FILEAGE} -name \"${FILESTRING}\" -exec /bin/ls -lth '{}' \;"
$CMD > $TEMPFILE # write output to file
while read thefile; do
do_somthing_to $thefile
done < $TEMPFILE
rm $TEMPFILE # clean up after
Or, if you're only going to use the list once, pipe the output directly to the while construct:
$CMD | while read thefile; do
do_something_to $thefile
done

Resources