madZeo
madZeo

Reputation: 3

Why don't I need to pass "\"{}\"" to xargs when handling filenames with spaces?

I have the following problem:

I want to use a xargs-find construction for finding files. The difference is, that I do not want to use find as command1 but as command2 in:

command1 | xargs command2

The problem occurs if the file names have spaces in their names

For example:

If I am trying:

echo 01.Here Comes The Night Time II.flac | xargs -pi find ~/Multimedia/Musik/flac/ -name "\""{}"\""

find ~/Multimedia/Musik/flac/ -name "01.Here Comes The Night Time II.flac" ?...yes

Nothing will be found. Also with the -0 option of xargs it is not working.

If I copy and paste the interactive printed request from xargs, the file will be found:

find ~/Multimedia/Musik/flac/ -name "01.Here Comes The Night Time II.flac"

~/Multimedia/Musik/flac/Arcade Fire/Reflektor (CD 2)/01.Here Comes The Night Time II.flac

Is there something wrong in the way I "feed" the pipe, or in the way I include the " in the find command (which I figured out by trial and error), or something else?

Upvotes: 0

Views: 579

Answers (1)

melpomene
melpomene

Reputation: 85767

You seem to be confused about the way programs are started internally and how commands are interpreted by the shell.

In unix, starting a program involves three parameters:

  1. A file name. This is a string containing the path to the program to be run.
  2. A list of strings. By convention we call these "command line arguments".
  3. Another list of strings. By convention we call these "the environment", but at the OS level it's just another string list. However, all programs/libraries conspire to keep this hidden from both the user and the application programmer.

When you type a command into the shell, many things happen, but in the simplest case it's just a bunch of space-separated words:

$ foo bar baz

($ represents the shell prompt, not something you type.)

The shell splits this line into three words (foo, bar, baz) and interprets the first one as the name of a program (to be looked up in the directories listed in the PATH variable). Let's assume PATH lists /usr/bin and there is indeed a /usr/bin/foo program.

Now the shell starts the program as follows (pseudo-code):

exec("/usr/bin/foo", ["foo", "bar", "baz"], [...])

I.e. we run the executable in /usr/bin/foo, passing a list of three strings as arguments. ([...] represents the environment, which we're going to ignore from now on.)

What happens if you do this instead?

$ foo "bar baz"

The quotes affect the way the shell splits the line into words. In particular, " " (a space) in quotes does not act as a separator but is taken literally. This gives us a two-element list (foo, bar baz). Note that the quotes are not part of the words themselves.

Internally this translates to the following invocation:

exec("/usr/bin/foo", ["foo", "bar baz"], [...])

Again, the second argument simply contains a space. There are no embedded quotes.

So what happens with a command like

$ xargs -pi find ~/Multimedia/Musik/flac/ -name "\""{}"\""

?

This will again be parsed into a list of words by the shell. ~ is replaced by the name of your home directory. "\"" is just a convoluted way of writing \" or '"' (i.e. a literal " character). The list we end up with is xargs, -pi, find, /home/madZeo/Multimedia/Musik/flac/, -name, "{}". This translates to the following invocation:

exec("/usr/bin/xargs", ["xargs", "-pi", "find", "/home/madZeo/Multimedia/Musik/flac/", "-name", "\"{}\""], [...])

Note how the last argument is the 4-character string "{}".

xargs treats its first argument (-pi) as an option specification. In particular, -i tells it to replace {} in the argument list by the current value read from standard input.

xargs then reads a line from its standard input, which (because of your echo pipe) gives 01.Here Comes The Night Time II.flac.

This gets substituted in in place of {}, yielding the list find, /home/madZeo/Multimedia/Musik/flac/, -name, "01.Here Comes The Night Time II.flac". xargs then invokes find like this:

exec("/usr/bin/find", ["find", "/home/madZeo/Multimedia/Musik/flac/", "-name", "\"01.Here Comes The Night Time II.flac\""], [...])

This tells find to look for a file whose name literally starts with " (a quote character). No such file exists, so this fails.


The fix is to write the command like this:

$ xargs -pi find ~/Multimedia/Musik/flac/ -name {}

This ultimately ends up running

exec("/usr/bin/find", ["find", "/home/madZeo/Multimedia/Musik/flac/", "-name", "01.Here Comes The Night Time II.flac"], [...])

, which is what you want.

The issue is that xargs runs its subcommand (find in this case) directly. It does not construct a new command line that gets re-parsed by the shell. It does not split its incoming argument on spaces, it does not interpret quotes, it doesn't care about "special" characters like $ or * or \. It simply takes the list of words it was given, replaces any occurrence of the substring {} by the current input, then executes it.

If you naively take this final command and paste it into your shell, it will undergo word splitting, quote removal, etc., leading to a different outcome.

Upvotes: 2

Related Questions