Makefile: why command substitution can't work in $(shell) function? - makefile

I have a variable in Makefile:
JAVABIN = $(shell dirname $(which java))
And when I echo the JAVA_HOME variable in Makefile, the variable definition complains:
dirname: missing operand
Try 'dirname --help' for more information.
When I quote the $(which java), the JAVABIN is ., so the result is wrong. And I didn't understand how make reads a Makefile, maybe it is the cause. Thank you very much.

You need to escape the dollar:
JAVABIN = $(shell dirname $$(which java))
See 6.1 Basics of Variable References.
The specific error message you received was caused by the fact that the $(which java) piece expanded to the empty string, since it was an undefined variable. Hence the dirname system command ended up seeing no arguments, in which case it complains of a "missing operand".

Related

Assignment statement stored in a variable causes command not found error in Bash

For example:
#!/bin/bash
sss='ls -l'
$sss
ttt='a=100'
$ttt
The output of ls is correct, however, the assignment statement will output an error message:
line 5: a=100: command not found
Why the difference?
If assignment is not command, what is it? I mean what is the difference between explicit a=100 and a=100 expanded from variable, I mean, the bash sees the same thing a=100, right? Why they got different interpretation?
That's because the output from variable expansion is run as a command, precisely get replaced in the command line as if you have inserted the content literally.
Here, you have ttt='a=100', so when you do $ttt next, it will be simple expanded as a=100 and as this would be the command to run being the only parameter present. And the error is due to the obvious fact that this is not a valid command.
You can tack the expansion with some actual-valid command to get the expansion as that command's argument (e.g. echo $ttt), as you can imagine.
If you ever need to do assignments like that, leverage declare:
$ ttt='a=100'
$ declare "$ttt"
$ echo "$a"
100
If you want to execute code, use a function, not a parameter expansion.
$ sss () { ls -l; }
$ sss
foo.txt bar.txt
$ ttt () { a=100; }
$ ttt
$ printf '%d\n' "$a"
100
Because a=100 is not a command, and you can't evaluate assignments like that.
In order to properly evaluate the assignment try:
eval $ttt

How can I force bash to expand a variable to pass it as an argument?

