13

I was just trying to list all directories and files under current directory and also write if they are file or directory with the following command:

find -exec echo `echo {} : ;if [ -f {} ]; then echo file; else echo directory;fi` \;

I know it is a silly command, I can use other things like -type f or -type d, but I want to learn why that piece of code did not work as I expected. It just prints directory to all of them. For example while output of find is:

.
./dir
./dir/file

output of my code is :

. : directory
./dir : directory
./dir/file : directory

And output of

echo `echo dir/file : ;if [ -f dir/file ]; then echo file; else echo directory;fi`

is

dir/file : file

I am working on Ubuntu 14.10 and using find (GNU findutils) 4.4.2

4
  • 1
    find -exec bash -c 'echo -n "{} : ";if [ -f "{}" ]; then echo file; else echo directory;fi' \;
    – Costas
    Feb 26, 2015 at 22:09
  • 1
    Thanks @Costas but I know that using bash or sh I can achieve my initial goal, but now I want to understand why if behaves different with the exec parameter.
    – Esref
    Feb 26, 2015 at 22:13
  • Put arguments into quotes "{}". Why you use echo twice?
    – Costas
    Feb 26, 2015 at 22:19
  • I already tried that and it gives the same output. find -exec echo echo "{}" : ;if [ -f "{}" ]; then echo file; else echo directory;fi \;
    – Esref
    Feb 26, 2015 at 22:23

2 Answers 2

17

First, your snippet executes the command

echo {} : ;if [ -f {} ]; then echo file; else echo directory;fi

because it needs its output to evaluate the command substitution. Since there is no file named {}, this produces the output

{} :
directory

Then the find command is executed with the arguments -exec, echo, {}, :, directory, so for every file, it outputs the file name followed by a space and : directory.

What you actually want to do is to execute the shell snippet echo {} :; … on each file found by find. This snippet must be executed by a shell spawned by find, not by the shell that starts find, since it is receiving data from find on its command line. Therefore you need to instruct find to run a shell:

find -exec sh -c 'echo {} : ;if [ -f {} ]; then echo file; else echo directory;fi' \;

This is better, but still not right. It'll work with some (not all) find implementations if your file names don't contain any special characters, but since you are interpolating the file name in a shell script, you allow file names to execute arbitrary shell commands, e.g. if you have a file called $(rm -rf /) then the command rm -rf / will be executed. To pass file names to the script, pass them as separate arguments.

Also the first echo prints a newline after the colon. Use echo -n (if your shell supports it) or printf to avoid this.

find -exec sh -c 'printf "%s :" "$0"; if [ -f "$0" ]; then echo file; else echo directory; fi' {} \;

You can use -exec … {} + to group shell invocations, which is faster.

find -exec sh -c 'for x; do printf "%s :" "$x"; if [ -f "$x" ]; then echo file; else echo directory; fi; done' _ {} +
0
6

Another way for executing if; then; else; fi together with find is:

find |
while IFS= read -r p; do if [ -f "$p" ]; then echo file; else echo directory; fi; done
3
  • 1
    Cool solution. It avoids complex quote patterns and subshell complications. You may have previously defined a shell function, and execute it on the files matching the "if" condition.
    – gerlos
    May 9, 2018 at 16:10
  • 1
    ... but see Why is looping over find's output bad practice?
    – Kusalananda
    Jan 31, 2023 at 9:38
  • In addition to the above comment, whie read p will trim filenames that contain leading and/or trailing whitespace, and it will also handle backslashes unpredictably. while IFS= read -r p is more robust. Jul 9, 2023 at 16:58

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .