Parameter substitution with date - bash

I've got a function that takes an optional date argument, and does calculations based on it. I'm trying to make sure that if that argument isn't provided, the current date/time is used as a default. I've tried this half a dozen ways, but I keep coming back to
${testDate:=$(date)}
Which works. The value of testDate is properly set, and the rest of the function works properly. However, I get an error message thrown with it:
Wed: command not found
(The date string is "Wed Mar 9 20:16:48 EST 2016" as of right now)
What am I doing wrong? Or is there a better way to do this?
Edit: To clarify, I'm emulating this, which works fine elsewhere in my script:
${menuTitle:=$"Choose from the Menu"}
How specifically is the line I posted at the top different from this one?

The typical approach in this situation is to use the null utility (builtin), :, as it both expands and ignores its arguments:
: "${testDate:=$(date)}" # double quotes not strictly required
While it won't make a difference in most scenarios, the double quotes eliminate unnecessary additional expansions, such as pathname expansion and word splitting.
Even though expansion ${testDate:=$(date)} expands to the effective value of $testDate - whether it had a previous nonempty value or was just initialized to the output from date - that value is ignored by :, resulting in the desired conditional initialization only.
Without :, with ${testDate:=$(date)} used as a statement by itself, Bash interprets whatever the expansion results in as a command, which in your case resulted in the attempt to execute the output from date as a command, which obviously failed (the first whitespace-separated token, Wed, was interpreted as the command name).
Note that the above is POSIX-compliant, so it therefore works not only in Bash, but in all major POSIX-compatible shells (bash, dash, ksh, zsh).

The problem is that this line:
${testDate:=$(date)}
not only assigns testDate a value but the variable is evaluated as well, and consequently executed as a command.
You can just do:
if [ -z "${testDate}" ]; then
testDate=$(date)
fi
Or, as a one-liner:
[ -z "${testDate}" ] && testDate=$(date)

Related

Extracting git commit information in GitHub action workflow- use of '$' symbol [duplicate]

