9

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.

3
  • The right side of a pipeline runs in a subshell in bash, that's why it's not available after the loop.
    – jordanm
    Jun 7, 2013 at 21:26
  • @jordanm so there is no way to extract the USERS array?
    – russ
    Jun 7, 2013 at 21:51
  • Use process substitution. while read col1 col2 col3 _; do ...; done < <(w)
    – jordanm
    Jun 7, 2013 at 21:53

4 Answers 4

5

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[@]}
}
4

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"
2
  • Did you mean USERS+=("$line") instead of USERS[]="$line"?
    – Runium
    Jun 7, 2013 at 23:36
  • @Sukminder Of course not. I replaced that because AFAIR the += notation has been added in recent versions of bash. Both commands do the same. Jun 7, 2013 at 23:40
4

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
3

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[@]//:/ }"

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.