Skip to content

Commit f2ad61c

Browse files
glennjIsaacG
andauthored
Arrays concept part 2 (#729)
* Arrays concept part 2 * remove the indirect expansion section * Apply suggestions from code review Co-authored-by: Isaac Good <[email protected]> * review suggestions * populate about --------- Co-authored-by: Isaac Good <[email protected]>
1 parent f941181 commit f2ad61c

File tree

5 files changed

+458
-0
lines changed

5 files changed

+458
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"authors": [
3+
"glennj"
4+
],
5+
"contributors": [
6+
"IsaacG"
7+
],
8+
"blurb": "More about Arrays in Bash programs."
9+
}

concepts/more-arrays/about.md

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# More about Arrays
2+
3+
We introduced arrays in the [Arrays][arrays] chapter.
4+
This document will show more ways to use arrays.
5+
6+
## Concatenating the Elements of an Array into a Single String
7+
8+
In the previous Arrays chapter, you saw `"${myarray[@]}"`, with the `@` index, used to expand the array into the individual elements.
9+
But sometimes you want to join all the elements into a single string.
10+
For this, use the `*` index:
11+
12+
```bash
13+
echo "${myarray[*]}"
14+
```
15+
16+
You are required to enclose the expansion in double quotes.
17+
18+
Bash uses the _first character_ of the `IFS` builtin variable as the separator character.
19+
By default, `$IFS` consists of space, tab and newline.
20+
21+
```bash
22+
myarray=(one two three four)
23+
mystring="${myarray[*]}"
24+
declare -p mystring
25+
# => declare -- mystring="one two three four"
26+
```
27+
28+
We can manipulate the `IFS` variable to use a different separator character:
29+
30+
```bash
31+
myarray=(one two three four)
32+
IFS=","
33+
mystring="${myarray[*]}"
34+
declare -p mystring
35+
# => declare -- mystring="one,two,three,four"
36+
```
37+
38+
~~~~exercism/advanced
39+
<details><summary>
40+
We can encapsulate this into a function.
41+
Click for details.
42+
</summary>
43+
44+
```bash
45+
join() {
46+
local IFS=$1
47+
shift
48+
local elements=("$@")
49+
echo "${elements[*]}"
50+
}
51+
52+
join ":" "${myarray[@]}" # note, the "@" index
53+
# => "one:two:three:four"
54+
```
55+
56+
Localizing `IFS` in the function means we don't have to save the old value and restore it back to it's previous value in the global scope.
57+
58+
As a refinement, the special parameter `"$*"`, when quoted, has the same functionality so we don't need to save a copy of the function's arguments:
59+
60+
```bash
61+
join() {
62+
local IFS=$1
63+
shift
64+
echo "$*"
65+
}
66+
```
67+
68+
</details>
69+
70+
<details><summary>
71+
Without using a function, modifying IFS in a subshell is a good way to avoid modifying it in the current shell.
72+
Click for details.
73+
</summary>
74+
75+
```bash
76+
(IFS=","; echo "${myarray[*]}")
77+
```
78+
79+
The parentheses create a subshell (a copy of the current shell). When the commands inside the parentheses complete, the subshell exits, and the changed IFS variable disappears.
80+
81+
Note that this will not work: `IFS="," echo "${myarray[*]}"` -- the parameter expansion is performed first, _before_ the shell applies the modified IFS variable to the `echo` command.
82+
83+
</details>
84+
~~~~
85+
86+
## Array Slices
87+
88+
You may have seen the `"${variable:offset:length}"` [parameter expansion][parameter-expansion] that expands into a _substring_ of the variable's value.
89+
We can do the same thing with arrays to expand a slice of the array.
90+
91+
```bash
92+
myarray=(one two three four)
93+
94+
subarray=("${myarray[@]:0:2}")
95+
declare -p subarray # => subarray=([0]="one" [1]="two")
96+
97+
subarray=("${myarray[@]:1:3}")
98+
declare -p subarray # => subarray=([0]="two" [1]="three" [2]="four")
99+
```
100+
101+
Omitting the length part means "from the offset to the end of the array":
102+
103+
```bash
104+
subarray=("${myarray[@]:2}")
105+
declare -p subarray # => subarray=([0]="three" [1]="four")
106+
```
107+
108+
## Passing an Array to a Function
109+
110+
This is not as straightforward as other languages you might be know.
111+
There are two main techniques to pass an array to a function.
112+
113+
### Pass the Elements
114+
115+
In the first technique, you pass all of the array's values and collect them into a local array in the function.
116+
117+
```bash
118+
myfunc() {
119+
local array_copy=("$@")
120+
# do stuff with array_copy
121+
declare -p array_copy
122+
}
123+
124+
array_original=(11 22 33 44)
125+
myfunc "${array_original[@]}"
126+
```
127+
128+
The function's array holds a _copy_ of the values.
129+
Any changes made to the array in the function are not reflected in the outer scope.
130+
131+
### Pass the Array Name
132+
133+
This technique is more like the "pass by reference" capability you might know from other languages.
134+
You pass the array _name_ as a string.
135+
The function will create a local variable with the "nameref" attribute.
136+
This local array and the global array (whose name we passed in) are _the same array_.
137+
138+
```bash
139+
myfunc() {
140+
# note the `-n` option
141+
local -n local_array=$1
142+
143+
# do stuff with local_array
144+
for i in "${!local_array[@]}"; do
145+
printf '%d => %s\n' "$i" "${local_array[i]}"
146+
end
147+
148+
# we can mutate it
149+
local_array+=(55 66 77)
150+
}
151+
152+
array_original=(11 22 33 44)
153+
myfunc "array_original"
154+
155+
# show the mutated array
156+
declare -p array_original
157+
# => declare -a array_original=([0]="11" [1]="22" [2]="33" [3]="44" [4]="55" [5]="66" [6]="77")
158+
```
159+
160+
Namerefs also work with associative arrays, and "scalar" variables (that contain a string value).
161+
162+
~~~~exercism/note
163+
Inside the function, `declare -p local_array` is not extremely helpful.
164+
It will just emit `declare -n local_array="array_original"`.
165+
You can get the detailed information about the array by inspecting the passed-in array name: `declare -p "$1"`
166+
~~~~
167+
168+
~~~~exercism/caution
169+
Take care that the local array has a different name than the passed-in array.
170+
The code will still work, but it will emit "circular name reference" warnings like this:
171+
172+
```bash
173+
myfunc() {
174+
local -n a=$1
175+
local IFS=,
176+
echo "${a[*]}"
177+
}
178+
179+
# same name as the function's local variable
180+
a=(one two three)
181+
myfunc a
182+
```
183+
184+
```none
185+
bash: local: warning: a: circular name reference
186+
bash: warning: a: circular name reference
187+
bash: warning: a: circular name reference
188+
bash: warning: a: circular name reference
189+
one,two,three
190+
```
191+
~~~~
192+
193+
## The Positional Parameters are "Array-like"
194+
195+
In shells that aim to conform to the POSIX standard only (such as `ash` and `dash`), there are no arrays.
196+
The closest you can get is to use the positional parameters.
197+
198+
* The positional parameters are accessed by index: `$1`, `$2`, etc.
199+
* They are expanded into individual elements with `"$@"`
200+
* They are concatenated into a single string with `"$*"`
201+
* The number of parameters is `$#`
202+
203+
Use the `set` command to assign values to them:
204+
205+
```sh
206+
set -- one two three
207+
set -- "$@" four
208+
209+
for item in "$@"; do
210+
echo "do something with $item"
211+
done
212+
```
213+
214+
If your goal is to write "portable" shell scripts, you'll use the positional parameters to store a "list" of values.
215+
216+
[arrays]: https://exercism.org/tracks/bash/concepts/arrays
217+
[parameter-expansion]: https://www.gnu.org/software/bash/manual/bash.html#Shell-Parameter-Expansion

0 commit comments

Comments
 (0)