Can't pass a dynamic value to AWS CLI query option [duplicate] - arrays

In Bash, what are the differences between single quotes ('') and double quotes ("")?

Single quotes won't interpolate anything, but double quotes will. For example: variables, backticks, certain \ escapes, etc.
Example:
$ echo "$(echo "upg")"
upg
$ echo '$(echo "upg")'
$(echo "upg")
The Bash manual has this to say:
3.1.2.2 Single Quotes
Enclosing characters in single quotes (') preserves the literal value of each character within the quotes. A single quote may not occur between single quotes, even when preceded by a backslash.
3.1.2.3 Double Quotes
Enclosing characters in double quotes (") preserves the literal value of all characters within the quotes, with the exception of $, `, \, and, when history expansion is enabled, !. The characters $ and ` retain their special meaning within double quotes (see Shell Expansions). The backslash retains its special meaning only when followed by one of the following characters: $, `, ", \, or newline. Within double quotes, backslashes that are followed by one of these characters are removed. Backslashes preceding characters without a special meaning are left unmodified. A double quote may be quoted within double quotes by preceding it with a backslash. If enabled, history expansion will be performed unless an ! appearing in double quotes is escaped using a backslash. The backslash preceding the ! is not removed.
The special parameters * and # have special meaning when in double quotes (see Shell Parameter Expansion).

The accepted answer is great. I am making a table that helps in quick comprehension of the topic. The explanation involves a simple variable a as well as an indexed array arr.
If we set
a=apple # a simple variable
arr=(apple) # an indexed array with a single element
and then echo the expression in the second column, we would get the result / behavior shown in the third column. The fourth column explains the behavior.
#
Expression
Result
Comments
1
"$a"
apple
variables are expanded inside ""
2
'$a'
$a
variables are not expanded inside ''
3
"'$a'"
'apple'
'' has no special meaning inside ""
4
'"$a"'
"$a"
"" is treated literally inside ''
5
'\''
invalid
can not escape a ' within ''; use "'" or $'\'' (ANSI-C quoting)
6
"red$arocks"
red
$arocks does not expand $a; use ${a}rocks to preserve $a
7
"redapple$"
redapple$
$ followed by no variable name evaluates to $
8
'\"'
\"
\ has no special meaning inside ''
9
"\'"
\'
\' is interpreted inside "" but has no significance for '
10
"\""
"
\" is interpreted inside ""
11
"*"
*
glob does not work inside "" or ''
12
"\t\n"
\t\n
\t and \n have no special meaning inside "" or ''; use ANSI-C quoting
13
"`echo hi`"
hi
`` and $() are evaluated inside "" (backquotes are retained in actual output)
14
'`echo hi`'
`echo hi`
`` and $() are not evaluated inside '' (backquotes are retained in actual output)
15
'${arr[0]}'
${arr[0]}
array access not possible inside ''
16
"${arr[0]}"
apple
array access works inside ""
17
$'$a\''
$a'
single quotes can be escaped inside ANSI-C quoting
18
"$'\t'"
$'\t'
ANSI-C quoting is not interpreted inside ""
19
'!cmd'
!cmd
history expansion character '!' is ignored inside ''
20
"!cmd"
cmd args
expands to the most recent command matching "cmd"
21
$'!cmd'
!cmd
history expansion character '!' is ignored inside ANSI-C quotes
See also:
ANSI-C quoting with $'' - GNU Bash Manual
Locale translation with $"" - GNU Bash Manual
A three-point formula for quotes

If you're referring to what happens when you echo something, the single quotes will literally echo what you have between them, while the double quotes will evaluate variables between them and output the value of the variable.
For example, this
#!/bin/sh
MYVAR=sometext
echo "double quotes gives you $MYVAR"
echo 'single quotes gives you $MYVAR'
will give this:
double quotes gives you sometext
single quotes gives you $MYVAR

Others explained it very well, and I just want to give something with simple examples.
Single quotes can be used around text to prevent the shell from interpreting any special characters. Dollar signs, spaces, ampersands, asterisks and other special characters are all ignored when enclosed within single quotes.
echo 'All sorts of things are ignored in single quotes, like $ & * ; |.'
It will give this:
All sorts of things are ignored in single quotes, like $ & * ; |.
The only thing that cannot be put within single quotes is a single quote.
Double quotes act similarly to single quotes, except double quotes still allow the shell to interpret dollar signs, back quotes and backslashes. It is already known that backslashes prevent a single special character from being interpreted. This can be useful within double quotes if a dollar sign needs to be used as text instead of for a variable. It also allows double quotes to be escaped so they are not interpreted as the end of a quoted string.
echo "Here's how we can use single ' and double \" quotes within double quotes"
It will give this:
Here's how we can use single ' and double " quotes within double quotes
It may also be noticed that the apostrophe, which would otherwise be interpreted as the beginning of a quoted string, is ignored within double quotes. Variables, however, are interpreted and substituted with their values within double quotes.
echo "The current Oracle SID is $ORACLE_SID"
It will give this:
The current Oracle SID is test
Back quotes are wholly unlike single or double quotes. Instead of being used to prevent the interpretation of special characters, back quotes actually force the execution of the commands they enclose. After the enclosed commands are executed, their output is substituted in place of the back quotes in the original line. This will be clearer with an example.
today=`date '+%A, %B %d, %Y'`
echo $today
It will give this:
Monday, September 28, 2015

Since this is the de facto answer when dealing with quotes in Bash, I'll add upon one more point missed in the answers above, when dealing with the arithmetic operators in the shell.
The Bash shell supports two ways to do arithmetic operation, one defined by the built-in let command and the other the $((..)) operator. The former evaluates an arithmetic expression while the latter is more of a compound statement.
It is important to understand that the arithmetic expression used with let undergoes word-splitting, pathname expansion just like any other shell commands. So proper quoting and escaping need to be done.
See this example when using let:
let 'foo = 2 + 1'
echo $foo
3
Using single quotes here is absolutely fine here, as there isn't any need for variable expansions here. Consider a case of
bar=1
let 'foo = $bar + 1'
It would fail miserably, as the $bar under single quotes would not expand and needs to be double-quoted as
let 'foo = '"$bar"' + 1'
This should be one of the reasons, the $((..)) should always be considered over using let. Because inside it, the contents aren't subject to word-splitting. The previous example using let can be simply written as
(( bar=1, foo = bar + 1 ))
Always remember to use $((..)) without single quotes
Though the $((..)) can be used with double quotes, there isn't any purpose to it as the result of it cannot contain content that would need the double quote. Just ensure it is not single quoted.
printf '%d\n' '$((1+1))'
-bash: printf: $((1+1)): invalid number
printf '%d\n' $((1+1))
2
printf '%d\n' "$((1+1))"
2
Maybe in some special cases of using the $((..)) operator inside a single quoted string, you need to interpolate quotes in a way that the operator either is left unquoted or under double quotes. E.g., consider a case, when you are tying to use the operator inside a curl statement to pass a counter every time a request is made, do
curl http://myurl.com --data-binary '{"requestCounter":'"$((reqcnt++))"'}'
Notice the use of nested double quotes inside, without which the literal string $((reqcnt++)) is passed to the requestCounter field.

There is a clear distinction between the usage of ' ' and " ".
When ' ' is used around anything, there is no "transformation or translation" done. It is printed as it is.
With " ", whatever it surrounds, is "translated or transformed" into its value.
By translation/ transformation I mean the following:
Anything within the single quotes will not be "translated" to their values. They will be taken as they are inside quotes. Example: a=23, then echo '$a' will produce $a on standard output. Whereas echo "$a" will produce 23 on standard output.

A minimal answer is needed for people to get going without spending a lot of time as I had to.
The following is, surprisingly (to those looking for an answer), a complete command:
$ echo '\'
whose output is:
\
Backslashes, surprisingly to even long-time users of bash, do not have any meaning inside single quotes. Nor does anything else.

Related

Delete newlines from all elements of a array in shell

I try to solve a problem in shell.
Im trying to find a way to delete all newlines from each element of an array. I tried to do this with a for loop.
The Strings look like this (always three numbers, separated with dots)
"14.1.3\n" and I need to get rid of the newline at the end.
This is what i tried to do:
As a single-liner
for i in ${backup_versions[*]}; do backup_versions[$i]=echo "$i" | tr '\n' ' ' ; done
Easier to read
for i in ${backup_versions[*]};
do
backup_versions[$i]=echo "$i" | tr '\n' ' '
done
I think I try to reassign the element with the wrong syntax, but I tried every kind of writing i which I found or knew myself.
The deletion of the newline works just fine and just the reassigning is my Problem.
If the strings are always of that form and don't contain any whitespace or wildcard characters, you can just use the shell's word-splitting to remove extraneous whitespace characters from the values.
backup_versions=(${backup_versions[*]})
If you used mapfile to create the array, you can use the -t option to prevent it from including the newline in the value in the first place.
Use Bash's string substitution expansion ${var//old/new} to delete all newlines, and dynamically create a declaration for a new array, with elements stripped of newlines:
#!/usr/bin/env bash
backup_versions=(
$'foo\nbar\n'
$'\nbaz\ncux\n\n'
$'I have spaces\n and newlines\n'
$'It\'s a \n\n\nsingle quote and spaces\n'
$'Quoted "foo bar"\n and newline'
)
# shellcheck disable=SC2155 # Dynamically generated declaration
declare -a no_newlines="($(
printf '%q ' "${backup_versions[#]//$'\n'/}"
))"
# Debug print original array declaration
declare -p backup_versions
# Debug print the declaration of no_newlines
declare -p no_newlines
declare -a no_newlines="($(: Creates a dynamically generated declaration for the no_newlines array.
printf '%q ': Print each argument with quotes if necessary and add a trailing space.
"${backup_versions[#]//$'\n'/}": Expand each element of the backup_versions array, // replacing all $'\n' newlines by nothing to delete them.
Finally the no_newlines array will contain all entries from backup_versions, with newlines stripped-out.
Debug output match expectations:
declare -a backup_versions=([0]=$'foo\nbar\n' [1]=$'\nbaz\ncux\n\n' [2]=$'I have spaces\n and newlines\n' [3]=$'It\'s a \n\n\nsingle quote and spaces\n' [4]=$'Quoted "foo bar"\n and newline')
declare -a no_newlines=([0]="foobar" [1]="bazcux" [2]="I have spaces and newlines" [3]="It's a single quote and spaces" [4]="Quoted \"foo bar\" and newline")
You can use a modifier when expanding the array, then save the modified contents. If the elements just have a single trailing newline, use substring removal to trim it:
backup_versions=("${backup_versions[#]%$'\n'}")
(Note: when expanding an array, you should almost always use [#] instead of [*], and put double-quotes around it to avoid weird parsing. Bash doesn't generally let you combine modifiers, but you can combo them with [#] to apply the modifier to each element as it's expanded.)
If you want to remove all newlines from the elements (in case there are multiple newlines in some elements), use a substitution (with an empty replacement string) instead:
backup_versions=("${backup_versions[#]//$'\n'/}")
(But as several comments have mentioned, it'd probably be better to look at how the array's being created, and see if it's possible to just avoid putting newlines in the array in the first place.)

Why glob or braces expansion from a variable to an array is impossible when containing spaces [duplicate]

This question already has answers here:
Bash arbitrary glob pattern (with spaces) in for loop
(2 answers)
Closed 2 years ago.
I'm trying to use internal bash globs and braces expansion mechanism from a variable to an array.
path='./tmp2/tmp23/*'
expanded=($(eval echo "$(printf "%q" "${path}")"))
results:
declare -- path="./tmp2/tmp23/*"
declare -a expanded=([0]="./tmp2/tmp23/testfile" [1]="./tmp2/tmp23/testfile2" [2]="./tmp2/tmp23/testfile3" [3]="./tmp2/tmp23/testfile4" [4]="./tmp2/tmp23/tmp231")
This is working.
(I have 4 file testfileX and 1 folder in the ./tmp2/tmp23 folder)
Each file/folder inside an index of the array.
Now if my path contains spaces:
path='./tmp2/tmp2 3/*'
expanded=($(eval echo "$(printf "%q" "${path}")"))
Results
declare -- path="./tmp2/tmp2 3/*"
declare -a expanded=([0]="./tmp2/tmp2" [1]="3/")
Not working nothing is expanded and path is splitted due to IFS calvary.
Now with same path containing spaces but without glob:
path='./tmp2/tmp2 3/'
expanded=($(eval echo "$(printf "%q" "${path}"*)")) => added glob here outside ""
Results:
declare -a expanded=([0]="./tmp2/tmp2" [1]="3/testfile./tmp2/tmp2" [2]="3/testfile2./tmp2/tmp2" [3]="3/testfile3./tmp2/tmp2" [4]="3/testfile4./tmp2/tmp2" [5]="3/tmp231")
Path is expanded but results are false and splitted due to IFS.
Now with a quoted $(eval)
expanded=("$(eval echo "$(printf "%q" "${path}"*)")")
Results:
declare -a expanded=([0]="./tmp2/tmp2 3/testfile./tmp2/tmp2 3/testfile2./tmp2/tmp2 3/testfile3./tmp2/tmp2 3/testfile4./tmp2/tmp2 3/tmp231")
Now all expansion is done inside the same array index.
Why glob or braces expansion works inside a variable if there is no space ?
Why this is not working anymore when there is a space. Exactly the same code but just a space. Globs or braces expansion need to be outside double quotes. eval seems to have no effects.
Is there any other alternative to use (as read or mapfile or is it possible to escape space character) ?
I found this question how-to-assign-a-glob-expression-to-a-variable-in-a-bash-script but nothing about spaces.
Is there any way to expand a variable which contains globs or braces expansion parameters with spaces or without spaces to an array using the same method without word splitting when they contain spaces ?
Kind Regards
Don't use eval. Don't use a subshell. Just clear IFS.
path='./tmp2/tmp2 3/*'
oIFS=${IFS:-$' \t\n'} IFS='' # backup prior IFS value
expanded=( $path ) # perform expansion, unquoted
IFS=$oIFS # reset to original value, or an equivalent thereto
When you perform an unquoted expansion, two separate things happen in order:
All the characters found in the $IFS variable are used to split the string into words
Each word is then expanded as a separate glob.
The default value of IFS contains the space, the tab and the newline. If you don't want spaces, tabs and newlines to be treated as delimiters between words, then you need to modify that default.

Parameter expansion for find command

Consider the code (the variable $i is there because it was in a loop, adding several conditions to the pattern, e.g. *.a and *.b, ... but to illustrate this problem only one wildcard pattern is enough):
#!/bin/bash
i="a"
PATTERN="-name bar -or -name *.$i"
find . \( $PATTERN \)
If ran on a folder containing files bar and foo.a, it works, outputting:
./foo.a
./bar
But if you now add a new file to the folder, namely zoo.a, then it no longer works:
find: paths must precede expression: zoo.a
Presumably, because the wildcard in *.$i gets expanded by the shell to foo.a zoo.a, which leads to an invalid find command pattern. So one attempt at a fix is to put quotes around the wildcard pattern. Except it does not work:
with single quotes -- PATTERN="-name bar -or -name '*.$i'" the find command outputs only bar. Escaping the single quotes (\') yields the same result.
idem with double quotes: PATTERN="-name bar -or -name \"*.$i\"" -- only bar is returned.
in the find command, if $PATTERN is replaced with "$PATTERN", out comes an error (for single quotes same error, but with single quotes around the wildcard pattern):
find: unknown predicate -name bar -or -name "*.a"'
Of course, replacing $PATTERN with '$PATTERN' also does not work... (no expansion whatsoever takes place).
The only way I could get it to work was to use... eval!
FINDSTR="find . \( $PATTERN \)"
eval $FINDSTR
This works properly:
./zoo.a
./foo.a
./bar
Now after a lot of googling, I saw it mentioned several times that to do this kind of thing, one should use arrays. But this doesn't work:
i="a"
PATTERN=( -name bar -or -name '*.$i' )
find . \( "${PATTERN[#]}" \)
# result: ./bar
In the find line the array has to be enclosed in double quotes, because we want it to be expanded. But single quotes around the wildcard expression don't work, and neither does not quotes at all:
i="a"
PATTERN=( -name bar -or -name *.$i )
find . \( "${PATTERN[#]}" \)
# result: find: paths must precede expression: zoo.a
BUT DOUBLE QUOTES DO WORK!!
i="a"
PATTERN=( -name bar -or -name "*.$i" )
find . \( "${PATTERN[#]}" \)
# result:
# ./zoo.a
# ./foo.a
# ./bar
So I guess my question are actually two questions:
a) in this last example using arrays, why are double quotes required around the *.$i?
b) using an array in this way is supposed to expand «to all elements individually quoted». How would do this with a variable (cf my first attempt)? After getting this to function, I went back and tried using a variable again, with blackslashed single quotes, or \\', but nothing worked (I just got bar). What would I have to do to emulate "by hand" as it were, the quoting done when using arrays?
Thank you in advance for your help.
Required reading:
BashFAQ — I'm trying to put a command in a variable, but the complex cases always fail!
a) in this last example using arrays, why are double quotes required around the *.$i?
You need to use some form of quoting to prevent the shell from performing glob expansion on *. Variables are not expanded in single quotes so '*.$i' doesn't work. It does inhibit glob expansion but it also stops variable expansion. "*.$i" inhibits glob expansion but allows variable expansion, which is perfect.
To really delve into the details, there are two things you need to do here:
Escape or quote * to prevent glob expansion.
Treat $i as a variable expansion, but quote it to prevent word splitting and glob expansion.
Any form of quoting will do for item 1: \*, "*", '*', and $'*' are all acceptable ways to ensure it's treated as a literal asterisk.
For item 2, double quoting is the only answer. A bare $i is subject to word splitting and globbing -- if you have i='foo bar' or i='foo*' the whitespace and globs will cause problems. \$i and '$i' both treat the dollar sign literally, so they're out.
"$i" is the only quoting that does everything right. It's why common shell advice is to always double quote variable expansions.
The end result is, any of the following would work:
"*.$i"
\*."$i"
'*'."$i"
"*"."$i"
'*.'"$i"
Clearly, the first is the simplest.
b) using an array in this way is supposed to expand «to all elements individually quoted». How would do this with a variable (cf my first attempt)? After getting this to function, I went back and tried using a variable again, with blackslashed single quotes, or \\', but nothing worked (I just got bar). What would I have to do to emulate "by hand" as it were, the quoting done when using arrays?
You'd have to cobble together something with eval, but that's dangerous. Fundamentally, arrays are more powerful than simple string variables. There's no magic combination of quotes and backslashes that will let you do what an array can do. Arrays are the right tool for the job.
Could you explain in a little more detail, why ... PATTERN="-name bar -or -name \"*.$i\"" does not work? The quoted double quotes should, when the find command is actually ran, expand the $i but not the glob.
Sure. Let's say we write:
i=a
PATTERN="-name bar -or -name \"*.$i\""
find . \( $PATTERN \)
After the first two line runs, what is the value of $PATTERN? Let's check:
$ i=a
$ PATTERN="-name bar -or -name \"*.$i\""
$ printf '%s\n' "$PATTERN"
-name bar -or -name "*.a"
You'll notice that $i has already been replaced with a, and the backslashes have been removed.
Now let's see how exactly the find command is parsed. In the last line $PATTERN is unquoted because we want all the words to be split apart, right? If you write a bare variable name Bash ends up performing an implied split+glob operation. It performs word splitting and glob expansion. What does that mean, exactly?
Let's take a look at how Bash performs command-line expansion. In the Bash man page under the "Expansion" section we can see the order of operations:
Brace expansion
Tilde expansion, parameter and variable expansion, arithmetic expansion, command substitution, and process substitution
Word splitting
Pathname (AKA glob) expansion
Quote removal
Let's run through these operations by hand and see how find . \( $PATTERN \) is parsed. The end result will be a list of strings, so I'll use a JSON-like syntax to show each stage. We'll start with a list containing a single string:
['find . \( $PATTERN \)']
As a preliminary step, the command-line as a whole is subject to word splitting.
['find', '.', '\(', '$PATTERN', '\)']
Brace expansion -- No change.
Variable expansion
['find', '.', '\(', '-name bar -or -name "*.a"', '\)']
$PATTERN is replaced. For the moment it is all a single string, whitespace and all.
Word splitting
['find', '.', '\(', '-name', 'bar', '-or', '-name', '"*.a"', '\)']
The shell scans the results of variable expansion that did not occur within double quotes for word splitting. $PATTERN was unquoted, so it's expanded. Now it is a bunch of individual words. So far so good.
Glob expansion
['find', '.', '\(', '-name', 'bar', '-or', '-name', '"*.a"', '\)']
Bash scans the results of word splitting for globs. Not the entire command-line, just the tokens -name, bar, -or, -name, and "*.a".
It looks like nothing happened, yes? Not so fast! Looks can be deceiving. Bash actually performed glob expansion. It just happened that the glob didn't match anything. But it could...†
Quote removal
['find', '.', '(', '-name', 'bar', '-or', '-name', '"*.a"', ')']
The backslashes are gone. But the double quotes are still there.
After the preceding expansions, all unquoted occurrences of the characters \, ', and " that did not result from one of the above expansions are removed.
And that's the end result. The double quotes are still there, so instead of searching for files named *.a it searches for ones named "*.a" with literal double quotes characters in their name. That search is bound to fail.
Adding a pair of escaped quotes \" didn't at all do what we wanted. The quotes didn't disappear like they were supposed to and broke the search. Not only that, but they also didn't inhibit globbing like they should have.
TL;DR — Quotes inside a variable aren't parsed the same way as quotes outside a variable.
† The first four tokens have no special characters. But the last one, "*.a", does. That asterisk is a wildcard. If you read the "pathname expansion" section of the man page carefully you'll see that there's no mention of quotes being ignored. The double quotes do not protect the asterisk.
Hang on! What? I thought quotes inhibit glob expansion!
They do—normally. If you write quotes out by hand they do indeed stop glob expansion. But if you put them inside an unquoted variable, they don't.
$ touch 'foobar' '"foobar"'
$ ls
foobar "foobar"
$ ls foo*
foobar
$ ls "foo*"
ls: foo*: No such file or directory
$ var="\"foo*\""
$ echo "$var"
"foo*"
$ ls $var
"foobar"
Read that over carefully. If we create a file named "foobar"—that is, it has literal double quotes in its filename—then ls $var prints "foobar". The glob is expanded and matches the (admittedly contrived) filename!
Why didn't the quotes help? Well, the explanation is subtle, and tricky. The man page says:
After word splitting ... bash scans each word for the characters *, ?, and [.
Any time Bash performs word splitting it also expands globs. Remember how I said unquoted variables are subject to an implied split+glob operator? This is what I meant. Splitting and globbing go hand in hand.
If you write ls "foo*" the quotes prevent foo* from being subject to splitting and globbing. However if you write ls $var then $var is expanded, split, and globbed. It wasn't surrounded by double quotes. It doesn't matter that it contains double quotes. By the time those double quotes show up it's too late. Word splitting has already been performed, and so globbing is done as well.

What is the difference between bash arrays with the notation ${array[*]} and ${array[#]} [duplicate]

I'm taking a stab at writing a bash completion for the first time, and I'm a bit confused about about the two ways of dereferencing bash arrays (${array[#]} and ${array[*]}).
Here's the relevant chunk of code (it works, but I would like to understand it better):
_switch()
{
local cur perls
local ROOT=${PERLBREW_ROOT:-$HOME/perl5/perlbrew}
COMPREPLY=()
cur=${COMP_WORDS[COMP_CWORD]}
perls=($ROOT/perls/perl-*)
# remove all but the final part of the name
perls=(${perls[*]##*/})
COMPREPLY=( $( compgen -W "${perls[*]} /usr/bin/perl" -- ${cur} ) )
}
bash's documentation says:
Any element of an array may be referenced using ${name[subscript]}. The braces are required to avoid conflicts with the shell's filename expansion operators. If the subscript is ‘#’ or ‘*’, the word expands to all members of the array name. These subscripts differ only when the word appears within double quotes. If the word is double-quoted, ${name[*]} expands to a single word with the value of each array member separated by the first character of the IFS variable, and ${name[#]} expands each element of name to a separate word.
Now I think I understand that compgen -W expects a string containing a wordlist of possible alternatives, but in this context I don't understand what "${name[#]} expands each element of name to a separate word" means.
Long story short: ${array[*]} works; ${array[#]} doesn't. I would like to know why, and I would like to understand better what exactly ${array[#]} expands into.
(This is an expansion of my comment on Kaleb Pederson's answer -- see that answer for a more general treatment of [#] vs [*].)
When bash (or any similar shell) parses a command line, it splits it into a series of "words" (which I will call "shell-words" to avoid confusion later). Generally, shell-words are separated by spaces (or other whitespace), but spaces can be included in a shell-word by escaping or quoting them. The difference between [#] and [*]-expanded arrays in double-quotes is that "${myarray[#]}" leads to each element of the array being treated as a separate shell-word, while "${myarray[*]}" results in a single shell-word with all of the elements of the array separated by spaces (or whatever the first character of IFS is).
Usually, the [#] behavior is what you want. Suppose we have perls=(perl-one perl-two) and use ls "${perls[*]}" -- that's equivalent to ls "perl-one perl-two", which will look for single file named perl-one perl-two, which is probably not what you wanted. ls "${perls[#]}" is equivalent to ls "perl-one" "perl-two", which is much more likely to do something useful.
Providing a list of completion words (which I will call comp-words to avoid confusion with shell-words) to compgen is different; the -W option takes a list of comp-words, but it must be in the form of a single shell-word with the comp-words separated by spaces. Note that command options that take arguments always (at least as far as I know) take a single shell-word -- otherwise there'd be no way to tell when the arguments to the option end, and the regular command arguments (/other option flags) begin.
In more detail:
perls=(perl-one perl-two)
compgen -W "${perls[*]} /usr/bin/perl" -- ${cur}
is equivalent to:
compgen -W "perl-one perl-two /usr/bin/perl" -- ${cur}
...which does what you want. On the other hand,
perls=(perl-one perl-two)
compgen -W "${perls[#]} /usr/bin/perl" -- ${cur}
is equivalent to:
compgen -W "perl-one" "perl-two /usr/bin/perl" -- ${cur}
...which is complete nonsense: "perl-one" is the only comp-word attached to the -W flag, and the first real argument -- which compgen will take as the string to be completed -- is "perl-two /usr/bin/perl". I'd expect compgen to complain that it's been given extra arguments ("--" and whatever's in $cur), but apparently it just ignores them.
Your title asks about ${array[#]} versus ${array[*]} (both within {}) but then you ask about $array[*] versus $array[#] (both without {}) which is a bit confusing. I'll answer both (within {}):
When you quote an array variable and use # as a subscript, each element of the array is expanded to its full content regardless of whitespace (actually, one of $IFS) that may be present within that content. When you use the asterisk (*) as the subscript (regardless of whether it's quoted or not) it may expand to new content created by breaking up each array element's content at $IFS.
Here's the example script:
#!/bin/sh
myarray[0]="one"
myarray[1]="two"
myarray[3]="three four"
echo "with quotes around myarray[*]"
for x in "${myarray[*]}"; do
echo "ARG[*]: '$x'"
done
echo "with quotes around myarray[#]"
for x in "${myarray[#]}"; do
echo "ARG[#]: '$x'"
done
echo "without quotes around myarray[*]"
for x in ${myarray[*]}; do
echo "ARG[*]: '$x'"
done
echo "without quotes around myarray[#]"
for x in ${myarray[#]}; do
echo "ARG[#]: '$x'"
done
And here's it's output:
with quotes around myarray[*]
ARG[*]: 'one two three four'
with quotes around myarray[#]
ARG[#]: 'one'
ARG[#]: 'two'
ARG[#]: 'three four'
without quotes around myarray[*]
ARG[*]: 'one'
ARG[*]: 'two'
ARG[*]: 'three'
ARG[*]: 'four'
without quotes around myarray[#]
ARG[#]: 'one'
ARG[#]: 'two'
ARG[#]: 'three'
ARG[#]: 'four'
I personally usually want "${myarray[#]}". Now, to answer the second part of your question, ${array[#]} versus $array[#].
Quoting the bash docs, which you quoted:
The braces are required to avoid conflicts with the shell's filename expansion operators.
$ myarray=
$ myarray[0]="one"
$ myarray[1]="two"
$ echo ${myarray[#]}
one two
But, when you do $myarray[#], the dollar sign is tightly bound to myarray so it is evaluated before the [#]. For example:
$ ls $myarray[#]
ls: cannot access one[#]: No such file or directory
But, as noted in the documentation, the brackets are for filename expansion, so let's try this:
$ touch one#
$ ls $myarray[#]
one#
Now we can see that the filename expansion happened after the $myarray exapansion.
And one more note, $myarray without a subscript expands to the first value of the array:
$ myarray[0]="one four"
$ echo $myarray[5]
one four[5]

How to fetch one item at a time from an array and print them? [duplicate]

I found some strange behavior in PowerShell surrounding arrays and double quotes. If I create and print the first element in an array, such as:
$test = #('testing')
echo $test[0]
Output:
testing
Everything works fine. But if I put double quotes around it:
echo "$test[0]"
Output:
testing[0]
Only the $test variable was evaluated and the array marker [0] was treated literally as a string. The easy fix is to just avoid interpolating array variables in double quotes, or assign them to another variable first. But is this behavior by design?
So when you are using interpolation, by default it interpolates just the next variable in toto. So when you do this:
"$test[0]"
It sees the $test as the next variable, it realizes that this is an array and that it has no good way to display an array, so it decides it can't interpolate and just displays the string as a string. The solution is to explicitly tell PowerShell where the bit to interpolate starts and where it stops:
"$($test[0])"
Note that this behavior is one of my main reasons for using formatted strings instead of relying on interpolation:
"{0}" -f $test[0]
EBGreen's helpful answer contains effective solutions, but only a cursory explanation of PowerShell's string expansion (string interpolation):
Only variables by themselves can be embedded directly inside double-quoted strings ("...") (by contrast, single-quoted strings ('...'), as in many other languages, are for literal contents).
This applies to both regular variables and variables referencing a specific namespace; e.g.:
"var contains: $var", "Path: $env:PATH"
If the first character after the variable name can be mistaken for part of the name - which notably includes : - use {...} around the variable name to disambiguate; e.g.:
"${var}", "${env:PATH}"
To use a $ as a literal, you must escape it with `, PowerShell's escape character; e.g.:
"Variable `$var"
Any character after the variable name - including [ and . is treated as a literal part of the string, so in order to index into embedded variables ($var[0]) or to access a property ($var.Count), you need $(...), the subexpression operator (in fact, $(...) allows you to embed entire statements); e.g.:
"1st element: $($var[0])"
"Element count: $($var.Count)"
"Today's date: $((Get-Date -DisplayHint Date | Out-String).Trim())"
Stringification (to-string conversion) is applied to any variable value / evaluation result that isn't already a string:
Caveat: Where culture-specific formatting can be applied, PowerShell chooses the invariant culture, which largely coincides with the US-English date and number formatting; that is, dates and numbers will be represented in US-like format (e.g., month-first date format and . as the decimal mark).
In essence, the .ToString() method is called on any resulting non-string object or collection (strictly speaking, it is .psobject.ToString(), which overrides .ToString() in some cases, notably for arrays / collections and PS custom objects)
Note that this is not the same representation you get when you output a variable or expression directly, and many types have no meaningful default string representations - they just return their full type name.
However, you can embed $(... | Out-String) in order to explicitly apply PowerShell's default output formatting.
For a more comprehensive discussion of stringification, see this answer.
As stated, using -f, the string-formatting operator (<format-string> -f <arg>[, ...]) is an alternative to string interpolation that separates the literal parts of a string from the variable parts:
'1st element: {0}; count: {1:x}' -f $var[0], $var.Count
Note the use of '...' on the LHS, because the format string (the template) is itself a literal. Using '...' in this case is a good habit to form, both to signal the intent of using literal contents and for the ability to embed $ characters without escaping.
In addition to simple positional placeholders ({0} for the 1st argument. {1} for the 2nd, ...), you may optionally exercise more formatting control over the to-string conversion; in the example above, x requests a hex representation of the number.
For available formats, see the documentation of the .NET framework's String.Format method, which the -f operator is based on.
Pitfall: -f has high precedence, so be sure to enclose RHS expressions other than simple index or property access in (...); e.g., '{0:N2}' -f 1/3 won't work as intended, only '{0:N2}' -f (1/3) will.
Caveats: There are important differences between string interpolation and -f:
Unlike expansion inside "...", the -f operator is culture-sensitive:
Therefore, the following two seemingly equivalent statements do not
yield the same result:
PS> [cultureinfo]::CurrentCulture='fr'; $n=1.2; "expanded: $n"; '-f: {0}' -f $n
expanded: 1.2
-f: 1,2
Note how only the -f-formatted command respected the French (fr) decimal mark (,).
Again, see the previously linked answer for a comprehensive look at when PowerShell is and isn't culture-sensitive.
Unlike expansion inside "...", -f stringifies arrays as <type-name>[]:
PS> $arr = 1, 2, 3; "`$arr: $arr"; '$arr: {0}' -f (, $arr)
$arr: 1 2 3
$arr: System.Object[]
Note how "..." interpolation created a space-separated list of the stringification of all array elements, whereas -f-formatting only printed the array's type name.
(As discussed, $arr inside "..." is equivalent to:
(1, 2, 3).psobject.ToString() and it is the generally invisible helper type [psobject] that provides the friendly representation.)
Also note how (, ...) was used to wrap array $arr in a helper array that ensures that -f sees the expression as a single operand; by default, the array's elements would be treated as individual operands.
In such cases you have to do:
echo "$($test[0])"
Another alternative is to use string formatting
echo "this is {0}" -f $test[0]
Note that this will be the case when you are accessing properties in strings as well. Like "$a.Foo" - should be written as "$($a.Foo)"

Resources