In the TXR language we can express this without any mutating state variables. At any given position in the file we can perform a multi-line pattern match with two branch alternatives: we either match one or more consecutive lines which contain the search string and then print the first, or else we match one line and print that. One possible way is this:
@ (collect :gap 0 :mintimes 1)
@ (require (search-str line "logical IO"))
@ (do (put-line (first line)))
@ (do (put-line line))
$ txr first-log-IO.txr data
select * from test1 where 1=1
testing logical IO 24
select * from test2 where condition=4
parsing logical IO 45
select * from test5 where 1=1
testing logical IO 24
select * from test5 where condition=78
parsing logical IO 346
@(repeat) establishes a walk through the data without collecting variable bindings; when this construct is seen it usually indicates that some side-effect takes place in the iteration. In this case it is output.
@(repeat) we have a
@(cases) construct: multi-way match consisting of cases separated by
@(or). The second branch, the fallback case, of this is just
@line which matches a line. The
@(do (put-line (first line))) directive which follows prints that line.
The main branch of the
@(cases) collects material via
@(collect). The matches must be consecutive, required by
:gap 0, and there must be at least one, required by
:mintimes 1. The collect body matches a single line, bound to the
line variable. Then there is a
@(require ...) assertion which fails unless the line contains the substring
"logical IO". The collection will therefore stop when it encounters a non-matching line, because the
:gap 0 prevents it from skipping it. The matching lines are implicitly collected into a list called
line which pops out of the
collect (a variable bound inside a collect automatically becomes a list outside of the collect, of all the values bound over the multiple iterations). We just print the first one, as required, suppressing the rest.
Note that the two
@line matches have nothing to do with each other; they bind the
line variable in different scopes.
Another way is to do some functional programming over lazy lists in TXR Lisp:
[(opip (partition-by (do cond
((search-str @1 "logical IO") t)
$ txr first-log-IO.tl
opip operator is a syntactic sugar for constructing a pipeline of functions. Its arguments are all treated as
op syntax: a macro for generating anonymous functions with implicit, numbered arguments.
The overall form is [(opip ...) (get-lines)] which just means "call the function produced by
opip, with the result of
(get-lines) as an argument". This
(get-lines) converts the standard input stream into a lazy list of strings. (Its "opposite" is
put-lines, which makes an appearance).
Now in the pipeline, we use
partition-by to (lazily!) convert the list of lines into a list of lists which are its partitions. The partitioning condition is such that each line which contains
logical IO is mapped to the symbol
t, and all other lines just map to themselves. This means that consecutive lines which contain
logical IO appear as a partition, and all other lines appear isolated as partitions of length one. All we have to do with this data now is map each partition to its first item, via
(mapcar* first) and pass it to
put-lines to dump the result.
mapcar* because that's the lazy version of
mapcar. We want everything to be lazy so that the action is actually triggered by
put-lines marches through the output list, it pulls items out of the lazy
mapcar*, which forces the elements produced by the
partition-by, which forces the list produced by
(get-lines) which causes I/O to take place to read those lines.
If we use the regular
mapcar by mistake, we cause the problem that the entire output is constructed in memory before being dumped out, which bodes badly for large files.