7

Here is an example of behavior I want to achieve:

Suppose I have a list of lines, each line containing space separated values:

lines='John Smith
James Johnson'

And I want to loop over lines echoing name or surname only as user asked by prompt, so I change IFS globally:

oIFS=$IFS
IFS='
'
for line in $lines; do
    IFS=$oIFS
    read name surname <<< $line
    read -p "Do you want to echo name? surname otherwise "
    if [[ $REPLY == "y" ]]; then
        echo $name
    else
        echo $surname
    fi
done

This works, and yet this approach doesn't look sane for me:

  1. We change IFS and may forget to restore it
  2. We restore IFS every single iteration of the loop

I discovered that while IFS=... can be used here like that:

while IFS='
' read line ; do
    echo $line
    read name surname <<< $line
    read -p "Do you want to echo name? surname otherwise "
    if [[ $REPLY == "y" ]]; then
        echo $name
    else
        echo $surname
    fi
done <<< "$lines"

But this is not an option because read -p prompt gets corrupted by continuous input stream

A solution would be IFS set only for one for statement like this:

IFS='
' for line in $lines; do
    ...
done

but bash disallows that.

0

2 Answers 2

9

You could read the input lines to an array first with mapfile/readarray:

lines='John Smith
James Johnson'
mapfile -t lines <<< "$lines"
for line in "${lines[@]}"; do
    read name surname <<< "$line"
    echo "name: $name surname: $surname"
done

If lines comes from the output of some command, you could similarly use mapfile -t lines < <(somecommand) directly. The process substitution there is a bit like a pipe, but avoids issues from pipeline parts running in subshells. Note that your lines is missing the newline from the end of the last line, but <<< adds one. mapfile doesn't mind if it's missing though, but if you did have the newline at the end of lines, you'd get an empty array entry for the extra one. Using the process substitution bypasses that issue.


Here,

while IFS=... read line ; do
    ...
    read -p "Do you want to echo name? surname otherwise "
done <<< "$lines"

both reads indeed read from the same input, but I think (didn't test) you could work around that by having the redirection to the loop use another file descriptor, e.g.:

while IFS=... read -u 3 line ; do
    ...
    read -p "Do you want to echo name? surname otherwise "
done 3<<< "$lines"

or read <&3 instead of read -u, I don't know if it matters.

3
  • "lines is missing the newline from the end of the last line" -- the <<< redirection adds a newline => x=$'first\nsecond'; od -c <<<"$x" Jul 9, 2021 at 16:05
  • 1
    @glennjackman, d'oh, indeed, silly me. Though mapfile still saves the last incomplete line, but an extra NL could add an extra empty element.
    – ilkkachu
    Jul 9, 2021 at 16:09
  • Both the read -u 3 and read <&3 loops work in bash, zsh, and ksh. The -u 3 version isn't portable to more basic shells like dash, but then <<< isn't either so if you're going for max portability none of these work. Jul 9, 2021 at 20:11
6

Use an actual list (array) instead of fiddling with IFS:


#!/bin/bash
lines=( 'John Smith' 'James Johnson' )

for line in "${lines[@]}"; do
  names=($line)
  echo "$line"
  read -p "Do you want to echo name? surname otherwise " 
    if [[ $REPLY == "y" ]]; then
        echo "${names[0]}"
    else
        echo "${names[1]}"
    fi
done

Of course, this assumes that you only ever have a single first name and single surname which isn't true for real people but may be true for your data. In any case, your original code was making the same assumption so I am guessing that isn't an issue for you.

You can also do it the way you had thought by running your loop in a subshell so any changes to the IFS will only affect the subshell:

#!/bin/bash
lines='John Smith
James Johnson'


(
  oIFS=$IFS
  IFS=$'\n'
  for line in $lines; do
    echo "$line"
    IFS=$oIFS
    read name surname <<< "$line"
    read -p "Do you want to echo name? surname otherwise "
    if [[ $REPLY == "y" ]]; then
        echo "$name"
    else
        echo "$surname"
    fi
  done
)

### The IFS hasn't been changed here outside the subshell.
2
  • 1
    Aside: echo $name should be echo "$name"; <<<$line should be <<<"$line" (granted, I'd need to go do some research to look up the bug triggered by unquoted herestrings in some versions of bash, but there's been one in recent, post-3.x years); etc. Jul 9, 2021 at 22:54
  • @CharlesDuffy fair enough. I was careful to quote my version but I'd just copied the OP's for the second. Fixed now.
    – terdon
    Jul 10, 2021 at 16:45

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .