Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

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.

share|improve this question
    
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 '13 at 21:26
    
@jordanm so there is no way to extract the USERS array? –  russ Jun 7 '13 at 21:51
    
Use process substitution. while read col1 col2 col3 _; do ...; done < <(w) –  jordanm Jun 7 '13 at 21:53

4 Answers 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"
share|improve this answer
    
Did you mean USERS+=("$line") instead of USERS[]="$line"? –  Sukminder Jun 7 '13 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. –  Hauke Laging Jun 7 '13 at 23:40

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|improve this answer

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
share|improve this answer

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[@]}
}
share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.