Why is "while read -r a" command unable to read string that is not newline terminated? - shell

Here is my shell script.
printf "foo" | (read -r a; echo $a)
printf "bar" | while read -r a
do
echo $a
done
Here is the output.
$ sh foo.sh
foo
My question is: Why is foo printed but bar not printed? When read -r a is able to read a string that is not newline-terminated why is it unable to do so when it used as a condition of a while loop?

Related

Read from a file and stdin in Bash

I would like to know if I can write a shell script that accepts two arguments simultaneously, one from a file and the another one from stdin. Could you give some example please?.
I trying
while read line
do
echo "$line"
done < "${1}" < "{/dev/stdin}"
But this does not work.
You can use cat - or cat /dev/stdin:
while read line; do
# your code
done < <(cat "$1" -)
or
while read line; do
# your code
done < <(cat "$1" /dev/stdin)
or, if you want to read from all files passed through command line as well as stdin, you could do this:
while read line; do
# your code
done < <(cat "$#" /dev/stdin)
See also:
How to read from a file or stdin in Bash?
This topic seems to be helpful here:
{ cat $1; cat; } | while read line
do
echo "$line"
done
Or just
cat $1
cat
if all you're doing is printing the content

How do you split a string from shell-redirect or `read`?

I'm trying to split key value pairs (around an = sign) which I then use to edit a config file, using bash. But I need an alternative to the <<< syntax for IFS.
The below works on my host system, but when i log in to my ubuntu virtual machine through ssh I have the wrong bash version. Whatever I try, <<< fails. (I am definitely calling the right version of bash at the top of the file, using #!/bin/bash (and I've tried #!/bin/sh etc too)).
I know I can use IFS as follows on my host mac os x system:
var="word=hello"
IFS='=' read -a array <<<"$var"
echo ${array[0]} ${array[1]]}
#alternative -for calling through e.g. sh file.sh param=value
for var in "$#"
do
IFS='=' read -a array <<<"$var"
echo ${array[0]} ${array[1]]}
done
#alternative
IFS='=' read -ra array <<< "a=b"
declare -p array
echo ${array[0]} ${array[1]}
But this doesn't work on my vm.
I also know that I can should be able to switch the <<< syntax through backticks, $() or echo "$var" | ... but I can't get it to work - as follows:
#Fails
IFS='=' read -ra myarray -d '' <"$var"
echo ${array[0]} ${array[1]]}
#Fails
echo "$var" | IFS='=' read -a array
echo ${array[0]} ${array[1]]}
#fails
echo "a=b" | IFS='=' read -a array
declare -p array
echo ${array[0]} ${array[1]}
Grateful for any pointers as I'm really new to bash.
Your first failed attempt is because < and <<< are different operators. < opens the named file.
The second fails because read only sets the value of array in the subshell started by the pipe; that shell exits after the completion of the pipe, and array disappears with it.
The third fails for the same reason as the second; the declare that follows doesn't make any difference.
Your attempts have been confounded because you have to use the variable in the same sub-shell as read.
$ echo 'foo=bar' | { IFS='=' read -a array; echo ${array[0]}; }
foo
And if you want your variable durable (ie, outside the sub-shell scope):
$ var=$(echo 'foo=bar' | { IFS='=' read -a array; echo ${array[0]}; })
$ echo $var
foo
Clearly, it isn't pretty.
Update: If -a is missing, that suggests you're out of the land of arrays. You can try parameter substitution:
str='foo=bar'
var=${str%=*}
val=${str#*=}
And if that doesn't work, fall back to good ole cut:
str='foo=bar'
var=$(echo $str | cut -f 1 -d =)
val=$(echo $str | cut -f 2 -d =)

Unix read command with option -d and IFS variable combination

Looking forward to understand the behavior of read -d when it comes along with IFS variable
$ cat f1
a:b:c
d:e:f
$ while IFS= read -d: ; do echo $REPLY; done < f1
a
b
c d
e
$ while IFS=: read; do echo $REPLY; done < f1
a:b:c
d:e:f
IFS is used when you're reading several variables with read:
$ echo foo:bar:baz | (IFS=: read FOO BAR BAZ; echo $FOO; echo $BAR; echo $BAZ)
foo
bar
baz
Whereas, the -d option specifies what your line separator for read is; read won't read beyond a single line:
$ echo foo:bar:baz%baz:qux:quux% | while IFS=: read -d% FOO BAR BAZ; do echo ---; echo $FOO; echo $BAR; echo $BAZ; done
---
foo
bar
baz
---
baz
qux
quux
IFS is inter field separator.
You say to shell which symbols is used to split fields.
It is used in two directions, not only when reading.
One special case that you use here is IFS=. You use IFS= here to correctly handle input starting with spaces.
You can compare:
echo " a" | IFS= read a
echo " a" | read a
That is important when you handle files and they can contain leading spaces in their names.
Please compare:
$ echo " a" | ( IFS= read a; echo .$a. )
. a.
$ echo " a" | ( read a; echo .$a. )
.a.
UPDATE. As you probably already noted,
this construction
$ echo a | read a
doesn't work. Because shell creates a subshell for the '''read''', and you can see the value of $a only inside it.
You can also use while, what is more often:
$ echo a | while read a; do echo $a; done

How to force the read command to correctly parse array arguments within quotation marks?

I have this script:
#!/usr/bin/env bash
read -p "Provide arguments: " -a arr <<< "foo \"bar baz\" buz"
for i in ${arr[#]}
do
echo $i
done
which incorrectly outputs:
foo
"bar
baz"
buz
How can I make it interpret user input so that parameters within quotation marks would make a single array element? Like this:
foo
bar baz
buz
EDIT:
To be clear: I don't want the user to input each element in separate line, so read-ing in loop is not what I'm looking for.
You're better off supplying user input using a different delimiter, e.g. ;.
OLDIFS=$IFS
IFS=$';'
read -p "Provide arguments: " -a var
for i in "${!var[#]}"
do
echo Argument $i: "${var[i]}"
done
IFS=$OLDIFS
Upon execution:
Provide arguments: foo;bar baz;buz
Argument 0: foo
Argument 1: bar baz
Argument 2: buz
Plus a modification to trim the variables:
echo Argument $i: $(echo "${var[i]}" | sed -e 's/^ *//g' -e 's/ *$//g')

bash/zsh input process substitution gives syntax error in conjunction with while

These work fine and do what they should (print the contents of the file foo):
cat <foo
while read line; do echo $line; done <foo
cat <(cat foo)
However this gives me a syntax error in zsh:
zsh$ while read line; do echo $line; done <(cat foo)
zsh: parse error near `<(cat foo)'
and bash:
bash$ while read line; do echo $line; done <(cat foo)
bash: syntax error near unexpected token `<(cat foo)'
Does anybody know the reason and maybe a workaround?
Note: This is obviously a toy example. In the real code I need the body of the while loop to be executed in the main shell process, so I can't just use
cat foo | while read line; do echo $line; done
You need to redirect the process substitution into the while loop:
You wrote
while read line; do echo $line; done <(cat foo)
You need
while read line; do echo $line; done < <(cat foo)
# ...................................^
Treat a process substitution like a filename.
bash/zsh replaces <(cat foo) by a pipe (kind of file) having a name as /dev/fd/n where n is the file descriptor (number).
You can check the pipe name using the command echo <(cat foo).
As you may know, bash/zsh also runs the command cat foo in another process. The output of this second process is written to that named pipe.
without process substitution:
while ... do ... done inputfile #error
while ... do ... done < inputfile #correct
same rules using process substitution:
while ... do ... done <(cat foo) #error
while ... do ... done < <(cat foo) #correct
Alternative:
cat foo >3 & while read line; do echo $line; done <3;
I can suggest only workaround like this:
theproc() { for((i=0;i<5;++i)) do echo $i; }
while read line ; do echo $line ; done <<<"$(theproc)"

Resources