How to port to bash-style arrays to ash?
Some time ago I have written a bash script which now should be able to run in environment with ash
.
In bash
it was like :
services=( "service1.service"
"service2.service"
"service3.service" )
for service in "${services[@]}"
do
START $service
done
START()
{
echo "Starting "$1
systemctl start $1
}
In reality there are like 40 services in array, and I want to make this transition as painless and clean as possible. Have always been using bash
isms. Now I’m in a pinch with the task to make scripts more portable.
For portability reasons probably it would be nice to have a pure ash
solution. But since I have a pretty robust busybox
at my disposal I might sacrifice some portability. Only if readability improves a lot, since “clean” script is a metric too.
What would be portable and clean solution in this case?
ash does not have arrays. The only thing that comes close is the positional parameters, so you could do
set -- "service1.service"
"service2.service"
"service3.service"
for service in "$@"
do
START $service
done
Before arrays were in bash
, ksh
, and other shells, the usual method was to pick a delimiter that wasn’t in any of the elements (or one that was uncommon to minimise any required escaping), and iterate over a string containing all the elements, separated by that delimiter. Whitespace is usually the most convenient delimiter choice because the shell already splits “words” by whitespace by default (you can set IFS if you want it to split on something different).
For example:
# backslash-escape any non-delimiter whitespace and all other characters that
# have special meaning to the shell, e.g. globs, parenthesis, ampersands, etc.
services='service1.service service2.service service3.service'
for s in $services ; do # NOTE: do not double-quote $services here.
START "$s"
done
$services
should NOT be double-quoted here because we want the shell to split it into “words”.
If you need to refer to the list of services only once, you can use a here-doc:
while IFS= read -r service
do
START "$service"
done << END
service1.service
service2.service
service3.service
END
Note that the service names should not be quoted in the list
(although "$service"
probably should be quoted,
unless you have a good reason not to).
If you would like to have the service names indented,
use <<-
instead of <<
and indent the names with tabs:
while IFS= read -r service
do
START "$service"
done <<- END
service1.service
service2.service
service3.service
END
You can simulate an array by creating variables with fix prefix
Example 1:
#!/bin/ash
# simulate array variables
comment_wifi='wifi-text'
comment_status='some-state'
comment_0=1234
comment_1=7878
comment_2=9999
for v in wifi status $(seq 0 2)
do
# construct variable and get its value
eval value="$comment_${v}"
echo "comment_${v}: ${value}"
done
Output:
comment_wifi: wifi-text
comment_status: some-state
comment_0: 1234
comment_1: 7878
comment_2: 9999
Example 2:
#!/bin/ash
# fill create and variables
for v in wifi status $(seq 0 2)
do
# example value
a_value="$(cat /proc/uptime)"
# construct variable and assign value
eval comment_${v}="$a_value";
done
# output
for v in wifi status $(seq 0 2)
do
# construct variable and get its value
eval value="$comment_${v}";
echo "comment_${v}: ${value}"
done
Output:
comment_wifi: 5954.23 22504.11
comment_status: 5954.24 22504.12
comment_0: 5954.24 22504.14
comment_1: 5954.25 22504.16
comment_2: 5954.25 22504.17