Iterate lines of string variable in bash

This is a good situation to use readarray/mapfile:

readarray -t usbs < <(lsblk -o NAME,TRAN,VENDOR,MODEL | grep usb)

This will create an array with your output where each line is separated into it's own element.

In your case it would make an array like:

usbs=(
'sdb   usb    Kingston DataTraveler 2.0'
'sdc   usb    Kingston DT 101 G2'
)

As is you are assigning your entire output to a single variable (not an array) which essentially does this:

usbs='sdb   usb    Kingston DataTraveler 2.0
sdc   usb    Kingston DT 101 G2 '

In order to make it an array you would do:

usbs=($(lsblk -o NAME,TRAN,VENDOR,MODEL | grep usb))

but this would make each word separated by whitespace into its own element, equivalent to:

usbs=(
sdb
usb
Kingston
DataTraveler
2.0
sdc
usb
Kingston
DT
101
G2
)

As has been pointed out by several commenters and is generally best practice at all times, variables should be quoted. In this case you must and generally always should quote your variables.

First, I'd say it's not the right way to address the problem. It's a bit like saying "You should not murder people because otherwise you'll go to jail."

Similarly, you don't quote your variable because otherwise you're introducing security vulnerabilities. You quote your variables because it is wrong not to (but if the fear of the jail can help, why not).

  • Stéphane Chazelas

In the case of for i in ${usbs[@]}; do, i will be set to every word (separated by whitespace) in usbs. If you quote it like for i in "${usbs[@]}"; do, then i will be set to every element of usbs, as is desired.


This is mostly a dupe of new lines and bash variable although that doesn't cover arrays. From there, to use a variable containing multiple lines, you need to make parameter expansion split at newline and skip globbing, and depending on your data possibly avoid other mangling:

 usbs=$( lsusb ... )
 IFS=$'\n'  # ksh bash zsh; in other shells you may need to quote an actual newline
 set -o noglob  # or more tersely set -f
 for i in $usbs; do
   printf '%s\n' "$i" # not echo which sometimes modifies some data
 done
 # if you do further things in the same script (or function) you may 
 # need to re-set IFS and/or glob, which may require saving them first

For an array, readarray/mapfile as suggested by Jesse_b is the simplest, because it already splits at lines. But you can do it 'manually' much as above:

set -o noglob  # ditto 
IFS=$'\n' usbs=( $( lsusb ... ) )  # only ksh up has arrays so $'' safe
# set +o noglob or set +f if needed
for i in "${usbs[@]}"; do # quoted array[@] forces splits equal to array elements only
  printf '%s\n' "$i"
done

Question: Is there a way in which, using the grep command, I can store the result of the command as two whole lines?

Yes, and your assignment code was correct:

usbs=$(lsblk -o NAME,TRAN,VENDOR,MODEL | grep usb)

This does exactly as required; in a single variable, (not an array), it stores two lines from lsblk separated by a newline. But a for loop is not the right tool to read that variable. A while loop is much better, here's an example with made-up data since some readers may not have any USB devices plugged in:

t=$(echo foo bar; echo baz bing;)
while read i ; do echo "$i" ; done <<< "$t"

Output:

foo bar
baz bing

Here's an even shorter method:

xargs -L 1 <<< "$t"

Note: while a plain POSIX style variable x is not an array, bash allows x to be identified using array notation and won't complain about x not being an array. Demo:

x=f
echo $x ${x[0]} ${x[@]}

Output:

f f f

But x is not an array. If it were, this code, (using the bash parameter transformation Assignment operator), would definitely output an array:

echo "${x[@]@A}"

...it doesn't:

x='f'

For contrast, let's compare the above to what it would look like if it were an array. First make a very similar array y, then use the Assignment operator to show the difference:

y=(f)
echo "${y[@]@A}"

Output:

declare -a y=([0]="f")