I found a weird behavior that I don't know how to workaround.
$ var1=*
$ echo $var1
Audiobooks Downloads Desktop (etc.)
$ ls $var1
Audiobooks:
Downloads:
(etc)
All seems OK. At declaration, the variable gets expanded and everything else works. But see this:
$ var2=~/rpmbuild/{SRPMS,RPMS/*}/enki-*.rpm
$ echo $var2
/home/yajo/rpmbuild/{SRPMS,RPMS/*}/enki-*.rpm
$ ls $var2
ls: no se puede acceder a /home/yajo/rpmbuild/{SRPMS,RPMS/*}/enki-*.rpm: No existe el fichero o el directorio
$ ls /home/yajo/rpmbuild/{SRPMS,RPMS/*}/enki-*.rpm
/home/yajo/rpmbuild/RPMS/noarch/enki-12.10.3-1.fc18.noarch.rpm /home/yajo/rpmbuild/SRPMS/enki-12.10.3-1.fc18.src.rpm
/home/yajo/rpmbuild/RPMS/noarch/enki-12.10.3-1.fc19.noarch.rpm /home/yajo/rpmbuild/SRPMS/enki-12.10.3-1.fc19.src.rpm
This time, at declaration only ~ gets expanded, which produces that I cannot pass it as an argument to ls. However, passing the same string literally produces the expected results.
Questions are:
Why sometimes expand and sometimes not?
How to mimic the behavior of $var1 with $var2?
Thanks.
Extra notes:
I tried the same with double and single quotes, but with the same bad results.
The order in which the shell parses various aspects of the command line is not obvious, and it matters for things like this.
First, the wildcards aren't expanded at declaration, they're expanded after the variable value is substituted (note: in these examples I'll pretend I have your filesystem):
$ var1=*
$ echo "$var1" # double-quotes prevent additional parsing of the variable's value
*
$ echo $var1 # without double-quotes, variable value undergoes wildcard expansion and word splitting
Audiobooks:
Downloads:
(etc)
BTW, the ~ is expanded at declaration, confusing things even further:
$ var2=~
$ echo "$var2" # again, double-quotes let me see what's actually in the variable
/home/yajo
The problem with ~/rpmbuild/{SRPMS,RPMS/*}/enki-*.rpm1 is that while the shell does wildcard expansion (*) on the value after substitution, it doesn't do brace expansion ({SRPMS,RPMS/*}), so it's actually looking for directory names with braces and commas in the name... and not finding any.
The best way to handle this is generally to store the file list as an array; if you do this right, everything gets expanded at declaration:
$ var2=(~/rpmbuild/{SRPMS,RPMS/*}/enki-*.rpm)
$ echo "${var2[#]}" # This is the proper way to expand an array into a word list
/home/yajo/rpmbuild/RPMS/noarch/enki-12.10.3-1.fc18.noarch.rpm etc...
Note that arrays are a bash extension, and will not work in plain POSIX shells. So be sure to start your script with #!/bin/bash, not #!/bin/sh.

Using ifdef and ifndef directives

I'm trying to check whether a variable is defined using ifndef/ifdef, but I keep getting a not found error from the execution. I'm using GNU Make 3.81, and here is a snippet of what I have:
all: _images
$(call clean, .)
$(call compile, .)
#$(OPENER) *.pdf &
_images:
$(call clean, "images")
$(call compile, "images")
define clean
#rm -f ${1}/*.log ${1}/*.aux ${1}/*.pdf
endef
define compile
ifdef ${1}
dir = ${1}
else
dir = .
endif
ifdef ${2}
outdir = ${2}
else
outdir = ${1}
endif
#$(COMPILER) -output-directory ${outdir} ${dir}/*.tex
endef
And the exact error:
$ make
ifdef "images"
/bin/sh: 1: ifdef: not found
make: *** [_images] Error 127
Edit:
Considering Barmar comments, here goes the conclusions:
The contents of a define are shell command lines, not make directives;
to break lines inside commands within a define block, the linebreak must be escaped -- with \;
also, each block corresponding to one-liner commands is executed separately, each in a different shell execution, which means that, defining local variables won't work if the intention is to access the variable value in the next one-liner block.
Thanks tripleee for the nice work around.
You can combine the shell's facilities with Make's to get a fairly succinct definition.
define compile
#dir="${1}"; outdir="${2}"; outdir=$${outdir:-"$dir"}; \
$(COMPILER) -output-directory "$${outdir}" "$${dir:-.}/*.tex
The double-dollar is an escape which passes a single dollar sign to the shell. The construct ${variable:-value} returns the value of $variable unless it is unset or empty, in which case it returns value. Because ${1} and ${2} are replaced by static strings before the shell evaluates this expression, we have to take the roundabout route of assigning them to variables before examining them.
This also demonstrates how to combine two "one-liners" into a single shell invocation. The semicolon is a statement terminator (basically equivalent to a newline) and the sequence of a backslash and a newline causes the next line to be merged with the current line into a single "logical line".
This is complex enough that I would recommend you omit the leading # but I left it in just to show where it belongs. If you want silent operation, once you have it properly debugged, run with make -s.

Failing to use $PWD with sed, bash

I am trying to create a script that replaces sub strings with an absolute path, so I'm using
sed s/TEMPDIR/$PWD/ file.py > newfile.py
So obviously I want to replace TEMP with the absolute path, but I keep getting
sed: -e expression #1, char 12: unknown option to `s'
I think it might be because the path contains '/', but I don't know how to solve it. Any ideas?
Also, deos PWD guaranties an absolute path, given that I don't deal with links?
Thanks!!
use a different delimiter:
sed "s#TEMPDIR#$PWD#" file.py > newfile.py
Using PWD probably does not guarantee an absolute path. (Depends on the shell). But pwd will return an absolute path, so use:
sed "s#TEMPDIR#$(pwd)#" file.py > newfile.py
Note that neither of these will work if your paths contain #. Pick a delimiter that does not appear in either. Note the following from the shell standard:
In cases where PWD is set to the pathname that would be output by pwd -P, if there is insufficient permission on the current working directory, or on any parent of that directory, to determine what that pathname would be, the value of PWD is unspecified. Assignments to this variable may be ignored. If an application sets or unsets the value of PWD , the behaviors of the cd and pwd utilities are unspecified.
So you can probably rely on $PWD. Note that neither pwd nor $PWD will resolve symbolic links, so you might prefer to use readlink -f .

Bash eval replacement $() not always equivalent?

Everybody says eval is evil, and you should use $() as a replacement. But I've run into a situation where the unquoting isn't handled the same inside $().
Background is that I've been burned too often by file paths with spaces in them, and so like to quote all such paths. More paranoia about wanting to know where all my executables are coming from. Even more paranoid, not trusting myself, and so like being able to display the created commands I'm about to run.
Below I try variations on using eval vs. $(), and whether the command name is quoted (cuz it could contain spaces)
BIN_LS="/bin/ls"
thefile="arf"
thecmd="\"${BIN_LS}\" -ld -- \"${thefile}\""
echo -e "\n Running command '${thecmd}'"
$($thecmd)
Running command '"/bin/ls" -ld -- "arf"'
./foo.sh: line 8: "/bin/ls": No such file or directory
echo -e "\n Eval'ing command '${thecmd}'"
eval $thecmd
Eval'ing command '"/bin/ls" -ld -- "arf"'
/bin/ls: cannot access arf: No such file or directory
thecmd="${BIN_LS} -ld -- \"${thefile}\""
echo -e "\n Running command '${thecmd}'"
$($thecmd)
Running command '/bin/ls -ld -- "arf"'
/bin/ls: cannot access "arf": No such file or directory
echo -e "\n Eval'ing command '${thecmd}'"
eval $thecmd
Eval'ing command '/bin/ls -ld -- "arf"'
/bin/ls: cannot access arf: No such file or directory
$("/bin/ls" -ld -- "${thefile}")
/bin/ls: cannot access arf: No such file or directory
So... this is confusing. A quoted command path is valid everywhere except inside a $() construct? A shorter, more direct example:
$ c="\"/bin/ls\" arf"
$ $($c)
-bash: "/bin/ls": No such file or directory
$ eval $c
/bin/ls: cannot access arf: No such file or directory
$ $("/bin/ls" arf)
/bin/ls: cannot access arf: No such file or directory
$ "/bin/ls" arf
/bin/ls: cannot access arf: No such file or directory
How does one explain the simple $($c) case?
The use of " to quote words is part of your interaction with Bash. When you type
$ "/bin/ls" arf
at the prompt, or in a script, you're telling Bash that the command consists of the words /bin/ls and arf, and the double-quotes are really emphasizing that /bin/ls is a single word.
When you type
$ eval '"/bin/ls" arf'
you're telling Bash that the command consists of the words eval and "/bin/ls" arf. Since the purpose of eval is to pretend that its argument is an actual human-input command, this is equivalent to running
$ "/bin/ls" arf
and the " gets processed just like at the prompt.
Note that this pretense is specific to eval; Bash doesn't usually go out of its way to pretend that something was an actual human-typed command.
When you type
$ c='"/bin/ls" arf'
$ $c
the $c gets substituted, and then undergoes word splitting (see ยง3.5.7 "Word Splitting" in the Bash Reference Manual), so the words of the command are "/bin/ls" (note the double-quotes!) and arf. Needless to say, this doesn't work. (It's also not very safe, since in addition to word-splitting, $c also undergoes filename-expansion and whatnot. Generally your parameter-expansions should always be in double-quotes, and if they can't be, then you should rewrite your code so they can be. Unquoted parameter-expansions are asking for trouble.)
When you type
$ c='"/bin/ls" arf'
$ $($c)
this is the same as before, except that now you're also trying to use the output of the nonworking command as a new command. Needless to say, that doesn't cause the nonworking command to suddenly work.
As Ignacio Vazquez-Abrams says in his answer, the right solution is to use an array, and handle the quoting properly:
$ c=("/bin/ls" arf)
$ "${c[#]}"
which sets c to an array with two elements, /bin/ls and arf, and uses those two elements as the word of a command.
With the fact that it doesn't make sense in the first place. Use an array instead.
$ c=("/bin/ls" arf)
$ "${c[#]}"
/bin/ls: cannot access arf: No such file or directory
From the man page for bash, regarding eval:
eval [arg ...]:
The args are read and concatenated together into a single command.
This command is then read and executed by the shell, and its exit
status is returned as the value of eval.
When c is defined as "\"/bin/ls\" arf", the outer quotes will cause the entire thing to be processed as the first argument to eval, which is expected to be a command or program. You need to pass your eval arguments in such a way that the target command and its arguments are listed separately.
The $(...) construct behaves differently than eval because it is not a command that takes arguments. It can process the entire command at once instead of processing arguments one at a time.
A note on your original premise: The main reason that people say that eval is evil was because it is commonly used by scripts to execute a user-provided string as a shell command. While handy at times, this is a major security problem (there's typically no practical way to safety-check the string before executing it). The security problem doesn't apply if you are using eval on hard-coded strings inside your script, as you are doing. However, it's typically easier and cleaner to use $(...) or `...` inside of scripts for command substitution, leaving no real use case left for eval.
Using set -vx helps us understand how bash process the command string.
As seen in the picture, "command" works cause quotes will be stripped when processing. However, when $c(quoted twice) is used, only the outside single quotes are removed. eval can process the string as the argument and outside quotes are removed step by step.
It is probably just related to how bash semanticallly process the string and quotes.
Bash does have many weird behaviours about quotes processing:
Bash inserting quotes into string before execution
How do you stop bash from stripping quotes when running a variable as a command?
Bash stripping quotes - how to preserve quotes

Resources