4

I understand command substitution. I understand subshells. I do not understand why using a subshell changes the structure of my array.

Given this command output: (use of openstack command is not intended to be relevant)

bash$ floating ip list -c 'Floating IP Address' -f value
172.25.250.106
172.25.250.107
172.25.250.101

Attempt to capture in an array, but all addresses end up in element 0:

bash$ float=$( openstack floating ip list -c 'Floating IP Address' -f value )
bash$ echo ${float[@]}
172.25.250.106 172.25.250.107 172.25.250.101
bash$ echo ${#float[@]}
1
bash$ echo ${float[0]}
172.25.250.106 172.25.250.107 172.25.250.101
bash$ echo ${#float[0]}
44

The whole output was captured as a string and not parsed into elements. I was expecting each word to become an element. When I repeat this to ensure that each IP address is quoted (using -f csv instead of -f value), the results are the same.

Next, I put the command substitution in a subshell:

bash$ unset float
bash$ float=( $( openstack floating ip list -c 'Floating IP Address' -f value ) )
bash$ echo ${float[@]}
172.25.250.106 172.25.250.107 172.25.250.101
bash$ echo ${#float[@]}
3
echo ${float[0]}
172.25.250.106
echo ${#float[0]}
14

This was the behavior I expected originally. I also notice that building the array using a read statement worked as expected:

bash$ unset float
bash$ read -a float <<< $( openstack floating ip list -c 'Floating IP Address' -f value )
bash$ echo ${float[@]}
172.25.250.106 172.25.250.107 172.25.250.101
bash$ echo ${#float[@]}
3
echo ${float[0]}
172.25.250.106
echo ${#float[0]}
14

I would like to see the original command substitution work. Wondering if I should have set a field separator first or what else I am missing. I am trying to understand what causes the difference in behavior.

  • 1
    float=$( ...) is command substitution, not an array at all. – Michael Homer Jun 17 '17 at 22:40
  • Yes, I understand the $( ... ) is command substitution. I am trying to understand why capturing the output of that command substitution does not capture the distnct words in the string as elements. But place the same command substitution in a subshell, and it does. – Philip Sweany Jun 17 '17 at 22:43
  • Because it's a string, not an array. You know how to make an array. I'm not really sure what the question is here. – Michael Homer Jun 17 '17 at 22:44
  • The question is: why does placing that same command substitution inside of a subshell cause the output string to be parsed into elements when it did not when it was not placed into a subshell? If I knew better what causes the difference in behavior, I would ask a better question. – Philip Sweany Jun 17 '17 at 22:49
4
float=$( openstack floating ip list -c 'Floating IP Address' -f value )

This creates a string variable, not an array variable. The string is the output of the command, minus any trailing newline.

If you attempt to use a string variable as an array, it's treated as a single-element array with the string value at position 0.

float=( $( openstack floating ip list -c 'Floating IP Address' -f value ) )

This does not “put the command substitution in a subshell”. The command substitution itself $(…) creates a subshell. The parentheses around it do not create another subshell: they create an array. The array content it the list of words resulting from taking the output of the command, removing trailing newlines, splitting into a list of whitespace-separated words, and replacing any element of this list that contains wildcard characters that match one or more files by the list of matching file names.

Parentheses create a subshell when they're at a location where a command is expected. In var=(…), what is expected immediately after the equal sign is not a command, but a value for assignment. In this context, the parentheses indicate that the value is an array.

  • Thank you, also. Very helpful. There was my confusion, misinterpreting that the parantheses are an array evaluation, not another subshell. I can now create results with this. – Philip Sweany Jun 17 '17 at 23:00
  • In a script the right syntax would be declare -a float=( $( openstack ... ) but as you see, it works anyway. – hschou Jun 18 '17 at 7:41
  • @hschou Using declare isn't “more right” than not using declare. In a function, declare makes the variable local. Outside of a function, it doesn't make any difference. – Gilles Jun 18 '17 at 21:16
2

The parenthesis in var=( some things ) don't mark a subshell, they're part of the array assignment syntax. So, without them, you're assigning to a regular (scalar) variable. The right-hand side of a regular assignment doesn't go through word splitting, so var=$(echo foo bar) will put the string foo bar in var, with the spaces. On the other hand, the array assignment takes multiple words, so both arr=(foo bar) and arr=( $(echo foo bar) ) give a two-element array.

  • Thank you. That was the missing piece, that the outer parenthesis only signify array assignment and not a subshell. Now it makes sense. – Philip Sweany Jun 17 '17 at 22:58
2
float=$( openstack floating ip list -c 'Floating IP Address' -f value )

This is command substitution inside a regular variable assignment, not an array assignment. All compound array assignments have the form x=( ... ), as you used below. There is no subshell here (other than briefly as the execution context of the substituted command).

When you use a command substitution it behaves in the same way as a variable with those contents would in that position, and so analogously with float=$x here, but also with ls $x or foo=($x). Word splitting is performed on the result of the expansion, which separates the value into separate arguments at any character in the IFS variable.

You can suppress word splitting by quoting the expansion "$(...)".

If you want to create an array of split words, you need both to create an array and to get the words split: that means an array assignment foo=(...) and a parameter or command substitution $... combined, as in your second case:

float=( $( openstack floating ip list -c 'Floating IP Address' -f value ) )

The float=(...) makes an array, and the elements come from the word-splitting performed during the command substitution $(...).


What may be confusing is that Bash automatically converts non-arrays into singleton arrays when you use them as one, so it looks like you did get an array, but of a single item.

echo ${float[0]}
echo ${#float[@]}

This is very subtly documented:

Any reference to a variable using a valid subscript is legal, and bash will create an array if necessary.

You'll see that more clearly if you use another index:

float[1]=abc
echo ${#float[@]} # => 2

The conversion process just uses the existing value of the variable, if any, as the item at index 0 in the array.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.