I have a variable which contains multiline output of a command. What's the most effecient way to read the output line by line from the variable?
For example:
jobs="$(jobs)"
if [ "$jobs" ]; then
# read lines from $jobs
fi
7 Answers 7
You can use a while loop with process substitution:
while read -r line
do
echo "$line"
done < <(jobs)
An optimal way to read a multiline variable is to set a blank IFS
variable and printf
the variable in with a trailing newline:
# Printf '%s\n' "$var" is necessary because printf '%s' "$var" on a
# variable that doesn't end with a newline then the while loop will
# completely miss the last line of the variable.
while IFS= read -r line
do
echo "$line"
done < <(printf '%s\n' "$var")
Note: As per shellcheck sc2031, the use of process substition is preferable to a pipe to avoid [subtly] creating an subshell.
Also, please realize that by naming the variable jobs
it may cause confusion since that is also the name of a common shell command.
-
5If you want to keep all your whitespace, then use
while IFS= read
.... If you want to prevent \ interpretation, then useread -r
Peter.O– Peter.O2011年03月21日 15:41:25 +00:00Commented Mar 21, 2011 at 15:41 -
1I've fixed the points fred.bear mentioned, as well as changed
echo
toprintf %s
, so that your script would work even with non-tame input.Gilles 'SO- stop being evil'– Gilles 'SO- stop being evil'2011年03月21日 20:57:56 +00:00Commented Mar 21, 2011 at 20:57 -
To read from a multiline variable, a herestring is preferable to piping from printf (see l0b0's answer).ata– ata2011年11月25日 20:34:16 +00:00Commented Nov 25, 2011 at 20:34
-
1@ata Though I've heard this "preferable" often enough, it must be noted that a herestring always requires the
/tmp
directory to be writable, as it relies on being able to create a temporary work file. Should you ever find yourself on a restricted system with/tmp
being read-only (and not changeable by you), you will be happy about the possibility of using an alternate solution, e. g. with theprintf
pipe.syntaxerror– syntaxerror2014年12月04日 22:07:49 +00:00Commented Dec 4, 2014 at 22:07 -
1In the second example, if the multi-line variable doesn't contain a trailing newline you will loose the last element. Change it to:
printf "%s\n" "$var" | while IFS= read -r line
David H. Bennett– David H. Bennett2015年01月15日 03:15:11 +00:00Commented Jan 15, 2015 at 3:15
To process the output of a command line by line (explanation):
jobs |
while IFS= read -r line; do
process "$line"
done
If you have the data in a variable already:
printf %s "$foo" | ...
printf %s "$foo"
is almost identical to echo "$foo"
, but prints $foo
literally, whereas echo "$foo"
might interpret $foo
as an option to the echo command if it begins with a -
, and might expand backslash sequences in $foo
in some shells.
Note that in some shells (ash, bash, pdksh, but not ksh or zsh), the right-hand side of a pipeline runs in a separate process, so any variable you set in the loop is lost. For example, the following line-counting script prints 0 in these shells:
n=0
printf %s "$foo" |
while IFS= read -r line; do
n=$(($n + 1))
done
echo $n
A workaround is to put the remainder of the script (or at least the part that needs the value of $n
from the loop) in a command list:
n=0
printf %s "$foo" | {
while IFS= read -r line; do
n=$(($n + 1))
done
echo $n
}
If acting on the non-empty lines is good enough and the input is not huge, you can use word splitting:
IFS='
'
set -f
for line in $(jobs); do
# process line
done
set +f
unset IFS
Explanation: setting IFS
to a single newline makes word splitting occur at newlines only (as opposed to any whitespace character under the default setting). set -f
turns off globbing (i.e. wildcard expansion), which would otherwise happen to the result of a command substitution $(jobs)
or a variable substitution $foo
. The for
loop acts on all the pieces of $(jobs)
, which are all the non-empty lines in the command output. Finally, restore the globbing and IFS
settings to values that are equivalent to the defaults.
-
I have had trouble with setting IFS and unsetting IFS. I think the right thing to do is store the old value of IFS and set IFS back to that old value. I'm not a bash expert, but in my experience, this gets you back to the original bahavior.Bjorn Roche– Bjorn Roche2013年06月03日 20:06:39 +00:00Commented Jun 3, 2013 at 20:06
-
2@BjornRoche: inside a function, use
local IFS=something
. It won't affect the global-scope value. IIRC,unset IFS
doesn't get you back to the default (and certainly doesn't work if it wasn't the default beforehand).Peter Cordes– Peter Cordes2015年08月31日 01:24:23 +00:00Commented Aug 31, 2015 at 1:24 -
I am wondering whether using
set
in the way shown in the last example is correct. The code snippet assumes thatset +f
was active at the begin, and therefore restores that setting at the end. However, this assumption might be wrong. What ifset -f
was active at the beginning?Binarus– Binarus2020年08月21日 06:43:03 +00:00Commented Aug 21, 2020 at 6:43 -
@Binarus I only restore settings equivalent to the defaults. Indeed, if you want to restore the original settings, you need to do more work. For
set -f
, save the original$-
. ForIFS
, it's annoyingly fiddly if you don't havelocal
and you want to support the unset case; if you do want to restore it, I recommend enforcing the invariant thatIFS
remains set.Gilles 'SO- stop being evil'– Gilles 'SO- stop being evil'2020年08月21日 07:32:12 +00:00Commented Aug 21, 2020 at 7:32 -
Using
local
would indeed be the best solution, becauselocal -
makes the shell options local, andlocal IFS
makesIFS
local. Unfortunately,local
is only valid within functions, which makes code restructuring necessary. Your suggestion to introduce the policy thatIFS
is always set also sounds very reasonable and solves the biggest part of the problem. Thanks!Binarus– Binarus2020年08月21日 08:06:48 +00:00Commented Aug 21, 2020 at 8:06
jobs="$(jobs)"
while IFS= read -r line
do
echo "$line"
done <<< "$jobs"
References:
-
3
-r
is a good idea too; It prevents\` interpretation... (it is in your links, but its probably worth mentioning, just to round out your
IFS=` (which is essential to prevent losing whitespace)Peter.O– Peter.O2011年03月21日 15:33:26 +00:00Commented Mar 21, 2011 at 15:33 -
Only this solution worked for me. Thanks brah.GeneCode– GeneCode2020年01月16日 02:32:06 +00:00Commented Jan 16, 2020 at 2:32
-
1Doesn't this solution suffer from the same problem which is mentioned in the comments to @dogbane's answer? What if the last line of the variable is not terminated by a newline character?Binarus– Binarus2020年08月21日 06:57:41 +00:00Commented Aug 21, 2020 at 6:57
-
2This answer provides the cleanest way to feed the content of a variable to the
while read
construct.Mladen B.– Mladen B.2021年02月12日 08:44:12 +00:00Commented Feb 12, 2021 at 8:44 -
This and the
printf
solution in the accepted answer are the only things that seem to work when the variable has already been defined with command substitution earlier, and this way is a lot cleaner than theprintf
hackiness.Hashim Aziz– Hashim Aziz2024年10月03日 17:38:51 +00:00Commented Oct 3, 2024 at 17:38
Problem: if you use while loop it will run in subshell and all variables will be lost. Solution: use for loop
# change delimiter (IFS) to new line.
IFS_BAK=$IFS
IFS=$'\n'
for line in $variableWithSeveralLines; do
echo "$line"
# return IFS back if you need to split new line by spaces:
IFS=$IFS_BAK
IFS_BAK=
lineConvertedToArraySplittedBySpaces=( $line )
echo "{lineConvertedToArraySplittedBySpaces[0]}"
# return IFS back to newline for "for" loop
IFS_BAK=$IFS
IFS=$'\n'
done
# return delimiter to previous value
IFS=$IFS_BAK
IFS_BAK=
-
3THANK YOU SO MUCH!! All of the above solutions failed for me.nullByteMe– nullByteMe2013年06月11日 18:33:04 +00:00Commented Jun 11, 2013 at 18:33
-
piping into a
while read
loop in bash means the while loop is in a subshell, so variables aren't global.while read;do ;done <<< "$var"
makes the loop body not a subshell. (Recent bash has an option to put the body of acmd | while
loop not in a subshell, like ksh has always had.)Peter Cordes– Peter Cordes2015年08月31日 01:30:04 +00:00Commented Aug 31, 2015 at 1:30 -
Also see this related post.Wildcard– Wildcard2016年02月17日 08:15:56 +00:00Commented Feb 17, 2016 at 8:15
-
In similar situations, I found it surprisingly difficult to treat
IFS
correctly. This solution has a problem as well: What ifIFS
is not set at all in the beginning (i.e. is undefined)? It will be defined in every case after that code snippet; this doesn't seem to be correct.Binarus– Binarus2020年08月21日 06:53:45 +00:00Commented Aug 21, 2020 at 6:53
In recent bash versions, use mapfile
or readarray
to efficiently read command output into arrays
$ readarray test < <(ls -ltrR)
$ echo ${#test[@]}
6305
Disclaimer: horrible example, but you can prolly come up with a better command to use than ls yourself
-
It's a nice way, but litters /var/tmp with temporary files on my system. +1 anywayEugene Yarmash– Eugene Yarmash2011年03月22日 13:18:16 +00:00Commented Mar 22, 2011 at 13:18
-
@eugene: that's funny. What system (distro/OS) is that on?sehe– sehe2011年03月23日 00:41:25 +00:00Commented Mar 23, 2011 at 0:41
-
It's FreeBSD 8. How to reproduce: put
readarray
in a function and call the function a few times.Eugene Yarmash– Eugene Yarmash2011年03月23日 08:16:50 +00:00Commented Mar 23, 2011 at 8:16 -
Nice one, @sehe. +1admirabilis– admirabilis2013年02月19日 21:58:37 +00:00Commented Feb 19, 2013 at 21:58
The common patterns to solve this issue have been given in the other answers.
However, I'd like to add my approach, although I am not sure how efficient it is. But it is (at least for me) quite understandable, does not alter the original variable (all solutions which use read
must have the variable in question with a trailing newline and therefore add it, which alters the variable), does not create subshells (which all pipe-based solutions do), does not use here-strings (which have their own issues), and does not use process substitution (nothing against it, but a bit hard to understand sometimes).
Actually, I don't understand why bash
's integrated REs are used so rarely. Perhaps they are not portable, but since the OP has used the bash
tag, that won't stop me :-)
#!/bin/bash
function ProcessLine() {
printf '%s' "1ドル"
}
function ProcessText1() {
local Text=1ドル
local Pattern=$'^([^\n]*\n)(.*)$'
while [[ "$Text" =~ $Pattern ]]; do
ProcessLine "${BASH_REMATCH[1]}"
Text="${BASH_REMATCH[2]}"
done
ProcessLine "$Text"
}
function ProcessText2() {
local Text=1ドル
local Pattern=$'^([^\n]*\n)(.*)$'
while [[ "$Text" =~ $Pattern ]]; do
ProcessLine "${BASH_REMATCH[1]}"
Text="${BASH_REMATCH[2]}"
done
}
function ProcessText3() {
local Text=1ドル
local Pattern=$'^([^\n]*\n?)(.*)$'
while [[ ("$Text" != '') &&
("$Text" =~ $Pattern) ]]; do
ProcessLine "${BASH_REMATCH[1]}"
Text="${BASH_REMATCH[2]}"
done
}
MyVar1=$'a1\nb1\nc1\n'
MyVar2=$'a2\n\nb2\nc2'
MyVar3=$'a3\nb3\nc3'
ProcessText1 "$MyVar1"
ProcessText1 "$MyVar2"
ProcessText1 "$MyVar3"
Output:
root@cerberus:~/scripts# ./test4
a1
b1
c1
a2
b2
c2a3
b3
c3root@cerberus:~/scripts#
A few notes:
The behavior depends on what variant of ProcessText
you use. In the example above, I have used ProcessText1
.
Note that
ProcessText1
keeps newline characters at the end of linesProcessText1
processes the last line of the variable (which contains the textc3
) although that line does not contain a trailing newline character. Because of the missing trailing newline, the command prompt after the script execution is appended to the last line of the variable without being separated from the output.ProcessText1
always considers the part between the last newline in the variable and the end of the variable as a line, even if it is empty; of course, that line, whether empty or not, does not have a trailing newline character. That is, even if the last character in the variable is a newline,ProcessText1
will treat the empty part (null string) between that last newline and the end of the variable as a (yet empty) line and will pass it to line processing. You can easily prevent this behavior by wrapping the second call toProcessLine
into an appropriate check-if-empty condition; however, I think it is more logical to leave it as-is.
ProcessText1
needs to call ProcessLine
at two places, which might be uncomfortable if you would like to place a block of code there which directly processes the line, instead of calling a function which processes the line; you would have to repeat the code which is error-prone.
In contrast, ProcessText3
processes the line or calls the respective function only at one place, making replacing the function call by a code block a no-brainer. This comes at the cost of two while
conditions instead of one. Apart from the implementation differences, ProcessText3
behaves exactly the same as ProcessText1
, except that it does not consider the part between the last newline character in the variable and the end of the variable as line if that part is empty. That is, ProcessText3
will not go into line processing after the last newline character of the variable if that newline character is the last character in the variable.
ProcessText2
works like ProcessText1
, except that lines must have a trailing newline character. That is, the part between the last newline character in the variable and the end of the variable is not considered to be a line and is silently thrown away. Consequently, if the variable does not contain any newline character, no line processing happens at all.
I like that approach more than the other solutions shown above, but probably I have missed something (not being very experienced in bash
programming, and not being interested in other shells very much).
You can use <<< to simply read from the variable containing the newline-separated data:
while read -r line
do
echo "A line of input: $line"
done <<<"$lines"
-
2Welcome to Unix&Linux! This essentially duplicates an answer from four years ago. Please don’t post an answer unless you have something new to contribute.G-Man Says 'Reinstate Monica'– G-Man Says 'Reinstate Monica'2015年05月04日 06:30:14 +00:00Commented May 4, 2015 at 6:30