POSIX alternative to multiple variables assignments with read - bash

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

Related

Can't add a new element to an array in bash [duplicate]

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)"

Bash: What is the scope of the process substitution?

As far as I know, process substitution <(...) / >(...) creates the fd
and stores the output of commands in parentheses into the generated fd.
Therefore, these two commands are equivalent
$ ls -al
$ cat <(ls -al)
Here, my question is, how long the generated file descriptors remain?
I've read this article, but seems my understanding is wrong.
If a process substitution is expanded as an argument to a function, expanded to an environment variable during calling of a function, or expanded to any assignment within a function, the process substitution will be "held open" for use by any command within the function or its callees, until the function in which it was set returns. If the same variable is set again within a callee, unless the new variable is local, the previous process substitution is closed and will be unavailable to the caller when the callee returns.
In essence, process substitutions expanded to variables within functions remain open until the function in which the process substitution occured returns - even when assigned to locals that were set by a function's caller. Dynamic scope doesn't protect them from closing.
My best guess, after reading it, was that the created fd will not be closed until it is used.
From this, I wrote a very dumb code like below
#!/bin/bash
test_subs () {
echo "Inside a function"
FD2=<(ls -al)
cat $FD1
cat $FD2
}
FD1=<(ls -al)
test_subs
Result======================================
Inside a function
cat: /dev/fd/63: No such file or directory
cat: /dev/fd/63: No such file or directory
It seems that the newly opened fd close just right after one line of command run.
How long does the generated fd maintained, and then what is the scope of process substitution?
TL;DR
There seems to be no documentation and therefore no guarantee on the scope of process substitution <(...). I assume the only safe way to keep process substitutions in scope is to define them directly as arguments cmd <(...), on-the-fly exported variables VAR=<(...) cmd, or redirection cmd < <(...). Process substitution defined in this manner remains in scope while cmd is running.
The Long Story
I interpreted the quoted article from Bash Hackers Wiki like you did. Likewise, I came to the same conclusion that declaring variables for process substitution inside a function does not guarantee that they stay open. On some systems there are many other ways to keep them open, especially with command groups like subshells (...) and contextes {...}. However, these tricks still fail on some systems.
I could not find any documentation of this apart from the wrong comments in the linked Bash Hackers Wiki. Even bash's manual does not talk about the scope of process substitution. So we are stuck with experimenting (or reading bash's source code, which I did not).
The following script creates some scenarios to check when process substitution <(...) remains in scope. Note that there are very subtle differences. For instance: It makes a difference whether you write two commands in the same line using ; or each command in its own line. Of course this list is not complete. Feel free to extend it.
#! /usr/bin/env bash
echo 'define, use'
a=<(echo ok);
cat "$a"; unset a
echo 'define and use in same line'
a=<(echo ok); cat "$a"; unset a
echo 'define and use in subshell'
(a=<(echo ok);
cat "$a")
echo 'define and use in context'
{ a=<(echo ok)
cat "$a"; }; unset a
echo 'define and use in && chain'
a=<(echo ok) &&
cat "$a"; unset a
echo 'define in context and use in || chain'
{ a=<(echo ok); false; } || cat "$a"; unset a
echo 'define and use in for loop body'
for i in 1; do
a=<(echo ok)
cat "$a"
done
echo 'define and use in while loop head'
while
a=<(echo ok)
cat "$a"
false
do true; done; unset a
echo 'define and use in same case'
case x in
x)
a=<(echo ok)
cat "$a"
;;
esac; unset a
echo 'define in case, use in fall-through'
case x in
x)
a=<(echo ok)
;&
y)
cat "$a"
;;
esac; unset a
echo 'define and use inside function in same line'
f() { a=<(echo ok); cat "$a"; }; f; unset a f
echo 'define local and use inside function in same line'
f() { local a=<(echo ok); cat "$a"; }; f; unset a f
echo 'define, use as function argument'
f() { cat "$1"; }; a=<(echo ok)
f "$a"; unset a f
echo 'define, use as function argument in same line'
f() { cat "$1"; }; a=<(echo ok); f "$a"; unset a f
echo 'on-the-fly export, use in different shell'
a=<(echo ok) dash -c 'cat "$a"'
echo 'export, use in different shell'
export a=<(echo ok)
dash -c 'cat "$a"'; unset a
echo 'define in command substitution, use in parent in same line'
a=$(echo <(echo ok)); cat "$a"; unset a
echo 'read from here-string, use in parent in same line'
read a <<< <(echo ok); cat "$a"; unset a
echo 'read from process substitution, use in parent in same line'
read a < <(echo <(echo ok)); cat $a; unset a
echo 'read from pipe and use in same line'
shopt -s lastpipe; # TODO add `set +m` when running interactively
echo <(echo ok) | read -r a; cat "$a"
shopt -u lastpipe; unset a
echo 'define, unrelated read from file, use in same line'
a=<(echo ok); read < /etc/passwd; cat "$a"; unset a
echo 'define, unrelated read from process substitution, use in same line'
a=<(echo ok); read < <(echo unused); cat "$a"; unset a
echo 'define, unrelated cat from process substitution, use in same line'
a=<(echo ok); cat <(echo unused) > /dev/null; cat "$a"; unset a
echo 'define, unrelated read ... in subshell, use in same line'
a=<(echo ok); (read < <(echo unused)); cat "$a"; unset a b
echo 'define, unrelated read ... in command substitution, use in same line'
a=<(echo ok); b=$(read < <(echo unused)); cat "$a"; unset a b
# output can be prettified using
# ./script 2> /dev/null |
# awk 'p!="ok"{if($0=="ok")print "yes " p;else print "no " p}{p=$0}'
These are the (prettyfied) outputs for my systems
In scope on bash 5.0.17 on Arch Linux (kernel 5.6.15-arch1-1)
| In scope on bash 5.0.3 on Debian 10 Buster inside WSL 1
| | In scope on bash 4.3.48 on Ubuntu 16.04.6 LTS
↓ ↓ ↓
no no no define, use
yes yes no define and use in same line
yes yes no define and use in subshell
yes yes no define and use in context
yes yes no define and use in && chain
yes yes no define in context and use in || chain
yes yes no define and use in for loop body
yes yes no define and use in while loop head
yes yes no define and use in same case
yes yes no define in case, use in fall-through
no no no define and use inside function in same line
no no no define local and use inside function in same line
no no no define, use as function argument
yes yes no define, use as function argument in same line
yes yes yes on-the-fly export, use in different shell
no no no export, use in different shell
no no no define in command substitution, use in parent in same line
no no no read from here-string, use in parent in same line
no no no read from process substitution, use in parent in same line
no no no read from pipe and use in same line
yes yes no define, unrelated read from file, use in same line
yes no no define, unrelated read from process substitution, use in same line
yes yes no define, unrelated cat from process substitution, use in same line
no no no define, unrelated read ... in subshell, use in same line
yes yes no define, unrelated read ... in command substitution, use in same line
For my interpretation of these results see the TL;DR at the beginning of this answer.
As of bash-5.1 I think the only safe way is to never store process subsitution in a variable, not even in the same line. The script featured in the other answer outputs "no" for every line in bash-5.1, and only immediate subsittution works, e.g. paste <(echo 1) <(echo 2).

