variable expansion in curly braces - bash

This is code
a=''
b=john
c=${a-$b}
echo $c
And the output is empty line
And for similar code where first variable is not initialized
b1=doe
c1=${a1-$b1}
echo $c1
And the output is
doe
I do not understand how bash deals with expanding of variables leading to different results.

There are two variants of the ${var-value} notation, one without a colon, as shown, and one with a colon: ${var:-value}.
The first version, without colon, means 'if $var is set to any value (including an empty string), use it; otherwise, use value instead'.
The second version, with colon, means 'if $var is set to any value except the empty string, use it; otherwise, use value instead'.
This pattern holds for other variable substitutions too, notably:
${var:=value}
if $var is set to any non-empty string, leave it unchanged; otherwise, set $var to value.
${var=value}
if $var is set to any value (including an empty string), leave it unchanged; otherwise, set $var to value.
${var:?message}
if $var is set to any non-empty string, do nothing; otherwise, complain using the given message' (where a default message is supplied if message is itself empty).
${var?message}
if $var is set to any value (including an empty string), do nothing; otherwise, complain using the given message'.
These notations all apply to any POSIX-compatible shell (Bourne, Korn, Bash, and others). You can find the manual for the bash version online — in the section Shell Parameter Expansion. Bash also has a number of non-standard notations, many of which are extremely useful but not necessarily shared with other shells.

Related

Filter out non-aplhanumeric characters within Bash brace expansion

