Pipe a list of strings to for loop - bash

How do I pass a list to for in bash?
I tried
echo "some
different
lines
" | for i ; do
echo do something with $i;
done
but that doesn't work. I also tried to find an explanation with man but there is no man for
EDIT:
I know, I could use while instead, but I think I once saw a solution with for where they didn't define the variable but could use it inside the loop

for iterates over a list of words, like this:
for i in word1 word2 word3; do echo "$i"; done
use a while read loop to iterate over lines:
echo "some
different
lines" | while read -r line; do echo "$line"; done
Here is some useful reading on reading lines in bash.

This might work but I don't recommend it:
echo "some
different
lines
" | for i in $(cat) ; do
...
done
$(cat) will expand everything on stdin but if one of the lines of the echo contains spaces, for will think that's two words. So it might eventually break.
If you want to process a list of words in a loop, this is better:
a=($(echo "some
different
lines
"))
for i in "${a[#]}"; do
...
done
Explanation: a=(...) declares an array. $(cmd...) expands to the output of the command. It's still vulnerable for white space but if you quote properly, this can be fixed.
"${a[#]}" expands to a correctly quoted list of elements in the array.
Note: for is a built-in command. Use help for (in bash) instead.

This seems to work :
for x in $(echo a b c); do
echo $x
done

This is not a pipe, but quite similar:
args="some
different
lines";
set -- $args;
for i ; do
echo $i;
done
cause for defaults to $# if no in seq is given.
maybe you can shorten this a bit somehow?

Related

Unix Bash content of a file as argument stops at first line

I'm having an issue in something that seems to be a rookie error, but I can't find a way to find a solution.
I have a bash script : log.sh
which is :
#!/bin/bash
echo $1 >> log_out.txt
And with a file made of filenames (taken from the output of "find" which names is filesnames.txt and contains 53 lines of absolute paths) I try :
./log.sh $(cat filenames.txt)
the only output I have in the log_out.txt is the first line.
I need each line to be processed separately as I need to put them in arguments in a pipeline with 2 softwares.
I checked for :
my lines being terminated with /n
using a simple echo without writing to a file
all the sorts of cat filenames.txt or (< filenames.txt) found on internet
I'm sure it's a very dumb thing, but I can't find why I can't iterate more than one line :(
Thanks
It is because your ./log.sh $(cat filenames.txt) is being treated as one argument.
while IFS= read -r line; do
echo "$line";
done < filenames.txt
Edit according to: https://mywiki.wooledge.org/DontReadLinesWithFor
Edit#2:
To preserve leading and trailing whitespace in the result, set IFS to the null string.
You could simplify more and skip using explicit variable and use the default $REPLY
Source: http://wiki.bash-hackers.org/commands/builtin/read
You need to quote the command substitution. Otherwise $1 will just be the first word in the file.
./log.sh "$(cat filenames.txt)"
You should also quote the variable in the script, otherwise all the newlines will be converted to spaces.
echo "$1" >> log_out.txt
If you want to process each word separately, you can leave out the quotes
./log.sh $(cat filenames.txt)
and then use a loop in the script:
#!/bin/bash
for word in "$#"
do
echo "$word"
done >> log_out.txt
Note that this solution only works correctly when the file has one word per line and there are no wildcards in the words. See mywiki.wooledge.org/DontReadLinesWithFor for why this doesn't generalize to more complex lines.
You can iterate with each line.
#!/bin/bash
for i in $*
do
echo $i >> log_out.txt
done

bash: print x number of blank lines

Sometimes I want to create lots of whitespace at once (slightly different than a specific character). I attempted to do this using a for loop, but I am only printing \n once with this implementation. Furthermore, the actual '\n' character is actually printed instead of a blank line. What is a better way to do this?
for i in {1....100}
> do
> echo "\n"
> done
To print 5 blank lines:
yes '' | sed 5q
To print N blank lines:
yes '' | sed ${N}q
Brace expansion expects two dots, not any other number:
$ echo {1....5}
{1....5}
$ echo {1..5}
1 2 3 4 5
That's why your loop was executed just once.
If you want echo to interpret your escape sequences, you need to call it with echo -e.
echo outputs a newline anyway, so echo -e "\n" prints two newlines. To prevent echo from printing a newline you have to use echo -n, and echo -ne "\n" is the same as just echo.
You can print repeating characters, in this case a newline, like this:
printf '\n%.0s' {1..100}
As Jerry commented you made a syntax error.
This seems to work :
for i in {1..100}
do
echo "\n"
done
Try either of the following
for i in {1..100}; do echo ; done
Or
for i in {1..100}
do
echo
done
Worked for Ubuntu 16LTS and MacOS X
You can abuse printf for that:
printf '\n%.s' {1..100}
This prints \n followed by a zero-length string 100 times (so effectively, \n 100 times).
One caveat of using brace expansion though is that you cannot use variables in it, unless you eval it:
count=100
eval "printf '\n%.s' {1..$count}"
To avoid the eval you can use a loop; it's slightly slower but shouldn't mater unless you need thousands of them:
count=100
for ((i=0; i<$count; i++)); do printf '\n'; done
NB: If you use the eval method, make sure you trust your count, else it's easy to inject commands into the shell:
$ count='1}; /bin/echo "Hello World ! " # '
$ eval "printf '\n%.s' {1..$count}"
Hello World !
One easy fix in Bash is to declare your variable as an integer (ex: declare -i count). Any attempts to pass something else than an number will fail. It may still be possible to trigger DOS attacks by passing very large values for the brace expansion, which may cause bash to trigger an OOM condition.

Read an input file in shell script and store its lines in a variable

I'm new to UNIX and have this really simple problem:
I have a text-file (input.txt) containing a string in each line. It looks like this:
House
Monkey
Car
And inside my shell script I need to read this input file line by line to get to a variable like this:
things="House,Monkey,Car"
I know this sounds easy, but I just couldnt find any simple solution for this. My closest attempt so far:
#!/bin/sh
things=""
addToString() {
things="${things},$1"
}
while read line; do addToString $line ;done <input.txt
echo $things
But this won't work. Regarding to my google research I thought the while loop would create a new sub shell, but this I was wrong there (see the comment section). Nevertheless the variable "things" was still not available in the echo later on. (I cannot just write the echo inside the while loop, because I need to work with that string later on)
Could you please help me out here? Any help will be appreciated, thank you!
What you proposed works fine! I've only made two changes here: Adding missing quotes, and handling the empty-string case.
things=""
addToString() {
if [ -n "$things" ]; then
things="${things},$1"
else
things="$1"
fi
}
while read -r line; do addToString "$line"; done <input.txt
echo "$things"
If you were piping into while read, this would create a subshell, and that would eat your variables. You aren't piping -- you're doing a <input.txt redirection. No subshell, code works without changes.
That said, there are better ways to read lists of items into shell variables. On any version of bash after 3.0:
IFS=$'\n' read -r -d '' -a things <input.txt # read into an array
printf -v things_str '%s,' "${things[#]}" # write array to a comma-separated string
echo "${things_str%,}" # print that string w/o trailing comma
...on bash 4, that first line can be:
readarray -t things <input.txt # read into an array
This is not a shell solution, but the truth is that solutions in pure shell are often excessively long and verbose. So e.g. to do string processing it is better to use special tools that are part of the “default” Unix environment.
sed ':b;N;$!bb;s/\n/,/g' < input.txt
If you want to omit empty lines, then:
sed ':b;N;$!bb;s/\n\n*/,/g' < input.txt
Speaking about your solution, it should work, but you should really always use quotes where applicable. E.g. this works for me:
things=""
while read line; do things="$things,$line"; done < input.txt
echo "$things"
(Of course, there is an issue with this code, as it outputs a leading comma. If you want to skip empty lines, just add an if check.)
This might/might not work, depending on the shell you are using. On my Ubuntu 14.04/x64, it works with both bash and dash.
To make it more reliable and independent from the shell's behavior, you can try to put the whole block into a subshell explicitly, using the (). For example:
(
things=""
addToString() {
things="${things},$1"
}
while read line; do addToString $line ;done
echo $things
) < input.txt
P.S. You can use something like this to avoid the initial comma. Without bash extensions (using short-circuit logical operators instead of the if for shortness):
test -z "$things" && things="$1" || things="${things},${1}"
Or with bash extensions:
things="${things}${things:+,}${1}"
P.P.S. How I would have done it:
tr '\n' ',' < input.txt | sed 's!,$!\n!'
You can do this too:
#!/bin/bash
while read -r i
do
[[ $things == "" ]] && things="$i" || things="$things","$i"
done < <(grep . input.txt)
echo "$things"
Output:
House,Monkey,Car
N.B:
Used grep to tackle with empty lines and the probability of not having a new line at the end of file. (Normal while read will fail to read the last line if there is no newline at the end of file.)

why echo strings in bash reading from stdin doesn't show space characters

Here is my code
michal#argon:~$ cat test.txt
1
2
3
4
5
michal#argon:~$ cat test.txt | while read line;do echo $line;done > new.txt
michal#argon:~$ cat new.txt
1
2
3
4
5
I don't know why the command echo $line filtered the space character, I want test.txt and new.txt to be exactly the same.
Please help, thanks.
Several issues with your code.
$parameter outside of " ". Don't.
read uses $IFS to split input line into words, you have to disable this.
UUOC
To summarize:
while IFS= read line ; do echo "$line" ; done < test.txt > new.txt
While the provided answers solves your task at hand, they do not explain why bash and echo "forgot" to print the spaces you have in your string. Lets first make an small example to show the problem. I simply run the commands in my shell, no real script needed for this one:
mogul#linuxine:~$ echo something
something
mogul#linuxine:~$ echo something
something
Two echo commands that both print something right at the beginning of the line, even if the first one had plenty space between echo and something. And now with quoting:
mogul#linuxine:~$ echo " something"
something
Notice, here echo printed the leading spaces before something
If we stuff the string into a variable it work exactly the same:
mogul#linuxine:~$ str=" something"
mogul#linuxine:~$ echo $str
something
mogul#linuxine:~$ echo "$str"
something
Why?
Because the shell, bash in your case, removes space between arguments to commands before passing them on to the sub process. By quoting the strings we tell bash that we mean this literally and it should not mess with out strings.
This knowledge will become quite valuable if you are going to handle files with funny names, like "this is a file with blanks in its name.txt"
Try this --
$ oldIFS="$IFS"; IFS=""; while read line;do echo $line >> new.txt ;done < test.txt; IFS="$oldIFS"
$ cat new.txt
1
2
3
4
5

How to use the read command in Bash?

When I try to use the read command in Bash like this:
echo hello | read str
echo $str
Nothing echoed, while I think str should contain the string hello. Can anybody please help me understand this behavior?
The read in your script command is fine. However, you execute it in the pipeline, which means it is in a subshell, therefore, the variables it reads to are not visible in the parent shell. You can either
move the rest of the script in the subshell, too:
echo hello | { read str
echo $str
}
or use command substitution to get the value of the variable out of the subshell
str=$(echo hello)
echo $str
or a slightly more complicated example (Grabbing the 2nd element of ls)
str=$(ls | { read a; read a; echo $a; })
echo $str
Other bash alternatives that do not involve a subshell:
read str <<END # here-doc
hello
END
read str <<< "hello" # here-string
read str < <(echo hello) # process substitution
Typical usage might look like:
i=0
echo -e "hello1\nhello2\nhello3" | while read str ; do
echo "$((++i)): $str"
done
and output
1: hello1
2: hello2
3: hello3
The value disappears since the read command is run in a separate subshell: Bash FAQ 24
To put my two cents here: on KSH, reading as is to a variable will work, because according to the IBM AIX documentation, KSH's read does affects the current shell environment:
The setting of shell variables by the read command affects the current shell execution environment.
This just resulted in me spending a good few minutes figuring out why a one-liner ending with read that I've used a zillion times before on AIX didn't work on Linux... it's because KSH does saves to the current environment and BASH doesn't!
I really only use read with "while" and a do loop:
echo "This is NOT a test." | while read -r a b c theRest; do
echo "$a" "$b" "$theRest"; done
This is a test.
For what it's worth, I have seen the recommendation to always use -r with the read command in bash.
You don't need echo to use read
read -p "Guess a Number" NUMBER
Another alternative altogether is to use the printf function.
printf -v str 'hello'
Moreover, this construct, combined with the use of single quotes where appropriate, helps to avoid the multi-escape problems of subshells and other forms of interpolative quoting.
Do you need the pipe?
echo -ne "$MENU"
read NUMBER

Resources