Home » Linux » Find and basename not playing nicely

Find and basename not playing nicely

Posted by: admin November 30, 2017 Leave a comment

Questions:

I want to echo out the filename portion of a find on the linux commandline. I’ve tried to use the following:

find www/*.html -type f -exec sh -c "echo $(basename {})" \;

and

find www/*.html -type f -exec sh -c "echo `basename {}`" \;

and a whole host of other combinations of escaping and quoting various parts of the text. The result is that the path isn’t stripped:

www/channel.html
www/definition.html
www/empty.html
www/index.html
www/privacypolicy.html

Why not?

Update: While I have a working solution below, I’m still interested in why “basename” doesn’t do what it should do.

Answers:

The trouble with your original attempt:

find www/*.html -type f -exec sh -c "echo $(basename {})" \;

is that the $(basename {}) code is executed once, before the find command is executed. The output of the single basename is {} since that is the basename of {} as a filename. So, the command that is executed by find is:

sh -c "echo {}" 

for each file found, but find actually substitutes the original (unmodified) file name each time because the {} characters appear in the string to be executed.

If you wanted it to work, you could use single quotes instead of double quotes:

find www/*.html -type f -exec sh -c 'echo $(basename {})' \;

However, making echo repeat to standard output what basename would have written to standard output anyway is a little pointless:

find www/*.html -type f -exec sh -c 'basename {}' \;

and we can reduce that still further, of course, to:

find www/*.html -type f -exec basename {} \;

Could you also explain the difference between single quotes and double quotes here?

This is routine shell behaviour. Let’s take a slightly different command (but only slightly — the names of the files could be anywhere under the www directory, not just one level down), and look at the single-quote (SQ) and double-quote (DQ) versions of the command:

find www -name '*.html' -type f -exec sh -c "echo $(basename {})" \;   # DQ
find www -name '*.html' -type f -exec sh -c 'echo $(basename {})' \;   # SQ

The single quotes pass the material enclosed direct to the command. Thus, in the SQ command line, the shell that launches find removes the enclosing quotes and the find command sees its $9 argument as:

echo $(basename {})

because the shell removes the quotes. By comparison, the material in the double quotes is processed by the shell. Thus, in the DQ command line, the shell (that launches find — not the one launched by find) sees the $(basename {}) part of the string and executes it, getting back {}, so the string it passes to find as its $9 argument is:

echo {}

Now, when find does its -exec action, in both cases it replaces the {} by the filename that it just found (for sake of argument, www/pics/index.html). Thus, you get two different commands being executed:

sh -c 'echo $(basename www/pics/index.html)'    # SQ
sh -c "echo www/pics/index.html"                # DQ

There’s a (slight) notational cheat going on there — those are the equivalent commands that you’d type at the shell. The $2 of the shell that is launched actually has no quotes in it in either case — the launched shell does not see any quotes.

As you can see, the DQ command simply echoes the file name; the SQ command runs the basename command and captures its output, and then echoes the captured output. A little bit of reductionist thinking shows that the DQ command could be written as -print instead of using -exec, and the SQ command could be written as -exec basename {} \;.

If you’re using GNU find, it supports the -printf action which can be followed by Format Directives such that running basename is unnecessary. However, that is only available in GNU find; the rest of the discussion here applies to any version of find you’re likely to encounter.

Questions:
Answers:

Try this instead :

 find www/*.html -type f -printf '%f\n'

If you want to do it with a pipe (more resources needed) :

find www/*.html -type f -print0 | xargs -0 -n1 basename

Questions:
Answers:

Thats how I batch resize files with imagick, rediving output filename from source

find . -name header.png -exec sh -c 'convert -geometry 600 {} $(dirname {})/$(basename {} ".png")_mail.png' \;

Questions:
Answers:

I had to accomplish something similar, and found following the practices mentioned for avoiding looping over find’s output and using find with sh sidestepped these problems with {} and -printfentirely.

You can try it like this:

find www/*.html -type f -exec sh -c 'echo $(basename $1)' find-sh {} \;

The summary is “Don’t reference {} directly inside of a sh -c but instead pass it to sh -c as an argument, then you can reference it with a number variable inside of sh -c” the find-sh is just there as a dummy to take up the $0, there is more utility in doing it that way and using {} for $1.

I’m assuming the use of echo is really to simplify the concept and test function. There are easier ways to simply echo as others have mentioned, But an ideal use case for this scenario might be using cp, mv, or any more complex commands where you want to reference the found file names more than once in the command and you need to get rid of the path, eg. when you have to specify filename in both source and destination or if you are renaming things.

So for instance, if you wanted to copy only the html documents to your public_html directory (Why? because Example!) then you could:

 find www/*.html -type f -exec sh -c 'cp /var/www/$(basename $1) /home/me/public_html/$(basename $1)' find-sh {} \;

Over on unix stackexchange, user wildcard’s answer on looping with find goes into some great gems on usage of -exec and sh -c. (You can find it here: https://unix.stackexchange.com/questions/321697/why-is-looping-over-finds-output-bad-practice)