3

I was writing an answer to a question on Stack Overflow, and the answer involved a nested for loop similar to

for jpeg in **/foo.jpg; do
    d=${f%/foo.jpg}   # Directory containing foo.jpg
    for m4a in "$d"/*.m4a; do
        someCommand "$m4a" --arg "$jpeg" 
    done
done

I had this idea to make the whole thing into a single find command that used another find command in its -exec primary, something like:

find . -type d -exec test -f {}/foo.jpg \;
               -exec find {} -name '*.m4a' \
                             -exec someCommand ??? --args {}/foo.jpg \;

The problem is with the ???. I would want to pass a literal {} to be used as an argument in the nested find's -exec without the outer find replacing it with a directory name as it would with {}/foo.jpg.

The POSIX standard doesn't really say anything about passing a literal {} as an argument to a command. It only says

If a utility_name or argument string contains the two characters "{}", but not just the two characters "{}", it is implementation-defined whether find replaces those two characters or uses the string without change.

This would seem to preclude some sort of trickery involving sh -c, like

-exec sh -c 'someCommand {} --args "$1"' _ {}/foo.jpg \;

since {} may or may not be replaced by the outer find.

So, is there any way to nest find in this fashion?

chepner
  • 7,501
  • 1
    My respect for the search function on Stack Exchange sites continues to drop. I'm pretty sure that question did not come up as a potential duplicate even when I was using a similar title. – chepner Feb 12 '17 at 21:24

1 Answers1

2

Find does not have any escape mechanism

This fact will not allow you to insert {} inside an existing -exec and/or -execdir option. The substitution is made with plain strncpy(). The exec/execdir is performed inside GNU-find (which I'm using as reference) through bc_push_arg. For the {} + form we have:

  /* "+" terminator, so we can just append our arguments after the
   * command and initial arguments.
   */
  execp->replace_vec = NULL;
  execp->ctl.replace_pat = NULL;
  execp->ctl.rplen = 0;
  execp->ctl.lines_per_exec = 0; /* no limit */
  execp->ctl.args_per_exec = 0; /* no limit */

  /* remember how many arguments there are */
  execp->ctl.initial_argc = (end-start) - 1;

  /* execp->state = xmalloc(sizeof struct buildcmd_state); */
  bc_init_state (&execp->ctl, &execp->state, execp);

  /* Gather the initial arguments.  Skip the {}. */
  for (i=start; i<end-1; ++i)
{
  bc_push_arg (&execp->ctl, &execp->state,
           argv[i], strlen (argv[i])+1,
           NULL, 0,
           1);
}

It just appends everything at the end, since you can't have more than one instance of {} in the {} + form. And for the {} ; we have:

  /* Semicolon terminator - more than one {} is supported, so we
   * have to do brace-replacement.
   */
  execp->num_args = end - start;

  execp->ctl.replace_pat = "{}";
  execp->ctl.rplen = strlen (execp->ctl.replace_pat);
  execp->ctl.lines_per_exec = 0; /* no limit */
  execp->ctl.args_per_exec = 0; /* no limit */
  execp->replace_vec = xmalloc (sizeof(char*)*execp->num_args);


  /* execp->state = xmalloc(sizeof(*(execp->state))); */
  bc_init_state (&execp->ctl, &execp->state, execp);

  /* Remember the (pre-replacement) arguments for later. */
  for (i=0; i<execp->num_args; ++i)
{
  execp->replace_vec[i] = argv[i+start];
}

So we have execp->ctl.replace_pat = "{}";. This is all in parser.c

The above is substituted as:

  size_t len;               /* Length in ARG before `replace_pat'.  */
  char *s = mbsstr (arg, ctl->replace_pat);
  if (s)
    {
      len = s - arg;
    }
  else
    {
      len = arglen;
    }

  if (bytes_left <= len)
    break;
  else
bytes_left -= len;

  strncpy (p, arg, len);
  p += len;
  arg += len;
  arglen -= len;

In bc_do_insert() in buildcmd.c.

So no, there is no way of escaping the {}. Yet, some versions of find will not substitute {}/foo but only {} itself so you may be able to use two different versions of find together with an -exec sh -c 'someCommad {}'.

Assuming that gfind is GNU-find and afind is AIX find you probably can achieve:

afind . -type d -execdir test -f foo.jpg \
        -exec sh -c 'gfind . -name "*.m4a" -exec someCommand {} \;' \;

But that would be a horrible hack.

Decent workaround

The problem you are facing is that you are performing globing to get the all files of a type in a directory. In other words, you are first finding a directory and globbing all files of that directory to be part of a single command line.

This is the kind of problem -execdir is meant to solve. It will run the command in the directory that contain the found file. For example:

$ mkdir -p a/a b/b c/c d e
$ touch a/a/foo.m4a b/b/foo.m4a
$ touch a/a/bar.m4a b/b/bar.m4a c/c/foobar.m4a
$ touch a/a/yay.jpg c/c/yay.jpg
$ find . -type f -name '*.m4a' -execdir test -e yay.jpg \; \
                               -execdir echo someCommand --arg yay.jpg {} +
someCommand --arg yay.jpg ./foobar.m4a
someCommand --arg yay.jpg ./bar.m4a ./foo.m4a

Moreover, I'm using the {} + form instead of the {} ; form for the exec. This will place all files found (in the directory it is running in) into the same command line.

Note that the command did not run in b/b because the test -e prevented it.

grochmal
  • 8,657
  • I'm not looking for a workaround; I'm asking specifically about the syntactic issue. – chepner Feb 12 '17 at 15:46
  • @chepner - I don't think there is a syntactic way. -exec family does not invoke a shell, it simply performs an execvp() after going through each argument and searching for the {} string to substitute. Maybe you could compile a non-GNU find which would not substitute {} if it is not a single argument (i.e. {}/foo.jpg is not substituted by AIX/HP-UX find) and then call the non-GNU find from the -exec in the GNU find. But that would be much more convoluted and even less intuitive to do. – grochmal Feb 12 '17 at 18:23
  • I'm just surprised there isn't a work around, complex though it might be. Python, for instance, allows literal { and } in a format string by doubling them: "{{{}}}".format("hi") evaluates to {hi}. – chepner Feb 12 '17 at 18:58
  • @chepner - I went to savannah to check, but no GNU-find uses plain strncpy() to make the substitution, so there really is not escaping whatsoever. – grochmal Feb 12 '17 at 20:01