Repository: benhoyt/goawk (go)

Issue:
Add support for 'nextfile'
https://www.gnu.org/software/gawk/manual/html_node/Nextfile-Statement.html

This is not in the current POSIX spec, but onetrueawk, Gawk, mawk, and busybox awk all support it (and [apparently](https://unix.stackexchange.com/questions/588906/is-awks-nextfile-specified-in-posix) it was [accepted](https://www.austingroupbugs.net/view.php?id=607) into POSIX years ago, but the spec hasn't been updated yet).

Pull request description:
Add support for "nextfile"
Fixes #171.

Patch:
diff --git a/internal/ast/ast.go b/internal/ast/ast.go
index c6cdb55..88d9ffc 100644
--- a/internal/ast/ast.go
+++ b/internal/ast/ast.go
@@ -122,6 +122,7 @@ func (s *DoWhileStmt) node()    {}
 func (s *BreakStmt) node()      {}
 func (s *ContinueStmt) node()   {}
 func (s *NextStmt) node()       {}
+func (s *NextfileStmt) node()   {}
 func (s *ExitStmt) node()       {}
 func (s *DeleteStmt) node()     {}
 func (s *ReturnStmt) node()     {}
@@ -521,6 +522,7 @@ func (s *DoWhileStmt) stmt()  {}
 func (s *BreakStmt) stmt()    {}
 func (s *ContinueStmt) stmt() {}
 func (s *NextStmt) stmt()     {}
+func (s *NextfileStmt) stmt() {}
 func (s *ExitStmt) stmt()     {}
 func (s *DeleteStmt) stmt()   {}
 func (s *ReturnStmt) stmt()   {}
@@ -537,6 +539,7 @@ func (s *DoWhileStmt) StartPos() Position  { return s.Start }
 func (s *BreakStmt) StartPos() Position    { return s.Start }
 func (s *ContinueStmt) StartPos() Position { return s.Start }
 func (s *NextStmt) StartPos() Position     { return s.Start }
+func (s *NextfileStmt) StartPos() Position { return s.Start }
 func (s *ExitStmt) StartPos() Position     { return s.Start }
 func (s *DeleteStmt) StartPos() Position   { return s.Start }
 func (s *ReturnStmt) StartPos() Position   { return s.Start }
@@ -553,6 +556,7 @@ func (s *DoWhileStmt) EndPos() Position  { return s.End }
 func (s *BreakStmt) EndPos() Position    { return s.End }
 func (s *ContinueStmt) EndPos() Position { return s.End }
 func (s *NextStmt) EndPos() Position     { return s.End }
+func (s *NextfileStmt) EndPos() Position { return s.End }
 func (s *ExitStmt) EndPos() Position     { return s.End }
 func (s *DeleteStmt) EndPos() Position   { return s.End }
 func (s *ReturnStmt) EndPos() Position   { return s.End }
@@ -721,6 +725,16 @@ func (s *NextStmt) String() string {
 	return "next"
 }
 
+// NextfileStmt is a nextfile statement.
+type NextfileStmt struct {
+	Start Position
+	End   Position
+}
+
+func (s *NextfileStmt) String() string {
+	return "nextfile"
+}
+
 // ExitStmt is an exit statement.
 type ExitStmt struct {
 	Status Expr
diff --git a/internal/ast/walk.go b/internal/ast/walk.go
index e3cd557..96a2f75 100644
--- a/internal/ast/walk.go
+++ b/internal/ast/walk.go
@@ -140,6 +140,7 @@ func Walk(v Visitor, node Node) {
 	case *BreakStmt: // leaf
 	case *ContinueStmt: // leaf
 	case *NextStmt: // leaf
+	case *NextfileStmt: // leaf
 	case *ExitStmt:
 		Walk(v, n.Status)
 
diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go
index 9d5b60e..71285bb 100644
--- a/internal/compiler/compiler.go
+++ b/internal/compiler/compiler.go
@@ -416,6 +416,9 @@ func (c *compiler) stmt(stmt ast.Stmt) {
 	case *ast.NextStmt:
 		c.add(Next)
 
+	case *ast.NextfileStmt:
+		c.add(Nextfile)
+
 	case *ast.ExitStmt:
 		if s.Status != nil {
 			c.expr(s.Status)
diff --git a/internal/compiler/opcode_string.go b/internal/compiler/opcode_string.go
index bfa2f0c..c58f27e 100644
--- a/internal/compiler/opcode_string.go
+++ b/internal/compiler/opcode_string.go
@@ -77,32 +77,33 @@ func _() {
 	_ = x[JumpLessOrEqual-66]
 	_ = x[JumpGreaterOrEqual-67]
 	_ = x[Next-68]
-	_ = x[Exit-69]
-	_ = x[ForIn-70]
-	_ = x[BreakForIn-71]
-	_ = x[CallBuiltin-72]
-	_ = x[CallSplit-73]
-	_ = x[CallSplitSep-74]
-	_ = x[CallSprintf-75]
-	_ = x[CallUser-76]
-	_ = x[CallNative-77]
-	_ = x[Return-78]
-	_ = x[ReturnNull-79]
-	_ = x[Nulls-80]
-	_ = x[Print-81]
-	_ = x[Printf-82]
-	_ = x[Getline-83]
-	_ = x[GetlineField-84]
-	_ = x[GetlineGlobal-85]
-	_ = x[GetlineLocal-86]
-	_ = x[GetlineSpecial-87]
-	_ = x[GetlineArray-88]
-	_ = x[EndOpcode-89]
+	_ = x[Nextfile-69]
+	_ = x[Exit-70]
+	_ = x[ForIn-71]
+	_ = x[BreakForIn-72]
+	_ = x[CallBuiltin-73]
+	_ = x[CallSplit-74]
+	_ = x[CallSplitSep-75]
+	_ = x[CallSprintf-76]
+	_ = x[CallUser-77]
+	_ = x[CallNative-78]
+	_ = x[Return-79]
+	_ = x[ReturnNull-80]
+	_ = x[Nulls-81]
+	_ = x[Print-82]
+	_ = x[Printf-83]
+	_ = x[Getline-84]
+	_ = x[GetlineField-85]
+	_ = x[GetlineGlobal-86]
+	_ = x[GetlineLocal-87]
+	_ = x[GetlineSpecial-88]
+	_ = x[GetlineArray-89]
+	_ = x[EndOpcode-90]
 }
 
-const _Opcode_name = "NopNumStrDupeDropSwapFieldFieldIntFieldByNameFieldByNameStrGlobalLocalSpecialArrayGlobalArrayLocalInGlobalInLocalAssignFieldAssignGlobalAssignLocalAssignSpecialAssignArrayGlobalAssignArrayLocalDeleteDeleteAllIncrFieldIncrGlobalIncrLocalIncrSpecialIncrArrayGlobalIncrArrayLocalAugAssignFieldAugAssignGlobalAugAssignLocalAugAssignSpecialAugAssignArrayGlobalAugAssignArrayLocalRegexIndexMultiConcatMultiAddSubtractMultiplyDividePowerModuloEqualsNotEqualsLessGreaterLessOrEqualGreaterOrEqualConcatMatchNotMatchNotUnaryMinusUnaryPlusBooleanJumpJumpFalseJumpTrueJumpEqualsJumpNotEqualsJumpLessJumpGreaterJumpLessOrEqualJumpGreaterOrEqualNextExitForInBreakForInCallBuiltinCallSplitCallSplitSepCallSprintfCallUserCallNativeReturnReturnNullNullsPrintPrintfGetlineGetlineFieldGetlineGlobalGetlineLocalGetlineSpecialGetlineArrayEndOpcode"
+const _Opcode_name = "NopNumStrDupeDropSwapFieldFieldIntFieldByNameFieldByNameStrGlobalLocalSpecialArrayGlobalArrayLocalInGlobalInLocalAssignFieldAssignGlobalAssignLocalAssignSpecialAssignArrayGlobalAssignArrayLocalDeleteDeleteAllIncrFieldIncrGlobalIncrLocalIncrSpecialIncrArrayGlobalIncrArrayLocalAugAssignFieldAugAssignGlobalAugAssignLocalAugAssignSpecialAugAssignArrayGlobalAugAssignArrayLocalRegexIndexMultiConcatMultiAddSubtractMultiplyDividePowerModuloEqualsNotEqualsLessGreaterLessOrEqualGreaterOrEqualConcatMatchNotMatchNotUnaryMinusUnaryPlusBooleanJumpJumpFalseJumpTrueJumpEqualsJumpNotEqualsJumpLessJumpGreaterJumpLessOrEqualJumpGreaterOrEqualNextNextfileExitForInBreakForInCallBuiltinCallSplitCallSplitSepCallSprintfCallUserCallNativeReturnReturnNullNullsPrintPrintfGetlineGetlineFieldGetlineGlobalGetlineLocalGetlineSpecialGetlineArrayEndOpcode"
 
-var _Opcode_index = [...]uint16{0, 3, 6, 9, 13, 17, 21, 26, 34, 45, 59, 65, 70, 77, 88, 98, 106, 113, 124, 136, 147, 160, 177, 193, 199, 208, 217, 227, 236, 247, 262, 276, 290, 305, 319, 335, 355, 374, 379, 389, 400, 403, 411, 419, 425, 430, 436, 442, 451, 455, 462, 473, 487, 493, 498, 506, 509, 519, 528, 535, 539, 548, 556, 566, 579, 587, 598, 613, 631, 635, 639, 644, 654, 665, 674, 686, 697, 705, 715, 721, 731, 736, 741, 747, 754, 766, 779, 791, 805, 817, 826}
+var _Opcode_index = [...]uint16{0, 3, 6, 9, 13, 17, 21, 26, 34, 45, 59, 65, 70, 77, 88, 98, 106, 113, 124, 136, 147, 160, 177, 193, 199, 208, 217, 227, 236, 247, 262, 276, 290, 305, 319, 335, 355, 374, 379, 389, 400, 403, 411, 419, 425, 430, 436, 442, 451, 455, 462, 473, 487, 493, 498, 506, 509, 519, 528, 535, 539, 548, 556, 566, 579, 587, 598, 613, 631, 635, 643, 647, 652, 662, 673, 682, 694, 705, 713, 723, 729, 739, 744, 749, 755, 762, 774, 787, 799, 813, 825, 834}
 
 func (i Opcode) String() string {
 	if i < 0 || i >= Opcode(len(_Opcode_index)-1) {
diff --git a/internal/compiler/opcodes.go b/internal/compiler/opcodes.go
index 36c4c93..e8938d9 100644
--- a/internal/compiler/opcodes.go
+++ b/internal/compiler/opcodes.go
@@ -107,6 +107,7 @@ const (
 	JumpLessOrEqual    // offset
 	JumpGreaterOrEqual // offset
 	Next
+	Nextfile
 	Exit
 	ForIn // varScope varIndex arrayScope arrayIndex offset
 	BreakForIn
diff --git a/interp/interp.go b/interp/interp.go
index 4980626..ad1f3fb 100644
--- a/interp/interp.go
+++ b/interp/interp.go
@@ -34,9 +34,10 @@ import (
 )
 
 var (
-	errExit  = errors.New("exit")
-	errBreak = errors.New("break")
-	errNext  = errors.New("next")
+	errExit     = errors.New("exit")
+	errBreak    = errors.New("break")
+	errNext     = errors.New("next")
+	errNextfile = errors.New("nextfile")
 
 	errCSVSeparator = errors.New("invalid CSV field separator or comment delimiter")
 
@@ -662,11 +663,15 @@ lineLoop:
 
 			// Execute the body statements
 			err := p.execute(action.Body)
-			if err == errNext {
+			switch {
+			case err == errNext:
 				// "next" statement skips straight to next line
 				continue lineLoop
-			}
-			if err != nil {
+			case err == errNextfile:
+				// Tell nextLine to move on to next file
+				p.scanner = nil
+				continue lineLoop
+			case err != nil:
 				return err
 			}
 		}
diff --git a/interp/vm.go b/interp/vm.go
index 49b07f5..3da5588 100644
--- a/interp/vm.go
+++ b/interp/vm.go
@@ -587,6 +587,9 @@ func (p *interp) execute(code []compiler.Opcode) error {
 		case compiler.Next:
 			return errNext
 
+		case compiler.Nextfile:
+			return errNextfile
+
 		case compiler.Exit:
 			p.exitStatus = int(p.pop().num())
 			// Return special errExit value "caught" by top-level executor
diff --git a/lexer/token.go b/lexer/token.go
index b3be569..02dae30 100644
--- a/lexer/token.go
+++ b/lexer/token.go
@@ -70,6 +70,7 @@ const (
 	IF
 	IN
 	NEXT
+	NEXTFILE
 	PRINT
 	PRINTF
 	RETURN
@@ -127,6 +128,7 @@ var keywordTokens = map[string]Token{
 	"if":       IF,
 	"in":       IN,
 	"next":     NEXT,
+	"nextfile": NEXTFILE,
 	"print":    PRINT,
 	"printf":   PRINTF,
 	"return":   RETURN,
@@ -223,6 +225,7 @@ var tokenNames = map[Token]string{
 	IF:       "if",
 	IN:       "in",
 	NEXT:     "next",
+	NEXTFILE: "nextfile",
 	PRINT:    "print",
 	PRINTF:   "printf",
 	RETURN:   "return",
diff --git a/parser/parser.go b/parser/parser.go
index 45653a8..d449030 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -271,7 +271,7 @@ func (p *parser) simpleStmt() ast.Stmt {
 			p.expect(RBRACKET)
 		}
 		return &ast.DeleteStmt{ref, index, startPos, p.pos}
-	case IF, FOR, WHILE, DO, BREAK, CONTINUE, NEXT, EXIT, RETURN:
+	case IF, FOR, WHILE, DO, BREAK, CONTINUE, NEXT, NEXTFILE, EXIT, RETURN:
 		panic(p.errorf("expected print/printf, delete, or expression"))
 	default:
 		return &ast.ExprStmt{p.expr(), startPos, p.pos}
@@ -394,6 +394,12 @@ func (p *parser) stmt() ast.Stmt {
 		}
 		p.next()
 		s = &ast.NextStmt{startPos, p.pos}
+	case NEXTFILE:
+		if !p.inAction && p.funcName == "" {
+			panic(p.errorf("nextfile can't be inside BEGIN or END"))
+		}
+		p.next()
+		s = &ast.NextfileStmt{startPos, p.pos}
 	case EXIT:
 		p.next()
 		var status ast.Expr


Test patch:
diff --git a/goawk_test.go b/goawk_test.go
index 6909ecb..052dd0c 100644
--- a/goawk_test.go
+++ b/goawk_test.go
@@ -426,6 +426,18 @@ func TestCommandLine(t *testing.T) {
 		{[]string{`BEGIN { print "1"; print "2"|"cat" }`}, "", "1\n2\n", ""},
 		{[]string{`BEGIN { print "1"; "echo 2" | getline x; print x }`}, "", "1\n2\n", ""},
 
+		// The "nextfile" statement
+		{[]string{`{ print FILENAME ":" FNR "/" NR ": " $0 }  FNR==2 { nextfile }`,
+			"testdata/g.5", "-", "testdata/g.6"},
+			"a\nb\nc\n", `
+testdata/g.5:1/1: one
+testdata/g.5:2/2: two 2
+-:1/3: a
+-:2/4: b
+testdata/g.6:1/5: Uno
+testdata/g.6:2/6: Duo
+`[1:], ""},
+
 		// Parse error formatting
 		{[]string{"`"}, "", "", "<cmdline>:1:1: unexpected char\n`\n^"},
 		{[]string{"BEGIN {\n\tx*;\n}"}, "", "", "<cmdline>:2:4: expected expression instead of ;\n    x*;\n      ^"},
diff --git a/interp/interp_test.go b/interp/interp_test.go
index b15f72a..10bcc14 100644
--- a/interp/interp_test.go
+++ b/interp/interp_test.go
@@ -170,11 +170,15 @@ BEGIN {
 }
 `, "", "0 0\n", "", ""},
 
-	// next statement
+	// next and nextfile statements (more tests of nextfile in goawk_test.go)
 	{`{ if (NR==2) next; print }`, "a\nb\nc", "a\nc\n", "", ""},
 	{`{ if (NR==2) f(); print }  function f() { next }`, "a\nb\nc", "a\nc\n", "", ""},
 	{`BEGIN { next }`, "", "", "parse error at 1:9: next can't be inside BEGIN or END", "BEGIN"},
 	{`END { next }`, "", "", "parse error at 1:7: next can't be inside BEGIN or END", "END"},
+	{`{ if (NR==2) nextfile; print }`, "a\nb\nc", "a\n", "", ""},
+	{`{ if (NR==2) f(); print }  function f() { nextfile }`, "a\nb\nc", "a\n", "", ""},
+	{`BEGIN { nextfile }`, "", "", "parse error at 1:9: nextfile can't be inside BEGIN or END", "BEGIN"},
+	{`END { nextfile }`, "", "", "parse error at 1:7: nextfile can't be inside BEGIN or END", "END"},
 
 	// Arrays, "in", and delete
 	{`BEGIN { a["x"] = 3; print "x" in a, "y" in a }`, "", "1 0\n", "", ""},
diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go
index 8e0b85c..c0f651f 100644
--- a/lexer/lexer_test.go
+++ b/lexer/lexer_test.go
@@ -222,7 +222,7 @@ func TestAllTokens(t *testing.T) {
 		"+ += && = : , -- /\n/= $ @ == >= > >> ++ { [ < ( #\n" +
 		"<= ~ % %= * *= !~ ! != | || ^ ^= ** **= ? } ] ) ; - -= " +
 		"BEGIN break continue delete do else END exit " +
-		"for function getline if in next print printf return while " +
+		"for function getline if in next nextfile print printf return while " +
 		"atan2 close cos exp fflush gsub index int length log match rand " +
 		"sin split sprintf sqrt srand sub substr system tolower toupper " +
 		"x \"str\\n\" 1234\n" +
@@ -245,7 +245,7 @@ func TestAllTokens(t *testing.T) {
 		"+ += && = : , -- / <newline> /= $ @ == >= > >> ++ { [ < ( <newline> " +
 		"<= ~ % %= * *= !~ ! != | || ^ ^= ^ ^= ? } ] ) ; - -= " +
 		"BEGIN break continue delete do else END exit " +
-		"for function getline if in next print printf return while " +
+		"for function getline if in next nextfile print printf return while " +
 		"atan2 close cos exp fflush gsub index int length log match rand " +
 		"sin split sprintf sqrt srand sub substr system tolower toupper " +
 		"name string number <newline> " +
diff --git a/parser/parser_test.go b/parser/parser_test.go
index bf0b5ab..b5ce4e8 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -63,6 +63,7 @@ $0 {
         exit status
     } while (x)
     next
+    nextfile
     "cmd" |getline
     "cmd" |getline x
     "cmd" |getline a[1]
diff --git a/testdata/g.5 b/testdata/g.5
new file mode 100644
index 0000000..13f70c4
--- /dev/null
+++ b/testdata/g.5
@@ -0,0 +1,4 @@
+one
+two 2
+3
+Four
diff --git a/testdata/g.6 b/testdata/g.6
new file mode 100644
index 0000000..320eefb
--- /dev/null
+++ b/testdata/g.6
@@ -0,0 +1,3 @@
+Uno
+Duo
+Tri