Bash - Multidimensional Arrays and Extracting variables from output

21,727

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[@]//:/ }"
Share:
21,727
russ
Author by

russ

Updated on September 18, 2022

Comments

  • russ
    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
      jordanm almost 11 years
      The right side of a pipeline runs in a subshell in bash, that's why it's not available after the loop.
    • russ
      russ almost 11 years
      @jordanm so there is no way to extract the USERS array?
    • jordanm
      jordanm almost 11 years
      Use process substitution. while read col1 col2 col3 _; do ...; done < <(w)
  • Runium
    Runium almost 11 years
    Did you mean USERS+=("$line") instead of USERS[]="$line"?
  • Hauke Laging
    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.