Say I have this file:
hello
world
hello world
This program
#!/bin/bash
for i in $(cat 1ドル); do
echo "tester: $i"
done
outputs
tester: hello
tester: world
tester: hello
tester: world
I'd like to have the for
iterate over each line individually ignoring whitespaces though, i.e. the last two lines should be replaced by
tester: hello world
Using quotes for i in "$(cat 1ドル)";
results in i
being assigned the whole file at once. What should I change?
6 Answers 6
With for
and IFS:
#!/bin/bash
IFS=$'\n' # make newlines the only separator
set -f # disable globbing
for i in $(cat < "1ドル"); do
echo "tester: $i"
done
Note however that it will skip empty lines as newline being an IFS-white-space character, sequences of it count as 1 and the leading and trailing ones are ignored. With zsh
and ksh93
(not bash
), you can change it to IFS=$'\n\n'
for newline not to be treated specially, however note that all trailing newline characters (so that includes trailing empty lines) will always be removed by the command substitution.
Or with read
(no more cat
):
#!/bin/bash
while IFS= read -r line; do
echo "tester: $line"
done < "1ドル"
There, empty lines are preserved, but note that it would skip the last line if it was not properly delimited by a newline character.
-
7thanks, I didn't know one could
<
into a whole loop. Although it makes perfectly sense now I saw itTobias Kienzler– Tobias Kienzler2011年02月07日 11:06:31 +00:00Commented Feb 7, 2011 at 11:06 -
2I see
IFS \ read -r line' in second example. Is really
IFS=` needed ? IMHO it enough to say :while read -r line; do echo "tester: $line"; done < "1ドル"
Grzegorz Wierzowiecki– Grzegorz Wierzowiecki2012年03月19日 16:45:42 +00:00Commented Mar 19, 2012 at 16:45 -
5@GrzegorzWierzowiecki
IFS=
turns off the stripping of leading and trailing whitespace. See Inwhile IFS= read..
, why does IFS have no effect?Gilles 'SO- stop being evil'– Gilles 'SO- stop being evil'2014年01月12日 00:29:39 +00:00Commented Jan 12, 2014 at 0:29 -
1@BenMares To prevent globbing expressions possibly appearing in the text we are reading from being expanded to matching file names. Try, for instance,
printf '%s\n' '*o' 'bar' >afile; touch foo; IFS=$'\n'; for i in $(cat afile); do echo "$i"; done
.fra-san– fra-san2020年03月04日 08:00:25 +00:00Commented Mar 4, 2020 at 8:00 -
3A
while IFS= read -r line || [ "$line" ]; do
will process a trailing line not properly delimited by a newline character (but it will be added back).user232326– user2323262020年03月04日 16:15:44 +00:00Commented Mar 4, 2020 at 16:15
(9 years later:)
Both provided answers would fail on files without a newline at the end, this will effectively skip the last line, produce no errors, would lead to disaster (learned hard way:).
The best concise solution I found so far that "Just Works" (in both bash and sh):
while IFS='' read -r LINE || [ -n "${LINE}" ]; do
echo "processing line: ${LINE}"
done < /path/to/input/file.txt
For more in-depth discussion see this StackOverflow discussion: How to use "while read" (Bash) to read the last line in a file if there’s no newline at the end of the file?
Beware: this approach adds an additional newline to the last line if there is none already.
-
files with characters after the last newline are not text files, and those characters don't constitute a line. In many cases those bogus characters are better left ignored or removed, though there are cases where you may want to treat it as a extra line, so it's good you show how.Stéphane Chazelas– Stéphane Chazelas2023年03月10日 07:53:48 +00:00Commented Mar 10, 2023 at 7:53
-
Note that files with NUL characters in them are also not text files, and except in zsh that loop would also fail if the intention was to keep them as if they were allowed in text files.Stéphane Chazelas– Stéphane Chazelas2023年03月10日 07:59:27 +00:00Commented Mar 10, 2023 at 7:59
If you can avoid it, don't especially if it's to process text.
Most text utilities are already designed to process text one line at a time, and, at least for the GNU implementations, do it efficiently, correctly and handle error conditions nicely. Piping one to another which runs them in parallel also means you can leverage more than one processor to do the job.
Here:
<input.txt sed 's/^/tester /' > output.txt
Or:
<input.txt awk '{print "tester", 0ドル}' > output.txt
More on that at: Why is using a shell loop to process text considered bad practice?
If it's not about text processing and you do need to run some command per line of a file, also note GNU xargs
where you can do:
xargs -rd'\n' -I@ -a input.txt cp -- @ @.back
for instance.
With the bash shell, you can get each line of a file into an array with the readarray
builtin:
readarray -t lines < input.txt &&
for line in "${lines[@]}"; do
do-some-non-text-processing-you-cannot-easily-do-with-xargs "$line" || break
done
POSIXly, you can use IFS= read -r line
to read one line off some input, but beware that if you redirect the whole while read
loop with the input file on stdin, then commands inside the loop will also have their stdin redirected to the file, so best is to use a different fd which you close inside the loop:
while
IFS= read -r line <&3 ||
[ -n "$line" ] # to cover for an unterminated last line.
do
{
do-some-non-text-processing-you-cannot-easily-do-with-xargs "$line" ||
break # abort upon failure if relevant
} 3<&-
done 3< input.txt > output.txt
read -r line
removes leading and trailing whitespace characters from the line that it reads provided they are in the $IFS
variable, though only the yash
shell honours that POSIX requirement. With most shells, that's limited to space and tab. ksh93 and recent versions of bash
do it for all single-byte characters considered as whitespace in the locale.
So to read a line and also strip leading and trailing blanks, you can do: IFS=$' \t' read -r line
. With ksh93, yash1 or recent versions of bash
. IFS=$' \t\r'
would also strip the trailing CR character found in text files from the Microsoft world.
1 though yash
doesn't support the $'...'
syntax yet, you'd need IFS=$(printf ' \t\r')
there.
For what it is worth, I need to do that quite often, and can never remember the exact way of using while IFS= read...
, so I defined the following function in my bash profile:
# iterate the line of a file and call input function
iterlines() {
(( $# < 2 )) && { echo "Usage: iterlines <File> <Callback>"; return; }
local File=1ドル
local Func=2ドル
n=$(cat "$File" | wc -l)
for (( i=1; i<=n; i++ )); do
"$Func" "$(sed "${i}q;d" "$File")"
done
}
This function first determines the number of lines in the file, then uses sed
to extract line after line, and passes each line as a single string argument to any given function. I suppose this might get really inefficient with large files, but that hasn't been a problem for me so far (suggestions on how to improve this welcome of course).
The usage is pretty sweet IMO:
>> cat example.txt # note the use of spaces, whitespace, etc.
a/path
This is a sentence.
"wi\th quotes"
$End
>> iterlines example.txt echo # preserves quotes, $ and whitespace
a/path
This is a sentence.
"wi\th quotes"
$End
>> x() { echo "$#"; }; iterlines example.txt x # line always passed as single input string
1
1
1
1
1
To read all the lines, regardless of whether they are ended with a new line or not:
cat "somefile" | { cat ; echo ; } | while read line; do echo $line; done
Source : My open source project https://sourceforge.net/projects/command-output-to-html-table/
-
It is not acceptable to post identical answers to multiple questions.Kamil Maciorowski– Kamil Maciorowski2021年10月10日 08:06:34 +00:00Commented Oct 10, 2021 at 8:06
-
Hi Kamil Maciorowski, Answers are identical because Questions are identical too !Nathan S.R.– Nathan S.R.2021年10月10日 15:36:06 +00:00Commented Oct 10, 2021 at 15:36
There is an answer here that may be useful to people who this doesn't work for
-
Not an answer as these usually contain some sort of explanation and/or a procedure to follow. A comment at best but not even a good one?muthuh– muthuh2022年01月26日 12:27:37 +00:00Commented Jan 26, 2022 at 12:27