The common patterns to solve this issue have been given in the other answers.
However, I'd like to add my approach, which is probably quite inefficient (in terms of CPU time), but is (at least for me) more understandable, does not alter the original variable (all solutions which use read -r 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 lines
ProcessText1 processes the last line of the variable (which contains the text c3) 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 to ProcessLine 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).