Why do I need "\$$(variable)" instead of "$$(variable)" to get "$(variable)"? - makefile

new_contents = "\$$(cooly)"
all:
mkdir -p subdir
echo $(new_contents) | sed -e 's/^ //' > subdir/makefile
#echo "---MAKEFILE CONTENTS---"
#cd subdir && cat makefile
#echo "---END MAKEFILE CONTENTS---"
#cd subdir && $(MAKE)
# Note that variables and exports. They are set/affected globally.
cooly = "The subdirectory can see me!"
export cooly
# This would nullify the line above: unexport cooly
clean:
rm -rf subdir
What I want is a "$(cooly)" string, not the variable value.
I tried several combinations:
new_contents = "$(cooly)", gives the variable value, The subdirectory can see me!
new_contents = "$$", gives $
new_contents = "\$(cooly)", gives Syntax error: Unterminated quoted string error
Why new_contents = "$$(cooly)" doesn't give "$(cooly)" but result in nothing?
"$$" -> "$", so why isn't "$$(cooly)" ---> "$(cooly)"?

You have to understand both how make expansion works, and how shell expansion works, in order to write more complicated recipes in make. That's because, make recipes are passed to the shell after make is done expanding them.
Make passes recipe lines to the shell virtually verbatim: there is only one character that's special (not counting backslash/newlines at the end) and that's $. If make sees a $ it will try to expand it as a variable reference. To avoid that, you have to escape it as $$ to hide it from make.
So let's look at your makefile:
cooly = "The subdirectory can see me!"
echo $(new_contents) ...
If new_contents is "$(cooly)", make sees the $(cooly) as a variable reference and expands it before it even invokes the shell. So first make expands $(new_contents) to "$(cooly)", then it expands that to ""The subdirectory can see me!"" (because the quotes are in both variables, and quotes are not special to make: they're just like any other character like a or b). The result will be:
echo ""The subdirectory can see me!""
The shell will toss the quotes since they're no-ops and echo that value (into the pipe).
If new_contents is "\$(cooly)", that backslash doesn't mean anything to make. Just like quotes, backslashes (unless they are at the end of a line) are not special to make. So make expands just as before, but this time the command it passes to the shell is this:
echo "\"The subdirectory can see me!""
backslashes are not special to make, but they are special to the shell. Here you've escaped the second quote so the shell doesn't treat it as a quote character, which means you have an odd number of quotes in your command, which is why you get an error from the shell about non-terminated quotes.
If new_contents is "$$(cooly)", make doesn't expand the variable, it is passed along to the shell like this:
echo "$(cooly)"
However, $ is also special to the shell. Putting it in double quotes doesn't prevent the shell from trying to expand it. This tells the shell to run the command cooly and substitute the output. Almost certainly there is no command named cooly and so you'll get an error message to stderr (maybe you didn't notice it) and the shell will replace it with nothing because it didn't print anything to stdout.
If new_contents is "\$$(cooly)" then make will not expand, and run this shell command:
echo "\$(cooly)"
The shell sees the backslash and doesn't expand the $ but instead uses it literally, and you get the result you want.
Here are some hints:
First, do not include quotes in your make variables (unless the variable contains an entire shell command and you need quotes inside it). Make doesn't care about quotes and having them embedded in variables makes it very difficult to reason about what the shell will see.
Include the quotes only in the recipe.
Second, remember that since make doesn't care about quotes, it doesn't have the same behavior as the shell WRT single vs. double quotes. You can use single quotes around make variables to reduce the need to escape things from the shell, without hiding them from make.
So, I would write this:
new_contents = $$(cooly)
cooly = The subdirectory can see me!
all:
mkdir -p subdir
echo '$(new_contents)' | sed -e 's/^ //' > subdir/makefile
...
BTW, it's never a good idea to add # values to your makefile until it's completely done and working. Seeing the output make prints (which is what it's sending to the shell) is a great help in figuring out whether your recipes are right, and whether the problem is with your make constructs or shell constructs.

Related

Do Not interpret as Make variable in Make command

I have this code in a Makefile:
$(TCLNAME).batch.tcl: $(TCLNAME).tcl
echo source $::env(TOOLS_DIR)/my.tcl > $#
What I want to be printed in $(TCLNAME) is:
source $::env(TOOLS_DIR)/my.tcl
But I get an error because $::env(TOOLS_DIR) is being interpreted as a Make variable and it is expecting ( after the $.
How do I make it to print that line as is and not interpret it as Make variable ?
I tried to use escape character such as \$::env(TOOLS_DIR) but that also did not work.
Escape the $ with another $, and the parentheses with backslashes:
$(TCLNAME).batch.tcl: $(TCLNAME).tcl
echo source $$::env\(TOOLS_DIR\)/my.tcl > $#
A more universal escape method, as suggested by MadScientist:
Replace each $ with $$. The $ is the only symbol that Make has special treatment for.
Enclose the whole string in single quotes. Make doesn't care about them, they are for the shell.
If you need to print single quotes, replace each ' with '\''.
The end result:
echo 'source $$::env(TOOLS_DIR)/my.tcl' > $#
This way, only $ needs to be doubled (to prevent Make from interpreting it as a variable). Everything else is handled by the single quotes (which have no special meaning for Make, and are there for the shell).

How to use bash regex inside Makefile Target

In a bash script, the following regex appears to work well:
MY_STRING="FirstName-LastName-Gender-Age"
MY_REGEX="([^-]+)-([^-]+)$"
if [[ $MY_STRING =~ $MY_REGEX ]]; then
echo "Match: $BASH_REMATCH"
fi
I'm interested in using this script inside the Makefile. It appears to have syntax issues. For example:
my-target:
MY_STRING="FirstName-LastName-Gender-Age"
MY_REGEX="([^-]+)-([^-]+)$"
if [[ $MY_STRING =~ $MY_REGEX ]]; then
echo "Match: $BASH_REMATCH"
fi
What would be the correct syntax for this in make? The above appears to have issues with variable assignment, issues with the "$" in the regex, etc.
You have many problems here. The first one is that make doesn't invoke bash as its shell, it invokes /bin/sh (POSIX shell). On many systems that is a link to bash, but on many other systems it's a link to dash or some other POSIX shell. If you try to use bash-isms like this in your makefile recipes your makefile is not portable. You can, if you like, add SHELL := /bin/bash to your makefile to force make to use bash always... but it will fail anywhere that bash isn't available.
The second problem is that make invokes each logical line of your recipe in a separate shell. The way you have this written, every variable assignment is created in a new shell, then the shell exits and that assignment is lost. Further, only the first line of the if-statement is sent to a shell which is clearly a syntax error. You need to use backslashes before your newline to ensure the entire script is one logical line and is sent to the same shell... and that means you need to add semicolons to break lines where needed, as well.
Even in a shell script, the assignment of MY_REGEX is suspect because you have an unescaped dollar sign in a double-quoted string. However it happens to work for you because there's no character after the dollar sign that could be a variable. Nevertheless you should be using single-quoted strings here to be safe.
And finally, dollar signs ($) are special to make, so if you want to pass a $ to the shell you have to escape it. Make uses two dollar signs $$ to escape a single dollar sign.
So, your makefile should look like this:
SHELL := /bin/bash
my-target:
MY_STRING="FirstName-LastName-Gender-Age"; \
MY_REGEX='([^-]+)-([^-]+)$$'; \
if [[ $$MY_STRING =~ $$MY_REGEX ]]; then \
echo "Match: $$BASH_REMATCH"; \
fi
Personally I would rewrite this to use standard tools rather than relying on bash etc. but that's your call.

What's the use of \$$ in bash?

I found this as a suggestion of how to store the output of "eval" into a variable called line. So, what's the use of \$$?
command = "some command"
line = $(eval \$$command)
The \$ prevents the shell from trying to treat the $ as the beginning of a parameter expansion. However, the code as a whole doesn't do anything useful. After fixing the whitespace issues and adding a real command to the example, your code looks like
command="ls -l"
line=$(eval \$$command)
command is simply a string ls -l. To evaluate the next line, the shell first evaluates the command substitution. The first step is to expand the parameter command, yielding line=$(eval \$ls -l). Quote removal gets rid of the backslash, so eval receives the arguments $ls and -l. Since ls presumably is not a variable, $ls is expanded to the empty string, and eval is left simply with -l to execute. There being no such command, you get an error.
You might think, then, that the correct form is simply
line=$(eval $command)
or slightly better
line=$(eval "$command")
That will work for simple cases, but not in general. This has been hashed over many times in many questions; see Bash FAQ 50, "I'm trying to put a command in a variable, but the complex cases always fail!" for the details.
To answer the literal question, though, \$$ is useful for outputing the string $$, instead of expanding it to the current process ID:
# The exact output will vary
$ echo $$
86542
# Literal quotes
$ echo \$\$
$$
# Escaping either quote is sufficient
$ echo \$$ $\$
$$ $$

shell script exit with no match with question mark symbol

Why ./script.sh ? throws No match. ./script.sh is running fine.
script.sh
#!/bin/sh
echo "Hello World"
? is a glob character on UNIX. By default, in POSIX shells, a glob that matches no files at all will evaluate to itself; however, many shells have the option to modify this behavior and either pass no arguments in this case or make it an error.
If you want to pass this (or any other string which can be interpreted as a glob) literally, quote it:
./script.sh '?'
If you didn't use quotes, consider what the following would do:
touch a b c
./script.sh ? ## this is the same as running: ./script.sh a b c
That said -- the behavior of your outer shell (exiting when no matches exist, rather than defaulting to pass the non-matching glob expression as a literal) is non-default. If this shell is bash, you can modify it with:
shopt -u failglob
Note, however, that this doesn't really fix your problem, but only masks it when your current directory has no single-character filenames. The only proper fix is to correct your usage to quote and escape values properly.

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.

Resources