This question already has answers here:
Backticks vs braces in Bash
(3 answers)
Brackets ${}, $(), $[] difference and usage in bash
(1 answer)
Closed 4 years ago.
I have two questions and could use some help understanding them.
What is the difference between ${} and $()? I understand that ()
means running command in separate shell and placing $ means passing
the value to variable. Can someone help me in understanding
this? Please correct me if I am wrong.
If we can use for ((i=0;i<10;i++)); do echo $i; done and it works fine then why can't I use it as while ((i=0;i<10;i++)); do echo $i; done? What is the difference in execution cycle for both?
The syntax is token-level, so the meaning of the dollar sign depends on the token it's in. The expression $(command) is a modern synonym for `command` which stands for command substitution; it means run command and put its output here. So
echo "Today is $(date). A fine day."
will run the date command and include its output in the argument to echo. The parentheses are unrelated to the syntax for running a command in a subshell, although they have something in common (the command substitution also runs in a separate subshell).
By contrast, ${variable} is just a disambiguation mechanism, so you can say ${var}text when you mean the contents of the variable var, followed by text (as opposed to $vartext which means the contents of the variable vartext).
The while loop expects a single argument which should evaluate to true or false (or actually multiple, where the last one's truth value is examined -- thanks Jonathan Leffler for pointing this out); when it's false, the loop is no longer executed. The for loop iterates over a list of items and binds each to a loop variable in turn; the syntax you refer to is one (rather generalized) way to express a loop over a range of arithmetic values.
A for loop like that can be rephrased as a while loop. The expression
for ((init; check; step)); do
body
done
is equivalent to
init
while check; do
body
step
done
It makes sense to keep all the loop control in one place for legibility; but as you can see when it's expressed like this, the for loop does quite a bit more than the while loop.
Of course, this syntax is Bash-specific; classic Bourne shell only has
for variable in token1 token2 ...; do
(Somewhat more elegantly, you could avoid the echo in the first example as long as you are sure that your argument string doesn't contain any % format codes:
date +'Today is %c. A fine day.'
Avoiding a process where you can is an important consideration, even though it doesn't make a lot of difference in this isolated example.)
$() means: "first evaluate this, and then evaluate the rest of the line".
Ex :
echo $(pwd)/myFile.txt
will be interpreted as
echo /my/path/myFile.txt
On the other hand ${} expands a variable.
Ex:
MY_VAR=toto
echo ${MY_VAR}/myFile.txt
will be interpreted as
echo toto/myFile.txt
Why can't I use it as bash$ while ((i=0;i<10;i++)); do echo $i; done
I'm afraid the answer is just that the bash syntax for while just isn't the same as the syntax for for.
your understanding is right. For detailed info on {} see bash ref - parameter expansion
'for' and 'while' have different syntax and offer different styles of programmer control for an iteration. Most non-asm languages offer a similar syntax.
With while, you would probably write i=0; while [ $i -lt 10 ]; do echo $i; i=$(( i + 1 )); done in essence manage everything about the iteration yourself

bash array slicing strange syntax in perl path: `${PATH:+:${PATH}}"`

On Linux Ubuntu, when you do sudo apt update && sudo apt install perl, it adds the following to the bottom of your ~/.bashrc file (at least, many months later, I think that is what added those lines):
PATH="/home/gabriel/perl5/bin${PATH:+:${PATH}}"; export PATH;
PERL5LIB="/home/gabriel/perl5/lib/perl5${PERL5LIB:+:${PERL5LIB}}"; export PERL5LIB;
PERL_LOCAL_LIB_ROOT="/home/gabriel/perl5${PERL_LOCAL_LIB_ROOT:+:${PERL_LOCAL_LIB_ROOT}}"; export PERL_LOCAL_LIB_ROOT;
PERL_MB_OPT="--install_base \"/home/gabriel/perl5\""; export PERL_MB_OPT;
PERL_MM_OPT="INSTALL_BASE=/home/gabriel/perl5"; export PERL_MM_OPT;
What does this strange syntax do in many of the lines, including in the first line? It appears to be some sort of bash array slicing:
${PATH:+:${PATH}}
The ${PATH} part is pretty straightforward: it reads the contents of the PATH variable, but the rest is pretty cryptic to me.
It's not array slicing; it's a use of one of the POSIX parameter expansion operators. From the bash man page, in the Parameter Expansions section,
${parameter:+word}
Use Alternate Value. If parameter is null or unset, nothing is
substituted, otherwise the expansion of word is substituted.
It's a complex way of making sure that you only add a : to the value if PATH isn't empty to start with. A longer, clearer way of writing it would be
if [ -n "$PATH" ]; then
PATH=/home/gabriel/perl5/bin:$PATH
else
PATH=/home/gabriel/perl5/bin
fi
However, since it if almost inconceivable that PATH is empty when .basrhc is sourced, it would be simpler to just prepend the new path and be done with it.
PATH=/home/gabriel/perl5/bin:$PATH
If PATH actually ended with a :, it would implicitly include the current working directory in the search path, which isn't a good idea for security reasons. Also from the bash man page, in the section on Shell Variables under the entry for PATH:
A zero-length (null) directory name in the
value of PATH indicates the current directory. A null directory
name may appear as two adjacent colons, or as an initial or
trailing colon.
As an aside, it's good to understand what various installers try to add to your shell configuration. It's not always necessary, and sometimes can actively change something you already have configure.
I would much prefer if packages simply printed instructions for what needs to be added to your configuration (and why), and leave it to the user to make the appropriate modifications.
What does this strange syntax do in many of the lines, including in the first line?
It's the ${parameter:+word} form of parameter expansion where word becomes the expanded value if parameter is not unset and not having the value of an empty string (a.k.a. null).

Why expanding on left hand side arg is leading to error

I accidentally wrote next assignment in one of my scripts:
$X=$(echo 'astring')
which fails with =astring: command not found.
The correct and intended assignment was X=$(echo 'astring') which works and sets X='astring'.
The question is what happens with the first one? Is $ trying to execute the result of the right hand side? And if that is so then why is it also incorporating = in it? I'm confused.
The behaviour of $X=$(echo 'astring') depends on the contents of $X. When it's empty (which it probably was), it expands to an empty string, and the remaining string is interpreted as a command
$X=$(echo 'astring')
=astring
If $X contains something, e.g. "astring", the string is expanded to
astring=astring
But it doesn't set the $astring variable as one might think, because of the order of expansions. Assignments are identified before any expansion happens. So, it's interpreted as a command again
astring=astring: command not found

Bash - Why does $VAR1=FOO or 'VAR=FOO' (with quotes) return command not found?

For each of two examples below I'll try to explain what result I expected and what I got instead. I'm hoping for you to help me understand why I was wrong.
1)
VAR1=VAR2
$VAR1=FOO
result: -bash: VAR2=FOO: command not found
In the second line, $VAR1 gets expanded to VAR2, but why does Bash interpret the resulting VAR2=FOO as a command name rather than a variable assignment?
2)
'VAR=FOO'
result: -bash: VAR=FOO: command not found
Why do the quotes make Bash treat the variable assignment as a command name?
Could you please describe, step by step, how Bash processes my two examples?
How best to indirectly assign variables is adequately answered in other Q&A entries in this knowledgebase. Among those:
Indirect variable assignment in bash
Saving function output into a variable named in an argument
If that's what you actually intend to ask, then this question should be closed as a duplicate. I'm going to make a contrary assumption and focus on the literal question -- why your other approaches failed -- below.
What does the POSIX sh language specify as a valid assignment? Why does $var1=foo or 'var=foo' fail?
Background: On the POSIX sh specification
The POSIX shell command language specification is very specific about what constitutes an assignment, as quoted below:
4.21 Variable Assignment
In the shell command language, a word consisting of the following parts:
varname=value
When used in a context where assignment is defined to occur and at no other time, the value (representing a word or field) shall be assigned as the value of the variable denoted by varname.
The varname and value parts shall meet the requirements for a name and a word, respectively, except that they are delimited by the embedded unquoted equals-sign, in addition to other delimiters.
Also, from section 2.9.1, on Simple Commands, with emphasis added:
The words that are recognized as variable assignments or redirections according to Shell Grammar Rules are saved for processing in steps 3 and 4.
The words that are not variable assignments or redirections shall be expanded. If any fields remain following their expansion, the first field shall be considered the command name and remaining fields are the arguments for the command.
Redirections shall be performed as described in Redirection.
Each variable assignment shall be expanded for tilde expansion, parameter expansion, command substitution, arithmetic expansion, and quote removal prior to assigning the value.
Also, from the grammar:
If all the characters preceding '=' form a valid name (see the Base Definitions volume of IEEE Std 1003.1-2001, Section 3.230, Name), the token ASSIGNMENT_WORD shall be returned. (Quoted characters cannot participate in forming a valid name.)
Note from this:
The command must be recognized as an assignment at the very beginning of the parsing sequence, before any expansions (or quote removal!) have taken place.
The name must be a valid name. Literal quotes are not part of a valid variable name.
The equals sign must be unquoted. In your second example, the entire string was quoted.
Assignments are recognized before tilde expansion, parameter expansion, command substitution, etc.
Why $var1=foo fails to act as an assignment
As given in the grammar, all characters before the = in an assignment must be valid characters within a variable name for an assignment to be recognized. $ is not a valid character in a name. Because assignments are recognized in step 1 of simple command processing, before expansion takes place, the literal text $var1, not the value of that variable, is used for this matching.
Why 'var=foo' fails to act as an assignment
First, all characters before the = must be valid in variable names, and ' is not valid in a variable name.
Second, an assignment is only recognized if the = is not quoted.
1)
VAR1=VAR2
$VAR1=FOO
You want to use a variable name contained in a variable for the assignment. Bash syntax does not allow this. However, there is an easy workaround :
VAR1=VAR2
declare "$VAR1"=FOO
It works with local and export too.
2)
By using single quotes (double quotes would yield the same result), you are telling Bash that what is inside is a string and to treat it as a single entity. Since it is the first item on the line, Bash tries to find an alias, or shell builtin, or an executable file in its PATH, that would be named VAR=FOO. Not finding it, it tells you there is no such command.
An assignment is not a normal command. To perform an assignment contained in a quote, you would need to use eval, like so :
eval "$VAR1=FOO" # But please don't do that in real life
Most experienced bash programmers would probably tell you to avoid eval, as it has serious drawbacks, and I am giving it as an example just to recommend against its use : while in the example above it would not involve any security risk or error potential because the value of VAR1 is known and safe, there are many cases where an arbitrary (i.e. user-supplied) value could cause a crash or unexpected behavior. Quoting inside an eval statement is also more difficult and reduces readability.
You declare VAR2 earlier in the program, right?
If you are trying to assign the value of VAR2 to VAR1, then you need to make sure and use $ in front of VAR2, like so:
VAR1=$VAR2
That will set the value of VAR2 equal to VAR1, because when you utilize the $, you are saying that value that is stored in the variable. Otherwise it doesn't recognize it as a variable.
Basically, a variable that doesn't have a $ in front of it will be interpreted as a command. Any word will. That's why we have the $ to clarify "hey this is a variable".

