Bash - Multidimensional Arrays and Extracting variables from output
Solution 1
Your main problem is that the last command in a pipeline runs in a subshell, like all other commands in the pipeline. This is the case in most shells. ATT ksh and zsh are exceptions: they run the last command of the pipeline in the parent shell.
Since bash 4.2, you can tell bash to behave like ksh and zsh by setting the lastpipe
option.
#!/bin/bash
USERS=()
shopt -s lastpipe
w | awk '{if(NR > 2) print $1,$2,$3}' | while read line; do
USERS+=("$line")
done
echo ${#USERS[@]}
Alternatively, you can use process substitution instead of a pipe, so that the read
command runs in the main shell process.
#!/bin/bash
USERS=()
while read line; do
USERS+=("$line")
done < <(w | awk '{if(NR > 2) print $1,$2,$3}')
echo ${#USERS[@]}
Alternatively, you can use the portable approach, which works in shells that don't have process susbtitution nor ksh/zsh behavior, such as Bourne, dash and pdksh. (You still need (pd)ksh, bash or zsh for arrays.) Run everything that requires the data from the pipeline inside the pipeline.
#!/bin/bash
USERS=()
shopt -s lastpipe
w | awk '{if(NR > 2) print $1,$2,$3}' | {
while read line; do
USERS+=("$line")
done
echo ${#USERS[@]}
}
Solution 2
Bash arrays are one dimensional. If you want to hold ordered separate values for each line one solution is to use associative arrays. A crude example:
Also be careful with those uppercase variable names as they can clash with environment variables.
#!/bin/bash
declare -i i=0 j=0
declare -A w
while read -r user tty from _;do
((++i > 2)) || continue
w["$j.user"]="$user"
w["$j.tty"]="$tty"
w["$j.from"]="$from"
((++j))
done < <(w)
for ((i = 0; i < j; ++i)); do
printf "entry %-2d {\n %-5s: %s\n %-5s: %s\n %-5s: %s\n}\n" \
"$i" \
"user" "${w[$i.user]}" \
"tty" "${w[$i.tty]}" \
"from" "${w[$i.from]}"
done
Solution 3
With shopt -s lastpipe
you can take the last command of a pipeline into the current shell environment. That solves your problem. I guess this feature has not always been in bash so avoid it if you want broadly compatible code.
The compatible alternative:
export_array="$(w | awk '{if(NR > 2) print $1,$2,$3}' |
{ USERS=(); while read line; do
USERS[]="$line"
done
declare -p USERS; } )"
eval "$export_array"
Solution 4
for storage in bash arrays, using a delimiter other than space is often simpler.
readarray -s2 -t my_w_array < <(w | awk '{ print $1":"$2":"$3 }')
you can then split it when printing it, like:
printf '%s\n' "${my_w_array[@]//:/ }"
russ
Updated on September 18, 2022Comments
-
russ over 1 year
I am trying to do something simple however I'm not sure how to achieve my goal here.
I am trying to extract the: USER, TTY and FROM values that are given by the
w
command on the console. In bash I am trying to take this output and get these values into a multidimensional array (or just an array with a space delimiter).#!/bin/bash w|awk '{if(NR > 2) print $1,$2,$3}' | while read line do USERS+=("$line") echo ${#USERS[@]} done echo ${#USERS[@]}
I have found my way to the point of reading in the values by line in a single array however I cannot seem to get the USERS array value out of the scope of the while loop. It prints the values 1,2,3,4 and then 0 after the loop. Every example I read they use the variable outside the scope perfectly fine but I cannot seem to.
-
jordanm almost 11 yearsThe right side of a pipeline runs in a subshell in bash, that's why it's not available after the loop.
-
russ almost 11 years@jordanm so there is no way to extract the USERS array?
-
jordanm almost 11 yearsUse process substitution.
while read col1 col2 col3 _; do ...; done < <(w)
-
-
Runium almost 11 yearsDid you mean
USERS+=("$line")
instead ofUSERS[]="$line"
? -
Hauke Laging almost 11 years@Sukminder Of course not. I replaced that because AFAIR the
+=
notation has been added in recent versions of bash. Both commands do the same.