Is it possible to modify an environment variable prior to expanding it?
Example:
set ENV_VAR=some-stuff
echo ${ENV_VAR} - get this to print "somestuff" instead of "some-stuff"
$ ENV_VAR=some-stuff
$ echo ${ENV_VAR//[^a-zA-Z]}
somestuff
Parameter expansion (that's the proper terminology for what you call "brace expansion", which is actually something else entirely) accepts modifiers, such as making the first character (or all characters) lower- or upper-case, eliminating a prefix or a suffix, and replacing a pattern with some other string (or with the null string). See "Parameter expansion" in the section "Expansion" in the manual page.

Bash assignment value to variable as command substitution and print value output

I would like to achieve this in Bash: echo $(a=1)and print the value of variable a
I test eval, $$a,{}, $() but none of them work as most of the times either I got literally a=1 or in one case (I don't remember which) it tried to execute the value.
I known that I can do: a=1;echo $a but because I'm little fun one command per line (even if sometimes is getting little complicated) I was wondering if is possible to do this either with echo or with printf
If you know that $a is previously unset, you can do this using the following syntax:
echo ${a:=1}
This, and other types of parameter expansion, are defined in the POSIX shell command language specification.
If you want to assign a numeric value, another option, which doesn't depend on the value previously being unset, would be to use an arithmetic expansion:
echo $(( a = 1 ))
This assigns the value and echoes the number that has been assigned.
It's worth mentioning that what you're trying to do cannot be done in a subshell by design because a child process cannot modify the environment of its parent.

How do you take a suffix of a string in bash using negative offsets?

I am trying to take the suffix of a string in Bash using the ${string:pos} substring syntax, but I cannot figure out why it won't work. I have managed to simplify my example code to this:
STRING="hello world"
POS=4
echo ${STRING:POS} # prints "o world"
echo ${STRING:4} # prints "o world"
POS=-4
echo ${STRING:POS} # prints "orld"
echo ${STRING:-4} # prints "hello world"
The first three lines work exactly as I would expect, but why does the final line print "hello world" instead of "orld"?
Because :- is parameter expansion syntax to "Use default values".
From the documentation:
When not performing substring expansion, using the form described
below (e.g., ‘:-’), Bash tests for a parameter that is unset or
null.
So by doing ${STRING:-4} you are actually asking bash to expand
STRING and if it is unset (have never been assigned before) or null
(a null string, printed as '') it will substitute the expansion with
4. In your example, STRING is set and thus it is expanded to its value.
As another answer states, you need to scape the expression to not
trigger the default value behavior, the manual specifies it:
Note that a negative offset must be separated from the colon by at
least one space to avoid being confused with the :- expansion.
For example:
${STRING:(-4)}
${STRING: -4}
You need to "escape" parameters starting with dash with a parenthesis or a space, otherwise bash will treat it as a normal string:
echo ${STRING:(-4)}
echo ${STRING: -4}

What does the POSIX spec mean when it says this is necessary to avoid ambiguity?

When responding to this comment:
Now I got the the two ":"s are independent, and that's why I couldn't find any document about them. Is the first one needed in this case?
I noticed this paragraph in the spec for the first time:
In the parameter expansions shown previously, use of the <colon> in the format shall result in a test for a parameter that is unset or null; omission of the <colon> shall result in a test for a parameter that is only unset. If parameter is '#' and the colon is omitted, the application shall ensure that word is specified (this is necessary to avoid ambiguity with the string length expansion).
I've seen the matching explanation in the bash reference manual:
When not performing substring expansion, using the form described below (e.g., ‘:-’), Bash tests for a parameter that is unset or null. Omitting the colon results in a test only for a parameter that is unset. Put another way, if the colon is included, the operator tests for both parameter’s existence and that its value is not null; if the colon is omitted, the operator tests only for existence.
before and I understand what the difference is with the colon versions of these expansions.
What confused me just now is this sentence from the spec:
If parameter is '#' and the colon is omitted, the application shall ensure that word is specified (this is necessary to avoid ambiguity with the string length expansion).
I don't understand what ambiguity is possible here if word is unspecified.
None of the expansion sigils are valid in shell variable names so they cannot possibly start a single-character variable name. If they could then using a parameter of # would always be ambiguous without a colon since you could never tell if ${#+foo} meant the length of the variable foo or an alternate expansion on #, etc.
What am I missing here? What ambiguity requires ensuring that word exist? (I mean not having word in this expansion is clearly not useful but that's not the same thing.)
- is also a shell special parameter, whose value is a string indicating which shell options are currently set. For example,
$ echo $-
himBH
${#parameter} is the syntax for the length of a parameter.
$ foo=bar
$ echo ${#foo}
3
The expression ${#-}, therefore is ambiguous: is it the length of the value of $-, or is does it expand to the empty string if $# is empty? (Unlikely, since $# is always an integer and cannot be unset, but syntactically legal.) I interpret the spec to meant that ${#-} should resolve the ambiguity by expanding to the length of $- (which is what most shells seem to do).

Bash bad substitution with subshell and substring

A contrived example... given
FOO="/foo/bar/baz"
this works (in bash)
BAR=$(basename $FOO) # result is BAR="baz"
BAZ=${BAR:0:1} # result is BAZ="b"
this doesn't
BAZ=${$(basename $FOO):0:1} # result is bad substitution
My question is which rule causes this [subshell substitution] to evaluate incorrectly? And what is the correct way, if any, to do this in 1 hop?
First off, note that when you say this:
BAR=$(basename $FOO) # result is BAR="baz"
BAZ=${BAR:0:1} # result is BAZ="b"
the first bit in the construct for BAZ is BAR and not the value that you want to take the first character of. So even if bash allowed variable names to contain arbitrary characters your result in the second expression wouldn't be what you want.
However, as to the rule that's preventing this, allow me to quote from the bash man page:
DEFINITIONS
The following definitions are used throughout the rest of this docu‐
ment.
blank A space or tab.
word A sequence of characters considered as a single unit by the
shell. Also known as a token.
name A word consisting only of alphanumeric characters and under‐
scores, and beginning with an alphabetic character or an under‐
score. Also referred to as an identifier.
Then a bit later:
PARAMETERS
A parameter is an entity that stores values. It can be a name, a num‐
ber, or one of the special characters listed below under Special Param‐
eters. A variable is a parameter denoted by a name. A variable has a
value and zero or more attributes. Attributes are assigned using the
declare builtin command (see declare below in SHELL BUILTIN COMMANDS).
And later when it defines the syntax you're asking about:
${parameter:offset:length}
Substring Expansion. Expands to up to length characters of
parameter starting at the character specified by offset.
So the rules as articulated in the manpage say that the ${foo:x:y} construct must have a parameter as the first part, and that a parameter can only be a name, a number, or one of the few special parameter characters. $(basename $FOO) is not one of the allowed possibilities for a parameter.
As for a way to do this in one assignment, use a pipe to other commands as mentioned in other responses.
Modified forms of parameter substitution such as ${parameter#word} can only modify a parameter, not an arbitrary word.
In this case, you might pipe the output of basename to a dd command, like
BAR=$(basename -- "$FOO" | dd bs=1 count=1 2>/dev/null)
(If you want a higher count, increase count and not bs, otherwise you may get fewer bytes than requested.)
In the general case, there is no way to do things like this in one assignment.
It fails because ${BAR:0:1} is a variable expansion. Bash expects to see a variable name after ${, not a value.
I'm not aware of a way to do it in a single expression.
As others have said, the first parameter of ${} needs to be a variable name. But you can use another subshell to approximate what you're trying to do.
Instead of:
BAZ=${$(basename $FOO):0:1} # result is bad substitution
Use:
BAZ=$(_TMP=$(basename $FOO); echo ${_TMP:0:1}) # this works
A contrived solution for your contrived example:
BAZ=$(expr $(basename $FOO) : '\(.\)')
as in
$ FOO=/abc/def/ghi/jkl
$ BAZ=$(expr $(basename $FOO) : '\(.\)')
$ echo $BAZ
j
${string:0:1},string must be a variable name
for example:
FOO="/foo/bar/baz"
baz="foo"
BAZ=eval echo '${'"$(basename $FOO)"':0:1}'
echo $BAZ
the result is 'f'

Resources