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 A
ssignment 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 A
ssignment operator to show the difference:
y=(f)
echo "${y[@]@A}"
Output:
declare -a y=([0]="f")