Makefile Variable Expansion and Command Substitution with Square Brackets

command-substitutiongnu-makeshell-script

To embrace the DRY (Don’t Repeat Yourself) principle, I sometimes need to share pieces of shell commands in a Makefile. So there is a recipe somewhere in that file like:

shell=/bin/bash
# …
.ONESHELL:
run:
    # Create bash sub-shell «cmd» var, holding a built-in test as a string
    @cmd='[ $$(grep -iE _dev </etc/hosts | wc -l) -eq 0 ]'
    # "$$" tells make to escape the dollar sign. So echoing …
    @echo "$$cmd"
    # gives «[ $(grep -iE _dev </etc/hosts | wc -l) -eq 0 ]» as I expected
    # I need this variable to be expanded and interpreted as a real
    # built-in square bracket test, so I wrote
    @$$cmd && echo "Pass ! do more magical things …" || true

I expected make to escape $ sign $$cmd$cmd which would in turn be expanded within bash context into the bracket test unquoted string … right ?
But I get an error instead /bin/bash: line 2: [: too many arguments

Does anybody have an idea of why this error is being raised ?
Why bash is not given the bracket test I expect ?
[ $(grep -iE _dev </etc/hosts | wc -l) -eq 0 ] && echo "Pass!"

Thank you.

Best Answer

Variable and command substitions in the shell occur after deciding on command boundaries, and parsing redirections (and assignments). You can put a program name and/or arguments in a variable and substitute them, but not pipes or redirection or other substitutions including $( command ) or assignments or shell keywords like if and for.

In this case you could eliminate the pipe and wc and substitution by changing the command and reversing your test:

cmd='grep -qi _dev /etc/hosts' # note file as argument not redirection
$$cmd || do_blah

where the substituted grep command fails (silently) if it doesn't find any match in the file, and if it fails do_blah is executed.

In general to use shell syntax (not just program arguments) in a substituted value, you must use eval to execute the substituted value (or values), or else run a child shell like sh -c "$$cmd" (substitute other shell if needed depending on environment and/or command).

Related Question