Howto iterate through an input stream without a subshell or a read.

It sometimes comes up that the usual trick of reading in a stream with a pipe and a while loop doesn't work, since the pipe causes a subshell and any variables set in the subshell are unavailable to the parent.
For example the normal trick (output the first few lines of the /etc/services file, just the first two columns and not empty or comment lines):

[uphill@zagreb]: grep -v ^# /etc/services | grep -v ^$ | head | awk '{print $1" "$2;}'
tcpmux 1/tcp
tcpmux 1/udp
rje 5/tcp
rje 5/udp
echo 7/tcp
echo 7/udp
discard 9/tcp
discard 9/udp
systat 11/tcp
systat 11/udp
[uphill@zagreb]: grep -v ^# /etc/services | grep -v ^$ | head | awk '{print $1" "$2;}' | while read x y; do echo x=$x y=$y; done
x=tcpmux y=1/tcp
x=tcpmux y=1/udp
x=rje y=5/tcp
x=rje y=5/udp
x=echo y=7/tcp
x=echo y=7/udp
x=discard y=9/tcp
x=discard y=9/udp
x=systat y=11/tcp
x=systat y=11/udp
[uphill@zagreb]: echo $x
 
[uphill@zagreb]: echo $y
[uphill@zagreb]: i=0; grep -v ^# /etc/services | grep -v ^$ | head | awk '{print $1" "$2;}' | while read x y
> do
> names[$i]=$x
> ports[$i]=$y
> i=$((i+1))
>done
[uphill@zagreb]: echo ${ports[1]}

This leaves the parent shell without any information.
But if you feed a function the output, you can retain the information.

[uphill@zagreb]: function parse {
> i=0
> while [ $# -gt 0 ]
> do
>  names[$i]=$1
>  ports[$i]=$2
>  i=$((i+1))
>  shift; shift;
> done; }
[uphill@zagreb]: parse `grep -v ^# /etc/services | grep -v ^$ | head | awk '{print $1" "$2;}'`
uphill@zagreb[1063]: echo ${ports[0]}
1/tcp
uphill@zagreb[1064]: echo ${names[1]} ${ports[1]}
tcpmux 1/udp

This way you have access to the variables you set and can put the data collection part of this task into the subshell.
If you have lines with 3,4 or more input variables, just reference them as $3 $4 etc but remember to use the same number of shifts to move the pointer along the input the correct number of slots.

If you have input that has spaces in it within elements, that is, you have tabs for separation and spaces within (like say mysql output).
You can pipe to sed first to replace and then send your input to sed again to undo the damage

[uphill@surrey]: function parse {
i=0
while [ $# -gt 0 ]
do 
 usernames[$i]=$1
 name=`echo $5|sed -e 's/xXx/ /g'`
 names[$i]=$name
 i=$((i+1))
 shift; shift; shift; shift; shift; shift; shift
done; }
[uphill@surrey]: parse `cat /etc/passwd |tr ':' '\t' |tail -5 |sed -e 's/ /xXx/g'`
[uphill@surrey]: echo ${usernames[3]} " -> " ${names[3]}
canna  ->  Canna Service User

We inline sed to change the spaces to xXx, then we do the same to change back before assignment to the names array. This trick works fairly well, but depends on your input having tabs or some other character than spaces to deliminate initially.

About the Author...

Slides from LISA 2019 Linux systems troubleshooting #LISA2019 https://t.co/D4dMKflK6R Tue Oct 29 05:59:30 +0000 2019

https://t.co/AGeihMALAv configuring grub2 with EFI Fri Sep 13 05:20:01 +0000 2019

I published a Thing on @thingiverse! https://t.co/IYpRyEb7Hz #thingalert Tue Jul 23 19:27:57 +0000 2019

Nokogiri install on MacOSX https://t.co/v3An0miW9L Fri Jul 12 15:06:49 +0000 2019

HTML email with plain mailer plugin on Jenkins https://t.co/Z6FSDMDjy8 Thu Jul 11 21:07:25 +0000 2019