What is the POSIX shell equivalent of bash <<<

I have a variable that looks sort of like this:
msg="newton apple tree"
I want to assign each of these words into separate variables. This is easy to do in bash:
read a b c <<< $msg
Is there a compact, readable way to do this in POSIX shell?
A here string is just syntactic sugar for a single-line here document:
$ msg="foo * bar"
$ read a b c <<EOF
> $msg
> EOF
$ echo "$a"
foo
$ echo "$b"
*
$ echo "$c"
bar
To write idiomatic scripts, you can't just look at each individual syntax element and try to find a POSIX equivalent. That's like translating text by replacing each individual word with its entry in the dictionary.
The POSIX way of splitting a string known to have three words into three arguments, similar but not identical to read is:
var="newton apple tree"
set -f
set -- $var
set +f
a=$1 b=$2 c=$3
echo "$a was hit by an $b under a $c"
It's not pretty, but as a general-purpose solution, you can work around this with a named pipe.
From BashFAQ #24:
mkfifo mypipe
printf '%s\n' "$msg" >mypipe &
read -r a b c <mypipe
printf is more reliable / better-specified than echo; echo behavior varies between implementations if you have a message containing only, say, -E or -n.
That said, for what you're doing here, you could just use parameter expansion:
a=${msg%% *}; msg=${msg#* }
b=${msg%% *}; msg=${msg#* }
c=${msg%% *}; msg=${msg#* }

Set a parent shell's variable from a subshell

How do I set a variable in the parent shell, from a subshell?
a=3
(a=4)
echo $a
The whole point of a subshell is that it doesn't affect the calling session. In bash a subshell is a child process, other shells differ but even then a variable setting in a subshell does not affect the caller. By definition.
Do you need a subshell? If you just need a group then use braces:
a=3
{ a=4;}
echo $a
gives 4 (be careful of the spaces in that one). Alternatively, write the variable value to stdout and capture it in the caller:
a=3
a=$(a=4;echo $a)
echo $a
avoid using back-ticks ``, they are deprecated and can be difficult to read.
There is the gdb-bash-variable hack:
gdb --batch-silent -ex "attach $$" -ex 'set bind_variable("a", "4", 0)';
although that always sets a variable in the global scope, not just the parent scope
You don't. The subshell doesn't have access to its parent's environment. (At least within the abstraction that Bash provides. You could potentially try to use gdb, or smash the stack, or whatnot, to gain such access clandestinely. I wouldn't recommend that, though.)
One alternative is for the subshell to write assignment statements to a temporary file for its parent to read:
a=3
(echo 'a=4' > tmp)
. tmp
rm tmp
echo "$a"
If the problem is related to a while loop, one way to fix this is by using Process Substitution:
var=0
while read i;
do
# perform computations on $i
((var++))
done < <(find . -type f -name "*.bin" -maxdepth 1)
as shown here: https://stackoverflow.com/a/13727116/2547445
To change variables in a script called from a parent script, you can call the script preceded with a "."
(EDIT - for explanation)
In most shells "." is an alias for "source". the source command just inserts the text of another file at that position in the executing script. In the context of this question this answer avoids a sub-shell
a=3
echo $a
. ./calledScript.sh
echo $a
in calledScript.sh
a=4
Expected output
3
4
By reading the answer from #ruakh (thank you) with a temporary file approach and the comments asking for a file descriptors solution, I got the following idea:
a=3
. <(echo a=4; echo b=5)
echo $a
echo $b
It allows returning different variables at once (which could be an issue in the subshell variant of the accepted answer).
No iteration is needed,
No temporary file to take care of.
Close to the syntax proposed by the OP.
Result:
4
5
With xtrace enabled is visible that we are sourcing from the file descriptor created for the output of the subshell:
+ a=3
+ . /dev/fd/63 # <-- the file descriptor ;)
++ echo a=4
++ echo b=5
++ a=4
++ b=5
+ echo 4
4
+ echo 5
5
You can output the value in the subshell and assign the subshell output to a variable in the caller script:
# subshell.sh
echo Value
# caller
myvar=$(subshell.sh)
If the subshell has more to output you can separate the variable value and other messages by redirecting them into different output streams:
# subshell.sh
echo "Writing value" 1>&2
echo Value
# caller
myvar=$(subshell.sh 2>/dev/null) # or to somewhere else
echo $myvar
Alternatively, you can output variable assignments in the subshell, evaluate them in the caller script and avoid using files to exchange information:
# subshell.sh
echo "a=4"
# caller
# export $(subshell.sh) would be more secure, since export accepts name=value only.
eval $(subshell.sh)
echo $a
The last way I can think of is to use exit codes but this covers the integer values exchange only (and in a limited range) and breaks the convention for interpreting exit codes (0 for success non-0 for everything else).
Instead of accessing the variable from the parent shell, change the order of the commands and use the process substitution:
a=3
echo 5 | (read a)
echo $a
prints 3
a=3
read a < <(echo 5)
echo $a
prints 5
Another example:
let i=0
seq $RANDOM | while read r
do
let i=r
done
echo $i
vs
let i=0
while read r
do
let i=r
done < <(seq $RANDOM)
echo $i
Alternatively, when job control is inactive (e.g. in scripts) you can use the lastpipe shell option to achieve the same result without changing the order of the commands:
#!/bin/bash
shopt -s lastpipe
let i=0
seq $RANDOM | while read r
do
let i=r
done
echo $i
Unless you can apply all io to pipes and use file handles, basic variable updating is impossible within $(command) and any other sub-process.
Regular files, however, are bash's global variables for normal sequential processing. Note: Due to race conditions, this simple approach is not good for parallel processing.
Create an set/get/default function like this:
globalVariable() { # NEW-VALUE
# set/get/default globalVariable
if [ 0 = "$#" ]; then
# new value not given -- echo the value
[ -e "$aRam/globalVariable" ] \
&& cat "$aRam/globalVariable" \
|| printf "default-value-here"
else
# new value given -- set the value
printf "%s" "$1" > "$aRam/globalVariable"
fi
}
"$aRam" is the directory where values are stored. I like it to be a ram disk for speed and volatility:
aRam="$(mktemp -td $(basename "$0").XXX)" # temporary directory
mount -t tmpfs ramdisk "$aRam" # mount the ram disk there
trap "umount "$aRam" && rm -rf "$aRam"" EXIT # auto-eject
To read the value:
v="$(globalVariable)" # or part of any command
To set the value:
globalVariable newValue # newValue will be written to file
To unset the value:
rm -f "$aRam/globalVariable"
The only real reason for the access function is to apply a default value because cat will error given a non-existent file. It is also useful to apply other get/set logic. Otherwise, it would not be needed at all.
An ugly read method avoiding cat's non-existent file error:
v="$(cat "$aRam/globalVariable 2>/dev/null")"
A cool feature of this mess is that you can open another terminal and examine the contents of the files while the program is running.
While it's harder to get multiple variables out of a subshell, you can set multiple variables inside a function without using globals.
You can pass the name of a variable into a function that uses local -n to turn it into a special variable called a nameref:
myfunc() {
local -n OUT=$1
local -n SIDEEFFECT=$2
OUT='foo'
SIDEEFFECT='bar'
}
myfunc A B
echo $A
> foo
echo $B
> bar
This is the technique I ended up using instead of getting subshell FOO=$(myfunc) working setting multiple variables.
A very simple and practical method that allows multiple variables is as follows, eventually may add parameters to the call:
function ComplexReturn(){
# do your processing...
a=123
b=456
echo -n "AAA=${a}; BBB=${b};"
}
# ... this can be internal function or any subshell command
eval $(ComplexReturn)
echo $AAA $BBB

curious problem: for `` vs. while-read loops and visibility of variables in shell scripts

I have a problem updating a value of a variable in a shell script from inside of a while loop. It can be simulated with the following piece of code:
printf "aaa\nbbb\n" | \
while read x ; do
y=$x
echo "INSIDE: $y"
done
echo "OUTSIDE: $y"
Output:
INSIDE: aaa
INSIDE: bbb
OUTSIDE:
Here printf command just display two lines, while-read loop read it line by line, updating certain variable, but as soon as control going out of the loop the value of the variable gets lost.
I guess the problem is related to the fact that 'pipe-while-read' statement causes shell to execute the body of the loop in a subprocess, which cannot update the shell variables in the main loop.
If I rewrite the first two lines of code as
for x in `printf "aaa\nbbb\n" ` ; do
Output:
INSIDE: aaa
INSIDE: bbb
OUTSIDE: bbb
It could be a workaround, but not for my case because in reality I have not 'aaa' and 'bbb' but more complex strings including whitespaces etc.
Any idea how to tackle the problem, namely: read a command output line by line in a loop and be able to update shell variables?
Thanks.
An excerpt from man bash:
Each command in a pipeline is executed as a separate process (i.e., in a subshell).
And Subshell cannot change the variable in Parent.
One of the possible Solution is:
IFS='\n'
while read x ; do
y=${x}
echo "INSIDE: ${y}"
done <<EOT
aaa
bbb
EOT
echo "OUTSIDE: ${y}"
Or if the input is a file:
IFS='\n'
while read x ; do
y=${x}
echo "INSIDE: ${y}"
done < /path/to/file
echo "OUTSIDE: ${y}"
This reads one line at a time, and doesn't have any issue with spaces.
You can get rid of the pipe-into-while by using process substitution instead:
while read x ; do
y=$x
echo "INSIDE: $y"
done < <(printf "aaa\nbbb\n")
echo "OUTSIDE: $y"
Alternatively, if your input is in a file, you can redirect it into while:
while read x ; do
y=$x
echo "INSIDE: $y"
done < file
echo "OUTSIDE: $y"

Resources