I need to read some configuration data into environment variables in a bash script.
The "obvious" (but incorrect) pattern is:
egrep "pattern" config-file.cfg | read VAR1 VAR2 VAR3 etc...
This fails because the read is run in a subshell and therefore cannot set variables in the invoking shell. So I came up with this as an alternative
coproc egrep "pattern" config-file.cfg
read -u ${COPROC[0]} VAR1 VAR2 VAR3 etc...
which works fine.
To test what happens if the coprocess returns more than one line, I tried this:
coproc cat config-file.cfg
read -u ${COPROC[0]} VAR1 VAR2 VAR3 etc...
where config-file.cfg contains three lines.
$ cat config-file.cfg
LINE1 A1 B1 C1
LINE2 A2 B2 C2
LINE3 A3 B3 C3
I expected this to process the first line in the file, followed by some kind of "broken pipe" error message. While it did process the first line, there was no error message and no coprocess was left running.
So I then tried the following in a script:
$ cat test.sh
coproc cat config-file.cfg
read -u ${COPROC[0]} VAR1 VAR2 VAR3 VAR4
echo $VAR1 $VAR2 $VAR3 $VAR4
wait
echo $?
Running it:
$ bash -x test.sh
+ read -u 63 VAR1 VAR2 VAR3 VAR4
+ cat config-file.cfg
LINE1 A1 B1 C1
+ wait
+ echo 0
0
Where did the remaining two lines go? I would have expected either "broken pipe", or the wait to hang since there was nothing to read the remaining lines, but as you can see the return code was zero.
As per comments above, you can use process substitution to achieve just that. This way, read is not run in a subshell and the captured vars will be available within the current shell.
read VAR1 VAR2 VAR3 < <(egrep "pattern" config-file.cfg)
"If the <(list) form is used, the file passed as an argument should be read to obtain the output of list" -- what "file passed as an agrument" are they talking about?
That is rather cryptic to me too. The chapter on process substitution in Advanced Bash-scripting Guide has a more comprehensive explanation.
The way I see it, when the <(cmd) syntax is used, the ouput of cmd is made available via a named pipe (or temp file) and the syntax is replaced by the filename of the pipe/file. So for the example above, it would end up being equivalent to:
read VAR1 VAR2 VAR3 < /dev/fd/63
where /dev/fd/63 is the named pipe connected to the stdout of cmd.
If I understand correctly your question (and I hope I'm not stating the obvious),
read reads one line at a time, as in:
$ read a b c < config-file.cfg && echo $?
0
or:
$ printf '%s\n%s\n' one two | { read; echo "$REPLY";}
one
$ echo ${PIPESTATUS[#]}
0 0
To read all the input you'll need a loop:
$ coproc cat config-file.cfg
[1] 3460
$ while read -u ${COPROC[0]} VAR1 VAR2 VAR3; do echo $VAR1 $VAR2 $VAR3; done
LINE1 A1 B1 C1
LINE2 A2 B2 C2
LINE3 A3 B3 C3
[1]+ Done coproc COPROC cat config-file.cfg
Just to add that this is explained in the FAQ.
What happens is, as soon as the subshell finishes, the parent shell cleans up and closes the FDs. You're lucky you even got to read the first line!
Try this in an interactive shell:
$ coproc ECHO { echo foo; echo bar; }
[2] 16472
[2]+ Done coproc ECHO { echo foo; echo bar; }
$ read -u ${ECHO[0]}; echo $REPLY
bash: read: -u: option requires an argument
read: usage: read [-ers] [-a array] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]
It even mops up the environment variable.
Now try this:
$ coproc ECHO { echo foo; echo bar; sleep 30; }
[2] 16485
$ read -u ${ECHO[0]}; echo $REPLY
foo
$ read -u ${ECHO[0]}; echo $REPLY
bar
$ read -u ${ECHO[0]}; echo $REPLY # blocks until the 30 seconds are up
[2]+ Done coproc ECHO { echo foo; echo bar; sleep 30; }
As for solving the problem behind the question: Yes, redirection and process substitution is the better choice for the particular example given.
Related
Let's consider these two simple pieace of bash code.
Code A (without eval mgrep):
while read line; do
echo $line
done <<EOF
1234567
2345678
3456789
EOF
Code B (with eval mgrep):
while read line; do
echo $line
eval "mgrep $line"
done <<EOF
1234567
2345678
3456789
EOF
After running Code A, I get the expected output, i.e.:
1234567
2345678
3456789
While running Code B will produce something like:
1234567
[...mgrep results here...]
So it means, the while loop is only executed for the first input line and not for the remaining ones. What am I doing wrong?
In the following program, if I set the variable $foo to the value 1 inside the first if statement, it works in the sense that its value is remembered after the if statement. However, when I set the same variable to the value 2 inside an if which is inside a while statement, it's forgotten after the while loop. It's behaving like I'm using some sort of copy of the variable $foo inside the while loop and I am modifying only that particular copy. Here's a complete test program:
#!/bin/bash
set -e
set -u
foo=0
bar="hello"
if [[ "$bar" == "hello" ]]
then
foo=1
echo "Setting \$foo to 1: $foo"
fi
echo "Variable \$foo after if statement: $foo"
lines="first line\nsecond line\nthird line"
echo -e $lines | while read line
do
if [[ "$line" == "second line" ]]
then
foo=2
echo "Variable \$foo updated to $foo inside if inside while loop"
fi
echo "Value of \$foo in while loop body: $foo"
done
echo "Variable \$foo after while loop: $foo"
# Output:
# $ ./testbash.sh
# Setting $foo to 1: 1
# Variable $foo after if statement: 1
# Value of $foo in while loop body: 1
# Variable $foo updated to 2 inside if inside while loop
# Value of $foo in while loop body: 2
# Value of $foo in while loop body: 2
# Variable $foo after while loop: 1
# bash --version
# GNU bash, version 4.1.10(4)-release (i686-pc-cygwin)
echo -e $lines | while read line
...
done
The while loop is executed in a subshell. So any changes you do to the variable will not be available once the subshell exits.
Instead you can use a here string to re-write the while loop to be in the main shell process; only echo -e $lines will run in a subshell:
while read line
do
if [[ "$line" == "second line" ]]
then
foo=2
echo "Variable \$foo updated to $foo inside if inside while loop"
fi
echo "Value of \$foo in while loop body: $foo"
done <<< "$(echo -e "$lines")"
You can get rid of the rather ugly echo in the here-string above by expanding the backslash sequences immediately when assigning lines. The $'...' form of quoting can be used there:
lines=$'first line\nsecond line\nthird line'
while read line; do
...
done <<< "$lines"
UPDATED#2
Explanation is in Blue Moons's answer.
Alternative solutions:
Eliminate echo
while read line; do
...
done <<EOT
first line
second line
third line
EOT
Add the echo inside the here-is-the-document
while read line; do
...
done <<EOT
$(echo -e $lines)
EOT
Run echo in background:
coproc echo -e $lines
while read -u ${COPROC[0]} line; do
...
done
Redirect to a file handle explicitly (Mind the space in < <!):
exec 3< <(echo -e $lines)
while read -u 3 line; do
...
done
Or just redirect to the stdin:
while read line; do
...
done < <(echo -e $lines)
And one for chepner (eliminating echo):
arr=("first line" "second line" "third line");
for((i=0;i<${#arr[*]};++i)) { line=${arr[i]};
...
}
Variable $lines can be converted to an array without starting a new sub-shell. The characters \ and n has to be converted to some character (e.g. a real new line character) and use the IFS (Internal Field Separator) variable to split the string into array elements. This can be done like:
lines="first line\nsecond line\nthird line"
echo "$lines"
OIFS="$IFS"
IFS=$'\n' arr=(${lines//\\n/$'\n'}) # Conversion
IFS="$OIFS"
echo "${arr[#]}", Length: ${#arr[*]}
set|grep ^arr
Result is
first line\nsecond line\nthird line
first line second line third line, Length: 3
arr=([0]="first line" [1]="second line" [2]="third line")
You are asking this bash FAQ. The answer also describes the general case of variables set in subshells created by pipes:
E4) If I pipe the output of a command into read variable, why
doesn't the output show up in $variable when the read command finishes?
This has to do with the parent-child relationship between Unix
processes. It affects all commands run in pipelines, not just
simple calls to read. For example, piping a command's output
into a while loop that repeatedly calls read will result in
the same behavior.
Each element of a pipeline, even a builtin or shell function,
runs in a separate process, a child of the shell running the
pipeline. A subprocess cannot affect its parent's environment.
When the read command sets the variable to the input, that
variable is set only in the subshell, not the parent shell. When
the subshell exits, the value of the variable is lost.
Many pipelines that end with read variable can be converted
into command substitutions, which will capture the output of
a specified command. The output can then be assigned to a
variable:
grep ^gnu /usr/lib/news/active | wc -l | read ngroup
can be converted into
ngroup=$(grep ^gnu /usr/lib/news/active | wc -l)
This does not, unfortunately, work to split the text among
multiple variables, as read does when given multiple variable
arguments. If you need to do this, you can either use the
command substitution above to read the output into a variable
and chop up the variable using the bash pattern removal
expansion operators or use some variant of the following
approach.
Say /usr/local/bin/ipaddr is the following shell script:
#! /bin/sh
host `hostname` | awk '/address/ {print $NF}'
Instead of using
/usr/local/bin/ipaddr | read A B C D
to break the local machine's IP address into separate octets, use
OIFS="$IFS"
IFS=.
set -- $(/usr/local/bin/ipaddr)
IFS="$OIFS"
A="$1" B="$2" C="$3" D="$4"
Beware, however, that this will change the shell's positional
parameters. If you need them, you should save them before doing
this.
This is the general approach -- in most cases you will not need to
set $IFS to a different value.
Some other user-supplied alternatives include:
read A B C D << HERE
$(IFS=.; echo $(/usr/local/bin/ipaddr))
HERE
and, where process substitution is available,
read A B C D < <(IFS=.; echo $(/usr/local/bin/ipaddr))
Hmmm... I would almost swear that this worked for the original Bourne shell, but don't have access to a running copy just now to check.
There is, however, a very trivial workaround to the problem.
Change the first line of the script from:
#!/bin/bash
to
#!/bin/ksh
Et voila! A read at the end of a pipeline works just fine, assuming you have the Korn shell installed.
This is an interesting question and touches on a very basic concept in Bourne shell and subshell. Here I provide a solution that is different from the previous solutions by doing some kind of filtering. I will give an example that may be useful in real life. This is a fragment for checking that downloaded files conform to a known checksum. The checksum file look like the following (Showing just 3 lines):
49174 36326 dna_align_feature.txt.gz
54757 1 dna.txt.gz
55409 9971 exon_transcript.txt.gz
The shell script:
#!/bin/sh
.....
failcnt=0 # this variable is only valid in the parent shell
#variable xx captures all the outputs from the while loop
xx=$(cat ${checkfile} | while read -r line; do
num1=$(echo $line | awk '{print $1}')
num2=$(echo $line | awk '{print $2}')
fname=$(echo $line | awk '{print $3}')
if [ -f "$fname" ]; then
res=$(sum $fname)
filegood=$(sum $fname | awk -v na=$num1 -v nb=$num2 -v fn=$fname '{ if (na == $1 && nb == $2) { print "TRUE"; } else { print "FALSE"; }}')
if [ "$filegood" = "FALSE" ]; then
failcnt=$(expr $failcnt + 1) # only in subshell
echo "$fname BAD $failcnt"
fi
fi
done | tail -1) # I am only interested in the final result
# you can capture a whole bunch of texts and do further filtering
failcnt=${xx#* BAD } # I am only interested in the number
# this variable is in the parent shell
echo failcnt $failcnt
if [ $failcnt -gt 0 ]; then
echo $failcnt files failed
else
echo download successful
fi
The parent and subshell communicate through the echo command. You can pick some easy to parse text for the parent shell. This method does not break your normal way of thinking, just that you have to do some post processing. You can use grep, sed, awk, and more for doing so.
I use stderr to store within a loop, and read from it outside.
Here var i is initially set and read inside the loop as 1.
# reading lines of content from 2 files concatenated
# inside loop: write value of var i to stderr (before iteration)
# outside: read var i from stderr, has last iterative value
f=/tmp/file1
g=/tmp/file2
i=1
cat $f $g | \
while read -r s;
do
echo $s > /dev/null; # some work
echo $i > 2
let i++
done;
read -r i < 2
echo $i
Or use the heredoc method to reduce the amount of code in a subshell.
Note the iterative i value can be read outside the while loop.
i=1
while read -r s;
do
echo $s > /dev/null
let i++
done <<EOT
$(cat $f $g)
EOT
let i--
echo $i
How about a very simple method
+call your while loop in a function
- set your value inside (nonsense, but shows the example)
- return your value inside
+capture your value outside
+set outside
+display outside
#!/bin/bash
# set -e
# set -u
# No idea why you need this, not using here
foo=0
bar="hello"
if [[ "$bar" == "hello" ]]
then
foo=1
echo "Setting \$foo to $foo"
fi
echo "Variable \$foo after if statement: $foo"
lines="first line\nsecond line\nthird line"
function my_while_loop
{
echo -e $lines | while read line
do
if [[ "$line" == "second line" ]]
then
foo=2; return 2;
echo "Variable \$foo updated to $foo inside if inside while loop"
fi
echo -e $lines | while read line
do
if [[ "$line" == "second line" ]]
then
foo=2;
echo "Variable \$foo updated to $foo inside if inside while loop"
return 2;
fi
# Code below won't be executed since we returned from function in 'if' statement
# We aready reported the $foo var beint set to 2 anyway
echo "Value of \$foo in while loop body: $foo"
done
}
my_while_loop; foo="$?"
echo "Variable \$foo after while loop: $foo"
Output:
Setting $foo 1
Variable $foo after if statement: 1
Value of $foo in while loop body: 1
Variable $foo after while loop: 2
bash --version
GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
Copyright (C) 2007 Free Software Foundation, Inc.
Though this is an old question and asked several times, here's what I'm doing after hours fidgeting with here strings, and the only option that worked for me is to store the value in a file during while loop sub-shells and then retrieve it. Simple.
Use echo statement to store and cat statement to retrieve. And the bash user must chown the directory or have read-write chmod access.
#write to file
echo "1" > foo.txt
while condition; do
if (condition); then
#write again to file
echo "2" > foo.txt
fi
done
#read from file
echo "Value of \$foo in while loop body: $(cat foo.txt)"
In bash (works on v4 at least), the following commands allow to assign multiple variables from a string :
IFS=',' read a b <<< "s1,s2"
IFS=',' read a b < <(echo "s1,s2") # an equivalent
After one of these commands :
$ echo $a
s1
$ echo $b
s2
But provided commands are not POSIX-compliant; if run in sh (dash) :
sh: 1: Syntax error: redirection unexpected
What is a POSIX-compliant alternative to those commands? I tried :
IFS=',' echo "s1,s2" | read a b
The command succeeds (return code 0), but echo $a and echo $b then prints nothing.
a and b are set, but due to the pipline, the read command runs in a subshell. When the subshell exits, the variables disappear.
You can read from a here-doc
IFS=, read a b <<END
s1,s2
END
To replace any arbitrary pipeline (or process substitution), you can capture the pipeline's output and put that variable in a heredoc:
output=$( seq 10 20 | paste -sd, )
IFS=, read a b <<END
$output
END
echo "$a"
echo "$b"
outputs
10
11,12,13,14,15,16,17,18,19,20
In addition to glenn jackman's answer, there is also the option of an explicit named pipe (which is, essentially, what a process substitution replaces):
mkfifo p
echo "s1,s2" > p &
IFS=, read a b < p
A very simple example of the "just run once" version of my Script:
./myscript.sh var1 "var2 with spaces" var3
#!/bin/bash
echo $1 #output: var1
echo $2 #output: var2 with spaces
echo $3 #output: var3
Working as intended!
Now I try to just start the script and enter the vars in a loop, because later I want to copy multiple datasets at once to the shell.
./myscript.sh
#!/bin/bash
while true; do
read var1 var2 var3
#input: var1 "var2 with spaces" var3
echo $var1 #output: var1
echo $var2 #output: "var2
echo $var3 #output: with spaces" var3
done
It seems read splits the input at the spaces, putting all thats left in the last var, right? Is there any better possibility to add vars in a loop? Or how do I get read to behave like I added the vars just behind the script?
And what is the English word for that kind of loop to execute one script in a loop while copying different vars to the shell? Can't google for samples if I don't know what it is called...
This reads STDIN and parses those lines as arguments with shell quoting:
# Clean input of potentially dangerous characters. If your valid input
# is restrictive, this could instead strip everything that is invalid
# s/[^a-z0-9" ]//gi
sed -ue 's/[][(){}`;$]//g' | \
while read input; do
if [ "x$input" = "x" ]; then exit; fi
eval "set -- $input"
# check argument count
if [ $(( $# % 3 )) -ne 0 ]; then
echo "Please enter 3 values at a time"
continue;
fi
echo $1
echo $2
echo $3
done
set -- $input does all of the magic. See the Bash manual page for set.
--
If no arguments follow this option, then the positional parameters are
unset. Otherwise, the positional parameters are set to the arguments,
even if some of them begin with a ‘-’.
I have a file that contains information in two columns:
box1 a1
box2 a2
I'm trying to read this file line by line into read and have each line items be put into a variable.
On the first pass, $a would contain box1 and $b would contain a1.
On the second pass, $a would contain box2 and $b would contain a2, etc.
An example of the code that I am using to try to achieve is this:
for i in text.txt; do
while read line; do
echo $line | read a b;
done < text.txt;
echo $a $b;
done
This gives me the following results:
box1 a1 box2 a2
When I expected the following results:
box1 a1
box2 a1
How can I fix this?
Piping into a read command causes the variables to be set in a subshell, which makes them inaccessible (indeed, they are gone) to the rest of your code. In this case, though, you don't even need the for loop or the second read command:
while read -r a b; do
echo "$a" "$b"
done < text.txt
To store the variables into the current shell:
~$ {
read -r a
read -r b
read -r c
} < <(printf '%s\n' one two three)
$ echo "a=$a b=$b c=$c"
a=one b=two c=three
Another approach
echo -e "one\ntwo\nthree" | {
read -r a;
read -r b;
read -r c;
echo "output of ($a, $b, $c)"
}