shell scripting process substitution <() inside command substitution $()

Viewed 49

I have a command with an output of 1 that is something like:

cmp -bl <(cmp -bl <(echo ABCDEF) <(echo ABDCEF)| wc -l) <(echo 0) |wc -l

I need to compare the command output with a value inside an if statement:

if [ $(cmp -bl <(echo ABCDEF) <(echo ABDCEF)| wc -l) = "1" ]; then ...

There error that I have is syntax related: syntax error near unexpected token ('

2 Answers

Just leave a space after [ and before ]. [ is an actual command similar to test. You need to separate the command from its parameters. ] is not a command but syntax rules for parameters of [ command requires it to be separated as well.

if [ $(cmp -bl <(cmp -bl <(echo ABCDEF) <(echo ABDCEF)| wc -l) <(echo 0) |wc -l) = "1" ]; then ...

EDIT: With the new version of your question, plus comments you made, answer is straightforward. Your code (the one in the current version of your question) is totally OK. It works fine on all my computers with bash. But it is bash. Not sh.

If you use it with /bin/sh (as you said you were doing in one of your comments), then, the error you get is

/bin/sh: 1: Syntax error: "(" unexpected

The unexpected parenthesis here is the one of the <(...) syntax. This is bash specific syntax. /bin/sh cannot use pip/process substitution.

Understand that <(command) syntax is a special substitution. It runs command in another process, with output redirected to a pipe, whose filename is substituted to the <(command) part. echo <(ls) for examples displays something like /dev/fd/63, Which is a named pipe, a "file" whose "content" is the result of ls (even tho it is not a file, and it as no content. But reading /dev/fd/63 is reading the output of ls. It is quite useful, even if most of the time, you don't need it. But it is quite recent (well, maybe one or two decades. But it didn't exist when I learned sh/bash), and not part of /bin/sh.

So answer is: you need bash. You can't do that in /bin/sh

If you want to use cmp with the output of two commands, and you really can't just use #!/bin/bash instead of #!/bin/sh, the simpler (and less efficient) solution would be to use temp files to store the result of the commands first (hoping that those commands do not produces gigabytes of output), and compare them.

Or, you could reproduce a quite similar behavior with named pipes.

#!/bin/sh
mknod /tmp/pipe1.$$ p
mknod /tmp/pipe2.$$ p
echo ABCDEF > /tmp/pipe1.$$ &
echo ABCxEF > /tmp/pite2.$$ &
if [ $(cmp -bl /tmp/pipe1.$$ /tmp/pite2.$$ | wc -l) = "1" ]; then
    echo One and only one diff
fi
rm /tmp/pipe1.$$
rm /tmp/pipe2.$$

It does roughly the same The echo stuff > /tmp/pipe1.$$ & for a echo stuff process (replace echo with whatever command you want). Whouse output is redirected in named pipe /tmp/pipe1.$$ we created in the first line. (the .$$ suffix, is just to ensure that no other process could use the same /tmp/pipe1 name, since $$ is the pid of our process) Then you have a quite classical if [ $(cmp /tmp/pipe1.$$ /tmp/pipe2.$$ | wc -l) ] in which, cmp is simply called with two files names, that happen to be named pipes (exactly as with <(...) after all)

But again, that is only if you have a very good reason to want a pure sh (not bash) solution. In which case you should also edit your question to remove the 'bash' tag.

================= Old answer ==========================

This seems very convoluted to me. For example the usage of cmp foo <(echo 0) to compare the content of foo (foo itself being the result of a wc -l on a cmp) to 0. But I surmise that you already understand that, that you understand that this is a Rube-Goldberg contraption instead of "== 0", and you understand that each <() fork a process, and return a name of pipes from this process (it has to be used when you need to pass a filename containing the result of an execution to command).

If not, you should explain what you are really trying to do, because, even replacing the <(echo ...) by real commands (I also surmise those are just examples), there is no way those several stages of cmp and wc are the correct way.

That being said, just add a space around [ and ], and it will do exactly what you intended. (edit: as already said by others in faster reply and comments. I type too long answers... I do not delete this answer, because the preamble may be relevant)

EDIT: to reply more on the usage part

  • No need, as seen in comments, for two layers of wc -l (one is already a strange way, but if it works...)
  • Be careful of how you compare things.
    • Mine (in comments), using == works only in linux, because wc output is a string of digits, with nothing more. It doesn't on mac, because of heading spaces. So 1 is not the same as 1.
    • Yours, is the same. But since you used [ instead of [[, [ being and external command, while [[ is bash syntax ([[ is better: no fork of a process), the spaces of 1 are lost during the passing of the argument to [ command
    • the cleanest solution is probably to use -eq which compares the integers, regardless of how they are printed.

So, altogether

if [[ $(cmp -bl <(echo string1) <(echo string2) | wc -l) -eq 1 ]]
then
   echo one and only one difference
fi

should work. Again, not sure it is the fastest, nor the shortest, nor the most elegant solution. But as long as nobody provides a better one...

Related