bash: Why is the value of ${#:-1} different to ${#: -1}? - arrays

Why does the addition of the space between the : and the -1 change the behaviour shown below?
(ins)$ set a b c d
(ins)$ echo ${#:-1}
a b c d
(ins)$ echo ${#: -1}
d
(ins)$
The same behaviour also affects $*.
I'm running GNU bash, version 4.4.5(1)-release (x86_64-unknown-linux-gnu).

To avoid ambiguity with the parameter expansion pattern:
${parameter:-word}
which means to substitute the expansion of word if `parameter is unset or null; otherwise the expansion of parameter is substituted.
So for the slicing operation, a space or parentheses is used:
$ set a b c d
$ echo "${#: -1}"
d
$ echo "${#:(-1)}"
d

Related

What does "+_" in bash mean? [duplicate]

What does this mean?
if ${ac_cv_lib_lept_pixCreate+:} false; then :
$as_echo_n "(cached) " >&6
else
ac_check_lib_save_LIBS=$LIBS
Looks like ac_cv_lib_lept_pixCreate is some variable, so what does +: mean?
Where to learn complete syntax of curly bracket expressions? What is the name of this syntax?
In the “plus colon” ${...+:} expression, only the + has special meaning in the shell. The colon is just a string value in this case, so we could write that snippet as ${...+":"}.
For convenience, let's pretend the variable is called var, and consider the expression:
if ${var+:} false; then ...
If the shell variable $var exists, the entire expression is replaced with :, if not, it returns an empty string.
Therefore the entire expression ${var+:} false becomes either : false (returning true) or false (returning false).
This comes down to a test for existence, which can be true even if the variable has no value assigned.
It is very cryptic, but as it happens, is one of the few tests for the existence of a variable that actually works in most, if not all, shells of Bourne descent.
Possible equivalents: (substitute any variable name here for var)
if [[ ${var+"is_set"} == is_set ]]; then ...
Or, probably more portable:
case ${var+"IS_SET"} in IS_SET) ...;; esac
Shell Parameter Expansion documentation for bash is here. No mention of +:, though it does mention :+:
${parameter:+word}
If parameter is null or unset, nothing is substituted, otherwise the expansion of word is substituted.
To illustrate what has already been said:
Unset variable (note the blank lines as a result of some echo commands):
$ unset foo
$ echo ${foo}
$ echo ${foo:+:}
$ echo ${foo+:}
Null variable:
$ foo=""
$ echo ${foo}
$ echo ${foo:+:}
$ echo ${foo+:}
:
Non-null variable:
$ foo="bar"
$ echo ${foo}
bar
$ echo ${foo:+:}
:
$ echo ${foo+:}
:
Simple examples will prove
I check for presence of a parameter TEST, if present echo "Yes" else I echo "No"
openvas:~$ ${TEST+:} false && echo "yes" || echo "no"
no
openvas:~$ TEST=1
openvas:~$ ${TEST+:} false && echo "yes" || echo "no"
yes
openvas:~$
If you see, the parameter TEST is evaluated and it is actually unset, so it returns false and exit the path and goes to the OR
Once I set the same, and test again, it flows to the echo part (continued &&) since it returns true
Refer: this and that

Paste single strings with an array in Bash

What would be a bash equivalent of the following R function?
vec=4:9
out=paste0("foo_",vec,"_bar")
out
"foo_4_bar" "foo_5_bar" "foo_6_bar" "foo_7_bar" "foo_8_bar" "foo_9_bar"
You can use declare an array with suffix and prefix and then use brace expansion to populate incrementing numbers:
arr=("foo_" "_bar") # array with suffix and prefix
echo "${arr[0]}"{4..9}"${arr[1]}" # brace expansion
foo_4_bar foo_5_bar foo_6_bar foo_7_bar foo_8_bar foo_9_bar
You can use brace expansion:
$ echo foo_{4..9}_bar
foo_4_bar foo_5_bar foo_6_bar foo_7_bar foo_8_bar foo_9_bar
$ out=( foo_{4..9}_bar )
$ echo "${out[1]}"
foo_5_bar
This works even if your vec is not generated via a brace expansion:
vec=( {4..9} ) # would work even with vec=( *.txt ) or readarray -t vec <file, etc.
out=( "${vec[#]/#/foo_}" ) # add foo_ prefix
out=( "${out[#]/%/_bar}" ) # add _bar suffix
declare -p out # print resulting array definition
See the Parameter Expansion page on the bash-hackers wiki, particularly the "Anchoring" section under "Search and Replace".

Surprising array expansion behaviour

I've been surprised with the line marked (!!) in the following example:
log1 () { echo $#; }
log2 () { echo "$#"; }
X=(a b)
IFS='|'
echo ${X[#]} # prints a b
echo "${X[#]}" # prints a b
echo ${X[*]} # prints a b
echo "${X[*]}" # prints a|b
echo "---"
log1 ${X[#]} # prints a b
log1 "${X[#]}" # prints a b
log1 ${X[*]} # prints a b
log1 "${X[*]}" # prints a b (!!)
echo "---"
log2 ${X[#]} # prints a b
log2 "${X[#]}" # prints a b
log2 ${X[*]} # prints a b
log2 "${X[*]}" # prints a|b
Here is my understanding of the behavior:
${X[*]} and ${X[#]} both expand to a b
"${X[*]}" expands to "a|b"
"${X[#]}" expands to "a" "b"
$* and $# have the same behavior as ${X[*]} and ${X[#]}, except for their content being the parameters of the program or function
This seems to be confirmed by the bash manual.
In the line log1 "${X[*]}", I therefore expect the quoted expression to expand to "a|b", then to be passed to the log1 function. The function has a single string parameter which it displays. Why does something else happen?
It'd be cool if your answers were backed by manual/standard references!
IFS is used not just to join the elements of ${X[*]}, but also to split the unquoted expansion $#. For log1 "${X[*]}", the following happens:
"${X[*]}" expands to a|b as expected, so $1 is set to a|b inside log1.
When $# (unquoted) is expanded, the resulting string is a|b.
The unquoted expansion undergoes word-splitting with | as the delimiter (due to the global value of IFS), so that echo receives two arguments, a and b.
That's because $IFS is set to |:
(X='a|b' ; IFS='|' ; echo $X)
Output:
a b
man bash says:
IFS The Internal Field Separator that is used for word splitting after expansion ...
In the POSIX spec section on [Special Parameters[(http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_05_02) we find.
#
Expands to the positional parameters, starting from one. When the expansion occurs within double-quotes, and where field splitting (see Field Splitting) is performed, each positional parameter shall expand as a separate field, with the provision that the expansion of the first parameter shall still be joined with the beginning part of the original word (assuming that the expanded parameter was embedded within a word), and the expansion of the last parameter shall still be joined with the last part of the original word. If there are no positional parameters, the expansion of '#' shall generate zero fields, even when '#' is double-quoted.
*
Expands to the positional parameters, starting from one. When the expansion occurs within a double-quoted string (see Double-Quotes), it shall expand to a single field with the value of each parameter separated by the first character of the IFS variable, or by a if IFS is unset. If IFS is set to a null string, this is not equivalent to unsetting it; its first character does not exist, so the parameter values are concatenated.
So starting with the quoted variants (they are simpler):
We see that the * expansion "expand[s] to a single field with the value of each parameter separated by the first character of the IFS variable". This is why you get a|b from echo "${X[*]" and log2 "${X[*]}".
We also see that the # expansion expands such that "each positional parameter shall expand as a separate field". This is why you get a b from echo "${X[#]}" and log2 "${X[#]}".
Did you see that note about field splitting in the spec text? "where field splitting (see Field Splitting) is performed"? That's the key to the mystery here.
Outside of quotes the behavior of the expansions is the same. The difference is what happens after that. Specifically, field/word splitting.
The simplest way to show the problem is to run your code with set -x enabled.
Which gets you this:
+ X=(a b)
+ IFS='|'
+ echo a b
a b
+ echo a b
a b
+ echo a b
a b
+ echo 'a|b'
a|b
+ echo ---
---
+ log1 a b
+ echo a b
a b
+ log1 a b
+ echo a b
a b
+ log1 a b
+ echo a b
a b
+ log1 'a|b'
+ echo a b
a b
+ echo ---
---
+ log2 a b
+ echo a b
a b
+ log2 a b
+ echo a b
a b
+ log2 a b
+ echo a b
a b
+ log2 'a|b'
+ echo 'a|b'
a|b
The thing to notice here is that by the time log1 is called in all but the final case the | is already gone.
The reason it is already gone is because without quotes the results of the variable expansion (in this case the * expansion) are field/word split. And since IFS is used both to combine the fields being expanded and then to split them again the | gets swallowed by field splitting.
And to finish the explanation (for the case actually in question), the reason this fails for log1 even with the quoted version of the expansion in the call (i.e. log1 "${X[*]}" which expands to log1 "a|b" correctly) is because log1 itself does not use a quoted expansion of # so the expansion of # in the function is itself word-split (as can be seen by echo a b in that log1 case as well as all the other log1 cases).

How to expand the elements of an array in zsh?

Say I have an array in zsh
a=(1 2 3)
I want to append .txt to each element
echo ${a}.txt # this doesn't work
So the output is
1.txt 2.txt 3.txt
UPDATE:
I guess I can do this, but I think there's a more idiomatic way:
for i in $a; do
echo $i.txt
done
You need to set RC_EXPAND_PARAM option:
$ setopt RC_EXPAND_PARAM
$ echo ${a}.txt
1.txt 2.txt 3.txt
From zsh manual:
RC_EXPAND_PARAM (-P)
Array expansions of the form `foo${xx}bar', where the parameter xx is set to
(a b c), are substituted with `fooabar foobbar foocbar' instead of the
default `fooa b cbar'. Note that an empty array will therefore cause all
arguments to be removed.
You can also set this option just for for one array expansion using ^ flag:
$ echo ${^a}.txt
1.txt 2.txt 3.txt
$ echo ${^^a}.txt
1 2 3.txt
Again citing zsh manual:
${^spec}
Turn on the RC_EXPAND_PARAM option for the evaluation of spec; if the `^' is
doubled, turn it off. When this option is set, array expansions of the form
foo${xx}bar, where the parameter xx is set to (a b c), are substituted with
`fooabar foobbar foocbar' instead of the default `fooa b cbar'. Note that an
empty array will therefore cause all arguments to be removed.

Bash arrays: compound assignments fail

I have no idea why the compound array initialization does not work for me.
minimal example:
#!/bin/bash
#
MINRADIUS=( 'foo' 'bar' 'foobar' )
for i in {0..2..1}; do echo ${MINRADIUS[$i]}; done
output is
$ sh test.sh
(foo bar foobar)
with 2 additional blank lines.
Fieldwise initialization works:
#!/bin/bash
#
MINRADIUS[0]="foo"
MINRADIUS[1]="bar"
MINRADIUS[2]="foobar"
for i in {0..2..1}; do echo ${MINRADIUS[$i]}; done
$ sh test.sh
foo
bar
foobar
I have tried every possible combination of braces, quotes and "declare -a".
Could it be related to my bash version? I'm running version 4.1.2(1).
The problem is, you are not using bash. Shebang doesn't matter if you run your script throught sh. Try bash instead.
I tried the below code and its working fine for me. Using bash 3.2.39(1)-release
#!/bin/bash
#
MINRADIUS=( 'foo' 'bar' 'foobar' )
for i in {0,1,2}; do echo ${MINRADIUS[$i]}; done
Output for this was
foo
bar
foobar
For me with your code it was giving an error
line 4: {0..1..2}: syntax error: operand expected (error token is "{0..1..2}")
I would suspect there is some quoting going on in your first example using compound assignments. Using this modified test script:
#!/bin/bash
echo "SHELL=${SHELL}"
echo 'Single-quoted v:'
v='(a b c)'; for i in {0..2}; do echo "v[$i]=${v[i]}"; done
echo 'Double-quoted v:'
v="(a b c)"; for i in {0..2}; do echo "v[$i]=${v[i]}"; done
echo 'Unquoted v:'
v=(a b c); for i in {0..2}; do echo "v[$i]=${v[i]}"; done
I get the following output:
$ sh test.sh
SHELL=/bin/bash
Single-quoted v:
v[0]=(a b c)
v[1]=
v[2]=
Double-quoted v:
v[0]=(a b c)
v[1]=
v[2]=
Unquoted v:
v[0]=a
v[1]=b
v[2]=c
If you quote the assignment, it becomes a simple variable assignment; a simple mistake to make that is easily overlooked.

Resources