randeepsp
randeepsp

Reputation: 3842

Iterate over lines instead of words in a for loop of shell script

Following is the shell script to read all the DSF present in the box. But since the line is having spaces, it is displaying them in different lines. For those of you who dont understand ioscan -m dsf, replace it by ls -ltr, then the output is such that the permission and names are displayed in different line, but i want them in the same line.

#!/usr/bin/ksh

for a in `ioscan -m dsf`
do
 echo  $a
done

Upvotes: 65

Views: 61594

Answers (3)

Lesmana
Lesmana

Reputation: 27053

The for loop is not designed to loop over lines. Instead it loops over words. Words are things separated by space. Lines are things separated by newline. More on that later.

The idiomatic way to loop over lines is to use a while loop in combination with read:

ioscan -m dsf | while read -r line
do
  printf '%s\n' "$line"
done

Alternatively:

while read -r line
do
  printf '%s\n' "$line"
done < <(ioscan -m dsf)

Both work fine for most simple cases. The second variant is using a process substitution which might not be available in all shells.

Both variants have advantages and disadvantages which mainly become apparent if you want to manipulate variables inside the loop.

For more information see http://mywiki.wooledge.org/BashFAQ/024


technical nitpick:

words, or fields as they are called in bash, are things separated by space but also by tab and newlines. basically things separated by whitespace.

the separator separating the fields is defined in the IFS variable (short for Internal Field Separator). Usually $IFS contains a space, a tab, and a newline.

Often you will see the suggestion to loop over lines by changing the value of $IFS to only newline.

# not recommended
OLDIFS="$IFS"
IFS=$'\n'
for line in $(ioscan -m dsf)
do
  printf '%s\n' "$line"
done
IFS="$OLDIFS"

(the $'\n' is is called ANSI-C Quoting and might not be available in all shells)

I do not recommend changing $IFS. Many commands rely on sane setting for $IFS. Changing $IFS will often cause an endless nightmare of obscure bug hunting.


See also:

Upvotes: 92

psreddyio
psreddyio

Reputation: 80

you need to use this basically IFS=$'\n' and grep -x instead of grep as it will work like a equal to operator instead of like operator.

Upvotes: 0

Lri
Lri

Reputation: 27613

Using for

for l in $() performs word splitting based on IFS:

$ for l in $(printf %b 'a b\nc'); do echo "$l"; done
a
b
c
$ IFS=$'\n'; for l in $(printf %b 'a b\nc'); do echo "$l"; done
a b
c

IFS doesn't have to be set back if it is not used later.

for l in $() also performs pathname expansion:

$ printf %b 'a\n*\n' > file.txt
$ IFS=$'\n'
$ for l in $(<file.txt); do echo "$l"; done
a
file.txt
$ set -f; for l in $(<file.txt); do echo "$l"; done; set +f
a
*

If IFS=$'\n', linefeeds are stripped and collapsed:

$ printf %b '\n\na\n\nb\n\n' > file.txt
$ IFS=$'\n'; for l in $(<file.txt); do echo "$l"; done
a
b

$(cat file.txt) (or $(<file.txt)) also reads the whole file to memory.

Using read

Without -r backslashes are used for line continuation and removed before other characters:

$ cat file.txt
\1\\2\
3
$ cat file.txt | while read l; do echo "$l"; done
1\23
$ cat file.txt | while read -r l; do echo "$l"; done
\1\\2\
3

Characters in IFS are stripped from the start and end of lines but not collapsed:

$ printf %b '1  2 \n\t3\n' | while read -r l; do echo "$l"; done
1  2
3
$ printf %b ' 1  2 \n\t3\n' | while IFS= read -r l; do echo "$l"; done
 1  2 
    3

If the last line doesn't end with a newline, read assigns l to it but exits before the body of the loop:

$ printf 'x\ny' | while read l; do echo $l; done
x
$ printf 'x\ny' | while read l || [[ $l ]]; do echo $l; done
x
y

If a while loop is in a pipeline, it is also in a subshell, so variables are not visible outside it:

$ x=0; seq 3 | while read l; do let x+=l; done; echo $x
0
$ x=0; while read l; do let x+=l; done < <(seq 3); echo $x
6
$ x=0; x=8 | x=9; echo $x
0

Upvotes: 21

Related Questions