Mark labels by their time along a curve, calculated by mirroring the origional graphviz label to find a vector.

#!/usr/bin/awk

# TODO:
# cat nfa.dot | dot -Tplain > /tmp/a.mp \
# && mpost -s 'outputformat="svg"' -s 'outputtemplate="%j-%c.%o"' /tmp/a.mp

# Only intended to consume Dot generated from libfsm's .dot output,
# and not graphviz diagrams in general.

function sname(s)
{
	if (s == "start") {
		return "S0";
	}

	return s;
}

# TODO: convert label from graphviz's html to a utf8
# This is either <xyz> or abc (with no quotes)
function slabel(s)
{
	# XXX: placeholder
	if (s == "<>") {
		return "";
	}
	if (s == "\"\"") {
		return "";
	}

# XXX: placeholder
gsub(/ε/, "e", s); # XXX: why not?
gsub(/"/, "", s)
gsub(/</, "", s)
gsub(/>/, "", s)

	return s;
}

BEGIN {

	# TODO: would be simpler to gvpr to rename start to S0
	# TODO: common stuff for multiple graphs

	print "pair S[];"
	print "pair q[];"
	print "path e;"

	print "ahlength := 10bp;"
	print "ahangle  := 45;"

	print "path S[].n;"

	# diameter of node
	print "numeric S[].diam;"

	# number of self-edges
	print "numeric S[].loops;"

#	print "secondarydef v projectedalong w =";
#	print "	if pair(v) and pair(w):";
#	print "		(v dotprod w) / (w dotprod w) * w";
#	print "	else:";
#	print "		errmessage \"arguments must be pairs\"";
#	print "	fi";
#	print "enddef;";

	# TODO: centralise to my equivalent of automata.mp, and include that
	# TODO: can use 'scantokens' to eval a string, .: don't need to pass diam etc

	print "vardef fsmnode(expr node, diam, final, lab) =";
	print "	draw fullcircle scaled diam shifted node withpen pencircle scaled 1bp;"
	print "	if final:"
	print "		draw fullcircle scaled (diam - 8bp) shifted node withpen pencircle scaled 1bp;"
	print "	fi;"
	print "	if lab <> \"\":"
	print "		label(lab, node);"
	print "	fi;"
	print "enddef;";

	print "vardef fsmedge(expr tail, head, e, t, lab) =";

	print "	drawarrow e withpen pencircle scaled 1bp;";

	print "	if lab <> \"\":"
	print "		pair bl;";
	print "		pair ml; ml := .5[tail, head];"

	print "		if t <> -1:"
	print "			bl := point t of e;"
	print "			draw bl withpen pencircle scaled 4bp withcolor blue;"
	print "		else:"
	print "			bl := point (length e / 2) of e;"
	print "			draw bl withpen pencircle scaled 2bp withcolor blue;"
	print "		fi;"

#	print "		draw bl withpen pencircle scaled 3bp withcolor blue;"
#	print "		draw tail -- head withpen pencircle scaled 1bp withcolor green;"
#	print "		draw ml withpen pencircle scaled 5bp withcolor green;"

	# TODO: plus label delta, distancing it from point. make a label function
	# TODO: maybe an "extend" function, to extend a path in its direction. use dotprod for that?

	# lft | rt | top | bot | ulft | urt | llft | lrt
	# TODO: round label coordinates to grid? quantize rather

	# labels ought to be below, if they're below the straight line from head-tail, or above otherwise
	# TODO: find what quadrant the line is, and make this .rt/.lft instead of .top/.bot
	print "		if ypart bl < ypart ml:" # TODO: threshold for considering "below"
	print "			label.bot(lab, bl shifted (0, -3bp));"
	print "		else:"
	print "			label.top(lab, bl shifted (0, +3bp));"
	print "		fi;"
	print "	fi;"

	print "enddef;";

	print "vardef fsmloop(expr node, diam, p, q, lab, loops) ="
	print "	path pp; pp = node -- 0.5[p, q];"
#	print "	draw p -- q withpen pencircle scaled 0.5bp withcolor green;"
#	print "	draw 0.5[p, q] withpen pencircle scaled 0.8bp withcolor green;"
	print "	draw node -- 0.5[p, q] withpen pencircle scaled 0.8bp withcolor green;"

	# extend loop 1.25 * diameter
	# count number of self-loops for this node, and increment extension
	print "	l := length pp;"
	print "	pair m; m := (0.5 + 2 * mlog(loops + 2) / 256) * diam * unitvector(direction l of pp) shifted node;"
	# u3 = u1 projectedalong u2;
	print "	draw node -- m withpen pencircle scaled 0.5bp withcolor green;"

	print "	path b; b := node .. q .. m .. p .. node;"
	print "	fsmedge(node, node, b cutbefore q cutafter p, -1, lab);"

	print "enddef;";

	print "vardef fsmgvedge(expr tail, taildiam, head, headdiam, e, pp, lab, loops) =";
	print "	draw e withpen pencircle scaled 0.25bp withcolor red;";

	# TODO: better explain this
	# TODO: explain reversed for q, else we'd find p again, where head == tail
	print "	path sn; sn := fullcircle scaled headdiam shifted head;" # outline
	print "	path st; st := fullcircle scaled taildiam shifted tail;" # outline
	print "	pair p; p = e intersectionpoint sn;\n";
	print "	pair q; q = (reverse e) intersectionpoint st;\n";

	print "	if head = tail:"
	# TODO: incr loops here
	print "		fsmloop(head, headdiam, p, q, lab, loops);"
	print "	else:"
	print "		path b; b = tail .. q .. p .. head;"
# TODO: explain this. we permit a 2% lengthening threshold relative to graphviz's b-spline
# this affects NFA especially
# XXX: or (arclength b > 6in), but wigglyness would be a better metric
	print "		r := arclength b / arclength e;";
	print "		if (r > 1.02) or (r < 0.93):";
	print "			b := e;"
	print "		fi;"
	print "		b := b cutbefore q cutafter p;"
	print "		t0 := -1;"
	print "		if lab <> \"\":"
# TODO: explain we find what time (lx,ly) is along graphviz's edge, e
# then pass the time and use that for placing our label on our edge
# use intersectiontimes to find the time of (lx,ly) along e
# or find by e cutbefore (lx,ly) and use arctime to find the time wrt e
# XXX: but lx,ly isn't even on the line. could mirror it about the line head--tail
# TODO: instead of extending like this, i'd rather use the line's direction,
# and find a lxy--whatever point on e
# u3 = u1 projectedalong u2;
# XXX: but i can't do that here, because e might be switched to a different curve
# .: pass in pp, not lqq
print "path qq;"
print "n := length pp;"
print "qq := (0, 0)--((arclength pp) * unitvector(direction n of pp));"
print "path lqq; lqq := pp & (qq shifted point n of pp);"
#print "draw lqq withpen pencircle scaled 0.25bp withcolor red;"

#print "numeric lll;";
#print "lll := 1in;"
#print "path luu; luu := (0, 0) -- lll * unitvector(direction 0 of pp);"
#print "point whatever of luu = point whatever of b;"
#print "drawarrow luu shifted point 0 of pp;"

	print "			pair tx; tx = lqq intersectiontimes b;"
	print "			t0 := ypart tx;" # XXX: cheesy
	print "		fi;"
	print "		fsmedge(tail, head, b, t0, lab);"
	print "	fi;"

	print "enddef;";
}

/^graph / {
	printf "beginfig(%s)\n", $2
}

# node S6 0.97222 0.41667 0.30556 0.30556 "" solid circle black lightgrey
# node name x y width height label style shape color fillcolor
/^node / {
	name   = sname($2);
	x      = $3;
	y      = $4;
	width  = $5;
	height = $6;
	label  = slabel($7);
	style  = $8;
	shape  = $9;
	# TODO: label delimited by <>, may contain spaces

	# TODO: quantise node positions
	printf "\t%s := (%fin, %fin);\n", name, x, y
	printf "\t%s.loops := 0;\n", name

# TODO: when plotting in graphviz, assume all nodes have the same width (not zero!). enough for "SXX" perhaps
# that should align them all on the same centre line. and pass fixedsize=true

	# TODO: explain this is diameter
	# TODO: store 8bp as a constant somewhere
# TODO: explain we take diameter from label presence and endstateness.
# ignore graphviz height and width, because it scales with label width and that looks bad for S1 vs S5
	diam = 0.3 + length(label) * 0.05;

	final = shape == "doublecircle";

	if (final) {
		printf "\t%s.diam = %fin + 8bp;\n", name, diam;
	} else {
		printf "\t%s.diam = %fin;\n", name, diam;
	}

	if ($2 != "start") {
		printf "\tfsmnode(%s, %s.diam, %s, \"%s\");\n", name, name, final ? "true" : "false", label
	}
}

# edge tail head n x1 y1 .. xn yn [label xl yl] style color
# edge S3 S6 10 3.5244 0.20119 3.2727 0.19014 2.7322 0.17358 2.2778 0.20833 1.873 0.23929 1.7755 0.28067 1.375 0.34722 1.3389 0.35322 1.3009 0.35974 1.2635 0.3663 <ε> 2.3264 0.3125 solid black
/^edge / {
	tail = sname($2);
	head = sname($3);
	n    = $4;

	v = split($0, a, " ");
	# TODO: assert n < v - whatever
	# TODO: assert n >= 4 (knot, control, control); mind trailing stuff... label etc

	# TODO: note we assume -Etailclip=false -Eheadclip=false -Edir=none
	# TODO: explain that requirement

	# things i wish the graphviz documentation said about -Tplain:
	# graphviz always outputs cubic b-splines
	# 3i+1 points total. format is { knot, control, control }* knot
	# bezier is ordered tail to head
	# first two points (knot, control) are ontop of each other, in the tail node
	# last two points (control, knot) are ontop of each other, in the head node
	# coordinates are in inches

	printf "\te :=\n";
	for (i = 1; i <= n - 1; i += 3) {
		# knot
		ox = a[(i + 0) * 2 + 3];
		oy = a[(i + 0) * 2 + 4];

		# control
		c1x = a[(i + 1) * 2 + 3];
		c1y = a[(i + 1) * 2 + 4];

		# control
		c2x = a[(i + 2) * 2 + 3];
		c2y = a[(i + 2) * 2 + 4];

		printf "\t(%fin, %fin) .. controls (%fin, %fin) and (%fin, %fin) ..\n", ox, oy, c1x, c1y, c2x, c2y;
	}
	# TODO: knot at end, which i haven't drawn
	# last point:
	{
		# knot
		ox = a[(n + 0) * 2 + 3];
		oy = a[(n + 0) * 2 + 4];
		printf "\t(%fin, %fin);\n", ox, oy;
	}

	# XXX: assuming no space in label
label = slabel(a[n * 2 + 5]);
lx    = (a[n * 2 + 6]);
ly    = (a[n * 2 + 7]);

	if (n * 2 == v - 4 - 2) {
		label = "";
	}

print "path pp;"

	if (head != tail && label != "") {
		printf "draw (%fin, %fin) withpen pencircle scaled 3bp withcolor red;", lx, ly
#		printf "draw (%fin, %fin) reflectedabout(%s, %s) withpen pencircle scaled 1bp withcolor green;", lx, ly, tail, head

		printf "pair lxy; lxy := (%fin, %fin);\n", lx, ly
		printf "pair llxy; llxy := lxy reflectedabout(%s, %s);\n", tail, head

		print "pp := llxy -- lxy;"
#		printf "draw pp withpen pencircle scaled 1bp withcolor green;\n"
	}

	printf "fsmgvedge(%s, %s.diam, %s, %s.diam, e, pp, \"%s\", %s.loops);", tail, tail, head, head, label, head;

	if (head == tail) {
		print "	%s.loops := %s.loops + 1;", head, head
	}
}

/^stop$/ {
	# XXX: ought to be per graph; stop is per file
	print "endfig;"
}

END {
	print "end."
}

#!/usr/bin/awk

# TODO:
# cat nfa.dot | dot -Tplain > /tmp/a.mp \
# && mpost -s 'outputformat="svg"' -s 'outputtemplate="%j-%c.%o"' /tmp/a.mp

# Only intended to consume Dot generated from libfsm's .dot output,
# and not graphviz diagrams in general.

function sname(s)
{
	if (s == "start") {
		return "S0";
	}

	return s;
}

# TODO: convert label from graphviz's html to a utf8
# This is either <xyz> or abc (with no quotes)
function slabel(s)
{
	# XXX: placeholder
	if (s == "<>") {
		return "";
	}
	if (s == "\"\"") {
		return "";
	}

# XXX: placeholder
gsub(/ε/, "e", s); # XXX: why not?
gsub(/"/, "", s)
gsub(/</, "", s)
gsub(/>/, "", s)

	return s;
}

BEGIN {

	# TODO: would be simpler to gvpr to rename start to S0
	# TODO: common stuff for multiple graphs
	print "pair S[];"
	print "pair q[];"
	print "path e;"

	print "ahlength := 10bp;"
	print "ahangle  := 45;"

	print "path S[].n;"

	# diameter of node
	print "numeric S[].diam;"

	# number of self-edges
	print "numeric S[].loops;"

#	print "secondarydef v projectedalong w =";
#	print "	if pair(v) and pair(w):";
#	print "		(v dotprod w) / (w dotprod w) * w";
#	print "	else:";
#	print "		errmessage \"arguments must be pairs\"";
#	print "	fi";
#	print "enddef;";

	# TODO: centralise to my equivalent of automata.mp, and include that

	print "vardef fsmnode(expr node, diam, final, lab) =";
	print "	draw fullcircle scaled diam shifted node withpen pencircle scaled 1bp;"
	print "	if final:"
	print "		draw fullcircle scaled (diam - 8bp) shifted node withpen pencircle scaled 1bp;"
	print "	fi;"
	print "	if lab <> \"\":"
	print "		label(lab, node);"
	print "	fi;"
	print "enddef;";

	print "vardef fsmedge(expr tail, head, e, lab) =";

	print "	drawarrow e withpen pencircle scaled 1bp;";

	print "	if lab <> \"\":"
	print "		pair bl;";
	print "		pair ml; ml := .5[tail, head];"

	# TODO: new idea; draw right angle from midpoint, and use that to find intersection with b
	# this way it should always align for multiple edges, even if on a slant

	print "		draw point (length b / 2) of b withpen pencircle scaled 4bp withcolor blue;"
#	print "		draw tail -- head withpen pencircle scaled 1bp withcolor green;"
#	print "		draw ml withpen pencircle scaled 5bp withcolor green;"

	print "		bl := point (length b / 2) of b;"

	# TODO: plus label delta, distancing it from point. make a label function
	# TODO: maybe an "extend" function, to extend a path in its direction. use dotprod for that?

	# lft | rt | top | bot | ulft | urt | llft | lrt
	# TODO: round label coordinates to grid? quantize rather

	# labels ought to be below, if they're below the straight line from head-tail, or above otherwise
	# TODO: find what quadrant the line is, and make this .rt/.lft instead of .top/.bot
	print "		if ypart bl < ypart ml:" # TODO: threshold for considering "below"
	print "			label.bot(lab, bl shifted (0, -3bp));"
	print "		else:"
	print "			label.top(lab, bl shifted (0, +3bp));"
	print "		fi;"
	print "	fi;"

	print "enddef;";

	print "vardef fsmloop(expr node, diam, p, q, lab, loops) ="
	print "	path pp; pp = node -- 0.5[p, q];"
	print "	draw p -- q withpen pencircle scaled 0.5bp withcolor green;"
	print "	draw 0.5[p, q] withpen pencircle scaled 0.8bp withcolor green;"
	print "	draw node -- 0.5[p, q] withpen pencircle scaled 0.8bp withcolor green;"

	# extend loop 1.25 * diameter
	# count number of self-loops for this node, and increment extension
	print "	l := length pp;"
	print "	pair m; m := (0.5 + 2 * mlog(loops + 2) / 256) * diam * unitvector(direction l of pp) shifted node;"
	# u3 = u1 projectedalong u2;
	print "	draw node -- m withpen pencircle scaled 0.5bp withcolor green;"

	print "	path b; b := node .. q .. m .. p .. node;"
	print "	fsmedge(node, node, b cutbefore q cutafter p, lab);"

	print "enddef;";

	print "vardef fsmgvedge(expr tail, taildiam, head, headdiam, e, lab, loops) =";
	print "	draw e withpen pencircle scaled 0.25bp withcolor red;";

	# TODO: better explain this
	# TODO: explain reversed for q, else we'd find p again, where head == tail
	print "	path sn; sn := fullcircle scaled headdiam shifted head;" # outline
	print "	path st; st := fullcircle scaled taildiam shifted tail;" # outline
	print "	pair p; p = e intersectionpoint sn;\n";
	print "	pair q; q = (reverse e) intersectionpoint st;\n";

	print "	if head = tail:"
	print "		fsmloop(head, headdiam, p, q, lab, loops);"
	print "	else:"
	print "		path b; b = tail .. q .. p .. head;"
	# TODO: explain this. we permit a 2% lengthening threshold relative to graphviz's b-spline
	# this affects NFA especially
	# XXX: or (arclength b > 6in), but wigglyness would be a better metric
	print "		r := arclength b / arclength e;";
	print "		if (r > 1.02) or (r < 0.93):";
	print "			b := e;"
	print "		fi;"
	print "		fsmedge(tail, head, b cutbefore q cutafter p, lab);"
	print "	fi;"

	print "enddef;";
}

/^graph / {
	printf "beginfig(%s)\n", $2
}

# node S6 0.97222 0.41667 0.30556 0.30556 "" solid circle black lightgrey
# node name x y width height label style shape color fillcolor
/^node / {
	name   = sname($2);
	x      = $3;
	y      = $4;
	width  = $5;
	height = $6;
	label  = slabel($7);
	style  = $8;
	shape  = $9;
	# TODO: label delimited by <>, may contain spaces

	# TODO: quantise node positions
	printf "\t%s := (%fin, %fin);\n", name, x, y
	printf "\t%s.loops := 0;\n", name

# TODO: when plotting in graphviz, assume all nodes have the same width (not zero!). enough for "SXX" perhaps
# that should align them all on the same centre line. and pass fixedsize=true

	# TODO: explain this is diameter
	# TODO: store 8bp as a constant somewhere
# TODO: explain we take diameter from label presence and endstateness.
# ignore graphviz height and width, because it scales with label width and that looks bad for S1 vs S5
	diam = 0.3 + length(label) * 0.05;

	final = shape == "doublecircle";

	if (final) {
		printf "\t%s.diam = %fin + 8bp;\n", name, diam;
	} else {
		printf "\t%s.diam = %fin;\n", name, diam;
	}

	if ($2 != "start") {
		printf "\tfsmnode(%s, %s.diam, %s, \"%s\");\n", name, name, final ? "true" : "false", label
	}
}

# edge tail head n x1 y1 .. xn yn [label xl yl] style color
# edge S3 S6 10 3.5244 0.20119 3.2727 0.19014 2.7322 0.17358 2.2778 0.20833 1.873 0.23929 1.7755 0.28067 1.375 0.34722 1.3389 0.35322 1.3009 0.35974 1.2635 0.3663 <ε> 2.3264 0.3125 solid black
/^edge / {
	tail = sname($2);
	head = sname($3);
	n    = $4;

	v = split($0, a, " ");
	# TODO: assert n < v - whatever
	# TODO: assert n >= 4 (knot, control, control); mind trailing stuff... label etc

	# TODO: note we assume -Etailclip=false -Eheadclip=false -Edir=none
	# TODO: explain that requirement

	# things i wish the graphviz documentation said about -Tplain:
	# graphviz always outputs cubic b-splines
	# 3i+1 points total. format is { knot, control, control }* knot
	# bezier is ordered tail to head
	# first two points (knot, control) are ontop of each other, in the tail node
	# last two points (control, knot) are ontop of each other, in the head node
	# coordinates are in inches

	printf "\te :=\n";
	for (i = 1; i <= n - 1; i += 3) {
		# knot
		ox = a[(i + 0) * 2 + 3];
		oy = a[(i + 0) * 2 + 4];

		# control
		c1x = a[(i + 1) * 2 + 3];
		c1y = a[(i + 1) * 2 + 4];

		# control
		c2x = a[(i + 2) * 2 + 3];
		c2y = a[(i + 2) * 2 + 4];

		printf "\t(%fin, %fin) .. controls (%fin, %fin) and (%fin, %fin) ..\n", ox, oy, c1x, c1y, c2x, c2y;
	}
	# TODO: knot at end, which i haven't drawn
	# last point:
	{
		# knot
		ox = a[(n + 0) * 2 + 3];
		oy = a[(n + 0) * 2 + 4];
		printf "\t(%fin, %fin);\n", ox, oy;
	}

	# XXX: assuming no space in label
label = slabel(a[n * 2 + 5]);
lx    = (a[n * 2 + 6]);
ly    = (a[n * 2 + 7]);

	if (n * 2 == v - 4 - 2) {
		label = "";
	}

	# TODO: find what time (lx,ly) is along graphviz's edge, e
	# then pass the time and use that for placing our label on our edge
	# use intersectiontimes to find the time of (lx,ly) along e
	# or find by e cutbefore (lx,ly) and use arctime to find the time wrt e

	printf "fsmgvedge(%s, %s.diam, %s, %s.diam, e, \"%s\", %s.loops);", tail, tail, head, head, label, head;

	if (head == tail) {
		print "	%s.loops := %s.loops + 1;", head, head
	}
}

/^stop$/ {
	# XXX: ought to be per graph; stop is per file
	print "endfig;"
}

END {
	print "end."
}


Handle string numSeasons

Handle string numSeasons

# Create column files with lists of series titles, descriptions, number
# of seasons, and number of episodes, to be pasted into a spreadsheet.
# Note that IN_CANADA affects processing.
#
# Create a separate file with a line for each episode containing
# seriesNumber, episodeURL, seriesTitle, seasonNumber, episodeNumber,
# episodeTitle, & episodeDuration with the same columns as the primary
# spreadsheet so they can be combined into one.
#
# Create a separate file with a line for each series duration
#
# Return the total duration of all series
#
# INVOCATION:
#       curl -s https://acorn.tv/192/ \
#           | awk -v TITLE_FILE=$TITLE_FILE -v DESCRIPTION_FILE=$DESCRIPTION_FILE \
#           -v NUM_SEASONS_FILE=$NUM_SEASONS_FILE -v NUM_EPISODES_FILE=$NUM_EPISODES_FILE \
#           -v EPISODE_CURL_FILE=$EPISODE_CURL_FILE -v IN_CANADA=$IN_CANADA \
#           -v EPISODE_INFO_FILE=$EPISODE_INFO_FILE -v SERIES_NUMBER=$lastRow \
#           -v DURATION_FILE=$DURATION_FILE -v ERROR_FILE=$ERROR_FILE -f getAcornFrom-seriesPages.awk
#
# INPUT:
#        <title>Acorn TV | 19-2</title>
#  ---
#        19-2
#        <meta itemprop="numberOfEpisodes" content="30" />
#        <meta itemprop="numberOfSeasons" content="3" />
#  ---
#        <meta itemprop="numberOfEpisodes" content="10" />
#        <meta itemprop="numberOfEpisodes" content="10" />
#        <meta itemprop="numberOfEpisodes" content="10" />
#  ---
#
"The writing is sublime" (New \
#        York Times) in this anything-but-a-procedural cop drama ... as reluctant partners \
#        patrolling the streets of Montreal. Not Available in Canada. CC Available.

#  ---
#
We're sorry, but Acorn TV is not available in this territory.

#  ---
#        <meta itemprop="seasonNumber" content="1" />
#  ---
#
#  ---
#        <meta itemprop="timeRequired" content="T44M06S" />
#  ---
#
Partners

#  ---
#
Season 1: Episode 1

#
Season 9 Christmas Special: Episode 1

#
#
Series 1: Episode 1

#
Series 1 (Set 1): Episode 1

#
Series 10 (Sets 13 - 14): Episode 1

#
Series 19 (Part One): Episode 1

#
#
Set 1: Episode 1

#
#
Mini Series: Episode 1

#
Miniseries: Episode 1

#
#
Black Widows Season 1: Episode 1

#
#
A Dance to the Music of Time: Episode 1

#
Christmas Special : Episode 1

#
Feature Film: Episode 1

#
#
#
#  OUTPUT:
#       $EPISODE_INFO_FILE
#

# Extract the series title
/<title>/ {
    sub (/.*<title>Acorn TV \| /,"")
    sub (/<.*/,"")
    gsub (/&/,"\\&")
    # gsub (/'/,"'")
    # gsub (/"/,"\"\"")
    if (match ($0, /^The /)) {
        $0 = substr($0, 5) ", The"
    }
    episodeLinesFound = 0
    numEpisodesStr = ""
    seriesTitle = $0
    print seriesTitle >> TITLE_FILE
    next
}

# Extract the number of episodes in the series
/itemprop="numberOfEpisodes"/ {
    split ($0,fld,"\"")
    episodeLinesFound += 1
    if (episodeLinesFound == 1)
        totalEpisodes = fld[4]
    if (episodeLinesFound != 1)
        numEpisodesStr = numEpisodesStr "+" fld[4]
    next
}

# Extract the number of seasons in the series
/itemprop="numberOfSeasons"/ {
    split ($0,fld,"\"")
    numSeasons = fld[4]
    if ((numSeasons + 0) == 0)
        printf ("==> No seasons: %d\t%s\n", SERIES_NUMBER, seriesTitle) >> ERROR_FILE
    print numSeasons >> NUM_SEASONS_FILE
    next
}

/Acorn TV is not available in this territory/ {
    printf ("==> Not available here: %d\t%s\n", SERIES_NUMBER, seriesTitle) >> ERROR_FILE
}

# Extract the series description
/id="franchise-description"/ {
    # get rid of boilerplate
    split ($0,fld,"[<>]")
    description = fld[3]
    # get rid of unnecessary characters and text
    gsub (/\\/,"",description)
    if (IN_CANADA != "yes") {
        sub (/Series [[:digit:]]* not available in Canada\./,"",description)
        sub (/Not [Aa]vailable in Canada\./,"",description)
        sub (/NOT AVAILABLE IN CANADA\./,"",description)
    }
    sub (/CC Available\. CC Available/,"CC Available",description)
    # fix sloppy input spacing
    sub (/\.CC Available/,". CC Available",description)
    gsub (/ \./,".",description)
    gsub (/  */," ",description)
    sub (/^ */,"",description)
    sub (/ *$/,"",description)
    # fix funky HTML characters
    gsub (/'/,"'",description)
    # fix unmatched quotes
    numQuotes = gsub(/"/,"\"",description)
    if ((numQuotes % 2) == 1) {
        printf ("==> Corrected unmatched quote (%d): %d\t%s\n", numQuotes, SERIES_NUMBER, \
                seriesTitle) >> ERROR_FILE
        description = description " \""
    }
    print description >> DESCRIPTION_FILE
    next
}

# Extract the seasonNumber
# Note that Acorn "season" numbers may not correspond to actual season numbers.
# They simply refer to the number of seasons available on Acorn.
/<meta itemprop="seasonNumber" content="/ {
    split ($0,fld,"\"")
    seasonNumber = fld[4]
    next
}

# Extract the episode URL
/> EPISODE_CURL_FILE
    next
}

# Extract the episode duration
/<meta itemprop="timeRequired"/ {
    split ($0,fld,"\"")
    split (fld[4],tm,/[TMS]/)
    secs = tm[3]
    mins = tm[2] + int(secs / 60)
    hrs =  int(mins / 60)
    secs %= 60; mins %= 60
    #
    seriesSecs += secs
    seriesMins += mins + int(seriesSecs / 60)
    seriesHrs += hrs + int(seriesMins / 60)
    seriesSecs %= 60; seriesMins %= 60
    #
    totalSecs += secs
    totalMins += mins + int(totalSecs / 60)
    totalHrs += hrs + int(totalMins / 60)
    totalSecs %= 60; totalMins %= 60
    #
    episodeDuration = sprintf ("%02d:%02d:%02d", hrs, mins, secs)
    if (episodeDuration == "00:00:00")
        printf ("==> No duration: %d\t%s  %s\n", SERIES_NUMBER, seriesTitle, episodeURL) >> ERROR_FILE
    next
}

# Extract the episode title
/
/ {
    split ($0,fld,"[<>]")
    episodeTitle = fld[3]
    gsub (/&/,"\\&", episodeTitle)
    next
}

# There are a number of variants in the "episodeNumber" string
#
#
Season 1: Episode 1

#
Season 9 Christmas Special: Episode 1

#
#
Series 1: Episode 1

#
Series 1 (Set 1): Episode 1

#
Series 10 (Sets 13 - 14): Episode 1

#
Series 19 (Part One): Episode 1

#
#
Set 1: Episode 1

#
#
Mini Series: Episode 1

#
Miniseries: Episode 1

#
#
Black Widows Season 1: Episode 1

#
#
A Dance to the Music of Time: Episode 1

#
Christmas Special : Episode 1

#
Feature Film: Episode 1

# Extract episode number
/
.*span itemprop="episodeNumber">/ {
    # If show is a Prequel, use "P"
    if (episodeURL ~ /\/prequel/)
        showType = "P"
    # If show is a Miniseries, use "M"
    if (episodeURL ~ /\/miniseries/)
        showType = "M"
    # If we don't know what kind of show it is, use "S"
    if (showType == "")
        showType = "S"
    # Default episodeType to "E"
    episodeType = "E"
    # Default bonus, parttwo, and christmasspecials episodeType to "X"
    if (episodeURL ~ /bonus\/|[0-9]{1,2}parttwo\/|christmasspecial\//)
        episodeType = "X"
    # jamaicainn, newworlds & newtonslaw bonus seasonNumber should be 1
    if (episodeURL ~ /\/jamaicainn\/bonus\/|\/newworlds\/bonus\/|\/newtonslaw\/bonus\// && seasonNumber != 1) {
        split (episodeURL, part, "/")
        printf ("==> Corrected mismatch: https://acorn.tv/%s/%s was series %d\n", \
               part[4], part[5], seasonNumber) >> ERROR_FILE
        seasonNumber = 1
    }
    # Plain christmasspecial, seriesfinale don't increment seasonNumber
    if (episodeURL ~ /\/christmasspecial\/|\/seriesfinale\//) {
        split (episodeURL, part, "/")
        printf ("==> Corrected mismatch: https://acorn.tv/%s/%s was series %d\n", \
               part[4], part[5], seasonNumber) >> ERROR_FILE
        seasonNumber -= 1
    }
    #
    split ($0,fld,"[<>]")
    episodeNumber = fld[5]
    # Feature films don't have episodes
    if (episodeURL ~ /\/featurefilm\//)
        next
    # neither should single episode series
    if (totalEpisodes == 1)
        next
    # The season number should match that in the URL
    # Birds of a Feather, Doc Martin, Murdoch, Poirot, Rebus, Vera, and others have problems
    if (episodeURL ~ \
           /\/series[0-9]{1,2}\/|[0-9]{1,2}bonus\/|[0-9]{1,2}parttwo\/|\/murdoch\/season[0-9]{1,2}/) {
        split (episodeURL, part, "/")
        URLseasonNumber = part[5]
        sub (/bonus/,"",URLseasonNumber)
        # Foyle's War has sets: foyleswar/series9set8bonus/
        sub (/set[0-9]/,"",URLseasonNumber)
        # Midsomer Murders has two parts: midsomermurders/series19parttwo/
        sub (/parttwo/,"",URLseasonNumber)
        sub (/christmasspecial/,"",URLseasonNumber)
        sub (/[[:alpha:]]*/,"",URLseasonNumber)
        if (URLseasonNumber != seasonNumber) {
            printf ("==> Corrected mismatch: https://acorn.tv/%s/%s was series %d\n", \
               part[4], part[5], seasonNumber) >> ERROR_FILE
            seasonNumber = URLseasonNumber
        }
    }
    if ((episodeNumber + 0) == 0)
        print "==> Episode number is 00: " episodeURL >> ERROR_FILE
    printf ("%d\t=HYPERLINK(\"%s\";\"%s, %s%02d%s%02d, %s\"\)\t\t\t%s\n", \
        SERIES_NUMBER, episodeURL, seriesTitle, showType, seasonNumber, episodeType, episodeNumber, \
        episodeTitle, episodeDuration) >> EPISODE_INFO_FILE
    showType = ""
    next
}

/-- Viewers Also Watched --/ {
    print (episodeLinesFound == 1 ? "=0" : "=" numEpisodesStr) >> NUM_EPISODES_FILE
    if (description == "")
        printf ("==> No description: %d\t%s\n", SERIES_NUMBER, seriesTitle) >> ERROR_FILE
    description = ""
    #
    seriesDuration = sprintf ("%02d:%02d:%02d", seriesHrs, seriesMins, seriesSecs)
    print seriesDuration >> DURATION_FILE
    if (seriesDuration == "00:00:00")
        printf ("==> No duration: %d\t%s\n", SERIES_NUMBER, seriesTitle) >> ERROR_FILE
    seriesSecs = 0
    seriesMins = 0
    seriesHrs = 0
    seriesDuration = ""
    #
    SERIES_NUMBER += 1
}

END {
    # Return the total duration of all series
    printf ("%02d:%02d:%02d\n", totalHrs, totalMins, totalSecs)
}

# Create column files with lists of series titles, descriptions, number
# of seasons, and number of episodes, to be pasted into a spreadsheet.
# Note that IN_CANADA affects processing.
#
# Create a separate file with a line for each episode containing
# seriesNumber, episodeURL, seriesTitle, seasonNumber, episodeNumber,
# episodeTitle, & episodeDuration with the same columns as the primary
# spreadsheet so they can be combined into one.
#
# Create a separate file with a line for each series duration
#
# Return the total duration of all series
#
# INVOCATION:
#       curl -s https://acorn.tv/192/ \
#           | awk -v TITLE_FILE=$TITLE_FILE -v DESCRIPTION_FILE=$DESCRIPTION_FILE \
#           -v NUM_SEASONS_FILE=$NUM_SEASONS_FILE -v NUM_EPISODES_FILE=$NUM_EPISODES_FILE \
#           -v EPISODE_CURL_FILE=$EPISODE_CURL_FILE -v IN_CANADA=$IN_CANADA \
#           -v EPISODE_INFO_FILE=$EPISODE_INFO_FILE -v SERIES_NUMBER=$lastRow \
#           -v DURATION_FILE=$DURATION_FILE -v ERROR_FILE=$ERROR_FILE -f getAcornFrom-seriesPages.awk
#
# INPUT:
#        <title>Acorn TV | 19-2</title>
#  ---
#        19-2
#        <meta itemprop="numberOfEpisodes" content="30" />
#        <meta itemprop="numberOfSeasons" content="3" />
#  ---
#        <meta itemprop="numberOfEpisodes" content="10" />
#        <meta itemprop="numberOfEpisodes" content="10" />
#        <meta itemprop="numberOfEpisodes" content="10" />
#  ---
#
"The writing is sublime" (New \
#        York Times) in this anything-but-a-procedural cop drama ... as reluctant partners \
#        patrolling the streets of Montreal. Not Available in Canada. CC Available.

#  ---
#
We're sorry, but Acorn TV is not available in this territory.

#  ---
#        <meta itemprop="seasonNumber" content="1" />
#  ---
#
#  ---
#        <meta itemprop="timeRequired" content="T44M06S" />
#  ---
#
Partners

#  ---
#
Season 1: Episode 1

#
Season 9 Christmas Special: Episode 1

#
#
Series 1: Episode 1

#
Series 1 (Set 1): Episode 1

#
Series 10 (Sets 13 - 14): Episode 1

#
Series 19 (Part One): Episode 1

#
#
Set 1: Episode 1

#
#
Mini Series: Episode 1

#
Miniseries: Episode 1

#
#
Black Widows Season 1: Episode 1

#
#
A Dance to the Music of Time: Episode 1

#
Christmas Special : Episode 1

#
Feature Film: Episode 1

#
#
#
#  OUTPUT:
#       $EPISODE_INFO_FILE
#

# Extract the series title
/<title>/ {
    sub (/.*<title>Acorn TV \| /,"")
    sub (/<.*/,"")
    gsub (/&/,"\\&")
    # gsub (/'/,"'")
    # gsub (/"/,"\"\"")
    if (match ($0, /^The /)) {
        $0 = substr($0, 5) ", The"
    }
    episodeLinesFound = 0
    numEpisodesStr = ""
    seriesTitle = $0
    print seriesTitle >> TITLE_FILE
    next
}

# Extract the number of episodes in the series
/itemprop="numberOfEpisodes"/ {
    split ($0,fld,"\"")
    episodeLinesFound += 1
    if (episodeLinesFound == 1)
        totalEpisodes = fld[4]
    if (episodeLinesFound != 1)
        numEpisodesStr = numEpisodesStr "+" fld[4]
    next
}

# Extract the number of seasons in the series
/itemprop="numberOfSeasons"/ {
    split ($0,fld,"\"")
    numSeasons = fld[4]
    if (numSeasons == 0)
        printf ("==> No seasons: %d\t%s\n", SERIES_NUMBER, seriesTitle) >> ERROR_FILE
    print numSeasons >> NUM_SEASONS_FILE
    next
}

/Acorn TV is not available in this territory/ {
    printf ("==> Not available here: %d\t%s\n", SERIES_NUMBER, seriesTitle) >> ERROR_FILE
}

# Extract the series description
/id="franchise-description"/ {
    # get rid of boilerplate
    split ($0,fld,"[<>]")
    description = fld[3]
    # get rid of unnecessary characters and text
    gsub (/\\/,"",description)
    if (IN_CANADA != "yes") {
        sub (/Series [[:digit:]]* not available in Canada\./,"",description)
        sub (/Not [Aa]vailable in Canada\./,"",description)
        sub (/NOT AVAILABLE IN CANADA\./,"",description)
    }
    sub (/CC Available\. CC Available/,"CC Available",description)
    # fix sloppy input spacing
    sub (/\.CC Available/,". CC Available",description)
    gsub (/ \./,".",description)
    gsub (/  */," ",description)
    sub (/^ */,"",description)
    sub (/ *$/,"",description)
    # fix funky HTML characters
    gsub (/'/,"'",description)
    # fix unmatched quotes
    numQuotes = gsub(/"/,"\"",description)
    if ((numQuotes % 2) == 1) {
        printf ("==> Corrected unmatched quote (%d): %d\t%s\n", numQuotes, SERIES_NUMBER, \
                seriesTitle) >> ERROR_FILE
        description = description " \""
    }
    print description >> DESCRIPTION_FILE
    next
}

# Extract the seasonNumber
# Note that Acorn "season" numbers may not correspond to actual season numbers.
# They simply refer to the number of seasons available on Acorn.
/<meta itemprop="seasonNumber" content="/ {
    split ($0,fld,"\"")
    seasonNumber = fld[4]
    next
}

# Extract the episode URL
/> EPISODE_CURL_FILE
    next
}

# Extract the episode duration
/<meta itemprop="timeRequired"/ {
    split ($0,fld,"\"")
    split (fld[4],tm,/[TMS]/)
    secs = tm[3]
    mins = tm[2] + int(secs / 60)
    hrs =  int(mins / 60)
    secs %= 60; mins %= 60
    #
    seriesSecs += secs
    seriesMins += mins + int(seriesSecs / 60)
    seriesHrs += hrs + int(seriesMins / 60)
    seriesSecs %= 60; seriesMins %= 60
    #
    totalSecs += secs
    totalMins += mins + int(totalSecs / 60)
    totalHrs += hrs + int(totalMins / 60)
    totalSecs %= 60; totalMins %= 60
    #
    episodeDuration = sprintf ("%02d:%02d:%02d", hrs, mins, secs)
    if (episodeDuration == "00:00:00")
        printf ("==> No duration: %d\t%s  %s\n", SERIES_NUMBER, seriesTitle, episodeURL) >> ERROR_FILE
    next
}

# Extract the episode title
/
/ {
    split ($0,fld,"[<>]")
    episodeTitle = fld[3]
    gsub (/&/,"\\&", episodeTitle)
    next
}

# There are a number of variants in the "episodeNumber" string
#
#
Season 1: Episode 1

#
Season 9 Christmas Special: Episode 1

#
#
Series 1: Episode 1

#
Series 1 (Set 1): Episode 1

#
Series 10 (Sets 13 - 14): Episode 1

#
Series 19 (Part One): Episode 1

#
#
Set 1: Episode 1

#
#
Mini Series: Episode 1

#
Miniseries: Episode 1

#
#
Black Widows Season 1: Episode 1

#
#
A Dance to the Music of Time: Episode 1

#
Christmas Special : Episode 1

#
Feature Film: Episode 1

# Extract episode number
/
.*span itemprop="episodeNumber">/ {
    # If show is a Prequel, use "P"
    if (episodeURL ~ /\/prequel/)
        showType = "P"
    # If show is a Miniseries, use "M"
    if (episodeURL ~ /\/miniseries/)
        showType = "M"
    # If we don't know what kind of show it is, use "S"
    if (showType == "")
        showType = "S"
    # Default episodeType to "E"
    episodeType = "E"
    # Default bonus, parttwo, and christmasspecials episodeType to "X"
    if (episodeURL ~ /bonus\/|[0-9]{1,2}parttwo\/|christmasspecial\//)
        episodeType = "X"
    # jamaicainn, newworlds & newtonslaw bonus seasonNumber should be 1
    if (episodeURL ~ /\/jamaicainn\/bonus\/|\/newworlds\/bonus\/|\/newtonslaw\/bonus\// && seasonNumber != 1) {
        split (episodeURL, part, "/")
        printf ("==> Corrected mismatch: https://acorn.tv/%s/%s was series %d\n", \
               part[4], part[5], seasonNumber) >> ERROR_FILE
        seasonNumber = 1
    }
    # Plain christmasspecial, seriesfinale don't increment seasonNumber
    if (episodeURL ~ /\/christmasspecial\/|\/seriesfinale\//) {
        split (episodeURL, part, "/")
        printf ("==> Corrected mismatch: https://acorn.tv/%s/%s was series %d\n", \
               part[4], part[5], seasonNumber) >> ERROR_FILE
        seasonNumber -= 1
    }
    #
    split ($0,fld,"[<>]")
    episodeNumber = fld[5]
    # Feature films don't have episodes
    if (episodeURL ~ /\/featurefilm\//)
        next
    # neither should single episode series
    if (totalEpisodes == 1)
        next
    # The season number should match that in the URL
    # Birds of a Feather, Doc Martin, Murdoch, Poirot, Rebus, Vera, and others have problems
    if (episodeURL ~ \
           /\/series[0-9]{1,2}\/|[0-9]{1,2}bonus\/|[0-9]{1,2}parttwo\/|\/murdoch\/season[0-9]{1,2}/) {
        split (episodeURL, part, "/")
        URLseasonNumber = part[5]
        sub (/bonus/,"",URLseasonNumber)
        # Foyle's War has sets: foyleswar/series9set8bonus/
        sub (/set[0-9]/,"",URLseasonNumber)
        # Midsomer Murders has two parts: midsomermurders/series19parttwo/
        sub (/parttwo/,"",URLseasonNumber)
        sub (/christmasspecial/,"",URLseasonNumber)
        sub (/[[:alpha:]]*/,"",URLseasonNumber)
        if (URLseasonNumber != seasonNumber) {
            printf ("==> Corrected mismatch: https://acorn.tv/%s/%s was series %d\n", \
               part[4], part[5], seasonNumber) >> ERROR_FILE
            seasonNumber = URLseasonNumber
        }
    }
    if ((episodeNumber + 0) == 0)
        print "==> Episode number is 00: " episodeURL >> ERROR_FILE
    printf ("%d\t=HYPERLINK(\"%s\";\"%s, %s%02d%s%02d, %s\"\)\t\t\t%s\n", \
        SERIES_NUMBER, episodeURL, seriesTitle, showType, seasonNumber, episodeType, episodeNumber, \
        episodeTitle, episodeDuration) >> EPISODE_INFO_FILE
    showType = ""
    next
}

/-- Viewers Also Watched --/ {
    print (episodeLinesFound == 1 ? "=0" : "=" numEpisodesStr) >> NUM_EPISODES_FILE
    if (description == "")
        printf ("==> No description: %d\t%s\n", SERIES_NUMBER, seriesTitle) >> ERROR_FILE
    description = ""
    #
    seriesDuration = sprintf ("%02d:%02d:%02d", seriesHrs, seriesMins, seriesSecs)
    print seriesDuration >> DURATION_FILE
    if (seriesDuration == "00:00:00")
        printf ("==> No duration: %d\t%s\n", SERIES_NUMBER, seriesTitle) >> ERROR_FILE
    seriesSecs = 0
    seriesMins = 0
    seriesHrs = 0
    seriesDuration = ""
    #
    SERIES_NUMBER += 1
}

END {
    # Return the total duration of all series
    printf ("%02d:%02d:%02d\n", totalHrs, totalMins, totalSecs)
}


RJH: test of RCS keyword munging

RJH: test of RCS keyword munging

#
# This script is used by the makefile in building util_version.f.
# It expects on standard input lines containing
#
# a CVS ID    -> produce a write statement with the ID
# module name -> produce a write statement for the module name
# other       -> produce nothing
#
# $Id: ids.awk,v 1.3 1995-02-02 20:21:59 d3g681 Exp $

BEGIN {
		underline = "---------------------------------------------------------";
		printf("      subroutine util_version\n");
		printf("      write(6,*)\n");
		printf("      write(6,*) ' Software version information'\n");
		printf("      write(6,*) ' ----------------------------'\n");
}

/^module/			{
				 len = length($0);
				 printf("      write(6,*)\n");
				 printf("      write(6,*) ' %s'\n",$0);
				 printf("      write(6,*) ' %s'\n",substr(underline,1,len));
				}

/\$\I\d: [^\n]*Exp \$/		{
				 i = index($0, "$Id: ") + 5;
				 j = index($0, "Exp $") - 1;
				 n = j - i + 1;
				 if (n > (72 - 20)) n = 72 - 20;
				 printf("      write(6,*) ' %s'\n",substr($0,i,n));
				}

END {
		printf("      write(6,*)\n");
		printf("      call util_flush(6)\n");
		printf("      end\n");
}

#
# This script is used by the makefile in building util_version.f.
# It expects on standard input lines containing
#
# a CVS ID    -> produce a write statement with the ID
# module name -> produce a write statement for the module name
# other       -> produce nothing
#
# $Id: ids.awk,v 1.2 1995-02-02 18:09:38 d3g681 Exp $

BEGIN {
		underline = "---------------------------------------------------------";
		printf("      subroutine util_version\n");
		printf("      write(6,*)\n");
		printf("      write(6,*) ' Software version information'\n");
		printf("      write(6,*) ' ----------------------------'\n");
}

/^module/			{
				 len = length($0);
				 printf("      write(6,*)\n");
				 printf("      write(6,*) ' %s'\n",$0);
				 printf("      write(6,*) ' %s'\n",substr(underline,1,len));
				}

/\$Id: ids.awk,v 1.2 1995-02-02 18:09:38 d3g681 Exp $/		{
				 i = index($0, "$Id: ") + 5;
				 j = index($0, "Exp $") - 1;
				 n = j - i + 1;
				 if (n > (72 - 20)) n = 72 - 20;
				 printf("      write(6,*) ' %s'\n",substr($0,i,n));
				}

END {
		printf("      write(6,*)\n");
		printf("      call util_flush(6)\n");
		printf("      end\n");
}


Working prototype

Working prototype

#! /bin/bash -

#########################
#
# Name: rewrite-to-bash-case.bash
#
# Purpose: Rewrite CMIP5-tables to bash case selection
#
# Usage: ./rewrite-to-bash-case.bash
#
# Revision history: 2016-02-25  --  Script created, Martin Evaldsson, Rossby Centre
#
# Contact persons:  martin.evaldsson@smhi.se
#
########################
function ceil(x){return x%1 ? int(x)+1 : x}
function floor(x){return x%1 ? int(x) : x}

function repeat( str, n,    rep, i )
{
    for( ; i<n; i++ )
        rep = rep str
    return rep
}

function pre_ws( str, maxws )
{
    prews=floor(maxws/2)
    str_ws = repeat(" ", prews, i, ii)
    return str_ws
}

function post_ws( str, maxws )
{
    postws=no_ws - length(str)-3
    str_ws = repeat(" ", postws, i, ii)
    return str_ws
}

BEGIN {
  newvar="none"
  no_ws=20
  ws=repeat(" ", no_ws, rep, i)
  print "case"
}

{
    #print newvar"  "$1",  --   "$0
    if ($1 == "variable_entry:")
    {
        if (newvar == "true")
            printf "%s", ";;\n\n"
        prews=pre_ws($2, length(ws))
        postws=post_ws($2, length(ws))
        printf "%s", "  "$2")"postws
        newvar="true"
    }
    if ($1 == "standard_name:" && newvar=="true")
    {
        printf "%s", "standard_name='"$2"'"
    }

    attrs[0] = "long_name:"
    attrs[1] = "units:"
    attrs[2] = "cell_methods:"

    for (attr in attrs)
    {
        if ($1 == attrs[attr] && newvar=="true")
        {
            output=substr($0,index($2,$9))
            sub(":[[:space:]]*", "='", output)
            printf "%s", "\n"ws""output"'"
        }
    }
}

END {
  print "\nesac"
}


Support indent 2 in checkFile.

Support indent 2 in checkFile.

#!/bin/gawk
# Checks that a file is valid.

function error(fname, linum, msg) {
    printf "%s:%d: %s\n", fname, linum, msg;
    if (0) print; # for debug
}
function _matchFile(fname) {
    return fname ~ "/mondrian/" \
       || fname ~ "/org/olap4j/" \
       || fname ~ "/aspen/" \
       || fname ~ "/farrago/" \
       || fname ~ "/fennel/" \
       || fname ~ "/extensions/" \
       || fname ~ "/com/sqlstream/" \
       || fname ~ "/linq4j/" \
       || fname ~ "/lambda/" \
       || fname ~ "/optiq/" \
       || strict > 0;
}
function _isCpp(fname) {
    return fname ~ /\.(cpp|h)$/;
}
function _isJava(fname) {
    return fname ~ /\.(java|jj)$/;
}
function _isMondrian(fname) {
    return fname ~ /mondrian/;
}
function _isOlap4j(fname) {
    return fname ~ "/org/olap4j/";
}
function push(val) {
    switchStack[switchStackLen++] = val;
}
function pop() {
    --switchStackLen
    val = switchStack[switchStackLen];
    delete switchStack[switchStackLen];
    return val;
}
function afterFile() {
    # Compute basename. If fname="/foo/bar/baz.txt" then basename="baz.txt".
    basename = fname;
    gsub(".*/", "", basename);
    gsub(lf, "", lastNonEmptyLine);
    terminator = "// End " basename;
    if (matchFile && (lastNonEmptyLine != terminator)) {
        error(fname, FNR, sprintf("Last line should be %c%s%c", 39, terminator, 39));
    }
}
# Returns whether there are unmatched open parentheses.
# unmatchedOpenParens("if ()") returns false.
# unmatchedOpenParens("if (") returns true.
# unmatchedOpenParens("if (foo) bar(") returns false
function unmatchedOpenParens(s) {
    i = index(s, "(");
    if (i == 0) {
        return 0;
    }
    openCount = 1;
    while (++i <= length(s)) {
        c = substr(s, i, 1);
        if (c == "(") {
            ++openCount;
        }
        if (c == ")") {
            if (--openCount == 0) {
                return 0;
            }
        }
    }
    return 1;
}

function countLeadingSpaces(str) {
    i = 0;
    while (i < length(str) && substr(str, i + 1, 1) == " ") {
        ++i;
    }
    return i;
}

function checkIndent(str) {
    return str % indent == 0;
}

function startsWith(s, p) {
    return length(s) > length(p) \
        && substr(s, 1, length(p)) == p;
}

BEGIN {
    # pre-compute regexp for quotes, linefeed
    apos = sprintf("%c", 39);
    quot = sprintf("%c", 34);
    lf = sprintf("%c", 13);
    pattern = apos "(\\" apos "|[^" apos "])" apos;
    if (0) printf "maxLineLength=%s strict=%s\n", maxLineLength, strict;
}
FNR == 1 {
    if (fname) {
        afterFile();
    }
    fname = FILENAME;
    matchFile = _matchFile(fname);
    isCpp = _isCpp(fname);
    isJava = _isJava(fname);
    mondrian = _isMondrian(fname);
    prevImport = "";
    prevImportGroup = "";
    indent = (fname ~ /lambda/ || fname ~ /linq4j/) ? 2 : 4;
    cindent = 4;

    delete headers;
    headerCount = 0;
    if ($0 ~ /Generated By:JavaCC/ \
        || $0 ~ /This class is generated/) {
        # generated; skip whole file
        nextfile
    } else if (fname ~ /XmlaOlap4jDriverVersion.java/ \
        || fname ~ /package-info.java/ \
        || fname ~ /MondrianOlap4jDriverVersion.java/ \
        || fname ~ /MondrianTestRunner.java/ \
        || fname ~ /MondrianResultPrinter.java/) {
        # generated; does not contain standard header
    } else if (mondrian) {
        headers[headerCount++] = "/\\*$";
        headers[headerCount++] = "// This software is subject to the terms of the Eclipse Public License v1.0$";
        headers[headerCount++] = "// Agreement, available at the following URL:$";
        headers[headerCount++] = "// http://www.eclipse.org/legal/epl-v10.html.$";
        headers[headerCount++] = "// You must accept the terms of that agreement to use this software.$";
        headers[headerCount++] = "//$";
        headers[headerCount++] = "// Copyright (C) [0-9]*-[0-9]* Pentaho$";
    } else if (1) {
        headers[headerCount++] = "/\\*$";
        if (_isOlap4j(fname)) {
            headers[headerCount++] = /^\/\/ \$Id: /;
            headers[headerCount++] = "//";
        }
        headers[headerCount++] = "// Licensed to Julian Hyde under one or more contributor license$";
        headers[headerCount++] = "// agreements. See the NOTICE file distributed with this work for$";
        headers[headerCount++] = "// additional information regarding copyright ownership.$";
        headers[headerCount++] = "//$";
        headers[headerCount++] = "// Julian Hyde licenses this file to you under the Apache License,$";
        headers[headerCount++] = "// Version 2.0 \\(the \"License\"\\); you may not use this file except in$";
        headers[headerCount++] = "// compliance with the License. You may obtain a copy of the License at:$";
        headers[headerCount++] = "//$";
        headers[headerCount++] = "// http://www.apache.org/licenses/LICENSE-2.0$";
        headers[headerCount++] = "//$";
        headers[headerCount++] = "// Unless required by applicable law or agreed to in writing, software$";
        headers[headerCount++] = "// distributed under the License is distributed on an \"AS IS\" BASIS,$";
        headers[headerCount++] = "// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.$";
        headers[headerCount++] = "// See the License for the specific language governing permissions and$";
        headers[headerCount++] = "// limitations under the License.$";
        headers[headerCount++] = "*/$";
    }
}
FNR < headerCount {
    if ($0 !~ headers[FNR - 1]) {
        error(fname, FNR, "Does not match expected header line: " headers[FNR - 1] "\n");
        headerCount = 0; # prevent further errors from this file
    }
}
{
    if (previousLineEndedInCloseBrace > 0) {
        --previousLineEndedInCloseBrace;
    }
    if (previousLineEndedInOpenBrace > 0) {
        --previousLineEndedInOpenBrace;
    }
    if (previousLineWasEmpty > 0) {
        --previousLineWasEmpty;
    }
    s = $0;
    # remove DOS linefeeds
    gsub(lf, "", s);
    # replace strings
    gsub(/"(\\"|[^"\\]|\\[^"])*"/, "string", s);
    # replace single-quoted strings
    gsub(pattern, "string", s);
    # replace {: and :} in .cup files
    if (fname ~ /\.cup$/) {
        gsub(/{:/, "{", s);
        gsub(/:}/, "}", s);
        gsub(/:/, " : ", s);
    }
    if (inComment && s ~ /\*\//) {
        # end of multiline comment "*/"
        inComment = 0;
        gsub(/^.*\*\//, "/* comment */", s);
    } else if (inComment) {
        s = "/* comment */";
    } else if (s ~ /\/\*/ && s !~ /\/\*.*\*\//) {
        # beginning of multiline comment "/*"
        inComment = 1;
        if (strict > 1 && FNR > 1 && s !~ /\/\*\*/ && s !~ /^\/\*/) {
            error(fname, FNR, "Multi-line c-style comment not allowed" s);
        }
        gsub(/\/\*.*$/, "/* comment */", s);
    } else {
        # mask out /* */ comments
        gsub(/\/\*.*\*\//, "/* comment */", s);
    }
    if (mondrian && s ~ /\/\/\$NON-NLS/) {
        error(fname, FNR, "NON-NLS not allowed");
    }
    if (s ~ /\/\/[A-Za-z]/ && strict > 1) {
        if (s ~ /noinspection/) {} # e.g. '//noinspection unchecked'
        else if (s ~ /;$/) {} # e.g. '//int dummy = 0;'
        else if (s ~ /:\/\//) {} # e.g. '// http://'
        else error(fname, FNR, "Need space after //");
    }
    # mask out // comments
    gsub(/\/\/.*$/, "// comment", s);
    # line starts with string or plus?
    if (s ~ /^ *string/ \
        && s !~ /)/)
    {
        stringCol = index(s, "string");
    } else if (s ~ /^ *[+] string/) {
        if (stringCol != 0 && index(s, "+") != stringCol) {
            error(fname, FNR, "String '+' must be aligned with string on line above");
        }
    } else if (s ~ /comment/) {
        # in comment; string target carries forward
    } else {
        stringCol = 0;
    }

    # Is the line indented as expected?
    if (nextIndent > 0) {
        x = countLeadingSpaces(s);
        if (x != nextIndent) {
            error(fname, FNR, "Incorrect indent for first line of arg list");
        }
    }
    nextIndent = -1;
}
/ $/ {
    error(fname, FNR, "Line ends in space");
}
/[\t]/ {
    if (matchFile) {
        error(fname, FNR, "Tab character");
    }
}
/[\r]/ {
    if (matchFile) {
        error(fname, FNR, "Carriage return character (file is in DOS format?)");
    }
}
/./ {
    lastNonEmptyLine = $0;
}
{
    # Rules beyond this point only apply to Java and C++.
    if (!isCpp && !isJava) {
        next;
    }
}

/^package / {
    thisPackage = $2;
}
/^package / && previousLineWasEmpty {
    error(fname, FNR, "'package' declaration must not occur after empty line");
}
/^import / {
    if (previousLineWasEmpty) {
        prevImport = "";
    } else {
        if (!prevImport) {
            error(fname, FNR, "Expected blank line before first import");
        }
    }
    thisImport = $2;
    gsub(/;/, "", thisImport);
    gsub(/\*/, "", thisImport);
    if (thisPackage ~ /^mondrian.*/ && thisImport ~ /^mondrian.*/) {
        importGroup = "a:mondrian";
    } else if (thisPackage ~ /^net.hydromatic.*/ && thisImport ~ /^net.hydromatic.*/) {
        importGroup = "a:net.hydromatic";
    } else if (thisPackage ~ /^org.olap4j.*/ && thisImport ~ /^org.olap4j.*/) {
        importGroup = "a:org.olap4j";
    } else if (thisImport ~ /^static/) {
        importGroup = "z:static";
    } else if (thisImport ~ /^java.*/) {
        if (thisPackage ~ /^org.eigenbase.*/) {
            importGroup = "aa:java";
        } else {
            importGroup = "y:java";
        }
    } else if (thisImport ~ /^junit.*/) {
        importGroup = "b:junit";
    } else if (thisImport ~ /^mondrian.*/) {
        importGroup = "bb:mondrian";
    } else if (thisImport ~ /^org.olap4j.xmla.server.*/) {
        importGroup = "bc:org.olap4j.xmla.server";
    } else if (thisImport ~ /^openjava.*/) {
        importGroup = "cc:openjava";
    } else if (thisImport ~ /^org.apache.*/) {
        importGroup = "c:org.apache";
    } else if (thisImport ~ /^org.eigenbase.*/) {
        importGroup = "d:org.eigenbase";
    } else if (thisImport ~ /^org.olap4j.*/) {
        importGroup = "e:org.olap4j";
    } else {
        importGroup = "f:other";
    }
    if (importGroup != prevImportGroup \
        && prevImportGroup)
    {
        if (!previousLineWasEmpty) {
            error(fname, FNR, "Expected blank line between import groups");
        } else if (prevImportGroup > importGroup) {
            error(fname, FNR, "Import group out of sequence (should precede " \
                substr(prevImportGroup, index(prevImportGroup, ":") + 1) ")");
        }
    } else if (prevImport \
        && prevImport > thisImport \
        && !startsWith(prevImport, thisImport) \
        && !startsWith(thisImport, prevImport))
    {
        error(fname, FNR, "Import out of sorted order");
    }
    prevImport = thisImport;
    prevImportGroup = importGroup;
}
/^\/\/ Copyright .* Pentaho/ && strict > 1 {
    # We assume that '--strict' is only invoked on files currently being
    # edited. Therefore we would expect the copyright to be current.
    if ($0 !~ /-2013/) {
        error(fname, FNR, "copyright is not current");
    }
}
/(static|public|private|protected|final|abstract)/ && !/import/ && strict > 1 {
    # Order of qualifiers: "public/private/protected static final abstract class ..."
    s2 = s;
    gsub(/\(.*$/, "", s2);
    if (s2 ~ /abstract .*final /) {
        error(fname, FNR, "'final' must come before 'abstract'");
    }
    if (s2 ~ /final .*static /) {
        error(fname, FNR, "'static' must come before 'final'");
    }
    if (s2 ~ /abstract .*static /) {
        error(fname, FNR, "'static' must come before 'abstract'");
    }
    if (s2 ~ /static .*(public|protected|private) /) {
        error(fname, FNR, "'public/private/protected' must come before 'static'");
    }
    if (s2 ~ /final .*(public|protected|private) /) {
        error(fname, FNR, "'public/private/protected' must come before 'final'");
    }
    if (s2 ~ /abstract .*(public|protected|private) /) {
        error(fname, FNR, "'public/private/protected' must come before 'abstract'");
    }
}
/^$/ {
    if (matchFile && previousLineEndedInOpenBrace) {
        error(fname, FNR, "Empty line following open brace");
    }
}
/^ +}( catch| finally| while|[;,)])/ ||
/^ +}$/ {
    if (matchFile && previousLineWasEmpty) {
        error(fname, FNR - 1, "Empty line before close brace");
    }
}
s ~ /\<if\>.*;$/ {
    if (!matchFile) {}
    else {
        error(fname, FNR, "if followed by statement on same line");
    }
}
s ~ /\<(if) *\(/ {
    if (!matchFile) {
    } else if (s !~ /\<(if) /) {
        error(fname, FNR, "if must be followed by space");
    } else if (s ~ / else if /) {
    } else if (s ~ /^#if /) {
    } else if (!checkIndent(s)) {
        error(fname, FNR, "if must be correctly indented");
    }
}
s ~ /\<(while) *\(/ {
    if (!matchFile) {
    } else if (s !~ /\<(while) /) {
        error(fname, FNR, "while must be followed by space");
    } else if (s ~ /} while /) {
    } else if (!checkIndent(s)) {
        error(fname, FNR, "while must be correctly indented");
    }
}
s ~ /\<(for|switch|synchronized|} catch) *\(/ {
    if (!matchFile) {}
    else if (!checkIndent(s)) {
        error(fname, FNR, "for/switch/synchronized/catch must be correctly indented");
    } else if (s !~ /\<(for|switch|synchronized|} catch) /) {
        error(fname, FNR, "for/switch/synchronized/catch must be followed by space");
    }
}
s ~ /\<(if|while|for|switch|catch)\>/ {
    # Check single-line if statements, such as
    #   if (condition) return;
    # We recognize such statements because there are equal numbers of open and
    # close parentheses.
    opens = s;
    gsub(/[^(]/, "", opens);
    closes = s;
    gsub(/[^)]/, "", closes);
    if (!matchFile) {
    } else if (s ~ /{( *\/\/ comment)?$/) {
        # lines which end with { and optional comment are ok
    } else if (s ~ /{.*\\$/ && isCpp) {
        # lines which end with backslash are ok in c++ macros
    } else if (s ~ /} while/) {
        # lines like "} while (foo);" are ok
    } else if (s ~ /^#/) {
        # lines like "#if 0" are ok
    } else if (s ~ /if \(true|false\)/) {
        # allow "if (true)" and "if (false)" because they are
        # used for commenting
    } else if (!unmatchedOpenParens(s)  \
               && length($0) != 79      \
               && length($0) != 80)
    {
        error(fname, FNR, "single-line if/while/for/switch/catch must end in {");
    }
}
s ~ /[[:alnum:]]\(/ &&
s !~ /\<(if|while|for|switch|assert)\>/ {
    ss = s;
    while (match(ss, /[[:alnum:]]\(/)) {
        ss = substr(ss, RSTART + RLENGTH - 1);
        parens = ss;
        gsub(/[^()]/, "", parens);
        while (substr(parens, 1, 2) == "()") {
            parens = substr(parens, 3);
        }
        opens = parens;
        gsub(/[^(]/, "", opens);
        closes = parens;
        gsub(/[^)]/, "", closes);
        if (length(opens) > length(closes)) {
            if (ss ~ /,$/) {
                bras = ss;
                gsub(/[^<]/, "", bras);
                kets = ss;
                gsub(/->/, "", kets);
                gsub(/[^>]/, "", kets);
                if (length(bras) > length(kets)) {
                    # Ignore case like 'for (Map.Entry<Foo,{nl} Bar> entry : ...'
                } else if (s ~ / for /) {
                    # Ignore case like 'for (int i = 1,{nl} j = 2; i < j; ...'
                } else if (indent == cindent) {
                    error(                                              \
                        fname, FNR,                                     \
                        "multi-line parameter list should start with newline");
                    break;
                }
            } else if (s ~ /[;(]( *\\)?$/) {
                # If open paren is at end of line (with optional backslash
                # for macros), we're fine.
            } else if (s ~ /@.*\({/) {
                # Ignore Java annotations.
            } else if (indent == cindent) {
                error(                                                  \
                    fname, FNR,                                         \
                    "Open parenthesis should be at end of line (function call spans several lines)");
                break;
            }
        }
        ss = substr(ss, 2); # remove initial "("
    }
}
s ~ /\<switch\>/ {
    push(switchCol);
    switchCol = index($0, "switch");
}
s ~ /{/ {
    braceCol = index($0, "{");
    if (braceCol == switchCol) {
        push(switchCol);
    }
}
s ~ /}/ {
    braceCol = index($0, "}");
    if (braceCol == switchCol) {
        switchCol = pop();
    }
}
s ~ /\<(case|default)\>/ {
    caseDefaultCol = match($0, /case|default/);
    if (!matchFile) {}
    else if (caseDefaultCol != switchCol) {
        error(fname, FNR, "case/default must be aligned with switch");
    }
}
s ~ /\<assert\>/ {
    if (!matchFile) {}
    else if (isCpp) {} # rule only applies to java
    else if (!checkIndent(s)) {
        error(fname, FNR, "assert must be correctly indented");
    } else if (s !~ /\<assert /) {
        error(fname, FNR, "assert must be followed by space");
    }
}
s ~ /\<return\>/ {
    if (!matchFile) {}
    else if (isCpp && s ~ /^#/) {
        # ignore macros
    } else if (!checkIndent(s)) {
        error(fname, FNR, "return must be correctly indented");
    } else if (s !~ /\<return[ ;]/ && s !~ /\<return$/) {
        error(fname, FNR, "return must be followed by space or ;");
    }
}
s ~ /\<throw\>/ {
    if (!matchFile) {}
    else if (isCpp) {
        # cannot yet handle C++ cases like 'void foo() throw(int)'
    } else if (!checkIndent(s)) {
        error(fname, FNR, "throw must be correctly indented");
    } else if (s !~ /\<throw / && s !~ /\<throw$/) {
        error(fname, FNR, "throw must be followed by space");
    }
}
s ~ /\<else\>/ {
    if (!matchFile) {}
    else if (isCpp && s ~ /^# *else$/) {} # ignore "#else"
    else if (!checkIndent(s)) {
        error(fname, FNR, "else must be correctly indented");
    } else if (s !~ /^ +} else (if |{$|{ *\/\/|{ *\/\*)/) {
        error(fname, FNR, "else must be preceded by } and followed by { or if");
    }
}
s ~ /\<do\>/ {
    if (!matchFile) {}
    else if (!checkIndent(s)) {
        error(fname, FNR, "do must be correctly indented");
    } else if (s !~ /^ *do {/) {
        error(fname, FNR, "do must be followed by space {");
    }
}
s ~ /\<try\>/ {
    if (!matchFile) {}
    else if (!checkIndent(s)) {
        error(fname, FNR, "try must be correctly indented");
    } else if (s !~ /^ +try {/) {
        error(fname, FNR, "try must be followed by space {");
    }
}
s ~ /\<catch\>/ {
    if (!matchFile) {}
    else if (!checkIndent(s)) {
        error(fname, FNR, "catch must be correctly indented");
    } else if (s !~ /^ +} catch /) {
        error(fname, FNR, "catch must be preceded by } and followed by space");
    }
}
s ~ /\<finally\>/ {
    if (!matchFile) {}
    else if (!checkIndent(s)) {
        error(fname, FNR, "finally must be correctly indented");
    } else if (s !~ /^ +} finally {/) {
        error(fname, FNR, "finally must be preceded by } and followed by space {");
    }
}
s ~ /\($/ {
    nextIndent = countLeadingSpaces(s) + cindent;
    if (s ~ / (if|while) .*\(.*\(/ && indent == cindent) {
        nextIndent += indent;
    }
}
match(s, /([]A-Za-z0-9()])(+|-|\*|\^|\/|%|=|==|+=|-=|\*=|\/=|>=|<=|!=|&|&&|\||\|\||^|\?|:) *[A-Za-z0-9(]/, a) {
    # < and > are not handled here - they have special treatment below
    if (!matchFile) {}
#    else if (s ~ /<.*>/) {} # ignore templates
    else if (a[2] == "-" && s ~ /\(-/) {} # ignore case "foo(-1)"
    else if (a[2] == "-" && s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e-5
    else if (a[2] == "+" && s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e+5
    else if (a[2] == ":" && s ~ /(case.*|default):$/) {} # ignore e.g. "case 5:"
    else if (isCpp && s ~ /[^ ][*&]/) {} # ignore e.g. "Foo* p;" in c++ - debatable
    else if (isCpp && s ~ /\<operator.*\(/) {} # ignore e.g. "operator++()" in c++
    else if (isCpp && a[2] == "/" && s ~ /#include/) {} # ignore e.g. "#include <x/y.hpp>" in c++
    else {
        error(fname, FNR, "operator '" a[2] "' must be preceded by space");
    }
}
match(s, /([]A-Za-z0-9() ] *)(+|-|\*|\^|\/|%|=|==|+=|-=|\*=|\/=|>=|<=|!=|&|&&|\||\|\||^|\?|:|,)[A-Za-z0-9(]/, a) {
    if (!matchFile) {}
#    else if (s ~ /<.*>/) {} # ignore templates
    else if (a[2] == "-" && s ~ /(\(|return |case |= )-/) {} # ignore prefix -
    else if (a[2] == ":" && s ~ /(case.*|default):$/) {} # ignore e.g. "case 5:"
    else if (s ~ /, *-/) {} # ignore case "foo(x, -1)"
    else if (s ~ /-[^ ]/ && s ~ /[^A-Za-z0-9] -/) {} # ignore case "x + -1" but not "x -1" or "3 -1"
    else if (a[2] == "-" && s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e-5
    else if (a[2] == "+" && s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e+5
    else if (a[2] == "*" && isCpp && s ~ /\*[^ ]/) {} # ignore e.g. "Foo *p;" in c++
    else if (a[2] == "&" && isCpp && s ~ /&[^ ]/) {} # ignore case "foo(&x)" in c++
    else if (isCpp && s ~ /\<operator[^ ]+\(/) {} # ignore e.g. "operator++()" in c++
    else if (isCpp && a[2] == "/" && s ~ /#include/) {} # ignore e.g. "#include <x/y.hpp>" in c++
    else if (strict < 2 && fname ~ /(fennel)/ && a[1] = ",") {} # not enabled yet
    else {
        error(fname, FNR, "operator '" a[2] "' must be followed by space");
    }
}
match(s, /( )(,)/, a) {
    # (, < and > are not handled here - they have special treatment below
    if (!matchFile) {}
    else {
        error(fname, FNR, "operator '" a[2] "' must not be preceded by space");
    }
}
match(s, / (+|-|\*|\/|==|>=|<=|!=|<<|<<<|>>|&|&&|\|\||\?|:)$/, a) || \
match(s, /(\.|->)$/, a) {
    if (strict < 2 && fname ~ /(aspen)/ && a[1] != ":") {} # not enabled yet
    else if (strict < 2 && fname ~ /(fennel|farrago|aspen)/ && a[1] = "+") {} # not enabled yet
    else if (a[1] == ":" && s ~ /(case.*|default):$/) {
        # ignore e.g. "case 5:"
    } else if ((a[1] == "*" || a[1] == "&") && isCpp && s ~ /^[[:alnum:]:_ ]* [*&]$/) {
        # ignore e.g. "const int *\nClass::Subclass2::method(int x)"
    } else {
        error(fname, FNR, "operator '" a[1] "' must not be at end of line");
    }
}
match(s, /^ *(=) /, a) {
    error(fname, FNR, "operator '" a[1] "' must not be at start of line");
}
match(s, /([[:alnum:]~]+)( )([(])/, a) {
    # (, < and > are not handled here - they have special treatment below
    if (!matchFile) {}
    else if (isJava && a[1] ~ /\<(if|while|for|catch|switch|case|return|throw|synchronized|assert)\>/) {}
    else if (isCpp && a[1] ~ /\<(if|while|for|catch|switch|case|return|throw|operator|void|PBuffer)\>/) {}
    else if (isCpp && s ~ /^#define /) {}
    else {
        error(fname, FNR, "there must be no space before '" a[3] "' in fun call or fun decl");
    }
}
s ~ /\<[[:digit:][:lower:]][[:alnum:]_]*</ {
    # E.g. "p<" but not "Map<"
    if (!matchFile) {}
    else if (isCpp) {} # in C++ 'xyz<5>' could be a template
    else {
        error(fname, FNR, "operator '<' must be preceded by space");
    }
}
s ~ /\<[[:digit:][:lower:]][[:alnum:]_]*>/ {
    # E.g. "g>" but not "String>" as in "List<String>"
    if (!matchFile) {}
    else if (isCpp) {} # in C++ 'xyz<int>' could be a template
    else {
        error(fname, FNR, "operator '>' must be preceded by space");
    }
}
match(s, /<([[:digit:][:lower:]][[:alnum:].]*)\>/, a) {
    if (!matchFile) {}
    else if (isCpp) {
        # in C++, template and include generate too many false positives
    } else if (isJava && a[1] ~ /(int|char|long|boolean|byte|double|float)/) {
        # Allow e.g. 'List<int[]>'
    } else if (isJava && a[1] ~ /^[[:lower:]]+\./) {
        # Allow e.g. 'List<java.lang.String>'
    } else {
        error(fname, FNR, "operator '<' must be followed by space");
    }
}
match(s, /^(.*[^-])>([[:digit:][:lower:]][[:alnum:]]*)\>/, a) {
    if (!matchFile) {}
    else if (isJava && a[1] ~ /.*\.<.*/) {
        # Ignore 'Collections.<Type>member'
    } else {
        error(fname, FNR, "operator '>' must be followed by space");
    }
}
s ~ /[[(] / {
    if (!matchFile) {}
    else if (s ~ /[[(] +\\$/) {} # ignore '#define foo(   \'
    else {
        error(fname, FNR, "( or [ must not be followed by space");
    }
}
s ~ / [])]/ {
    if (!matchFile) {}
    else if (s ~ /^ *\)/ && previousLineEndedInCloseBrace) {} # ignore "bar(new Foo() { } );"
    else {
        error(fname, FNR, ") or ] must not be followed by space");
    }
}
s ~ /}/ {
    if (!matchFile) {}
    else if (s !~ /}( |;|,|$|\)|\.)/) {
        error(fname, FNR, "} must be followed by space");
    } else if (s !~ /^ *}/) {
        # not at start of line - ignore
    } else if (!checkIndent(s)) {
        error(fname, FNR, "} must be correctly indented");
    }
}
$0 ~ /\* @param [A-Za-z0-9_]+$/ && strict > 1 {
    error(fname, FNR, "Empty javadoc param");
}
$0 ~ /\* @return *$/ && strict > 1 {
    error(fname, FNR, "Empty javadoc return");
}
s ~ /{/ {
    if (!matchFile) {}
    else if (s ~ /(\]\)?|=) *{/) {} # ignore e.g. "(int[]) {1, 2}" or "int[] x = {1, 2}"
    else if (s ~ /\({/) {} # ignore e.g. @SuppressWarnings({"unchecked"})
    else if (s ~ /{ *(\/\/|\/\*)/) {} # ignore e.g. "do { // a comment"
    else if (s ~ / \{\}$/) {} # ignore e.g. "Constructor() {}"
    else if (s ~ / },$/) {} # ignore e.g. "{ yada },"
    else if (s ~ / };$/) {} # ignore e.g. "{ yada };"
    else if (s ~ / \{\};$/) {} # ignore e.g. "template <> class Foo<int> {};"
    else if (s ~ / },? *\/\/.*$/) {} # ignore e.g. "{ yada }, // comment"
    else if (s ~ /\\$/) {} # ignore multiline macros
    else if (s ~ /{}/) { # e.g. "Constructor(){}"
        error(fname, FNR, "{} must be preceded by space and at end of line");
    } else if (isCpp && s ~ /{ *\\$/) {
        # ignore - "{" can be followed by "\" in c macro
    } else if (s !~ /{$/) {
        error(fname, FNR, "{ must be at end of line");
    } else if (s !~ /(^| ){/) {
        error(fname, FNR, "{ must be preceded by space or at start of line");
    } else {
        opens = s;
        gsub(/[^(]/, "", opens);
        closes = s;
        gsub(/[^)]/, "", closes);
        if (0 && strict < 2 && fname ~ /aspen/) {} # not enabled
        else if (length(closes) > length(opens) && indent == cindent) {
            error(fname, FNR, "Open brace should be on new line (function call/decl spans several lines)");
        }
    }
}
s ~ /(^| )(class|interface|enum) / ||
s ~ /(^| )namespace / && isCpp {
    if (isCpp && s ~ /;$/) {} # ignore type declaration
    else {
        classDeclStartLine = FNR;
        t = s;
        gsub(/.*(class|interface|enum|namespace) /, "", t);
        gsub(/ .*$/, "", t);
        if (s ~ /template/) {
            # ignore case "template <class INSTCLASS> static void foo()"
            classDeclStartLine = 0;
        } else if (t ~ /[[:upper:]][[:upper:]][[:upper:]][[:upper:]]/     \
            && t !~ /LRU/ \
            && t !~ /WAL/ \
            && t !~ /classUUID/ \
            && t !~ /classSQLException/ \
            && t !~ /BBRC/ \
            && t !~ /_/ \
            && t !~ /EncodedSqlInterval/)
        {
            error(fname, FNR, "Class name " t " has consecutive uppercase letters");
        }
    }
}
s ~ / throws\>/ {
    if (s ~ /\(/) {
        funDeclStartLine = FNR;
    } else {
        funDeclStartLine = FNR - 1;
    }
}
length($0) > maxLineLength                      \
&& $0 !~ /@(throws|see|link)/                   \
&& $0 !~ /\$Id: /                               \
&& $0 !~ /^import /                             \
&& $0 !~ /http:/                                \
&& $0 !~ /https:/                               \
&& $0 !~ /\/\/ Expect "/                        \
&& s !~ /^ *(\+ |<< |: |\?)?string\)*[;,]?$/ {
    error( \
        fname, \
        FNR, \
        "Line length (" length($0) ") exceeds " maxLineLength " chars");
}
/}$/ {
    previousLineEndedInCloseBrace = 2;
}
/;$/ {
    funDeclStartLine = 0;
}
/{$/ {
    # Ignore open brace if it is part of class or interface declaration.
    if (classDeclStartLine) {
        if (classDeclStartLine < FNR \
            && $0 !~ /^ *{$/ \
            && indent == cindent)
        {
            error(fname, FNR, "Open brace should be on new line (class decl spans several lines)");
        }
        classDeclStartLine = 0;
    } else {
        previousLineEndedInOpenBrace = 2;
    }
    if (funDeclStartLine) {
        if (funDeclStartLine < FNR \
            && $0 !~ /^ *{$/)
        {
            if (strict < 2 && fname ~ /aspen/) {} # not enabled
            else if (cindent != indent) {}
            else error(fname, FNR, "Open brace should be on new line (function decl spans several lines)");
        }
        funDeclStartLine = 0;
    }
}
/^$/ {
    previousLineWasEmpty = 2;
}
{
    next;
}
END {
    afterFile();
}

# End checkFile.awk

#!/bin/gawk
# Checks that a file is valid.

function error(fname, linum, msg) {
    printf "%s:%d: %s\n", fname, linum, msg;
    if (0) print; # for debug
}
function _matchFile(fname) {
    return fname ~ "/mondrian/" \
       || fname ~ "/org/olap4j/" \
       || fname ~ "/aspen/" \
       || fname ~ "/farrago/" \
       || fname ~ "/fennel/" \
       || fname ~ "/extensions/" \
       || fname ~ "/com/sqlstream/" \
       || fname ~ "/linq4j/" \
       || fname ~ "/optiq/" \
       || strict > 0;
}
function _isCpp(fname) {
    return fname ~ /\.(cpp|h)$/;
}
function _isJava(fname) {
    return fname ~ /\.(java|jj)$/;
}
function _isMondrian(fname) {
    return fname ~ /mondrian/;
}
function _isOlap4j(fname) {
    return fname ~ "/org/olap4j/";
}
function push(val) {
    switchStack[switchStackLen++] = val;
}
function pop() {
    --switchStackLen
    val = switchStack[switchStackLen];
    delete switchStack[switchStackLen];
    return val;
}
function afterFile() {
    # Compute basename. If fname="/foo/bar/baz.txt" then basename="baz.txt".
    basename = fname;
    gsub(".*/", "", basename);
    gsub(lf, "", lastNonEmptyLine);
    terminator = "// End " basename;
    if (matchFile && (lastNonEmptyLine != terminator)) {
        error(fname, FNR, sprintf("Last line should be %c%s%c", 39, terminator, 39));
    }
}
# Returns whether there are unmatched open parentheses.
# unmatchedOpenParens("if ()") returns false.
# unmatchedOpenParens("if (") returns true.
# unmatchedOpenParens("if (foo) bar(") returns false
function unmatchedOpenParens(s) {
    i = index(s, "(");
    if (i == 0) {
        return 0;
    }
    openCount = 1;
    while (++i <= length(s)) {
        c = substr(s, i, 1);
        if (c == "(") {
            ++openCount;
        }
        if (c == ")") {
            if (--openCount == 0) {
                return 0;
            }
        }
    }
    return 1;
}

function countLeadingSpaces(str) {
    i = 0;
    while (i < length(str) && substr(str, i + 1, 1) == " ") {
        ++i;
    }
    return i;
}

function startsWith(s, p) {
    return length(s) > length(p) \
        && substr(s, 1, length(p)) == p;
}

BEGIN {
    # pre-compute regexp for quotes, linefeed
    apos = sprintf("%c", 39);
    quot = sprintf("%c", 34);
    lf = sprintf("%c", 13);
    pattern = apos "(\\" apos "|[^" apos "])" apos;
    if (0) printf "maxLineLength=%s strict=%s\n", maxLineLength, strict;
}
FNR == 1 {
    if (fname) {
        afterFile();
    }
    fname = FILENAME;
    matchFile = _matchFile(fname);
    isCpp = _isCpp(fname);
    isJava = _isJava(fname);
    mondrian = _isMondrian(fname);
    prevImport = "";
    prevImportGroup = "";

    delete headers;
    headerCount = 0;
    if ($0 ~ /Generated By:JavaCC/ \
        || $0 ~ /This class is generated/) {
        # generated; skip whole file
        nextfile
    } else if (fname ~ /XmlaOlap4jDriverVersion.java/ \
        || fname ~ /package-info.java/ \
        || fname ~ /MondrianOlap4jDriverVersion.java/ \
        || fname ~ /MondrianTestRunner.java/ \
        || fname ~ /MondrianResultPrinter.java/) {
        # generated; does not contain standard header
    } else if (mondrian) {
        headers[headerCount++] = "/\\*$";
        headers[headerCount++] = "// This software is subject to the terms of the Eclipse Public License v1.0$";
        headers[headerCount++] = "// Agreement, available at the following URL:$";
        headers[headerCount++] = "// http://www.eclipse.org/legal/epl-v10.html.$";
        headers[headerCount++] = "// You must accept the terms of that agreement to use this software.$";
        headers[headerCount++] = "//$";
        headers[headerCount++] = "// Copyright (C) [0-9]*-[0-9]* Pentaho$";
    } else if (1) {
        headers[headerCount++] = "/\\*$";
        if (_isOlap4j(fname)) {
            headers[headerCount++] = /^\/\/ \$Id: /;
            headers[headerCount++] = "//";
        }
        headers[headerCount++] = "// Licensed to Julian Hyde under one or more contributor license$";
        headers[headerCount++] = "// agreements. See the NOTICE file distributed with this work for$";
        headers[headerCount++] = "// additional information regarding copyright ownership.$";
        headers[headerCount++] = "//$";
        headers[headerCount++] = "// Julian Hyde licenses this file to you under the Apache License,$";
        headers[headerCount++] = "// Version 2.0 \\(the \"License\"\\); you may not use this file except in$";
        headers[headerCount++] = "// compliance with the License. You may obtain a copy of the License at:$";
        headers[headerCount++] = "//$";
        headers[headerCount++] = "// http://www.apache.org/licenses/LICENSE-2.0$";
        headers[headerCount++] = "//$";
        headers[headerCount++] = "// Unless required by applicable law or agreed to in writing, software$";
        headers[headerCount++] = "// distributed under the License is distributed on an \"AS IS\" BASIS,$";
        headers[headerCount++] = "// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.$";
        headers[headerCount++] = "// See the License for the specific language governing permissions and$";
        headers[headerCount++] = "// limitations under the License.$";
        headers[headerCount++] = "*/$";
    }
}
FNR < headerCount {
    if ($0 !~ headers[FNR - 1]) {
        error(fname, FNR, "Does not match expected header line: " headers[FNR - 1] "\n");
        headerCount = 0; # prevent further errors from this file
    }
}
{
    if (previousLineEndedInCloseBrace > 0) {
        --previousLineEndedInCloseBrace;
    }
    if (previousLineEndedInOpenBrace > 0) {
        --previousLineEndedInOpenBrace;
    }
    if (previousLineWasEmpty > 0) {
        --previousLineWasEmpty;
    }
    s = $0;
    # remove DOS linefeeds
    gsub(lf, "", s);
    # replace strings
    gsub(/"(\\"|[^"\\]|\\[^"])*"/, "string", s);
    # replace single-quoted strings
    gsub(pattern, "string", s);
    # replace {: and :} in .cup files
    if (fname ~ /\.cup$/) {
        gsub(/{:/, "{", s);
        gsub(/:}/, "}", s);
        gsub(/:/, " : ", s);
    }
    if (inComment && $0 ~ /\*\//) {
        # end of multiline comment "*/"
        inComment = 0;
        gsub(/^.*\*\//, "/* comment */", s);
    } else if (inComment) {
        s = "/* comment */";
    } else if ($0 ~ /\/\*/ && $0 !~ /\/\*.*\*\//) {
        # beginning of multiline comment "/*"
        inComment = 1;
        gsub(/\/\*.*$/, "/* comment */", s);
        if (strict > 1 && FNR > 1 && $0 !~ /\/\*\*/ && $0 !~ /^\/\*/) {
            error(fname, FNR, "Multi-line c-style comment not allowed");
        }
    } else {
        # mask out /* */ comments
        gsub(/\/\*.*\*\//, "/* comment */", s);
    }
    if (mondrian && s ~ /\/\/\$NON-NLS/) {
        error(fname, FNR, "NON-NLS not allowed");
    }
    if (s ~ /\/\/[A-Za-z]/ && strict > 1) {
        if (s ~ /noinspection/) {} # e.g. '//noinspection unchecked'
        else if (s ~ /;$/) {} # e.g. '//int dummy = 0;'
        else if (s ~ /:\/\//) {} # e.g. '// http://'
        else error(fname, FNR, "Need space after //");
    }
    # mask out // comments
    gsub(/\/\/.*$/, "// comment", s);
    # line starts with string or plus?
    if (s ~ /^ *string/ \
        && s !~ /)/)
    {
        stringCol = index(s, "string");
    } else if (s ~ /^ *[+] string/) {
        if (stringCol != 0 && index(s, "+") != stringCol) {
            error(fname, FNR, "String '+' must be aligned with string on line above");
        }
    } else if (s ~ /comment/) {
        # in comment; string target carries forward
    } else {
        stringCol = 0;
    }

    # Is the line indented as expected?
    if (nextIndent > 0) {
        indent = countLeadingSpaces(s);
        if (indent != nextIndent) {
            error(fname, FNR, "Incorrect indent for first line of arg list");
        }
    }
    nextIndent = -1;
}
/ $/ {
    error(fname, FNR, "Line ends in space");
}
/[\t]/ {
    if (matchFile) {
        error(fname, FNR, "Tab character");
    }
}
/[\r]/ {
    if (matchFile) {
        error(fname, FNR, "Carriage return character (file is in DOS format?)");
    }
}
/./ {
    lastNonEmptyLine = $0;
}
{
    # Rules beyond this point only apply to Java and C++.
    if (!isCpp && !isJava) {
        next;
    }
}

/^package / {
    thisPackage = $2;
}
/^package / && previousLineWasEmpty {
    error(fname, FNR, "'package' declaration must not occur after empty line");
}
/^import / {
    if (previousLineWasEmpty) {
        prevImport = "";
    } else {
        if (!prevImport) {
            error(fname, FNR, "Expected blank line before first import");
        }
    }
    thisImport = $2;
    gsub(/;/, "", thisImport);
    gsub(/\*/, "", thisImport);
    if (thisPackage ~ /^mondrian.*/ && thisImport ~ /^mondrian.*/) {
        importGroup = "a:mondrian";
    } else if (thisPackage ~ /^net.hydromatic.*/ && thisImport ~ /^net.hydromatic.*/) {
        importGroup = "a:net.hydromatic";
    } else if (thisPackage ~ /^org.olap4j.*/ && thisImport ~ /^org.olap4j.*/) {
        importGroup = "a:org.olap4j";
    } else if (thisImport ~ /^static/) {
        importGroup = "z:static";
    } else if (thisImport ~ /^java.*/) {
        if (thisPackage ~ /^org.eigenbase.*/) {
            importGroup = "aa:java";
        } else {
            importGroup = "y:java";
        }
    } else if (thisImport ~ /^junit.*/) {
        importGroup = "b:junit";
    } else if (thisImport ~ /^mondrian.*/) {
        importGroup = "bb:mondrian";
    } else if (thisImport ~ /^org.olap4j.xmla.server.*/) {
        importGroup = "bc:org.olap4j.xmla.server";
    } else if (thisImport ~ /^openjava.*/) {
        importGroup = "cc:openjava";
    } else if (thisImport ~ /^org.apache.*/) {
        importGroup = "c:org.apache";
    } else if (thisImport ~ /^org.eigenbase.*/) {
        importGroup = "d:org.eigenbase";
    } else if (thisImport ~ /^org.olap4j.*/) {
        importGroup = "e:org.olap4j";
    } else {
        importGroup = "f:other";
    }
    if (importGroup != prevImportGroup \
        && prevImportGroup)
    {
        if (!previousLineWasEmpty) {
            error(fname, FNR, "Expected blank line between import groups");
        } else if (prevImportGroup > importGroup) {
            error(fname, FNR, "Import group out of sequence (should precede " \
                substr(prevImportGroup, index(prevImportGroup, ":") + 1) ")");
        }
    } else if (prevImport \
        && prevImport > thisImport \
        && !startsWith(prevImport, thisImport) \
        && !startsWith(thisImport, prevImport))
    {
        error(fname, FNR, "Import out of sorted order");
    }
    prevImport = thisImport;
    prevImportGroup = importGroup;
}
/^\/\/ Copyright .* Pentaho/ && strict > 1 {
    # We assume that '--strict' is only invoked on files currently being
    # edited. Therefore we would expect the copyright to be current.
    if ($0 !~ /-2013/) {
        error(fname, FNR, "copyright is not current");
    }
}
/(static|public|private|protected|final|abstract)/ && !/import/ && strict > 1 {
    # Order of qualifiers: "public/private/protected static final abstract class ..."
    s2 = s;
    gsub(/\(.*$/, "", s2);
    if (s2 ~ /abstract .*final /) {
        error(fname, FNR, "'final' must come before 'abstract'");
    }
    if (s2 ~ /final .*static /) {
        error(fname, FNR, "'static' must come before 'final'");
    }
    if (s2 ~ /abstract .*static /) {
        error(fname, FNR, "'static' must come before 'abstract'");
    }
    if (s2 ~ /static .*(public|protected|private) /) {
        error(fname, FNR, "'public/private/protected' must come before 'static'");
    }
    if (s2 ~ /final .*(public|protected|private) /) {
        error(fname, FNR, "'public/private/protected' must come before 'final'");
    }
    if (s2 ~ /abstract .*(public|protected|private) /) {
        error(fname, FNR, "'public/private/protected' must come before 'abstract'");
    }
}
/^$/ {
    if (matchFile && previousLineEndedInOpenBrace) {
        error(fname, FNR, "Empty line following open brace");
    }
}
/^ +}( catch| finally| while|[;,)])/ ||
/^ +}$/ {
    if (matchFile && previousLineWasEmpty) {
        error(fname, FNR - 1, "Empty line before close brace");
    }
}
s ~ /\<if\>.*;$/ {
    if (!matchFile) {}
    else {
        error(fname, FNR, "if followed by statement on same line");
    }
}
s ~ /\<(if) *\(/ {
    if (!matchFile) {
    } else if (s !~ /\<(if) /) {
        error(fname, FNR, "if must be followed by space");
    } else if (s ~ / else if /) {
    } else if (s ~ /^#if /) {
    } else if (s !~ /^(    )*(if)/) {
        error(fname, FNR, "if must be correctly indented");
    }
}
s ~ /\<(while) *\(/ {
    if (!matchFile) {
    } else if (s !~ /\<(while) /) {
        error(fname, FNR, "while must be followed by space");
    } else if (s ~ /} while /) {
    } else if (s !~ /^(    )+(while)/) {
        error(fname, FNR, "while must be correctly indented");
    }
}
s ~ /\<(for|switch|synchronized|} catch) *\(/ {
    if (!matchFile) {}
    else if (s !~ /^(    )*(for|switch|synchronized|} catch)/) {
        error(fname, FNR, "for/switch/synchronized/catch must be correctly indented");
    } else if (s !~ /\<(for|switch|synchronized|} catch) /) {
        error(fname, FNR, "for/switch/synchronized/catch must be followed by space");
    }
}
s ~ /\<(if|while|for|switch|catch)\>/ {
    # Check single-line if statements, such as
    #   if (condition) return;
    # We recognize such statements because there are equal numbers of open and
    # close parentheses.
    opens = s;
    gsub(/[^(]/, "", opens);
    closes = s;
    gsub(/[^)]/, "", closes);
    if (!matchFile) {
    } else if (s ~ /{( *\/\/ comment)?$/) {
        # lines which end with { and optional comment are ok
    } else if (s ~ /{.*\\$/ && isCpp) {
        # lines which end with backslash are ok in c++ macros
    } else if (s ~ /} while/) {
        # lines like "} while (foo);" are ok
    } else if (s ~ /^#/) {
        # lines like "#if 0" are ok
    } else if (s ~ /if \(true|false\)/) {
        # allow "if (true)" and "if (false)" because they are
        # used for commenting
    } else if (!unmatchedOpenParens(s)  \
               && length($0) != 79      \
               && length($0) != 80)
    {
        error(fname, FNR, "single-line if/while/for/switch/catch must end in {");
    }
}
s ~ /[[:alnum:]]\(/ &&
s !~ /\<(if|while|for|switch|assert)\>/ {
    ss = s;
    while (match(ss, /[[:alnum:]]\(/)) {
        ss = substr(ss, RSTART + RLENGTH - 1);
        parens = ss;
        gsub(/[^()]/, "", parens);
        while (substr(parens, 1, 2) == "()") {
            parens = substr(parens, 3);
        }
        opens = parens;
        gsub(/[^(]/, "", opens);
        closes = parens;
        gsub(/[^)]/, "", closes);
        if (length(opens) > length(closes)) {
            if (ss ~ /,$/) {
                bras = ss;
                gsub(/[^<]/, "", bras);
                kets = ss;
                gsub(/->/, "", kets);
                gsub(/[^>]/, "", kets);
                if (length(bras) > length(kets)) {
                    # Ignore case like 'for (Map.Entry<Foo,{nl} Bar> entry : ...'
                } else if (s ~ / for /) {
                    # Ignore case like 'for (int i = 1,{nl} j = 2; i < j; ...'
                } else {
                    error(                                              \
                        fname, FNR,                                     \
                        "multi-line parameter list should start with newline");
                    break;
                }
            } else if (s ~ /[;(]( *\\)?$/) {
                # If open paren is at end of line (with optional backslash
                # for macros), we're fine.
            } else if (s ~ /@.*\({/) {
                # Ignore Java annotations.
            } else {
                error(                                                  \
                    fname, FNR,                                         \
                    "Open parenthesis should be at end of line (function call spans several lines)");
                break;
            }
        }
        ss = substr(ss, 2); # remove initial "("
    }
}
s ~ /\<switch\>/ {
    push(switchCol);
    switchCol = index($0, "switch");
}
s ~ /{/ {
    braceCol = index($0, "{");
    if (braceCol == switchCol) {
        push(switchCol);
    }
}
s ~ /}/ {
    braceCol = index($0, "}");
    if (braceCol == switchCol) {
        switchCol = pop();
    }
}
s ~ /\<(case|default)\>/ {
    caseDefaultCol = match($0, /case|default/);
    if (!matchFile) {}
    else if (caseDefaultCol != switchCol) {
        error(fname, FNR, "case/default must be aligned with switch");
    }
}
s ~ /\<assert\>/ {
    if (!matchFile) {}
    else if (isCpp) {} # rule only applies to java
    else if (s !~ /^(    )+(assert)/) {
        error(fname, FNR, "assert must be correctly indented");
    } else if (s !~ /\<assert /) {
        error(fname, FNR, "assert must be followed by space");
    }
}
s ~ /\<return\>/ {
    if (!matchFile) {}
    else if (isCpp && s ~ /^#/) {
        # ignore macros
    } else if (s !~ /^(    )+(return)/) {
        error(fname, FNR, "return must be correctly indented");
    } else if (s !~ /\<return[ ;]/ && s !~ /\<return$/) {
        error(fname, FNR, "return must be followed by space or ;");
    }
}
s ~ /\<throw\>/ {
    if (!matchFile) {}
    else if (isCpp) {
        # cannot yet handle C++ cases like 'void foo() throw(int)'
    } else if (s !~ /^(    )+(throw)/) {
        error(fname, FNR, "throw must be correctly indented");
    } else if (s !~ /\<throw / && s !~ /\<throw$/) {
        error(fname, FNR, "throw must be followed by space");
    }
}
s ~ /\<else\>/ {
    if (!matchFile) {}
    else if (isCpp && s ~ /^# *else$/) {} # ignore "#else"
    else if (s !~ /^(    )+} else (if |{$|{ *\/\/|{ *\/\*)/) {
        error(fname, FNR, "else must be preceded by } and followed by { or if and correctly indented");
    }
}
s ~ /\<do\>/ {
    if (!matchFile) {}
    else if (s !~ /^(    )*do {/) {
        error(fname, FNR, "do must be followed by space {, and correctly indented");
    }
}
s ~ /\<try\>/ {
    if (!matchFile) {}
    else if (s !~ /^(    )+try {/) {
        error(fname, FNR, "try must be followed by space {, and correctly indented");
    }
}
s ~ /\<catch\>/ {
    if (!matchFile) {}
    else if (s !~ /^(    )+} catch /) {
        error(fname, FNR, "catch must be preceded by }, followed by space, and correctly indented");
    }
}
s ~ /\<finally\>/ {
    if (!matchFile) {}
    else if (s !~ /^(    )+} finally {/) {
        error(fname, FNR, "finally must be preceded by }, followed by space {, and correctly indented");
    }
}
s ~ /\($/ {
    nextIndent = countLeadingSpaces(s) + 4;
    if (s ~ / (if|while) .*\(.*\(/) {
        nextIndent += 4;
    }
}
match(s, /([]A-Za-z0-9()])(+|-|\*|\^|\/|%|=|==|+=|-=|\*=|\/=|>=|<=|!=|&|&&|\||\|\||^|\?|:) *[A-Za-z0-9(]/, a) {
    # < and > are not handled here - they have special treatment below
    if (!matchFile) {}
#    else if (s ~ /<.*>/) {} # ignore templates
    else if (a[2] == "-" && s ~ /\(-/) {} # ignore case "foo(-1)"
    else if (a[2] == "-" && s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e-5
    else if (a[2] == "+" && s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e+5
    else if (a[2] == ":" && s ~ /(case.*|default):$/) {} # ignore e.g. "case 5:"
    else if (isCpp && s ~ /[^ ][*&]/) {} # ignore e.g. "Foo* p;" in c++ - debatable
    else if (isCpp && s ~ /\<operator.*\(/) {} # ignore e.g. "operator++()" in c++
    else if (isCpp && a[2] == "/" && s ~ /#include/) {} # ignore e.g. "#include <x/y.hpp>" in c++
    else {
        error(fname, FNR, "operator '" a[2] "' must be preceded by space");
    }
}
match(s, /([]A-Za-z0-9() ] *)(+|-|\*|\^|\/|%|=|==|+=|-=|\*=|\/=|>=|<=|!=|&|&&|\||\|\||^|\?|:|,)[A-Za-z0-9(]/, a) {
    if (!matchFile) {}
#    else if (s ~ /<.*>/) {} # ignore templates
    else if (a[2] == "-" && s ~ /(\(|return |case |= )-/) {} # ignore prefix -
    else if (a[2] == ":" && s ~ /(case.*|default):$/) {} # ignore e.g. "case 5:"
    else if (s ~ /, *-/) {} # ignore case "foo(x, -1)"
    else if (s ~ /-[^ ]/ && s ~ /[^A-Za-z0-9] -/) {} # ignore case "x + -1" but not "x -1" or "3 -1"
    else if (a[2] == "-" && s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e-5
    else if (a[2] == "+" && s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e+5
    else if (a[2] == "*" && isCpp && s ~ /\*[^ ]/) {} # ignore e.g. "Foo *p;" in c++
    else if (a[2] == "&" && isCpp && s ~ /&[^ ]/) {} # ignore case "foo(&x)" in c++
    else if (isCpp && s ~ /\<operator[^ ]+\(/) {} # ignore e.g. "operator++()" in c++
    else if (isCpp && a[2] == "/" && s ~ /#include/) {} # ignore e.g. "#include <x/y.hpp>" in c++
    else if (strict < 2 && fname ~ /(fennel)/ && a[1] = ",") {} # not enabled yet
    else {
        error(fname, FNR, "operator '" a[2] "' must be followed by space");
    }
}
match(s, /( )(,)/, a) {
    # (, < and > are not handled here - they have special treatment below
    if (!matchFile) {}
    else {
        error(fname, FNR, "operator '" a[2] "' must not be preceded by space");
    }
}
match(s, / (+|-|\*|\/|==|>=|<=|!=|<<|<<<|>>|&|&&|\|\||\?|:)$/, a) || \
match(s, /(\.|->)$/, a) {
    if (strict < 2 && fname ~ /(aspen)/ && a[1] != ":") {} # not enabled yet
    else if (strict < 2 && fname ~ /(fennel|farrago|aspen)/ && a[1] = "+") {} # not enabled yet
    else if (a[1] == ":" && s ~ /(case.*|default):$/) {
        # ignore e.g. "case 5:"
    } else if ((a[1] == "*" || a[1] == "&") && isCpp && s ~ /^[[:alnum:]:_ ]* [*&]$/) {
        # ignore e.g. "const int *\nClass::Subclass2::method(int x)"
    } else {
        error(fname, FNR, "operator '" a[1] "' must not be at end of line");
    }
}
match(s, /^ *(=) /, a) {
    error(fname, FNR, "operator '" a[1] "' must not be at start of line");
}
match(s, /([[:alnum:]~]+)( )([(])/, a) {
    # (, < and > are not handled here - they have special treatment below
    if (!matchFile) {}
    else if (isJava && a[1] ~ /\<(if|while|for|catch|switch|case|return|throw|synchronized|assert)\>/) {}
    else if (isCpp && a[1] ~ /\<(if|while|for|catch|switch|case|return|throw|operator|void|PBuffer)\>/) {}
    else if (isCpp && s ~ /^#define /) {}
    else {
        error(fname, FNR, "there must be no space before '" a[3] "' in fun call or fun decl");
    }
}
s ~ /\<[[:digit:][:lower:]][[:alnum:]_]*</ {
    # E.g. "p<" but not "Map<"
    if (!matchFile) {}
    else if (isCpp) {} # in C++ 'xyz<5>' could be a template
    else {
        error(fname, FNR, "operator '<' must be preceded by space");
    }
}
s ~ /\<[[:digit:][:lower:]][[:alnum:]_]*>/ {
    # E.g. "g>" but not "String>" as in "List<String>"
    if (!matchFile) {}
    else if (isCpp) {} # in C++ 'xyz<int>' could be a template
    else {
        error(fname, FNR, "operator '>' must be preceded by space");
    }
}
match(s, /<([[:digit:][:lower:]][[:alnum:].]*)\>/, a) {
    if (!matchFile) {}
    else if (isCpp) {
        # in C++, template and include generate too many false positives
    } else if (isJava && a[1] ~ /(int|char|long|boolean|byte|double|float)/) {
        # Allow e.g. 'List<int[]>'
    } else if (isJava && a[1] ~ /^[[:lower:]]+\./) {
        # Allow e.g. 'List<java.lang.String>'
    } else {
        error(fname, FNR, "operator '<' must be followed by space");
    }
}
match(s, /^(.*[^-])>([[:digit:][:lower:]][[:alnum:]]*)\>/, a) {
    if (!matchFile) {}
    else if (isJava && a[1] ~ /.*\.<.*/) {
        # Ignore 'Collections.<Type>member'
    } else {
        error(fname, FNR, "operator '>' must be followed by space");
    }
}
s ~ /[[(] / {
    if (!matchFile) {}
    else if (s ~ /[[(] +\\$/) {} # ignore '#define foo(   \'
    else {
        error(fname, FNR, "( or [ must not be followed by space");
    }
}
s ~ / [])]/ {
    if (!matchFile) {}
    else if (s ~ /^ *\)/ && previousLineEndedInCloseBrace) {} # ignore "bar(new Foo() { } );"
    else {
        error(fname, FNR, ") or ] must not be followed by space");
    }
}
s ~ /}/ {
    if (!matchFile) {}
    else if (s !~ /}( |;|,|$|\)|\.)/) {
        error(fname, FNR, "} must be followed by space");
    } else if (s !~ /(    )*}/) {
        error(fname, FNR, "} must be at start of line and correctly indented");
    }
}
$0 ~ /\* @param [A-Za-z0-9_]+$/ && strict > 1 {
    error(fname, FNR, "Empty javadoc param");
}
$0 ~ /\* @return *$/ && strict > 1 {
    error(fname, FNR, "Empty javadoc return");
}
s ~ /{/ {
    if (!matchFile) {}
    else if (s ~ /(\]\)?|=) *{/) {} # ignore e.g. "(int[]) {1, 2}" or "int[] x = {1, 2}"
    else if (s ~ /\({/) {} # ignore e.g. @SuppressWarnings({"unchecked"})
    else if (s ~ /{ *(\/\/|\/\*)/) {} # ignore e.g. "do { // a comment"
    else if (s ~ / \{\}$/) {} # ignore e.g. "Constructor() {}"
    else if (s ~ / },$/) {} # ignore e.g. "{ yada },"
    else if (s ~ / };$/) {} # ignore e.g. "{ yada };"
    else if (s ~ / \{\};$/) {} # ignore e.g. "template <> class Foo<int> {};"
    else if (s ~ / },? *\/\/.*$/) {} # ignore e.g. "{ yada }, // comment"
    else if (s ~ /\\$/) {} # ignore multiline macros
    else if (s ~ /{}/) { # e.g. "Constructor(){}"
        error(fname, FNR, "{} must be preceded by space and at end of line");
    } else if (isCpp && s ~ /{ *\\$/) {
        # ignore - "{" can be followed by "\" in c macro
    } else if (s !~ /{$/) {
        error(fname, FNR, "{ must be at end of line");
    } else if (s !~ /(^| ){/) {
        error(fname, FNR, "{ must be preceded by space or at start of line");
    } else {
        opens = s;
        gsub(/[^(]/, "", opens);
        closes = s;
        gsub(/[^)]/, "", closes);
        if (0 && strict < 2 && fname ~ /aspen/) {} # not enabled
        else if (length(closes) > length(opens)) {
            error(fname, FNR, "Open brace should be on new line (function call/decl spans several lines)");
        }
    }
}
s ~ /(^| )(class|interface|enum) / ||
s ~ /(^| )namespace / && isCpp {
    if (isCpp && s ~ /;$/) {} # ignore type declaration
    else {
        classDeclStartLine = FNR;
        t = s;
        gsub(/.*(class|interface|enum|namespace) /, "", t);
        gsub(/ .*$/, "", t);
        if (s ~ /template/) {
            # ignore case "template <class INSTCLASS> static void foo()"
            classDeclStartLine = 0;
        } else if (t ~ /[[:upper:]][[:upper:]][[:upper:]][[:upper:]]/     \
            && t !~ /LRU/ \
            && t !~ /WAL/ \
            && t !~ /classUUID/ \
            && t !~ /classSQLException/ \
            && t !~ /BBRC/ \
            && t !~ /_/ \
            && t !~ /EncodedSqlInterval/)
        {
            error(fname, FNR, "Class name " t " has consecutive uppercase letters");
        }
    }
}
s ~ / throws\>/ {
    if (s ~ /\(/) {
        funDeclStartLine = FNR;
    } else {
        funDeclStartLine = FNR - 1;
    }
}
length($0) > maxLineLength                      \
&& $0 !~ /@(throws|see|link)/                   \
&& $0 !~ /\$Id: /                               \
&& $0 !~ /^import /                             \
&& $0 !~ /http:/                                \
&& $0 !~ /https:/                               \
&& $0 !~ /\/\/ Expect "/                        \
&& s !~ /^ *(\+ |<< |: |\?)?string\)?[;,]?$/ {
    error( \
        fname, \
        FNR, \
        "Line length (" length($0) ") exceeds " maxLineLength " chars");
}
/}$/ {
    previousLineEndedInCloseBrace = 2;
}
/;$/ {
    funDeclStartLine = 0;
}
/{$/ {
    # Ignore open brace if it is part of class or interface declaration.
    if (classDeclStartLine) {
        if (classDeclStartLine < FNR \
            && $0 !~ /^ *{$/)
        {
            error(fname, FNR, "Open brace should be on new line (class decl spans several lines)");
        }
        classDeclStartLine = 0;
    } else {
        previousLineEndedInOpenBrace = 2;
    }
    if (funDeclStartLine) {
        if (funDeclStartLine < FNR \
            && $0 !~ /^ *{$/)
        {
            if (strict < 2 && fname ~ /aspen/) {} # not enabled
            else error(fname, FNR, "Open brace should be on new line (function decl spans several lines)");
        }
        funDeclStartLine = 0;
    }
}
/^$/ {
    previousLineWasEmpty = 2;
}
{
    next;
}
END {
    afterFile();
}

# End checkFile.awk