Write a script

INPUT · Slides

`while` and `read`

01 / 08

When the number is not settled

for went round a row of things.

for F in *.txt; do ... done      as many as there are filesfor N in $(seq 1 5); do ... done  five times

Either way, the number of goes is settled before you start.

But what about times like these?

read a line at a time until the file runs outwait until the free space drops below 90%

You do not know how many. For these you use while.

while condition; do  the doingdone

You read it as "repeat while the condition holds".

for     a row of things, in orderwhile   while a condition lasts, on and on

Today there is one more important tool.

while read L; do  echo "[$L]"done < table.txt

read is the command that reads one line. Combine it with while and you can handle lines one at a time, properly.

This, in fact, is something for cannot do cleanly. You will see why later.

02 / 08

Going round with a count

First the plain shape.

I=1while [ $I -le 3 ]; do  echo "n=$I"  I=$((I+1))done
n=1n=2n=3

There are three parts.

I=1                 the starting valuewhile [ $I -le 3 ]  the condition to carry onI=$((I+1))          moving it along

Without all three it never stops. Forget the I=$((I+1)) and it goes round for ever.

I=1while [ $I -le 3 ]; do  echo n=$I        <- I never grows!done

That is an infinite loop. When it happens, stop it with Ctrl-C. The stopping you learned in chapter 8 earns its keep here.

The $((...)) is the arithmetic of chapter 10.

I=$((I+1))     add one to I

Honestly, this shape does not get much use. for N in $(seq 1 3) is shorter.

for N in $(seq 1 3); do echo n=$N; done      shortI=1; while [ $I -le 3 ]; do ... done         long

If the number is settled, use for. The real place for while comes on the next slide.

03 / 08

A line at a time with while read

This is the most important shape today.

while read L; do  echo "[$L]"done < table.txt

read is the command that reads one line and puts it in a variable.

read L  | read one line  | put it in L  | give back 0 if it read, 1 at the end

Because it gives back 1 at the end, the while stops. A neat arrangement.

while read L        go round while it can readdone < table.txt    where to read from goes here

Writing < table.txt after the done is distinctive. It pairs with the done > of for.

done < f     read from heredone > f     put out to here

You can split into columns, too.

while read COLOR NUM; do  echo "$COLOR is $NUM"done < table.txt
red is 3blue is 2yellow is 1

Split on the space and put in, in order. No need for cut. Handy for files shaped like a table.

04 / 08

for splits them

Here is the reason for using while read. Compare them on a line with a space.

The contents of spaced.txt are these.

my thingother

Going round with for

~ $ for L in $(cat spaced.txt); do echo "[$L]"; done[my][thing][other]

Three goes. The line was split.

With while read

~ $ while read L; do echo "[$L]"; done < spaced.txt[my thing][other]

Two. It read them as lines, properly.

The reason is from chapter 10.

$(cat f)     after expanding, split on spacesread         reads at the line breaks (spaces do not matter)

The basis for splitting differs.

> For lines, while read. Do not use for $(cat ...)

This is worth remembering. File names and people's names sometimes contain spaces. Written with for, it breaks only then — not found in testing, broken in production.

for         a tool for going round "words" in a rowwhile read  a tool for going round "lines" in a row

If what you have is lines, while read.

05 / 08

When the last line has no newline

while read has one quirk.

The contents of nonl.txt are these (with no newline at the end).

onetwothree          <- no newline here

Read it.

~ $ while read L; do echo "[$L]"; done < nonl.txt[one][two]

three is not read! A surprise, this one.

Here is why.

read reads "up to a newline" as one line  | there is no newline after three  | so it is taken as "the line is not finished"

This is the Unix way of thinking.

> A line in a text file ends with a newline

Write with echo, or \n in a printf, and the newline is there. So with a file you made yourself there is nothing to worry about.

The trouble is a file from elsewhere. This is how you check.

~ $ tail -c1 nonl.txt | od -c | head -10000000   e

If the last character is not \n, there is no newline.

Mending it is easy.

echo "" >> nonl.txt      add a newline

Known, and mended in a moment. Unknown, and you puzzle half a day over "a line short".

06 / 08

Hand it in on a pipe and the variable vanishes

One more large pitfall. The same thing, written two ways.

Handed in on a pipe

N=0cat table.txt | while read L; do  N=$((N+1))doneecho "outside=$N"
outside=0

Zero! It read three lines and yet it never grew.

Handed in by redirection

N=0while read L; do  N=$((N+1))done < table.txtecho "outside=$N"
outside=3

This one gives 3.

The reason is from chapter 10.

what follows a pipe runs in a child shell  | N grows inside the child shell  | the child shell ends, and it is gone

The same story as the difference between sh script.sh and . script.sh. What was done in a child shell does not come back to the parent.

So the manner is this.

> Hand while read its input with done < f. Not on a pipe

When you really must use a pipe, take the result back as output.

N=$(cat table.txt | grep -c .)

Carry it home as output, not in a variable. The $(...) of chapter 10, at work here too.

Everyone who uses a shell falls into this pitfall once. Know the reason and you will not hesitate.

07 / 08

until, and infinite loops

There is the reverse of while too.

I=0until [ $I -ge 3 ]; do  echo "i=$I"  I=$((I+1))done

until means "until the condition holds".

while cond   goes round while the condition is trueuntil cond   goes round while the condition is false

Either writes the same thing, so choose whichever reads better.

while [ $I -le 3 ]      while it is 3 or underuntil [ $I -gt 3 ]      until it goes past 3

Either will do, though while is more often used.

And last, the shape for making an infinite loop on purpose.

while true; do  date  sleep 5done

true is the command that always succeeds. So it goes round for ever.

You use it when making a watcher.

look every 5 seconds, and say so if it changed

You stop it with Ctrl-C, the way from chapter 8.

Ctrl-C   stop (INT)Ctrl-Z   pause (TSTP)

That said, you rarely use this in production, because the cron of chapter 12 is better.

while true + sleep   stops when you close the terminalcron                 runs whenever

What each tool is for. To watch something at hand, while true; to run it always, cron.

08 / 08

Now have a go

Here are the shapes for this lesson.

while [ $I -le 3 ]; do ... I=$((I+1)); done   go round countingwhile read L; do ... done < f                a line at a timewhile read A B; do ... done < f              split into columnsuntil [ cond ]; do ... done                  go round untilwhile true; do ... sleep 5; done             go round for ever

Three pitfalls to remember.

1. the last line is not read if it has no newline2. handed in on a pipe, a variable does not survive outside3. forget the line that moves it along and it never stops

And today's conclusion.

> For lines, while read

for $(cat f)      no, it splits on spaceswhile read < f    yes, read as lines

Two lessons left in chapter 11.

next: exit status ($? and && and ||)last: functions (giving a procedure a name)

The next one is something you have used many times already. You have typed $? and && and ||. That lesson puts them in order.

Let us go round.