p='[:punct:]' s='[:space:]'
sed -Ee'1!{/\n/!b' -e\} \
-e's/(\n*)(.*)/ \2 \1/' \
-e"s/is[$p]?[$s]/\n&/g" \
-e"s/([^$s])\n/\1/g;1G" \
-e:c -e"s/\ni(.* )\n{3}/u\1/" \
-e"/\n$/!s/\n//g;/\ni/G" \
-e's//i/;//tc' \
-e's/^ (.*) /\1/;P;$d;N;D'
That bit of sed
just carries a tally of is
occurrences from one line to the next. It should reliably handle as many is
es per line as you throw at it, and it doesn't need to buffer old lines while it does - it just retains a single newline character for every is
that it encounters which is not a part of another word.
The upshot is it will modify only the third occurrence in a file - and it will carry counts per line. So if a file looks like:
1. is is isis
2. is does
...it will print...
1. is is isis
2. us does
It first handles edge cases by inserting a space at the head and tail of every line. This makes word boundaries a little easier to ascertain.
It next looks for valid is
es by inserting a \n
ewline before all occurrences of is
that immediately precede zero or one punctuation characters followed by a space. It does another pass and removes all \n
ewlines that are immediately preceded by a not-space character. This markers left behind will match is.
and is
but not this
or ?is
.
It next gathers each marker to the tail of the string - for every \ni
match on a line it appends a \n
ewline to the tail of the string and replaces it with with either i
or u
. If there are 3 \n
ewlines in a row gathered at the tail of the string then it uses the u - else the i. The first time a u is used is also the last - the replacement sets off an infinite loop that boils down to get line, print line, get line, print line,
and so on.
At the end of each try loop cycle it cleans up the inserted spaces, prints only up to the first occurring newline in pattern space, and goes again.
I'll add in a l
ook command at the head of the loop like:
l; s/\ni(.* )\n{9}/u\1/...
...and take a look at what it does as it works with this input:
hai this is linux.
hai this is unix.
hai this is mac.
hai this is unchanged is.
...so here's what it does:
hai this \nis linux. \n$ #behind the scenes
hai this is linux. #actually printed
hai this \nis unix. \n\n$ #it builds the marker string
hai this is unix.
\n\n\n$ #only for lines matching the
\n\n\n$ #pattern - and not otherwise.
hai this \nis mac. \n\n\n$ #here's the match - 3 ises so far in file.
hai this us mac. #printed
hai this is unchanged is. #no look here - this line is never evaled
It makes more sense maybe with more is
es per line:
nthword()( p='[:punct:]' s='[:space:]'
sed -e '1!{/\n/!b' -e\} \
-e 's/\(\n*\)\(.*\)/ \2 \1/' \
-e "s/$1[$p]\{0,1\}[$s]/\n&/g" \
-e "s/\([^$s]\)\n/\1/g;1G;:c" \
-e "${dbg+l;}s/\n$1\(.* \)\n\{$3\}/$2\1/" \
-e '/\n$/!s/\n//g;/\n'"$1/G" \
-e "s//$1/;//tc" -e 's/^ \(.*\) /\1/' \
-e 'P;$d;N;D'
)
That's practically the same thing but written w/ POSIX BRE and rudimentary argument handling.
printf 'is is. is? this is%.0s\n' {1..4} | nthword is us 12
...gets...
is is. is? this is
is is. is? this is
is is. is? this us
is is. is? this is
...and if I enable ${dbg}
:
printf 'is is. is? this is%.0s\n' {1..4} |
dbg=1 nthword is us 12
...we can watch it iterate...
\nis \nis. \nis? this \nis \n$
is \nis. \nis? this \nis \n\n$
is is. \nis? this \nis \n\n\n$
is is. is? this \nis \n\n\n\n$
is is. is? this is
\nis \nis. \nis? this \nis \n\n\n\n\n$
is \nis. \nis? this \nis \n\n\n\n\n\n$
is is. \nis? this \nis \n\n\n\n\n\n\n$
is is. is? this \nis \n\n\n\n\n\n\n\n$
is is. is? this is
\nis \nis. \nis? this \nis \n\n\n\n\n\n\n\n\n$
is \nis. \nis? this \nis \n\n\n\n\n\n\n\n\n\n$
is is. \nis? this \nis \n\n\n\n\n\n\n\n\n\n\n$
is is. is? this \nis \n\n\n\n\n\n\n\n\n\n\n\n$
is is. is? this us
is is. is? this is
sed
is not the right tool for the job.