Bash Script Understanding

I'm trying to figure what exactly is the bash code mentioned below trying to do, specially the [-z $M ] part. here M is a variable with a value
if [ -z $M ] ; then
can not find module directory
exit 1
man test Enter
press /-zEnter
you see:
-z STRING
the length of STRING is zero
so your script does, if $M length==0, then exit with status code 1
As others have said, it's using the test command (aka [) to check whether a string is blank. At least, that's what it's trying to do; because the string ($M) isn't double-quoted, it's actually doing something slightly different. Without double-quotes, the value of $M undergoes word splitting and wildcard expansion after it's replaced, so it might not be treated as a simple string (which the -z operator works on) with ... potentially unexpected consequences. Let me run through some of the possibilities:
If the value of $M is a single word (non-blank) without wildcards (* and ?), everything works as expected.
If the value of $M is zero-length (blank), the test command only sees a single argument (-z); when test is only given a single argument, it simply tests whether it's blank -- it's not, so it evaluates to true.
This happens to be the expected result in this case, but it's purely by coincidence, and with many other operators it wouldn't be the right result. For instance, [ -n $M ] (which looks like it should test whether $M is *non*blank), [ -e $M ] (which looks like it should test whether $M is the name of a file/directory) etc will all evaluate to true if $M is blank.
If the value of $M consists entirely of whitespace (but isn't empty), it gets eliminated before test sees it, and test evaluates to true (see previous case). This might or might not be what the scripter had in mind.
If the value of $M has multiple words, test will attempt to evaluate it as (part of) an expression. It will probably not be a valid expression, in which case test will print an error and return false (which is right ... sort of).
On the other hand, if it is a valid expression... Suppose for example you had, M='= -z; test would evaluate the expression -z = -z which would be true, not at all what the scripter had in mind.
If the value of $M has any wildcards, the shell will try to match them against files and pass test the list of matches; it'll try to evaluate them as an expression (see previous case), probably giving an error and returning false (again, sort of right).
Mind you, if you happen to have set the nullglob shell option and the wildcard doesn't match any files, the shell will replace it with null, and the script will act as though "u*n*m*a*t*c*h*e*d" was the empty string.
The lesson here: if you don't want your scripts to behave in weird and unexpected ways, double-quote your variable references!
The [ is actually a standard Unix command (probably implemented internally in Bash, but available whatever shell you are using). It is an alias for the command test, so its manual entry can be found by typing man test. Here's an online copy of that manual page.
When invoked as [, test will generally expect its last argument to be a ], just for good looks, so [ -z $M ] is equivalent to test -z $M.
In this case, the -z argument causes test to return true if the following argument is a string of length zero. The variable $M, defined further up the script, can thus be tested for a valid value.
It checks whether the content of variable M is an empty string.
Check this link

Resources