pkgsrc-Changes archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

CVS commit: pkgsrc/pkgtools/pkglint



Module Name:    pkgsrc
Committed By:   rillig
Date:           Wed Oct  3 22:27:54 UTC 2018

Modified Files:
        pkgsrc/pkgtools/pkglint: Makefile select.mk
        pkgsrc/pkgtools/pkglint/files: alternatives.go alternatives_test.go
            autofix.go autofix_test.go buildlink3_test.go category_test.go
            check_test.go distinfo_test.go expecter.go files.go files_test.go
            licenses.go licenses_test.go line.go linechecker.go
            linechecker_test.go logging.go logging_test.go mkline.go
            mkline_test.go mklinechecker.go mklinechecker_test.go mklines.go
            mklines_test.go mkparser.go mkparser_test.go mkshparser_test.go
            mkshtypes.go mkshwalker.go mkshwalker_test.go options.go
            options_test.go package.go package_test.go parser.go
            patches_test.go pkglint.go pkglint_test.go pkgsrc.go pkgsrc_test.go
            plist.go plist_test.go shell.go shell_test.go shtokenizer.go
            shtokenizer_test.go shtypes.go substcontext_test.go tools.go
            tools_test.go toplevel_test.go util.go util_test.go vardefs.go
            vardefs_test.go vartype.go vartype_test.go vartypecheck.go
            vartypecheck_test.go
        pkgsrc/pkgtools/pkglint/files/getopt: getopt_test.go
        pkgsrc/pkgtools/pkglint/files/licenses: licenses.go licenses_test.go
        pkgsrc/pkgtools/pkglint/files/regex: regex.go
        pkgsrc/pkgtools/pkglint/files/textproc: prefixreplacer.go
Added Files:
        pkgsrc/pkgtools/pkglint/files: fuzzer_test.go testnames_test.go

Log Message:
pkgtools/pkglint: Update to 5.6.3

Changes since 5.6.2:

* Add check for version patterns 1.5*, which should rather be 1.5.*

* Re-enable check for "set -e" and commands that may silently fail
  because of missing error checking

* Lots of internal clean-up and tests


To generate a diff of this commit:
cvs rdiff -u -r1.548 -r1.549 pkgsrc/pkgtools/pkglint/Makefile
cvs rdiff -u -r1.4 -r1.5 pkgsrc/pkgtools/pkglint/select.mk
cvs rdiff -u -r1.4 -r1.5 pkgsrc/pkgtools/pkglint/files/alternatives.go \
    pkgsrc/pkgtools/pkglint/files/alternatives_test.go \
    pkgsrc/pkgtools/pkglint/files/logging_test.go \
    pkgsrc/pkgtools/pkglint/files/mkshwalker.go \
    pkgsrc/pkgtools/pkglint/files/options.go \
    pkgsrc/pkgtools/pkglint/files/options_test.go \
    pkgsrc/pkgtools/pkglint/files/tools.go \
    pkgsrc/pkgtools/pkglint/files/tools_test.go
cvs rdiff -u -r1.9 -r1.10 pkgsrc/pkgtools/pkglint/files/autofix.go \
    pkgsrc/pkgtools/pkglint/files/parser.go \
    pkgsrc/pkgtools/pkglint/files/pkgsrc.go \
    pkgsrc/pkgtools/pkglint/files/shtypes.go
cvs rdiff -u -r1.10 -r1.11 pkgsrc/pkgtools/pkglint/files/autofix_test.go \
    pkgsrc/pkgtools/pkglint/files/shtokenizer.go
cvs rdiff -u -r1.17 -r1.18 pkgsrc/pkgtools/pkglint/files/buildlink3_test.go \
    pkgsrc/pkgtools/pkglint/files/distinfo_test.go \
    pkgsrc/pkgtools/pkglint/files/files_test.go
cvs rdiff -u -r1.12 -r1.13 pkgsrc/pkgtools/pkglint/files/category_test.go \
    pkgsrc/pkgtools/pkglint/files/expecter.go \
    pkgsrc/pkgtools/pkglint/files/toplevel_test.go
cvs rdiff -u -r1.25 -r1.26 pkgsrc/pkgtools/pkglint/files/check_test.go \
    pkgsrc/pkgtools/pkglint/files/line.go \
    pkgsrc/pkgtools/pkglint/files/plist_test.go \
    pkgsrc/pkgtools/pkglint/files/shell.go
cvs rdiff -u -r1.18 -r1.19 pkgsrc/pkgtools/pkglint/files/files.go \
    pkgsrc/pkgtools/pkglint/files/mklinechecker.go
cvs rdiff -u -r0 -r1.1 pkgsrc/pkgtools/pkglint/files/fuzzer_test.go \
    pkgsrc/pkgtools/pkglint/files/testnames_test.go
cvs rdiff -u -r1.14 -r1.15 pkgsrc/pkgtools/pkglint/files/licenses.go \
    pkgsrc/pkgtools/pkglint/files/logging.go \
    pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go \
    pkgsrc/pkgtools/pkglint/files/mkparser_test.go \
    pkgsrc/pkgtools/pkglint/files/substcontext_test.go
cvs rdiff -u -r1.15 -r1.16 pkgsrc/pkgtools/pkglint/files/licenses_test.go
cvs rdiff -u -r1.7 -r1.8 pkgsrc/pkgtools/pkglint/files/linechecker.go \
    pkgsrc/pkgtools/pkglint/files/linechecker_test.go \
    pkgsrc/pkgtools/pkglint/files/mkshtypes.go \
    pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go \
    pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go \
    pkgsrc/pkgtools/pkglint/files/vartype_test.go
cvs rdiff -u -r1.37 -r1.38 pkgsrc/pkgtools/pkglint/files/mkline.go \
    pkgsrc/pkgtools/pkglint/files/pkglint.go
cvs rdiff -u -r1.41 -r1.42 pkgsrc/pkgtools/pkglint/files/mkline_test.go
cvs rdiff -u -r1.31 -r1.32 pkgsrc/pkgtools/pkglint/files/mklines.go \
    pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go
cvs rdiff -u -r1.27 -r1.28 pkgsrc/pkgtools/pkglint/files/mklines_test.go
cvs rdiff -u -r1.16 -r1.17 pkgsrc/pkgtools/pkglint/files/mkparser.go \
    pkgsrc/pkgtools/pkglint/files/vartype.go
cvs rdiff -u -r1.6 -r1.7 pkgsrc/pkgtools/pkglint/files/mkshparser_test.go
cvs rdiff -u -r1.3 -r1.4 pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go \
    pkgsrc/pkgtools/pkglint/files/vardefs_test.go
cvs rdiff -u -r1.35 -r1.36 pkgsrc/pkgtools/pkglint/files/package.go
cvs rdiff -u -r1.29 -r1.30 pkgsrc/pkgtools/pkglint/files/package_test.go
cvs rdiff -u -r1.20 -r1.21 pkgsrc/pkgtools/pkglint/files/patches_test.go
cvs rdiff -u -r1.24 -r1.25 pkgsrc/pkgtools/pkglint/files/pkglint_test.go
cvs rdiff -u -r1.28 -r1.29 pkgsrc/pkgtools/pkglint/files/plist.go \
    pkgsrc/pkgtools/pkglint/files/util.go
cvs rdiff -u -r1.30 -r1.31 pkgsrc/pkgtools/pkglint/files/shell_test.go
cvs rdiff -u -r1.13 -r1.14 pkgsrc/pkgtools/pkglint/files/util_test.go
cvs rdiff -u -r1.45 -r1.46 pkgsrc/pkgtools/pkglint/files/vardefs.go
cvs rdiff -u -r1.39 -r1.40 pkgsrc/pkgtools/pkglint/files/vartypecheck.go
cvs rdiff -u -r1.5 -r1.6 pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go
cvs rdiff -u -r1.2 -r1.3 pkgsrc/pkgtools/pkglint/files/licenses/licenses.go
cvs rdiff -u -r1.1 -r1.2 \
    pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go
cvs rdiff -u -r1.3 -r1.4 pkgsrc/pkgtools/pkglint/files/regex/regex.go
cvs rdiff -u -r1.5 -r1.6 \
    pkgsrc/pkgtools/pkglint/files/textproc/prefixreplacer.go

Please note that diffs are not public domain; they are subject to the
copyright notices on the relevant files.

Modified files:

Index: pkgsrc/pkgtools/pkglint/Makefile
diff -u pkgsrc/pkgtools/pkglint/Makefile:1.548 pkgsrc/pkgtools/pkglint/Makefile:1.549
--- pkgsrc/pkgtools/pkglint/Makefile:1.548      Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/Makefile    Wed Oct  3 22:27:53 2018
@@ -1,6 +1,6 @@
-# $NetBSD: Makefile,v 1.548 2018/09/05 17:56:22 rillig Exp $
+# $NetBSD: Makefile,v 1.549 2018/10/03 22:27:53 rillig Exp $
 
-PKGNAME=       pkglint-5.6.2
+PKGNAME=       pkglint-5.6.3
 DISTFILES=     # none
 CATEGORIES=    pkgtools
 

Index: pkgsrc/pkgtools/pkglint/select.mk
diff -u pkgsrc/pkgtools/pkglint/select.mk:1.4 pkgsrc/pkgtools/pkglint/select.mk:1.5
--- pkgsrc/pkgtools/pkglint/select.mk:1.4       Wed Mar 23 16:36:53 2016
+++ pkgsrc/pkgtools/pkglint/select.mk   Wed Oct  3 22:27:53 2018
@@ -1,4 +1,4 @@
-# $NetBSD: select.mk,v 1.4 2016/03/23 16:36:53 gdt Exp $
+# $NetBSD: select.mk,v 1.5 2018/10/03 22:27:53 rillig Exp $
 #
 # Selects the proper version of pkglint, depending on whether the
 # platform supports the Go programming language.
@@ -8,7 +8,7 @@
 
 # See lang/go/version.mk
 # While it's wrong in the above, go14 does not build on NetBSD 5.
-.if ${MACHINE_ARCH:Ni386:Nx86_64:Nevbarm} || ${MACHINE_PLATFORM:MSunOS-*-i386} || ${MACHINE_PLATFORM:MNetBSD-[1-5]*-*}
+.if ${MACHINE_ARCH:Ni386:Nx86_64:Nevbarm} || ${MACHINE_PLATFORM:MSunOS-*-i386} || ${MACHINE_PLATFORM:MNetBSD-[1-5].*-*}
 DEPENDS+=      pkglint4>=4.82<5:../../pkgtools/pkglint4
 .else
 DEPENDS+=      pkglint>=5:../../pkgtools/pkglint

Index: pkgsrc/pkgtools/pkglint/files/alternatives.go
diff -u pkgsrc/pkgtools/pkglint/files/alternatives.go:1.4 pkgsrc/pkgtools/pkglint/files/alternatives.go:1.5
--- pkgsrc/pkgtools/pkglint/files/alternatives.go:1.4   Thu Aug 16 20:41:42 2018
+++ pkgsrc/pkgtools/pkglint/files/alternatives.go       Wed Oct  3 22:27:53 2018
@@ -1,9 +1,6 @@
 package main
 
-import (
-       "netbsd.org/pkglint/regex"
-       "strings"
-)
+import "strings"
 
 func CheckfileAlternatives(filename string, plistFiles map[string]bool) {
        lines := Load(filename, NotEmpty|LogErrors)
@@ -19,7 +16,7 @@ func CheckfileAlternatives(filename stri
                                }
 
                                relImplementation := strings.Replace(implementation, "@PREFIX@/", "", 1)
-                               plistName := regex.Compile(`@(\w+)@`).ReplaceAllString(relImplementation, "${$1}")
+                               plistName := replaceAll(relImplementation, `@(\w+)@`, "${$1}")
                                if !plistFiles[plistName] && !G.Pkg.vars.Defined("ALTERNATIVES_SRC") {
                                        if plistName != implementation {
                                                line.Errorf("Alternative implementation %q must appear in the PLIST as %q.", implementation, plistName)
Index: pkgsrc/pkgtools/pkglint/files/alternatives_test.go
diff -u pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.4 pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.5
--- pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.4      Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/alternatives_test.go  Wed Oct  3 22:27:53 2018
@@ -2,25 +2,30 @@ package main
 
 import "gopkg.in/check.v1"
 
-func (s *Suite) Test_Alternatives_PLIST(c *check.C) {
+func (s *Suite) Test_CheckfileAlternatives__PLIST(c *check.C) {
        t := s.Init(c)
 
+       t.SetupPackage("category/package")
        t.Chdir("category/package")
-       t.SetupFileLines("ALTERNATIVES",
+       t.CreateFileLines("ALTERNATIVES",
                "sbin/sendmail @PREFIX@/sbin/sendmail.postfix@POSTFIXVER@",
                "sbin/sendmail @PREFIX@/sbin/sendmail.exim@EXIMVER@",
                "bin/echo bin/gnu-echo",
                "bin/editor bin/vim -e",
                "invalid")
+       t.CreateFileLines("PLIST",
+               PlistRcsID,
+               "bin/echo",
+               "bin/vim",
+               "sbin/sendmail.exim${EXIMVER}")
 
-       G.Pkg = NewPackage(".")
-       G.Pkg.PlistFiles["bin/echo"] = true
-       G.Pkg.PlistFiles["bin/vim"] = true
-       G.Pkg.PlistFiles["sbin/sendmail.exim${EXIMVER}"] = true
-
-       CheckfileAlternatives("ALTERNATIVES", G.Pkg.PlistFiles)
+       G.CheckDirent(".")
 
+       // TODO: Remove redundant diagnostics.
        t.CheckOutputLines(
+               "NOTE: ALTERNATIVES:1: @PREFIX@/ can be omitted from the file name.",
+               "NOTE: ALTERNATIVES:2: @PREFIX@/ can be omitted from the file name.",
+               "ERROR: ALTERNATIVES:5: Invalid ALTERNATIVES line \"invalid\".",
                "ERROR: ALTERNATIVES:1: Alternative implementation \"@PREFIX@/sbin/sendmail.postfix@POSTFIXVER@\" must appear in the PLIST as \"sbin/sendmail.postfix${POSTFIXVER}\".",
                "NOTE: ALTERNATIVES:1: @PREFIX@/ can be omitted from the file name.",
                "NOTE: ALTERNATIVES:2: @PREFIX@/ can be omitted from the file name.",
@@ -33,7 +38,7 @@ func (s *Suite) Test_CheckfileAlternativ
        t := s.Init(c)
 
        t.Chdir("category/package")
-       t.SetupFileLines("ALTERNATIVES")
+       t.CreateFileLines("ALTERNATIVES")
 
        G.Pkg = NewPackage(".")
 
Index: pkgsrc/pkgtools/pkglint/files/logging_test.go
diff -u pkgsrc/pkgtools/pkglint/files/logging_test.go:1.4 pkgsrc/pkgtools/pkglint/files/logging_test.go:1.5
--- pkgsrc/pkgtools/pkglint/files/logging_test.go:1.4   Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/logging_test.go       Wed Oct  3 22:27:53 2018
@@ -15,7 +15,7 @@ import "gopkg.in/check.v1"
 // To keep the output layout consistent between all these
 // modes, the source code is written below the diagnostic
 // also in the default (check-only) mode.
-func (s *Suite) Test_show_source_separator(c *check.C) {
+func (s *Suite) Test__show_source_separator(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--source")
@@ -47,7 +47,7 @@ func (s *Suite) Test_show_source_separat
                ">\tThe third line")
 }
 
-func (s *Suite) Test_show_source_separator_show_autofix(c *check.C) {
+func (s *Suite) Test__show_source_separator_show_autofix(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--source", "--show-autofix")
@@ -80,7 +80,7 @@ func (s *Suite) Test_show_source_separat
                "+\tThe bronze medal line")
 }
 
-func (s *Suite) Test_show_source_separator_autofix(c *check.C) {
+func (s *Suite) Test__show_source_separator_autofix(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--source", "--autofix")
@@ -114,7 +114,7 @@ func (s *Suite) Test_show_source_separat
 // Demonstrates how to filter log messages.
 // This is useful in combination with the --autofix option,
 // to restrict the fixes to exactly one group or topic.
-func (s *Suite) Test_Line_log_only(c *check.C) {
+func (s *Suite) Test_Line_log__only(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--autofix", "--source", "--only", "interesting")
@@ -136,7 +136,7 @@ func (s *Suite) Test_Line_log_only(c *ch
                "+\tThe new2 song")
 }
 
-func (s *Suite) Test_collect_explanations_with_only(c *check.C) {
+func (s *Suite) Test_Pkglint_PrintSummary__explanations_with_only(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--only", "interesting")
@@ -161,7 +161,7 @@ func (s *Suite) Test_collect_explanation
                "(Run \"pkglint -e\" to show explanations.)")
 }
 
-func (s *Suite) Test_explain_with_only(c *check.C) {
+func (s *Suite) Test_Explain__only(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--only", "interesting", "--explain")
@@ -232,10 +232,20 @@ func (s *Suite) Test_Explain__long_lines
        t := s.Init(c)
 
        Explain(
-               "123456789 12345678. abcdefghi. 123456789 123456789 123456789 123456789 123456789 ")
+               "123456789 12345678. abcdefghi. 123456789 123456789 123456789 123456789 123456789")
 
        t.CheckOutputLines(
-               "Long explanation line: 123456789 12345678. abcdefghi. 123456789 123456789 123456789 123456789 123456789 ",
+               "Long explanation line: 123456789 12345678. abcdefghi. 123456789 123456789 123456789 123456789 123456789",
                "Break after: 123456789 12345678. abcdefghi. 123456789 123456789 123456789",
-               "Short space after period: 123456789 12345678. abcdefghi. 123456789 123456789 123456789 123456789 123456789 ")
+               "Short space after period: 123456789 12345678. abcdefghi. 123456789 123456789 123456789 123456789 123456789")
+}
+
+func (s *Suite) Test_Explain__trailing_whitespace(c *check.C) {
+       t := s.Init(c)
+
+       Explain(
+               "This is a space: ")
+
+       t.CheckOutputLines(
+               "Trailing whitespace: \"This is a space: \"")
 }
Index: pkgsrc/pkgtools/pkglint/files/mkshwalker.go
diff -u pkgsrc/pkgtools/pkglint/files/mkshwalker.go:1.4 pkgsrc/pkgtools/pkglint/files/mkshwalker.go:1.5
--- pkgsrc/pkgtools/pkglint/files/mkshwalker.go:1.4     Sun Aug 12 16:31:56 2018
+++ pkgsrc/pkgtools/pkglint/files/mkshwalker.go Wed Oct  3 22:27:53 2018
@@ -1,211 +1,323 @@
 package main
 
+import (
+       "fmt"
+       "reflect"
+       "strings"
+)
+
 type MkShWalker struct {
+       Callback struct {
+               List               func(list *MkShList)
+               AndOr              func(andor *MkShAndOr)
+               Pipeline           func(pipeline *MkShPipeline)
+               Command            func(command *MkShCommand)
+               SimpleCommand      func(command *MkShSimpleCommand)
+               CompoundCommand    func(command *MkShCompoundCommand)
+               Case               func(caseClause *MkShCaseClause)
+               CaseItem           func(caseItem *MkShCaseItem)
+               FunctionDefinition func(funcdef *MkShFunctionDefinition)
+               If                 func(ifClause *MkShIfClause)
+               Loop               func(loop *MkShLoopClause)
+               Words              func(words []*ShToken)
+               Word               func(word *ShToken)
+               Redirects          func(redirects []*MkShRedirection)
+               Redirect           func(redirect *MkShRedirection)
+               For                func(forClause *MkShForClause)
+               Varname            func(varname string)
+       }
+       Context []MkShWalkerPathElement
+}
+
+type MkShWalkerPathElement struct {
+       Index   int
+       Element interface{}
 }
 
 func NewMkShWalker() *MkShWalker {
        return &MkShWalker{}
 }
 
+// Path returns a representation of the path in the AST that is
+// currently visited.
+func (w *MkShWalker) Path() string {
+       var path []string
+       for _, level := range w.Context {
+               typeName := reflect.TypeOf(level.Element).Elem().Name()
+               abbreviated := strings.Replace(typeName, "MkSh", "", 1)
+               if level.Index == -1 {
+                       path = append(path, abbreviated)
+               } else {
+                       path = append(path, fmt.Sprintf("%s[%d]", abbreviated, level.Index))
+               }
+       }
+       return strings.Join(path, ".")
+}
+
 // Walk calls the given callback for each node of the parsed shell program,
 // in visiting order from large to small.
-func (w *MkShWalker) Walk(list *MkShList, callback *MkShWalkCallback) {
-       w.walkList(list, callback)
+func (w *MkShWalker) Walk(list *MkShList) {
+       w.walkList(-1, list)
+
+       G.Assertf(len(w.Context) == 0, "MkShWalker.Walk %v", w.Context)
 }
 
-func (w *MkShWalker) walkList(list *MkShList, callback *MkShWalkCallback) {
-       if callback.List != nil {
-               callback.List(list)
+func (w *MkShWalker) walkList(index int, list *MkShList) {
+       w.push(index, list)
+
+       if callback := w.Callback.List; callback != nil {
+               callback(list)
        }
 
-       for _, andor := range list.AndOrs {
-               w.walkAndOr(andor, callback)
+       for i, andor := range list.AndOrs {
+               w.walkAndOr(i, andor)
        }
+
+       w.pop()
 }
 
-func (w *MkShWalker) walkAndOr(andor *MkShAndOr, callback *MkShWalkCallback) {
-       if callback.AndOr != nil {
-               callback.AndOr(andor)
+func (w *MkShWalker) walkAndOr(index int, andor *MkShAndOr) {
+       w.push(index, andor)
+
+       if callback := w.Callback.AndOr; callback != nil {
+               callback(andor)
        }
 
-       for _, pipeline := range andor.Pipes {
-               w.walkPipeline(pipeline, callback)
+       for i, pipeline := range andor.Pipes {
+               w.walkPipeline(i, pipeline)
        }
+
+       w.pop()
 }
 
-func (w *MkShWalker) walkPipeline(pipeline *MkShPipeline, callback *MkShWalkCallback) {
-       if callback.Pipeline != nil {
-               callback.Pipeline(pipeline)
+func (w *MkShWalker) walkPipeline(index int, pipeline *MkShPipeline) {
+       w.push(index, pipeline)
+
+       if callback := w.Callback.Pipeline; callback != nil {
+               callback(pipeline)
        }
 
-       for _, command := range pipeline.Cmds {
-               w.walkCommand(command, callback)
+       for i, command := range pipeline.Cmds {
+               w.walkCommand(i, command)
        }
+
+       w.pop()
 }
 
-func (w *MkShWalker) walkCommand(command *MkShCommand, callback *MkShWalkCallback) {
-       if callback.Command != nil {
-               callback.Command(command)
+func (w *MkShWalker) walkCommand(index int, command *MkShCommand) {
+       w.push(index, command)
+
+       if callback := w.Callback.Command; callback != nil {
+               callback(command)
        }
 
        switch {
        case command.Simple != nil:
-               w.walkSimpleCommand(command.Simple, callback)
+               w.walkSimpleCommand(-1, command.Simple)
        case command.Compound != nil:
-               w.walkCompoundCommand(command.Compound, callback)
-               w.walkRedirects(command.Redirects, callback)
+               w.walkCompoundCommand(-1, command.Compound)
+               w.walkRedirects(-1, command.Redirects)
        case command.FuncDef != nil:
-               w.walkFunctionDefinition(command.FuncDef, callback)
-               w.walkRedirects(command.Redirects, callback)
+               w.walkFunctionDefinition(-1, command.FuncDef)
+               w.walkRedirects(-1, command.Redirects)
        }
+
+       w.pop()
 }
 
-func (w *MkShWalker) walkSimpleCommand(command *MkShSimpleCommand, callback *MkShWalkCallback) {
-       if callback.SimpleCommand != nil {
-               callback.SimpleCommand(command)
+func (w *MkShWalker) walkSimpleCommand(index int, command *MkShSimpleCommand) {
+       w.push(index, command)
+
+       if callback := w.Callback.SimpleCommand; callback != nil {
+               callback(command)
        }
 
-       w.walkWords(command.Assignments, callback)
+       w.walkWords(0, command.Assignments)
        if command.Name != nil {
-               w.walkWord(command.Name, callback)
+               w.walkWord(-1, command.Name)
        }
-       w.walkWords(command.Args, callback)
-       w.walkRedirects(command.Redirections, callback)
+       w.walkWords(2, command.Args)
+       w.walkRedirects(-1, command.Redirections)
+
+       w.pop()
 }
 
-func (w *MkShWalker) walkCompoundCommand(command *MkShCompoundCommand, callback *MkShWalkCallback) {
-       if callback.CompoundCommand != nil {
-               callback.CompoundCommand(command)
+func (w *MkShWalker) walkCompoundCommand(index int, command *MkShCompoundCommand) {
+       w.push(index, command)
+
+       if callback := w.Callback.CompoundCommand; callback != nil {
+               callback(command)
        }
 
        switch {
        case command.Brace != nil:
-               w.walkList(command.Brace, callback)
+               w.walkList(-1, command.Brace)
        case command.Case != nil:
-               w.walkCase(command.Case, callback)
+               w.walkCase(command.Case)
        case command.For != nil:
-               w.walkFor(command.For, callback)
+               w.walkFor(command.For)
        case command.If != nil:
-               w.walkIf(command.If, callback)
+               w.walkIf(command.If)
        case command.Loop != nil:
-               w.walkLoop(command.Loop, callback)
+               w.walkLoop(command.Loop)
        case command.Subshell != nil:
-               w.walkList(command.Subshell, callback)
+               w.walkList(-1, command.Subshell)
        }
+
+       w.pop()
 }
 
-func (w *MkShWalker) walkCase(caseClause *MkShCaseClause, callback *MkShWalkCallback) {
-       if callback.Case != nil {
-               callback.Case(caseClause)
+func (w *MkShWalker) walkCase(caseClause *MkShCaseClause) {
+       w.push(-1, caseClause)
+
+       if callback := w.Callback.Case; callback != nil {
+               callback(caseClause)
        }
 
-       w.walkWord(caseClause.Word, callback)
-       for _, caseItem := range caseClause.Cases {
-               if callback.CaseItem != nil {
-                       callback.CaseItem(caseItem)
+       w.walkWord(0, caseClause.Word)
+       for i, caseItem := range caseClause.Cases {
+               w.push(i, caseItem)
+               if callback := w.Callback.CaseItem; callback != nil {
+                       callback(caseItem)
                }
-               w.walkWords(caseItem.Patterns, callback)
-               w.walkList(caseItem.Action, callback)
+               w.walkWords(0, caseItem.Patterns)
+               w.walkList(1, caseItem.Action)
+               w.pop()
        }
+
+       w.pop()
 }
 
-func (w *MkShWalker) walkFunctionDefinition(funcdef *MkShFunctionDefinition, callback *MkShWalkCallback) {
-       if callback.FunctionDefinition != nil {
-               callback.FunctionDefinition(funcdef)
+func (w *MkShWalker) walkFunctionDefinition(index int, funcdef *MkShFunctionDefinition) {
+       w.push(index, funcdef)
+
+       if callback := w.Callback.FunctionDefinition; callback != nil {
+               callback(funcdef)
        }
 
-       w.walkCompoundCommand(funcdef.Body, callback)
+       w.walkCompoundCommand(-1, funcdef.Body)
+
+       w.pop()
 }
 
-func (w *MkShWalker) walkIf(ifClause *MkShIfClause, callback *MkShWalkCallback) {
-       if callback.If != nil {
-               callback.If(ifClause)
+func (w *MkShWalker) walkIf(ifClause *MkShIfClause) {
+       w.push(-1, ifClause)
+
+       if callback := w.Callback.If; callback != nil {
+               callback(ifClause)
        }
 
        for i, cond := range ifClause.Conds {
-               w.walkList(cond, callback)
-               w.walkList(ifClause.Actions[i], callback)
+               w.walkList(2*i, cond)
+               w.walkList(2*i+1, ifClause.Actions[i])
        }
        if ifClause.Else != nil {
-               w.walkList(ifClause.Else, callback)
+               w.walkList(2*len(ifClause.Conds), ifClause.Else)
        }
+
+       w.pop()
 }
 
-func (w *MkShWalker) walkLoop(loop *MkShLoopClause, callback *MkShWalkCallback) {
-       if callback.Loop != nil {
-               callback.Loop(loop)
+func (w *MkShWalker) walkLoop(loop *MkShLoopClause) {
+       w.push(-1, loop)
+
+       if callback := w.Callback.Loop; callback != nil {
+               callback(loop)
        }
 
-       w.walkList(loop.Cond, callback)
-       w.walkList(loop.Action, callback)
+       w.walkList(0, loop.Cond)
+       w.walkList(1, loop.Action)
+
+       w.pop()
 }
 
-func (w *MkShWalker) walkWords(words []*ShToken, callback *MkShWalkCallback) {
-       if len(words) != 0 {
-               if callback.Words != nil {
-                       callback.Words(words)
-               }
+func (w *MkShWalker) walkWords(index int, words []*ShToken) {
+       if len(words) == 0 {
+               return
+       }
 
-               for _, word := range words {
-                       w.walkWord(word, callback)
-               }
+       w.push(index, words)
+
+       if callback := w.Callback.Words; callback != nil {
+               callback(words)
        }
+
+       for i, word := range words {
+               w.walkWord(i, word)
+       }
+
+       w.pop()
 }
 
-func (w *MkShWalker) walkWord(word *ShToken, callback *MkShWalkCallback) {
-       if callback.Word != nil {
-               callback.Word(word)
+func (w *MkShWalker) walkWord(index int, word *ShToken) {
+       w.push(index, word)
+
+       if callback := w.Callback.Word; callback != nil {
+               callback(word)
        }
+
+       w.pop()
 }
 
-func (w *MkShWalker) walkRedirects(redirects []*MkShRedirection, callback *MkShWalkCallback) {
-       if len(redirects) != 0 {
-               if callback.Redirects != nil {
-                       callback.Redirects(redirects)
-               }
+func (w *MkShWalker) walkRedirects(index int, redirects []*MkShRedirection) {
+       if len(redirects) == 0 {
+               return
+       }
 
-               for _, redirect := range redirects {
-                       if callback.Redirect != nil {
-                               callback.Redirect(redirect)
-                       }
+       w.push(index, redirects)
 
-                       w.walkWord(redirect.Target, callback)
+       if callback := w.Callback.Redirects; callback != nil {
+               callback(redirects)
+       }
+
+       for i, redirect := range redirects {
+               if callback := w.Callback.Redirect; callback != nil {
+                       callback(redirect)
                }
+
+               w.walkWord(i, redirect.Target)
+       }
+
+       w.pop()
+}
+
+func (w *MkShWalker) walkFor(forClause *MkShForClause) {
+       w.push(-1, forClause)
+
+       if callback := w.Callback.For; callback != nil {
+               callback(forClause)
+       }
+       if callback := w.Callback.Varname; callback != nil {
+               callback(forClause.Varname)
+       }
+
+       w.walkWords(-1, forClause.Values)
+       w.walkList(-1, forClause.Body)
+
+       w.pop()
+}
+
+// Current provides access to the element that the walker is currently
+// processing, especially its index as seen from its parent element.
+func (w *MkShWalker) Current() MkShWalkerPathElement {
+       return w.Context[len(w.Context)-1]
+}
+
+// Parent returns an ancestor element from the currently visited path.
+// Parent(0) is the element that is currently visited,
+// Parent(1) is its direct parent, and so on.
+func (w *MkShWalker) Parent(steps int) interface{} {
+       index := len(w.Context) - 1 - steps
+       if index >= 0 {
+               return w.Context[index].Element
        }
+       return nil
 }
 
-func (w *MkShWalker) walkFor(forClause *MkShForClause, callback *MkShWalkCallback) {
-       if callback.For != nil {
-               callback.For(forClause)
-       }
-       if callback.Varname != nil {
-               callback.Varname(forClause.Varname)
-       }
-
-       w.walkWords(forClause.Values, callback)
-       w.walkList(forClause.Body, callback)
-}
-
-type MkShWalkCallback struct {
-       List               func(list *MkShList)
-       AndOr              func(andor *MkShAndOr)
-       Pipeline           func(pipeline *MkShPipeline)
-       Command            func(command *MkShCommand)
-       SimpleCommand      func(command *MkShSimpleCommand)
-       CompoundCommand    func(command *MkShCompoundCommand)
-       Case               func(caseClause *MkShCaseClause)
-       CaseItem           func(caseItem *MkShCaseItem)
-       FunctionDefinition func(funcdef *MkShFunctionDefinition)
-       If                 func(ifClause *MkShIfClause)
-       Loop               func(loop *MkShLoopClause)
-       Words              func(words []*ShToken)
-       Word               func(word *ShToken)
-       Redirects          func(redirects []*MkShRedirection)
-       Redirect           func(redirect *MkShRedirection)
-       For                func(forClause *MkShForClause)
-       Varname            func(varname string)
+func (w *MkShWalker) push(index int, element interface{}) {
+       w.Context = append(w.Context, MkShWalkerPathElement{index, element})
 }
 
-func NewMkShWalkCallback() *MkShWalkCallback {
-       return &MkShWalkCallback{}
+func (w *MkShWalker) pop() {
+       w.Context = w.Context[:len(w.Context)-1]
 }
Index: pkgsrc/pkgtools/pkglint/files/options.go
diff -u pkgsrc/pkgtools/pkglint/files/options.go:1.4 pkgsrc/pkgtools/pkglint/files/options.go:1.5
--- pkgsrc/pkgtools/pkglint/files/options.go:1.4        Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/options.go    Wed Oct  3 22:27:53 2018
@@ -16,7 +16,7 @@ func ChecklinesOptionsMk(mklines *MkLine
                exp.CurrentLine().Warnf("Expected definition of PKG_OPTIONS_VAR.")
                Explain(
                        "The input variables in an options.mk file should always be",
-                       "mentioned in the same order: PKG_OPTIONS_VAR, ",
+                       "mentioned in the same order: PKG_OPTIONS_VAR,",
                        "PKG_SUPPORTED_OPTIONS, PKG_SUGGESTED_OPTIONS.  This way, the",
                        "options.mk files have the same structure and are easy to understand.")
                return
@@ -49,14 +49,9 @@ loop:
                        // The conditionals are typically for OPSYS and MACHINE_ARCH.
 
                case mkline.IsInclude():
-                       includedFile := mkline.IncludeFile()
-                       switch {
-                       case matches(includedFile, `/[^/]+\.buildlink3\.mk$`):
-                       case matches(includedFile, `/[^/]+\.builtin\.mk$`):
-                       case includedFile == "../../mk/bsd.options.mk":
+                       if mkline.IncludeFile() == "../../mk/bsd.options.mk" {
                                exp.Advance()
                                break loop
-                       case IsPrefs(includedFile):
                        }
 
                default:
Index: pkgsrc/pkgtools/pkglint/files/options_test.go
diff -u pkgsrc/pkgtools/pkglint/files/options_test.go:1.4 pkgsrc/pkgtools/pkglint/files/options_test.go:1.5
--- pkgsrc/pkgtools/pkglint/files/options_test.go:1.4   Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/options_test.go       Wed Oct  3 22:27:53 2018
@@ -15,7 +15,7 @@ func (s *Suite) Test_ChecklinesOptionsMk
        t.SetupOption("sqlite", "")
        t.SetupOption("x11", "")
 
-       t.SetupFileMkLines("mk/bsd.options.mk",
+       t.CreateFileLines("mk/bsd.options.mk",
                MkRcsID)
 
        mklines := t.SetupFileMkLines("category/package/options.mk",
@@ -70,7 +70,7 @@ func (s *Suite) Test_ChecklinesOptionsMk
        t.SetupOption("slang", "")
        t.SetupOption("x11", "")
 
-       t.SetupFileMkLines("mk/bsd.options.mk",
+       t.CreateFileLines("mk/bsd.options.mk",
                MkRcsID)
 
        mklines := t.SetupFileMkLines("category/package/options.mk",
@@ -99,7 +99,7 @@ func (s *Suite) Test_ChecklinesOptionsMk
        t.SetupOption("slang", "")
        t.SetupOption("x11", "")
 
-       t.SetupFileMkLines("mk/bsd.options.mk",
+       t.CreateFileLines("mk/bsd.options.mk",
                MkRcsID)
 
        mklines := t.SetupFileMkLines("category/package/options.mk",
@@ -109,6 +109,10 @@ func (s *Suite) Test_ChecklinesOptionsMk
                "PKG_SUPPORTED_OPTIONS=          # none",
                "PKG_SUGGESTED_OPTIONS=          # none",
                "",
+               "# Comment",
+               ".if ${OPSYS} == NetBSD",
+               ".endif",
+               "",
                ".include \"../../mk/bsd.options.mk\"",
                "",
                ".if ${OPSYS} == 'Darwin'",
@@ -117,5 +121,5 @@ func (s *Suite) Test_ChecklinesOptionsMk
        ChecklinesOptionsMk(mklines)
 
        t.CheckOutputLines(
-               "WARN: ~/category/package/options.mk:9: Invalid condition, unrecognized part: \"${OPSYS} == 'Darwin'\".")
+               "WARN: ~/category/package/options.mk:13: Invalid condition, unrecognized part: \"${OPSYS} == 'Darwin'\".")
 }
Index: pkgsrc/pkgtools/pkglint/files/tools.go
diff -u pkgsrc/pkgtools/pkglint/files/tools.go:1.4 pkgsrc/pkgtools/pkglint/files/tools.go:1.5
--- pkgsrc/pkgtools/pkglint/files/tools.go:1.4  Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/tools.go      Wed Oct  3 22:27:53 2018
@@ -279,11 +279,30 @@ func (tr *Tools) Usable(tool *Tool, time
 }
 
 func (tr *Tools) AddAll(other Tools) {
-       if trace.Tracing {
-               defer trace.Call(other.TraceName, "to", tr.TraceName)()
+       if trace.Tracing && len(other.byName) != 0 {
+               defer trace.Call(other.TraceName+" to "+tr.TraceName, len(other.byName))()
        }
 
-       for _, otherTool := range other.byName {
+       // Same as the code below, just a little faster.
+       if !trace.Tracing {
+               for _, otherTool := range other.byName {
+                       tool := tr.def(otherTool.Name, otherTool.Varname, nil)
+                       tool.MustUseVarForm = tool.MustUseVarForm || otherTool.MustUseVarForm
+                       if otherTool.Validity > tool.Validity {
+                               tool.SetValidity(otherTool.Validity, tr.TraceName)
+                       }
+               }
+               return
+       }
+
+       var names []string
+       for name := range other.byName {
+               names = append(names, name)
+       }
+       sort.Strings(names)
+
+       for _, name := range names {
+               otherTool := other.byName[name]
                if trace.Tracing {
                        trace.Stepf("Tools.AddAll %+v", *otherTool)
                }
Index: pkgsrc/pkgtools/pkglint/files/tools_test.go
diff -u pkgsrc/pkgtools/pkglint/files/tools_test.go:1.4 pkgsrc/pkgtools/pkglint/files/tools_test.go:1.5
--- pkgsrc/pkgtools/pkglint/files/tools_test.go:1.4     Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/tools_test.go Wed Oct  3 22:27:53 2018
@@ -7,7 +7,7 @@ func (s *Suite) Test_Tools_ParseToolLine
 
        t.SetupToolUsable("tool1", "")
        t.SetupVartypes()
-       t.SetupFileLines("Makefile",
+       t.CreateFileLines("Makefile",
                MkRcsID,
                "",
                "USE_TOOLS.NetBSD+=\ttool1")
@@ -18,7 +18,7 @@ func (s *Suite) Test_Tools_ParseToolLine
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_Tools_validateToolName__invalid(c *check.C) {
+func (s *Suite) Test_Tools_def__invalid_tool_name(c *check.C) {
        t := s.Init(c)
 
        reg := NewTools("")
@@ -54,7 +54,7 @@ func (s *Suite) Test_Tools__USE_TOOLS_pr
                "USE_TOOLS+=\tsed:pkgsrc")
        t.CreateFileLines("mk/tools/defaults.mk",
                "_TOOLS_VARNAME.sed=\tSED")
-       t.SetupFileMkLines("module.mk",
+       t.CreateFileLines("module.mk",
                MkRcsID,
                "",
                "do-build:",

Index: pkgsrc/pkgtools/pkglint/files/autofix.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix.go:1.9 pkgsrc/pkgtools/pkglint/files/autofix.go:1.10
--- pkgsrc/pkgtools/pkglint/files/autofix.go:1.9        Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/autofix.go    Wed Oct  3 22:27:53 2018
@@ -46,8 +46,9 @@ func NewAutofix(line Line) *Autofix {
 // If printAutofix or autofix is true, the fix should be done in
 // memory as far as possible (e.g. changes to the text of the line).
 //
-// If autofix is true, the fix should be done permanently
-// (e.g. direct changes to the file system).
+// If autofix is true, the fix should be done persistently
+// (e.g. direct changes to the file system). Except if the fix only
+// affects the current line, then SaveAutofixChanges will do that.
 func (fix *Autofix) Custom(fixer func(printAutofix, autofix bool)) {
        if fix.skip() {
                return
@@ -104,7 +105,7 @@ func (fix *Autofix) ReplaceRegex(from re
                                return toText
                        }
 
-                       if replaced := regex.Compile(from).ReplaceAllStringFunc(rawLine.textnl, replace); replaced != rawLine.textnl {
+                       if replaced := replaceAllFunc(rawLine.textnl, from, replace); replaced != rawLine.textnl {
                                if G.opts.PrintAutofix || G.opts.Autofix {
                                        rawLine.textnl = replaced
                                }
@@ -134,12 +135,12 @@ func (fix *Autofix) Realign(mkline MkLin
        }
 
        for _, rawLine := range fix.lines[1:] {
-               _, comment, space := regex.Match2(rawLine.textnl, `^(#?)([ \t]*)`)
+               _, comment, space := match2(rawLine.textnl, `^(#?)([ \t]*)`)
                width := tabWidth(comment + space)
                if (oldWidth == 0 || width < oldWidth) && width >= 8 && rawLine.textnl != "\n" {
                        oldWidth = width
                }
-               if !regex.Matches(space, `^\t* {0,7}$`) {
+               if !matches(space, `^\t* {0,7}$`) {
                        normalized = false
                }
        }
@@ -156,7 +157,7 @@ func (fix *Autofix) Realign(mkline MkLin
        }
 
        for _, rawLine := range fix.lines[1:] {
-               _, comment, oldSpace := regex.Match2(rawLine.textnl, `^(#?)([ \t]*)`)
+               _, comment, oldSpace := match2(rawLine.textnl, `^(#?)([ \t]*)`)
                newWidth := tabWidth(oldSpace) - oldWidth + newWidth
                newSpace := strings.Repeat("\t", newWidth/8) + strings.Repeat(" ", newWidth%8)
                replaced := strings.Replace(rawLine.textnl, comment+oldSpace, comment+newSpace, 1)
@@ -311,6 +312,7 @@ func SaveAutofixChanges(lines []Line) (a
                for _, line := range lines {
                        if line.autofix != nil && line.autofix.modified {
                                G.autofixAvailable = true
+                               G.fileCache.Evict(line.Filename)
                        }
                }
                return
@@ -338,6 +340,7 @@ func SaveAutofixChanges(lines []Line) (a
        }
 
        for fname := range changed {
+               G.fileCache.Evict(fname)
                changedLines := changes[fname]
                tmpname := fname + ".pkglint.tmp"
                text := ""
Index: pkgsrc/pkgtools/pkglint/files/parser.go
diff -u pkgsrc/pkgtools/pkglint/files/parser.go:1.9 pkgsrc/pkgtools/pkglint/files/parser.go:1.10
--- pkgsrc/pkgtools/pkglint/files/parser.go:1.9 Mon Jan  1 18:04:15 2018
+++ pkgsrc/pkgtools/pkglint/files/parser.go     Wed Oct  3 22:27:53 2018
@@ -12,7 +12,7 @@ type Parser struct {
 }
 
 func NewParser(line Line, s string, emitWarnings bool) *Parser {
-       return &Parser{line, textproc.NewPrefixReplacer(s), emitWarnings}
+       return &Parser{line, G.NewPrefixReplacer(s), emitWarnings}
 }
 
 func (p *Parser) EOF() bool {
Index: pkgsrc/pkgtools/pkglint/files/pkgsrc.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.9 pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.10
--- pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.9 Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc.go     Wed Oct  3 22:27:53 2018
@@ -5,6 +5,7 @@ import (
        "netbsd.org/pkglint/regex"
        "netbsd.org/pkglint/trace"
        "sort"
+       "strconv"
        "strings"
 )
 
@@ -138,37 +139,64 @@ func (src *Pkgsrc) LoadInfrastructure() 
 // Example:
 //  Latest("lang", `^php[0-9]+$`, "../../lang/$0") => "../../lang/php72"
 func (src *Pkgsrc) Latest(category string, re regex.Pattern, repl string) string {
-       key := category + "/" + string(re) + " => " + repl
-       if latest, found := src.latest[key]; found {
+       if G.Testing {
+               G.Assertf(
+                       hasPrefix(string(re), "^") && hasSuffix(string(re), "$"),
+                       "Regular expression %q must be anchored at both ends.", re)
+       }
+
+       cacheKey := category + "/" + string(re) + " => " + repl
+       if latest, found := src.latest[cacheKey]; found {
                return latest
        }
 
        categoryDir := src.File(category)
        error := func() string {
                dummyLine.Errorf("Cannot find latest version of %q in %q.", re, categoryDir)
-               src.latest[key] = ""
+               src.latest[cacheKey] = ""
                return ""
        }
 
-       all, err := ioutil.ReadDir(categoryDir)
-       sort.SliceStable(all, func(i, j int) bool {
-               return naturalLess(all[i].Name(), all[j].Name())
-       })
+       fileInfos, err := ioutil.ReadDir(categoryDir)
        if err != nil {
                return error()
        }
 
-       latest := ""
-       for _, fileInfo := range all {
-               if matches(fileInfo.Name(), re) {
-                       latest = regex.Compile(re).ReplaceAllString(fileInfo.Name(), repl)
+       var names []string
+       for _, fileInfo := range fileInfos {
+               name := fileInfo.Name()
+               if matches(name, re) {
+                       names = append(names, name)
+               }
+       }
+
+       keys := make(map[string]int)
+       for _, name := range names {
+               if m, pkgbase, versionStr := match2(name, `^(\D+)(\d+)$`); m {
+                       version, _ := strconv.Atoi(versionStr)
+                       if pkgbase == "postgresql" && version < 60 {
+                               version = 10 * version
+                       }
+                       keys[name] = version
                }
        }
+
+       sort.SliceStable(names, func(i, j int) bool {
+               if keyI, keyJ := keys[names[i]], keys[names[j]]; keyI != 0 && keyJ != 0 {
+                       return keyI < keyJ
+               }
+               return naturalLess(names[i], names[j])
+       })
+
+       latest := ""
+       for _, name := range names {
+               latest = replaceAll(name, re, repl)
+       }
        if latest == "" {
                return error()
        }
 
-       src.latest[key] = latest
+       src.latest[cacheKey] = latest
        return latest
 }
 
@@ -346,7 +374,7 @@ func (src *Pkgsrc) loadDocChangesFromFil
                                }
                        }
                } else if text := line.Text; len(text) >= 2 && text[0] == '\t' && 'A' <= text[1] && text[1] <= 'Z' {
-                       line.Warnf("Unknown doc/CHANGES line: %q", text)
+                       line.Warnf("Unknown doc/CHANGES line: %s", text)
                        Explain("See mk/misc/developer.mk for the rules.")
                }
        }
@@ -549,6 +577,9 @@ func (src *Pkgsrc) initDeprecatedVars() 
                "SVR4_PKGNAME":           "Just remove it.",
                "PKG_INSTALLATION_TYPES": "Just remove it.",
 
+               // November 2015, commit abccb56
+               "EVAL_PREFIX": "All packages are installed in PREFIX now.",
+
                // January 2016
                "SUBST_POSTCMD.*": "Has been removed, as it seemed unused.",
 
@@ -596,7 +627,7 @@ func (src *Pkgsrc) IsBuildDef(varname st
 func (src *Pkgsrc) loadMasterSites() {
        mklines := src.LoadMk("mk/fetch/sites.mk", MustSucceed|NotEmpty)
 
-       nameToUrl := src.MasterSiteVarToURL
+       nameToURL := src.MasterSiteVarToURL
        urlToName := src.MasterSiteURLToVar
        for _, mkline := range mklines.mklines {
                if mkline.IsVarassign() {
@@ -604,8 +635,8 @@ func (src *Pkgsrc) loadMasterSites() {
                        if hasPrefix(varname, "MASTER_SITE_") && varname != "MASTER_SITE_BACKUP" {
                                for _, url := range splitOnSpace(mkline.Value()) {
                                        if matches(url, `^(?:http://|https://|ftp://)`) {
-                                               if nameToUrl[varname] == "" {
-                                                       nameToUrl[varname] = url
+                                               if nameToURL[varname] == "" {
+                                                       nameToURL[varname] = url
                                                }
                                                urlToName[url] = varname
                                        }
@@ -615,7 +646,7 @@ func (src *Pkgsrc) loadMasterSites() {
        }
 
        // Explicitly allowed, although not defined in mk/fetch/sites.mk.
-       nameToUrl["MASTER_SITE_LOCAL"] = "ftp://ftp.NetBSD.org/pub/pkgsrc/distfiles/LOCAL_PORTS/";
+       nameToURL["MASTER_SITE_LOCAL"] = "ftp://ftp.NetBSD.org/pub/pkgsrc/distfiles/LOCAL_PORTS/";
 
        if trace.Tracing {
                trace.Stepf("Loaded %d MASTER_SITE_* URLs.", len(urlToName))
@@ -629,7 +660,7 @@ func (src *Pkgsrc) loadPkgOptions() {
                if m, optname, optdescr := match2(line.Text, `^([-0-9a-z_+]+)(?:\s+(.*))?$`); m {
                        src.PkgOptions[optname] = optdescr
                } else {
-                       line.Fatalf("Unknown line format.")
+                       line.Fatalf("Unknown line format: %s", line.Text)
                }
        }
 }
Index: pkgsrc/pkgtools/pkglint/files/shtypes.go
diff -u pkgsrc/pkgtools/pkglint/files/shtypes.go:1.9 pkgsrc/pkgtools/pkglint/files/shtypes.go:1.10
--- pkgsrc/pkgtools/pkglint/files/shtypes.go:1.9        Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/shtypes.go    Wed Oct  3 22:27:53 2018
@@ -40,15 +40,15 @@ type ShAtom struct {
        Type    ShAtomType
        MkText  string
        Quoting ShQuoting // The quoting state at the end of the token
-       Data    interface{}
+       data    interface{}
 }
 
 func (atom *ShAtom) String() string {
-       if atom.Type == shtWord && atom.Quoting == shqPlain && atom.Data == nil {
+       if atom.Type == shtWord && atom.Quoting == shqPlain && atom.data == nil {
                return fmt.Sprintf("%q", atom.MkText)
        }
        if atom.Type == shtVaruse {
-               varuse := atom.Data.(*MkVarUse)
+               varuse := atom.VarUse()
                return fmt.Sprintf("varuse(%q)", varuse.varname+varuse.Mod())
        }
        return fmt.Sprintf("ShAtom(%v, %q, %s)", atom.Type, atom.MkText, atom.Quoting)
@@ -57,7 +57,7 @@ func (atom *ShAtom) String() string {
 // VarUse returns a read access to a Makefile variable, or nil for plain shell tokens.
 func (atom *ShAtom) VarUse() *MkVarUse {
        if atom.Type == shtVaruse {
-               return atom.Data.(*MkVarUse)
+               return atom.data.(*MkVarUse)
        }
        return nil
 }
@@ -67,24 +67,25 @@ func (atom *ShAtom) VarUse() *MkVarUse {
 type ShQuoting uint8
 
 const (
-       shqPlain ShQuoting = iota
-       shqDquot
-       shqSquot
-       shqBackt
-       shqSubsh
-       shqDquotBackt
-       shqBacktDquot
-       shqBacktSquot
-       shqSubshSquot
-       shqDquotBacktDquot
-       shqDquotBacktSquot
+       shqPlain           ShQuoting = iota // e.g. word
+       shqDquot                            // e.g. "word"
+       shqSquot                            // e.g. 'word'
+       shqBackt                            // e.g. `word`
+       shqSubsh                            // e.g. $(word)
+       shqDquotBackt                       // e.g. "`word`"
+       shqBacktDquot                       // e.g. `"word"`
+       shqBacktSquot                       // e.g. `'word'`
+       shqSubshDquot                       // e.g. $("word")
+       shqSubshSquot                       // e.g. $('word')
+       shqDquotBacktDquot                  // e.g. "`"word"`"
+       shqDquotBacktSquot                  // e.g. "`'word'`"
 )
 
 func (q ShQuoting) String() string {
        return [...]string{
                "plain",
                "d", "s", "b", "S",
-               "db", "bd", "bs", "Ss",
+               "db", "bd", "bs", "Sd", "Ss",
                "dbd", "dbs",
        }[q]
 }
@@ -103,6 +104,13 @@ func (q ShQuoting) ToVarUseContext() vuc
        return vucQuotUnknown
 }
 
+// ShToken is an operator or a keyword or some text intermingled with variables.
+//
+// Examples:
+//  ;
+//  then
+//  "The number of pkgsrc packages in ${PREFIX} is $$packages."
+//
 // See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_10_02
 type ShToken struct {
        MkText string // The text as it appeared in the Makefile, after replacing `\#` with `#`

Index: pkgsrc/pkgtools/pkglint/files/autofix_test.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.10 pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.11
--- pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.10  Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/autofix_test.go       Wed Oct  3 22:27:53 2018
@@ -7,7 +7,7 @@ import (
        "strings"
 )
 
-func (s *Suite) Test_Autofix_ReplaceRegex(c *check.C) {
+func (s *Suite) Test_Autofix_ReplaceRegex__show_autofix(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--show-autofix")
@@ -36,7 +36,7 @@ func (s *Suite) Test_Autofix_ReplaceRege
                "AUTOFIX: ~/Makefile:2: Replacing \"2\" with \"X\".")
 }
 
-func (s *Suite) Test_Autofix_ReplaceRegex_with_autofix(c *check.C) {
+func (s *Suite) Test_Autofix_ReplaceRegex__autofix(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--autofix", "--source")
@@ -75,7 +75,7 @@ func (s *Suite) Test_Autofix_ReplaceRege
                "line3")
 }
 
-func (s *Suite) Test_Autofix_ReplaceRegex_with_show_autofix(c *check.C) {
+func (s *Suite) Test_Autofix_ReplaceRegex__show_autofix_and_source(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--show-autofix", "--source")
@@ -111,11 +111,11 @@ func (s *Suite) Test_Autofix_ReplaceRege
                "+\tYXXXX")
 }
 
-func (s *Suite) Test_autofix_MkLines(c *check.C) {
+func (s *Suite) Test_SaveAutofixChanges(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--autofix")
-       t.SetupFileLines("category/basename/Makefile",
+       t.CreateFileLines("category/basename/Makefile",
                "line1 := value1",
                "line2 := value2",
                "line3 := value3")
@@ -149,6 +149,27 @@ func (s *Suite) Test_autofix_MkLines(c *
                "XXXe3 := value3")
 }
 
+func (s *Suite) Test_SaveAutofixChanges__no_changes_necessary(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("--autofix")
+       lines := t.SetupFileLines("DESCR",
+               "Line 1",
+               "Line 2")
+
+       fix := lines[0].Autofix()
+       fix.Warnf("Dummy warning.")
+       fix.Replace("X", "Y")
+       fix.Apply()
+
+       // Since nothing has been effectively changed,
+       // nothing needs to be saved.
+       SaveAutofixChanges(lines)
+
+       // And therefore, no AUTOFIX action must appear in the log.
+       t.CheckOutputEmpty()
+}
+
 func (s *Suite) Test_Autofix__multiple_modifications(c *check.C) {
        t := s.Init(c)
 
@@ -255,7 +276,7 @@ func (s *Suite) Test_Autofix__multiple_m
                "AUTOFIX: fname:1: Deleting this line.")
 }
 
-func (s *Suite) Test_Autofix_show_source_code(c *check.C) {
+func (s *Suite) Test_Autofix__show_autofix_and_source(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--show-autofix", "--source")
@@ -319,7 +340,7 @@ func (s *Suite) Test_Autofix_Delete(c *c
 
 // Demonstrates that the --show-autofix option only shows those diagnostics
 // that would be fixed.
-func (s *Suite) Test_Autofix_suppress_unfixable_warnings(c *check.C) {
+func (s *Suite) Test_Autofix__suppress_unfixable_warnings(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--show-autofix", "--source")
@@ -328,14 +349,14 @@ func (s *Suite) Test_Autofix_suppress_un
                "line2",
                "line3")
 
-       lines[0].Warnf("This warning is not shown since it is not automatically fixed.")
+       lines[0].Warnf("This warning is not shown since it is not part of a fix.")
 
        fix := lines[1].Autofix()
        fix.Warnf("Something's wrong here.")
        fix.ReplaceRegex(`.`, "X", -1)
        fix.Apply()
 
-       fix.Warnf("The XXX marks are usually not fixed, use TODO instead.")
+       fix.Warnf("Since XXX marks are usually not fixed, use TODO instead to draw attention.")
        fix.Replace("XXX", "TODO")
        fix.Apply()
 
@@ -351,14 +372,14 @@ func (s *Suite) Test_Autofix_suppress_un
                "-\tline2",
                "+\tXXXXX",
                "",
-               "WARN: Makefile:2: The XXX marks are usually not fixed, use TODO instead.",
+               "WARN: Makefile:2: Since XXX marks are usually not fixed, use TODO instead to draw attention.",
                "AUTOFIX: Makefile:2: Replacing \"XXX\" with \"TODO\".",
                "-\tline2",
                "+\tTODOXX")
 }
 
 // If an Autofix doesn't do anything it must not log any diagnostics.
-func (s *Suite) Test_Autofix_failed_replace(c *check.C) {
+func (s *Suite) Test_Autofix__noop_replace(c *check.C) {
        t := s.Init(c)
 
        line := t.NewLine("Makefile", 14, "Original text")
@@ -372,28 +393,9 @@ func (s *Suite) Test_Autofix_failed_repl
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_SaveAutofixChanges(c *check.C) {
-       t := s.Init(c)
-
-       t.SetupCommandLine("--autofix")
-       lines := t.SetupFileLines("DESCR",
-               "Line 1",
-               "Line 2")
-
-       fix := lines[0].Autofix()
-       fix.Warnf("Dummy warning.")
-       fix.Replace("X", "Y")
-       fix.Apply()
-
-       // Since nothing has been effectively changed,
-       // nothing needs to be saved.
-       SaveAutofixChanges(lines)
-
-       // And therefore, no AUTOFIX action must appear in the log.
-       t.CheckOutputEmpty()
-}
-
-func (s *Suite) Test_Autofix_CustomFix(c *check.C) {
+// When using Autofix.CustomFix, it is tricky to get all the details right.
+// For best results, see the existing examples and the documentation.
+func (s *Suite) Test_Autofix_Custom(c *check.C) {
        t := s.Init(c)
 
        lines := t.NewLines("Makefile",
Index: pkgsrc/pkgtools/pkglint/files/shtokenizer.go
diff -u pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.10 pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.11
--- pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.10   Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/shtokenizer.go        Wed Oct  3 22:27:53 2018
@@ -12,7 +12,8 @@ func NewShTokenizer(line Line, text stri
 }
 
 // ShAtom parses a basic building block of a shell program.
-// Examples for such atoms are: variable reference, operator, text, quote, space.
+// Examples for such atoms are: variable reference (both make and shell),
+// operator, text, quote, space.
 //
 // See ShQuote.Feed
 func (p *ShTokenizer) ShAtom(quoting ShQuoting) *ShAtom {
@@ -45,6 +46,8 @@ func (p *ShTokenizer) ShAtom(quoting ShQ
                atom = p.shAtomBacktDquot()
        case shqBacktSquot:
                atom = p.shAtomBacktSquot()
+       case shqSubshDquot:
+               atom = p.shAtomSubshDquot()
        case shqSubshSquot:
                atom = p.shAtomSubshSquot()
        case shqDquotBacktDquot:
@@ -82,7 +85,7 @@ func (p *ShTokenizer) shAtomPlain() *ShA
        case repl.AdvanceRegexp(`^#.*`):
                return &ShAtom{shtComment, repl.Group(0), q, nil}
        case repl.AdvanceStr("$$("):
-               return &ShAtom{shtSubshell, repl.Str(), q, nil}
+               return &ShAtom{shtSubshell, repl.Str(), shqSubsh, nil}
        }
 
        return p.shAtomInternal(q, false, false)
@@ -133,31 +136,24 @@ func (p *ShTokenizer) shAtomBackt() *ShA
 // compatibility with /bin/sh from Solaris 7.
 func (p *ShTokenizer) shAtomSubsh() *ShAtom {
        const q = shqSubsh
-       if op := p.shOperator(q); op != nil {
-               return op
-       }
        repl := p.parser.repl
-       mark := repl.Mark()
-       atom := func(typ ShAtomType) *ShAtom {
-               return &ShAtom{typ, repl.Since(mark), shqSubsh, nil}
-       }
        switch {
        case repl.AdvanceHspace():
-               return atom(shtSpace)
+               return &ShAtom{shtSpace, repl.Str(), q, nil}
        case repl.AdvanceStr("\""):
-       //return &ShAtom{shtWord, repl.Str(), shqDquot, nil}
+               return &ShAtom{shtWord, repl.Str(), shqSubshDquot, nil}
        case repl.AdvanceStr("'"):
                return &ShAtom{shtWord, repl.Str(), shqSubshSquot, nil}
        case repl.AdvanceStr("`"):
-       //return &ShAtom{shtWord, repl.Str(), shqBackt, nil}
-       case repl.AdvanceRegexp(`^#.*`):
+               // FIXME: return &ShAtom{shtWord, repl.Str(), shqBackt, nil}
+       case repl.AdvanceRegexp(`^#[^)]*`):
                return &ShAtom{shtComment, repl.Group(0), q, nil}
        case repl.AdvanceStr(")"):
                return &ShAtom{shtWord, repl.Str(), shqPlain, nil}
        case repl.AdvanceRegexp(`^(?:[!#%*+,\-./0-9:=?@A-Z\[\]^_a-z{}~]+|\\[^$]|` + reShDollar + `)+`):
                return &ShAtom{shtWord, repl.Group(0), q, nil}
        }
-       return nil
+       return p.shOperator(q)
 }
 
 func (p *ShTokenizer) shAtomDquotBackt() *ShAtom {
@@ -206,6 +202,17 @@ func (p *ShTokenizer) shAtomBacktSquot()
        return nil
 }
 
+func (p *ShTokenizer) shAtomSubshDquot() *ShAtom {
+       repl := p.parser.repl
+       switch {
+       case repl.AdvanceStr("\""):
+               return &ShAtom{shtWord, repl.Str(), shqSubsh, nil}
+       case repl.AdvanceRegexp(`^(?:[\t !%&()*+,\-./0-9:;<=>?@A-Z\[\]^_a-z{|}~]+|\\[^$]|` + reShDollar + `)+`):
+               return &ShAtom{shtWord, repl.Group(0), shqSubshDquot, nil}
+       }
+       return nil
+}
+
 func (p *ShTokenizer) shAtomSubshSquot() *ShAtom {
        const q = shqSubshSquot
        repl := p.parser.repl
@@ -317,6 +324,7 @@ func (p *ShTokenizer) ShAtoms() []*ShAto
 func (p *ShTokenizer) ShToken() *ShToken {
        var curr *ShAtom
        q := shqPlain
+
        peek := func() *ShAtom {
                if curr == nil {
                        curr = p.ShAtom(q)
@@ -356,9 +364,7 @@ nextAtom:
        }
        repl.Reset(mark)
 
-       if len(atoms) == 0 {
-               return nil
-       }
+       G.Assertf(len(atoms) != 0, "ShTokenizer.ShToken")
        return NewShToken(repl.Since(initialMark), atoms...)
 }
 

Index: pkgsrc/pkgtools/pkglint/files/buildlink3_test.go
diff -u pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.17 pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.18
--- pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.17       Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/buildlink3_test.go    Wed Oct  3 22:27:53 2018
@@ -37,14 +37,14 @@ func (s *Suite) Test_ChecklinesBuildlink
 // Before version 5.3, pkglint wrongly warned here.
 // The mk/haskell.mk file takes care of constructing the correct PKGNAME,
 // but pkglint had not looked at that file.
-func (s *Suite) Test_ChecklinesBuildlink3Mk_name_mismatch(c *check.C) {
+func (s *Suite) Test_ChecklinesBuildlink3Mk__name_mismatch(c *check.C) {
        t := s.Init(c)
 
-       t.SetupVartypes()
-       G.Pkg = NewPackage(t.File("x11/hs-X11"))
-       G.Pkg.EffectivePkgbase = "X11"
-       G.Pkg.EffectivePkgnameLine = t.NewMkLine("Makefile", 3, "DISTNAME=\tX11-1.0")
-       mklines := t.NewMkLines("buildlink3.mk",
+       t.SetupCommandLine("-Wall")
+       t.SetupPackage("x11/hs-X11",
+               "DISTNAME=\tX11-1.0")
+       t.Chdir("x11/hs-X11")
+       t.CreateFileLines("buildlink3.mk",
                MkRcsID,
                "",
                "BUILDLINK_TREE+=\ths-X11",
@@ -59,13 +59,14 @@ func (s *Suite) Test_ChecklinesBuildlink
                "",
                "BUILDLINK_TREE+=\t-hs-X11")
 
-       ChecklinesBuildlink3Mk(mklines)
+       G.CheckDirent(".")
 
+       // This warning only occurs because pkglint cannot see mk/haskell.mk in this test.
        t.CheckOutputLines(
                "ERROR: buildlink3.mk:3: Package name mismatch between \"hs-X11\" in this file and \"X11\" from Makefile:3.")
 }
 
-func (s *Suite) Test_ChecklinesBuildlink3Mk_name_mismatch_multiple_inclusion(c *check.C) {
+func (s *Suite) Test_ChecklinesBuildlink3Mk__name_mismatch_multiple_inclusion(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -88,7 +89,7 @@ func (s *Suite) Test_ChecklinesBuildlink
                "WARN: buildlink3.mk:9: Definition of BUILDLINK_API_DEPENDS is missing.")
 }
 
-func (s *Suite) Test_ChecklinesBuildlink3Mk_name_mismatch_abi_api(c *check.C) {
+func (s *Suite) Test_ChecklinesBuildlink3Mk__name_mismatch_abi_api(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -115,7 +116,7 @@ func (s *Suite) Test_ChecklinesBuildlink
                "WARN: buildlink3.mk:10: Only buildlink variables for \"hs-X11\", not \"hs-X12\" may be set in this file.")
 }
 
-func (s *Suite) Test_ChecklinesBuildlink3Mk_abi_api_versions(c *check.C) {
+func (s *Suite) Test_ChecklinesBuildlink3Mk__abi_api_versions(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -140,7 +141,7 @@ func (s *Suite) Test_ChecklinesBuildlink
                "WARN: buildlink3.mk:9: ABI version \"1.6.0\" should be at least API version \"1.6.1\" (see line 8).")
 }
 
-func (s *Suite) Test_ChecklinesBuildlink3Mk_no_BUILDLINK_TREE_at_beginning(c *check.C) {
+func (s *Suite) Test_ChecklinesBuildlink3Mk__no_BUILDLINK_TREE_at_beginning(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -164,7 +165,7 @@ func (s *Suite) Test_ChecklinesBuildlink
                "WARN: buildlink3.mk:3: Expected a BUILDLINK_TREE line.")
 }
 
-func (s *Suite) Test_ChecklinesBuildlink3Mk_no_BUILDLINK_TREE_at_end(c *check.C) {
+func (s *Suite) Test_ChecklinesBuildlink3Mk__no_BUILDLINK_TREE_at_end(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -193,7 +194,7 @@ func (s *Suite) Test_ChecklinesBuildlink
                "WARN: buildlink3.mk:15: This line should contain the following text: BUILDLINK_TREE+=\t-hs-X11")
 }
 
-func (s *Suite) Test_ChecklinesBuildlink3Mk_multiple_inclusion_wrong(c *check.C) {
+func (s *Suite) Test_ChecklinesBuildlink3Mk__multiple_inclusion_wrong(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -212,7 +213,7 @@ func (s *Suite) Test_ChecklinesBuildlink
                "WARN: buildlink3.mk:6: This line should contain the following text: HS_X11_BUILDLINK3_MK:=")
 }
 
-func (s *Suite) Test_ChecklinesBuildlink3Mk_missing_endif(c *check.C) {
+func (s *Suite) Test_ChecklinesBuildlink3Mk__missing_endif(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -230,7 +231,7 @@ func (s *Suite) Test_ChecklinesBuildlink
                "WARN: buildlink3.mk:EOF: Expected \".endif\".")
 }
 
-func (s *Suite) Test_ChecklinesBuildlink3Mk_unknown_dependency_patterns(c *check.C) {
+func (s *Suite) Test_ChecklinesBuildlink3Mk__unknown_dependency_patterns(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -257,7 +258,7 @@ func (s *Suite) Test_ChecklinesBuildlink
                "WARN: buildlink3.mk:10: Unknown dependency pattern \"hs-X11!=1.6.1.2nb2\".")
 }
 
-func (s *Suite) Test_ChecklinesBuildlink3Mk_PKGBASE_with_variable(c *check.C) {
+func (s *Suite) Test_ChecklinesBuildlink3Mk__PKGBASE_with_variable(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -282,7 +283,7 @@ func (s *Suite) Test_ChecklinesBuildlink
                "WARN: buildlink3.mk:3: Please use \"py\" instead of \"${PYPKGPREFIX}\" (also in other variables in this file).")
 }
 
-func (s *Suite) Test_ChecklinesBuildlink3Mk_PKGBASE_with_unknown_variable(c *check.C) {
+func (s *Suite) Test_ChecklinesBuildlink3Mk__PKGBASE_with_unknown_variable(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -312,7 +313,7 @@ func (s *Suite) Test_ChecklinesBuildlink
 // This special exception might have been for backwards-compatibility,
 // but ideally should be handled like everywhere else.
 // See MkLineChecker.checkInclude.
-func (s *Suite) Test_ChecklinesBuildlink3Mk_indentation(c *check.C) {
+func (s *Suite) Test_ChecklinesBuildlink3Mk__indentation(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
Index: pkgsrc/pkgtools/pkglint/files/distinfo_test.go
diff -u pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.17 pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.18
--- pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.17 Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/distinfo_test.go      Wed Oct  3 22:27:53 2018
@@ -6,10 +6,10 @@ func (s *Suite) Test_ChecklinesDistinfo(
        t := s.Init(c)
 
        t.Chdir("category/package")
-       t.SetupFileLines("patches/patch-aa",
+       t.CreateFileLines("patches/patch-aa",
                RcsID+" line is ignored for computing the SHA1 hash",
                "patch contents")
-       t.SetupFileLines("patches/patch-ab",
+       t.CreateFileLines("patches/patch-ab",
                "patch contents")
        lines := t.SetupFileLines("distinfo",
                "should be the RCS ID",
@@ -34,7 +34,7 @@ func (s *Suite) Test_ChecklinesDistinfo(
                "WARN: distinfo:9: Patch file \"patch-nonexistent\" does not exist in directory \"patches\".")
 }
 
-func (s *Suite) Test_ChecklinesDistinfo_global_hash_mismatch(c *check.C) {
+func (s *Suite) Test_ChecklinesDistinfo__global_hash_mismatch(c *check.C) {
        t := s.Init(c)
 
        otherLine := t.NewLine("other/distinfo", 7, "dummy")
@@ -53,11 +53,11 @@ func (s *Suite) Test_ChecklinesDistinfo_
                "ERROR: distinfo:EOF: Expected SHA1, RMD160, SHA512, Size checksums for \"pkgname-1.1.tar.gz\", got SHA512.")
 }
 
-func (s *Suite) Test_ChecklinesDistinfo_uncommitted_patch(c *check.C) {
+func (s *Suite) Test_ChecklinesDistinfo__uncommitted_patch(c *check.C) {
        t := s.Init(c)
 
        t.Chdir("category/package")
-       t.SetupFileLines("patches/patch-aa",
+       t.CreateFileLines("patches/patch-aa",
                RcsID,
                "",
                "--- oldfile",
@@ -65,7 +65,7 @@ func (s *Suite) Test_ChecklinesDistinfo_
                "@@ -1,1 +1,1 @@",
                "-old",
                "+new")
-       t.SetupFileLines("CVS/Entries",
+       t.CreateFileLines("CVS/Entries",
                "/distinfo/...")
        lines := t.SetupFileLines("distinfo",
                RcsID,
@@ -79,13 +79,13 @@ func (s *Suite) Test_ChecklinesDistinfo_
                "WARN: distinfo:3: patches/patch-aa is registered in distinfo but not added to CVS.")
 }
 
-func (s *Suite) Test_ChecklinesDistinfo_unrecorded_patches(c *check.C) {
+func (s *Suite) Test_ChecklinesDistinfo__unrecorded_patches(c *check.C) {
        t := s.Init(c)
 
        t.Chdir("category/package")
-       t.SetupFileLines("patches/CVS/Entries")
-       t.SetupFileLines("patches/patch-aa")
-       t.SetupFileLines("patches/patch-src-Makefile")
+       t.CreateFileLines("patches/CVS/Entries")
+       t.CreateFileLines("patches/patch-aa")
+       t.CreateFileLines("patches/patch-src-Makefile")
        lines := t.SetupFileLines("distinfo",
                RcsID,
                "",
@@ -102,7 +102,7 @@ func (s *Suite) Test_ChecklinesDistinfo_
                "ERROR: distinfo: patch \"patches/patch-src-Makefile\" is not recorded. Run \""+confMake+" makepatchsum\".")
 }
 
-func (s *Suite) Test_ChecklinesDistinfo_manual_patches(c *check.C) {
+func (s *Suite) Test_ChecklinesDistinfo__manual_patches(c *check.C) {
        t := s.Init(c)
 
        t.Chdir("category/package")
@@ -142,7 +142,7 @@ func (s *Suite) Test_ChecklinesDistinfo_
                MkRcsID,
                "",
                "PHPEXT_MK=      # defined",
-               "PHPPKGSRCDIR=   lang/php72",
+               "PHPPKGSRCDIR=   ../../lang/php72",
                "LICENSE?=        unknown-license",
                "COMMENT?=       Some PHP package",
                "GENERATE_PLIST+=# none",
@@ -163,7 +163,7 @@ func (s *Suite) Test_ChecklinesDistinfo_
                "@@ -1,1 +1,1 @@",
                "-old",
                "+new")
-       t.SetupFileLines("lang/php72/distinfo",
+       t.CreateFileLines("lang/php72/distinfo",
                RcsID,
                "",
                "SHA1 (patch-php72) = c109b2089f5ddbc5372b2ab28115ff558ee4187d")
Index: pkgsrc/pkgtools/pkglint/files/files_test.go
diff -u pkgsrc/pkgtools/pkglint/files/files_test.go:1.17 pkgsrc/pkgtools/pkglint/files/files_test.go:1.18
--- pkgsrc/pkgtools/pkglint/files/files_test.go:1.17    Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/files_test.go Wed Oct  3 22:27:53 2018
@@ -4,7 +4,7 @@ import (
        "gopkg.in/check.v1"
 )
 
-func (s *Suite) Test_convertToLogicalLines_no_continuation(c *check.C) {
+func (s *Suite) Test_convertToLogicalLines__no_continuation(c *check.C) {
        rawText := "" +
                "first line\n" +
                "second line\n"
@@ -16,7 +16,7 @@ func (s *Suite) Test_convertToLogicalLin
        c.Check(lines[1].String(), equals, "fname_nocont:2: second line")
 }
 
-func (s *Suite) Test_convertToLogicalLines_continuation(c *check.C) {
+func (s *Suite) Test_convertToLogicalLines__continuation(c *check.C) {
        rawText := "" +
                "first line \\\n" +
                "second line\n" +
@@ -121,7 +121,7 @@ func (s *Suite) Test_convertToLogicalLin
                "ERROR: ~/comment.mk:23: Unknown Makefile line format: \"This is no comment\".")
 }
 
-func (s *Suite) Test_convertToLogicalLines_continuationInLastLine(c *check.C) {
+func (s *Suite) Test_convertToLogicalLines__continuation_in_last_line(c *check.C) {
        t := s.Init(c)
 
        rawText := "" +

Index: pkgsrc/pkgtools/pkglint/files/category_test.go
diff -u pkgsrc/pkgtools/pkglint/files/category_test.go:1.12 pkgsrc/pkgtools/pkglint/files/category_test.go:1.13
--- pkgsrc/pkgtools/pkglint/files/category_test.go:1.12 Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/category_test.go      Wed Oct  3 22:27:53 2018
@@ -6,7 +6,7 @@ func (s *Suite) Test_CheckdirCategory__t
        t := s.Init(c)
 
        t.SetupVartypes()
-       t.SetupFileLines("archivers/Makefile",
+       t.CreateFileLines("archivers/Makefile",
                "# $",
                "SUBDIR+=pkg1",
                "SUBDIR+=\u0020aaaaa",
@@ -36,16 +36,16 @@ func (s *Suite) Test_CheckdirCategory__i
        t := s.Init(c)
 
        t.SetupVartypes()
-       t.SetupFileLines("archivers/Makefile",
+       t.CreateFileLines("archivers/Makefile",
                MkRcsID,
                "COMMENT=\t\\Make $$$$ fast\"",
                "",
                "SUBDIR+=\tpackage",
                "",
                ".include \"../mk/misc/category.mk\"")
-       t.SetupFileLines("archivers/package/Makefile",
+       t.CreateFileLines("archivers/package/Makefile",
                "# dummy")
-       t.SetupFileLines("mk/misc/category.mk",
+       t.CreateFileLines("mk/misc/category.mk",
                "# dummy")
 
        CheckdirCategory(t.File("archivers"))
@@ -59,10 +59,10 @@ func (s *Suite) Test_CheckdirCategory__w
 
        t.SetupPkgsrc()
        t.SetupVartypes()
-       t.SetupFileLines("mk/misc/category.mk")
-       t.SetupFileLines("wip/package/Makefile")
-       t.SetupFileLines("wip/fs-only/Makefile")
-       t.SetupFileLines("wip/Makefile",
+       t.CreateFileLines("mk/misc/category.mk")
+       t.CreateFileLines("wip/package/Makefile")
+       t.CreateFileLines("wip/fs-only/Makefile")
+       t.CreateFileLines("wip/Makefile",
                MkRcsID,
                "COMMENT=\tCategory comment",
                "",
Index: pkgsrc/pkgtools/pkglint/files/expecter.go
diff -u pkgsrc/pkgtools/pkglint/files/expecter.go:1.12 pkgsrc/pkgtools/pkglint/files/expecter.go:1.13
--- pkgsrc/pkgtools/pkglint/files/expecter.go:1.12      Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/expecter.go   Wed Oct  3 22:27:53 2018
@@ -57,7 +57,7 @@ func (exp *Expecter) AdvanceIfMatches(re
        }
 
        if !exp.EOF() {
-               if m := regex.Match(exp.lines[exp.index].Text, re); m != nil {
+               if m := G.res.Match(exp.lines[exp.index].Text, re); m != nil {
                        exp.index++
                        exp.m = m
                        return true
Index: pkgsrc/pkgtools/pkglint/files/toplevel_test.go
diff -u pkgsrc/pkgtools/pkglint/files/toplevel_test.go:1.12 pkgsrc/pkgtools/pkglint/files/toplevel_test.go:1.13
--- pkgsrc/pkgtools/pkglint/files/toplevel_test.go:1.12 Sun Aug 12 16:31:56 2018
+++ pkgsrc/pkgtools/pkglint/files/toplevel_test.go      Wed Oct  3 22:27:53 2018
@@ -5,7 +5,7 @@ import "gopkg.in/check.v1"
 func (s *Suite) Test_CheckdirToplevel(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("Makefile",
+       t.CreateFileLines("Makefile",
                MkRcsID,
                "",
                "SUBDIR+= x11",
@@ -15,10 +15,10 @@ func (s *Suite) Test_CheckdirToplevel(c 
                "#SUBDIR+=\tignoreme",
                "SUBDIR+=\tnonexisting", // This doesn't happen in practice, therefore no warning.
                "SUBDIR+=\tbbb")
-       t.SetupFileLines("archivers/Makefile")
-       t.SetupFileLines("bbb/Makefile")
-       t.SetupFileLines("ccc/Makefile")
-       t.SetupFileLines("x11/Makefile")
+       t.CreateFileLines("archivers/Makefile")
+       t.CreateFileLines("bbb/Makefile")
+       t.CreateFileLines("ccc/Makefile")
+       t.CreateFileLines("x11/Makefile")
        t.SetupVartypes()
 
        CheckdirToplevel(t.File("."))

Index: pkgsrc/pkgtools/pkglint/files/check_test.go
diff -u pkgsrc/pkgtools/pkglint/files/check_test.go:1.25 pkgsrc/pkgtools/pkglint/files/check_test.go:1.26
--- pkgsrc/pkgtools/pkglint/files/check_test.go:1.25    Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/check_test.go Wed Oct  3 22:27:53 2018
@@ -57,7 +57,8 @@ func (s *Suite) SetUpTest(c *check.C) {
        t := &Tester{checkC: c}
        s.Tester = t
 
-       G = Pkglint{Testing: true}
+       G = NewPkglint()
+       G.Testing = true
        textproc.Testing = true
        G.logOut = NewSeparatorWriter(&t.stdout)
        G.logErr = NewSeparatorWriter(&t.stderr)
@@ -89,7 +90,7 @@ func (s *Suite) TearDownTest(c *check.C)
        G = Pkglint{} // unusable because of missing logOut and logErr
        textproc.Testing = false
        if out := t.Output(); out != "" {
-               fmt.Fprintf(os.Stderr, "Unchecked output in %q; check with: t.CheckOutputLines(%v)",
+               fmt.Fprintf(os.Stderr, "Unchecked output in %q; check with: t.CheckOutputLines(%#v)",
                        c.TestName(), strings.Split(out, "\n"))
        }
        t.tmpdir = ""
@@ -238,20 +239,112 @@ func (t *Tester) SetupPkgsrc() {
                MkRcsID)
 }
 
-func (t *Tester) CreateFileLines(relativeFilename string, lines ...string) (filename string) {
+// SetupCategory makes the given category valid by creating a dummy Makefile.
+func (t *Tester) SetupCategory(name string) {
+       if _, err := os.Stat(name + "/Makefile"); os.IsNotExist(err) {
+               t.CreateFileLines(name+"/Makefile",
+                       MkRcsID)
+       }
+}
+
+// SetupPackage sets up all files for a package so that it does not produce
+// any warnings.
+//
+// The given makefileLines start in line 20. Except if they are variable
+// definitions for already existing variables, then they replace that line.
+//
+// Returns the path to the package, ready to be used with Pkglint.CheckDirent.
+func (t *Tester) SetupPackage(pkgpath string, makefileLines ...string) string {
+       category := path.Dir(pkgpath)
+
+       t.SetupPkgsrc()
+       t.SetupVartypes()
+       t.SetupCategory(category)
+
+       t.CreateFileLines(pkgpath+"/DESCR",
+               "Package description")
+       t.CreateFileLines(pkgpath+"/PLIST",
+               PlistRcsID,
+               "bin/program")
+       t.CreateFileLines(pkgpath+"/distinfo",
+               RcsID,
+               "",
+               "SHA1 (distfile-1.0.tar.gz) = 12341234...",
+               "RMD160 (distfile-1.0.tar.gz) = 12341234...",
+               "SHA512 (distfile-1.0.tar.gz) = 12341234...",
+               "Size (distfile-1.0.tar.gz) = 12341234")
+
+       var mlines []string
+       mlines = append(mlines,
+               MkRcsID,
+               "",
+               "DISTNAME=\tdistname-1.0",
+               "CATEGORIES=\t"+category,
+               "MASTER_SITES=\t# none",
+               "",
+               "MAINTAINER=\tpkgsrc-users%NetBSD.org@localhost",
+               "HOMEPAGE=\t# none",
+               "COMMENT=\tDummy package",
+               "LICENSE=\t2-clause-bsd",
+               "")
+       for len(mlines) < 19 {
+               mlines = append(mlines, "# empty")
+       }
+
+line:
+       for _, line := range makefileLines {
+               if m, prefix := match1(line, `^(\w+=)`); m {
+                       for i, existingLine := range mlines {
+                               if hasPrefix(existingLine, prefix) {
+                                       mlines[i] = line
+                                       continue line
+                               }
+                       }
+               }
+               mlines = append(mlines, line)
+       }
+
+       mlines = append(mlines,
+               "",
+               ".include \"../../mk/bsd.pkg.mk\"")
+
+       t.CreateFileLines(pkgpath+"/Makefile",
+               mlines...)
+
+       return t.File(pkgpath)
+}
+
+func (t *Tester) CreateFileLines(relativeFilename string, lines ...string) (fileName string) {
        content := ""
        for _, line := range lines {
                content += line + "\n"
        }
 
-       filename = t.File(relativeFilename)
-       err := os.MkdirAll(path.Dir(filename), 0777)
+       fileName = t.File(relativeFilename)
+       err := os.MkdirAll(path.Dir(fileName), 0777)
        t.c().Assert(err, check.IsNil)
 
-       err = ioutil.WriteFile(filename, []byte(content), 0666)
+       err = ioutil.WriteFile(fileName, []byte(content), 0666)
        t.c().Check(err, check.IsNil)
 
-       return filename
+       G.fileCache.Evict(fileName)
+
+       return fileName
+}
+
+// CreateFileDummyPatch creates a patch file with the given name in the
+// temporary directory.
+func (t *Tester) CreateFileDummyPatch(relativeFileName string) {
+       t.CreateFileLines(relativeFileName,
+               RcsID,
+               "",
+               "Documentation",
+               "",
+               "--- oldfile",
+               "+++ newfile",
+               "@@ -1 +1 @@",
+               "-old",
+               "+new")
 }
 
 // File returns the absolute path to the given file in the
@@ -295,6 +388,14 @@ func (t *Tester) Chdir(relativeFilename 
        t.relcwd = relativeFilename
 }
 
+// Remove removes the file from the temporary directory. The file must exist.
+func (t *Tester) Remove(relativeFilename string) {
+       fileName := t.File(relativeFilename)
+       err := os.Remove(fileName)
+       t.c().Check(err, check.IsNil)
+       G.fileCache.Evict(fileName)
+}
+
 // ExpectFatal runs the given action and expects that this action calls
 // Line.Fatalf or uses some other way to panic with a pkglintFatal.
 //
Index: pkgsrc/pkgtools/pkglint/files/line.go
diff -u pkgsrc/pkgtools/pkglint/files/line.go:1.25 pkgsrc/pkgtools/pkglint/files/line.go:1.26
--- pkgsrc/pkgtools/pkglint/files/line.go:1.25  Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/line.go       Wed Oct  3 22:27:53 2018
@@ -39,6 +39,7 @@ type LineImpl struct {
        Text      string
        raw       []*RawLine
        autofix   *Autofix
+       Once
 }
 
 func NewLine(fname string, lineno int, text string, rawLines []*RawLine) Line {
@@ -47,7 +48,7 @@ func NewLine(fname string, lineno int, t
 
 // NewLineMulti is for logical Makefile lines that end with backslash.
 func NewLineMulti(fname string, firstLine, lastLine int, text string, rawLines []*RawLine) Line {
-       return &LineImpl{fname, path.Base(fname), int32(firstLine), int32(lastLine), text, rawLines, nil}
+       return &LineImpl{fname, path.Base(fname), int32(firstLine), int32(lastLine), text, rawLines, nil, Once{}}
 }
 
 // NewLineEOF creates a dummy line for logging, with the "line number" EOF.
Index: pkgsrc/pkgtools/pkglint/files/plist_test.go
diff -u pkgsrc/pkgtools/pkglint/files/plist_test.go:1.25 pkgsrc/pkgtools/pkglint/files/plist_test.go:1.26
--- pkgsrc/pkgtools/pkglint/files/plist_test.go:1.25    Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/plist_test.go Wed Oct  3 22:27:53 2018
@@ -62,10 +62,10 @@ func (s *Suite) Test_ChecklinesPlist__em
                "WARN: PLIST:1: PLIST files shouldn't be empty.")
 }
 
-func (s *Suite) Test_ChecklinesPlist__commonEnd(c *check.C) {
+func (s *Suite) Test_ChecklinesPlist__common_end(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("PLIST.common",
+       t.CreateFileLines("PLIST.common",
                PlistRcsID,
                "bin/common")
        lines := t.SetupFileLines("PLIST.common_end",
@@ -81,7 +81,6 @@ func (s *Suite) Test_ChecklinesPlist__co
        t := s.Init(c)
 
        G.Pkg = NewPackage(t.File("category/pkgbase"))
-       G.Pkg.plistSubstCond["PLIST.bincmds"] = true
        lines := t.NewLines("PLIST",
                PlistRcsID,
                "${PLIST.bincmds}bin/subdir/command")
@@ -111,7 +110,7 @@ func (s *Suite) Test_ChecklinesPlist__so
                "WARN: PLIST:6: \"bin/cat\" should be sorted before \"bin/otherprogram\".")
 }
 
-func (s *Suite) Test_PlistLineSorter_Sort(c *check.C) {
+func (s *Suite) Test_plistLineSorter_Sort(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--autofix")
@@ -167,7 +166,7 @@ func (s *Suite) Test_PlistLineSorter_Sor
                "@exec echo \"after lib/after.la\"") // The footer starts here
 }
 
-func (s *Suite) Test_PlistChecker_checkpathMan_gz(c *check.C) {
+func (s *Suite) Test_PlistChecker_checkpathMan__gz(c *check.C) {
        t := s.Init(c)
 
        G.Pkg = NewPackage(t.File("category/pkgbase"))
@@ -181,7 +180,7 @@ func (s *Suite) Test_PlistChecker_checkp
                "NOTE: PLIST:2: The .gz extension is unnecessary for manual pages.")
 }
 
-func (s *Suite) TestPlistChecker_checkpath__PKGMANDIR(c *check.C) {
+func (s *Suite) Test_PlistChecker_checkpath__PKGMANDIR(c *check.C) {
        t := s.Init(c)
 
        lines := t.NewLines("PLIST",
@@ -194,7 +193,7 @@ func (s *Suite) TestPlistChecker_checkpa
                "NOTE: PLIST:2: PLIST files should mention \"man/\" instead of \"${PKGMANDIR}\".")
 }
 
-func (s *Suite) TestPlistChecker_checkpath__python_egg(c *check.C) {
+func (s *Suite) Test_PlistChecker_checkpath__python_egg(c *check.C) {
        t := s.Init(c)
 
        lines := t.NewLines("PLIST",
Index: pkgsrc/pkgtools/pkglint/files/shell.go
diff -u pkgsrc/pkgtools/pkglint/files/shell.go:1.25 pkgsrc/pkgtools/pkglint/files/shell.go:1.26
--- pkgsrc/pkgtools/pkglint/files/shell.go:1.25 Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/shell.go      Wed Oct  3 22:27:53 2018
@@ -79,8 +79,9 @@ outer:
 
                case quoting == shqPlain:
                        switch {
-                       case repl.AdvanceRegexp(`^[!#\%&\(\)*+,\-.\/0-9:;<=>?@A-Z\[\]^_a-z{|}~]+`),
-                               repl.AdvanceRegexp(`^\\(?:[ !"#'\(\)*./;?\\^{|}]|\$\$)`):
+                       // FIXME: These regular expressions don't belong here, they are the job of the tokenizer.
+                       case repl.AdvanceRegexp(`^[!#%&()*+,\-./0-9:;<=>?@A-Z\[\]^_a-z{|}~]+`),
+                               repl.AdvanceRegexp(`^\\(?:[ !"#'()*./;?\\^{|}]|\$\$)`):
                        case repl.AdvanceStr("'"):
                                quoting = shqSquot
                        case repl.AdvanceStr("\""):
@@ -192,10 +193,10 @@ func (shline *ShellLine) checkVaruseToke
        switch {
        case quoting == shqPlain && varuse.IsQ():
                // Fine.
-       case quoting == shqBackt:
-               // Don't check anything here, to avoid false positives for tool names.
+
        case (quoting == shqSquot || quoting == shqDquot) && matches(varname, `^(?:.*DIR|.*FILE|.*PATH|.*_VAR|PREFIX|.*BASE|PKGNAME)$`):
                // This is ok if we don't allow these variables to have embedded [\$\\\"\'\`].
+
        case quoting == shqDquot && varuse.IsQ():
                shline.mkline.Warnf("Please don't use the :Q operator in double quotes.")
                Explain(
@@ -249,14 +250,12 @@ func (shline *ShellLine) unescapeBacktic
                                "",
                                "To avoid this uncertainty, escape the double quotes using \\\".")
 
-               case repl.AdvanceRegexp("^([^\\\\`]+)"):
-                       unescaped += repl.Group(1)
-
                default:
-                       line.Errorf("Internal pkglint error in ShellLine.unescapeBackticks at %q (rest=%q).", shellword, repl.AdvanceRest())
+                       G.Assertf(repl.AdvanceRegexp("^([^\\\\`]+)"), "incomplete switch")
+                       unescaped += repl.Group(1)
                }
        }
-       line.Errorf("Unfinished backquotes: rest=%q", repl.Rest())
+       line.Errorf("Unfinished backquotes: %s", repl.Rest())
        return unescaped, quoting
 }
 
@@ -304,7 +303,7 @@ func (shline *ShellLine) CheckShellComma
                line.Notef("You don't need to use \"-\" before %q.", cmd)
        }
 
-       repl := textproc.NewPrefixReplacer(shelltext)
+       repl := G.NewPrefixReplacer(shelltext)
        repl.AdvanceRegexp(`^\s+`)
        if repl.AdvanceRegexp(`^[-@]+`) {
                shline.checkHiddenAndSuppress(repl.Group(0), repl.Rest())
@@ -338,25 +337,29 @@ func (shline *ShellLine) CheckShellComma
        spc := &ShellProgramChecker{shline}
        spc.checkConditionalCd(program)
 
-       callback := NewMkShWalkCallback()
-       callback.SimpleCommand = func(command *MkShSimpleCommand) {
+       walker := NewMkShWalker()
+       walker.Callback.SimpleCommand = func(command *MkShSimpleCommand) {
                scc := NewSimpleCommandChecker(shline, command, time)
                scc.Check()
                if scc.strcmd.Name == "set" && scc.strcmd.AnyArgMatches(`^-.*e`) {
                        *pSetE = true
                }
        }
-       callback.List = func(list *MkShList) {
-               spc.checkSetE(list, pSetE)
+       walker.Callback.AndOr = func(andor *MkShAndOr) {
+               if G.opts.WarnExtra && !*pSetE && walker.Current().Index != 0 {
+                       spc.checkSetE(walker.Parent(1).(*MkShList), walker.Current().Index, andor)
+               }
        }
-       callback.Pipeline = func(pipeline *MkShPipeline) {
+       walker.Callback.Pipeline = func(pipeline *MkShPipeline) {
                spc.checkPipeExitcode(line, pipeline)
        }
-       callback.Word = func(word *ShToken) {
+       walker.Callback.Word = func(word *ShToken) {
+               // TODO: Try to replace false with true here; it had been set to false
+               // TODO: in 2016 for no apparent reason.
                spc.checkWord(word, false, time)
        }
 
-       NewMkShWalker().Walk(program, callback)
+       walker.Walk(program)
 }
 
 func (shline *ShellLine) CheckShellCommands(shellcmds string, time ToolTime) {
@@ -436,7 +439,7 @@ func (scc *SimpleCommandChecker) Check()
        }
 
        scc.checkCommandStart()
-       scc.checkAbsolutePathnames()
+       scc.checkRegexReplace()
        scc.checkAutoMkdirs()
        scc.checkInstallMulti()
        scc.checkPaxPe()
@@ -587,7 +590,7 @@ func (scc *SimpleCommandChecker) handleC
        return true
 }
 
-func (scc *SimpleCommandChecker) checkAbsolutePathnames() {
+func (scc *SimpleCommandChecker) checkRegexReplace() {
        if trace.Tracing {
                defer trace.Call()()
        }
@@ -598,7 +601,7 @@ func (scc *SimpleCommandChecker) checkAb
                if !isSubst {
                        CheckLineAbsolutePathname(scc.shline.mkline.Line, arg)
                }
-               if false && isSubst && !matches(arg, `"^[\"\'].*[\"\']$`) {
+               if G.Testing && isSubst && !matches(arg, `"^[\"\'].*[\"\']$`) {
                        scc.shline.mkline.Warnf("Substitution commands like %q should always be quoted.", arg)
                        Explain(
                                "Usually these substitution commands contain characters like '*' or",
@@ -739,20 +742,20 @@ func (spc *ShellProgramChecker) checkCon
                }
        }
 
-       callback := NewMkShWalkCallback()
-       callback.If = func(ifClause *MkShIfClause) {
+       walker := NewMkShWalker()
+       walker.Callback.If = func(ifClause *MkShIfClause) {
                for _, cond := range ifClause.Conds {
                        if simple := getSimple(cond); simple != nil {
                                checkConditionalCd(simple)
                        }
                }
        }
-       callback.Loop = func(loop *MkShLoopClause) {
+       walker.Callback.Loop = func(loop *MkShLoopClause) {
                if simple := getSimple(loop.Cond); simple != nil {
                        checkConditionalCd(simple)
                }
        }
-       callback.Pipeline = func(pipeline *MkShPipeline) {
+       walker.Callback.Pipeline = func(pipeline *MkShPipeline) {
                if pipeline.Negated {
                        spc.shline.mkline.Warnf("The Solaris /bin/sh does not support negation of shell commands.")
                        Explain(
@@ -761,17 +764,7 @@ func (spc *ShellProgramChecker) checkCon
                                "https://www.gnu.org/software/autoconf/manual/autoconf.html#Limitations-of-Builtins";)
                }
        }
-       NewMkShWalker().Walk(list, callback)
-}
-
-func (spc *ShellProgramChecker) checkWords(words []*ShToken, checkQuoting bool, time ToolTime) {
-       if trace.Tracing {
-               defer trace.Call()()
-       }
-
-       for _, word := range words {
-               spc.checkWord(word, checkQuoting, time)
-       }
+       walker.Walk(list)
 }
 
 func (spc *ShellProgramChecker) checkWord(word *ShToken, checkQuoting bool, time ToolTime) {
@@ -787,41 +780,14 @@ func (spc *ShellProgramChecker) checkPip
                defer trace.Call()()
        }
 
-       oneOf := func(s string, others ...string) bool {
-               for _, other := range others {
-                       if s == other {
-                               return true
-                       }
-               }
-               return false
-       }
-
-       // canFail tests whether one of the left-hand side commands of a
-       // shell pipeline can fail.
-       //
-       // Examples:
-       //  echo "hello" | sed 's,$, world,,'   => cannot fail
-       //  find . -print | xargs cat | wc -l   => can fail
        canFail := func() (bool, string) {
                for _, cmd := range pipeline.Cmds[:len(pipeline.Cmds)-1] {
-                       simple := cmd.Simple
-                       if simple == nil {
+                       if spc.canFail(cmd) {
+                               if cmd.Simple != nil && cmd.Simple.Name != nil {
+                                       return true, cmd.Simple.Name.MkText
+                               }
                                return true, ""
                        }
-                       commandName := simple.Name.MkText
-                       if len(simple.Redirections) != 0 {
-                               return true, commandName
-                       }
-                       tool, _ := G.Tool(commandName, RunTime)
-                       switch {
-                       case tool == nil:
-                               return true, commandName
-                       case oneOf(tool.Name, "echo", "printf"):
-                       case oneOf(tool.Name, "sed", "gsed", "grep", "ggrep") && len(simple.Args) == 1:
-                               break
-                       default:
-                               return true, commandName
-                       }
                }
                return false, ""
        }
@@ -844,29 +810,110 @@ func (spc *ShellProgramChecker) checkPip
        }
 }
 
-func (spc *ShellProgramChecker) checkSetE(list *MkShList, eflag *bool) {
+// canFail returns true if the given shell command can fail.
+// Most shell commands can fail for various reasons, such as missing
+// files or invalid arguments.
+//
+// Commands that can fail:
+//  echo "hello" > file
+//  sed 's,$, world,,' < input > output
+//  find . -print
+//  wc -l *
+//
+// Commands that cannot fail:
+//  echo "hello"
+//  sed 's,$, world,,'
+//  wc -l
+func (spc *ShellProgramChecker) canFail(cmd *MkShCommand) bool {
+       simple := cmd.Simple
+       if simple == nil {
+               return true
+       }
+
+       if simple.Name == nil {
+               for _, assignment := range simple.Assignments {
+                       if contains(assignment.MkText, "`") || contains(assignment.MkText, "$(") {
+                               if !contains(assignment.MkText, "|| ${TRUE}") {
+                                       return true
+                               }
+                       }
+               }
+               return false
+       }
+
+       for _, redirect := range simple.Redirections {
+               if redirect.Target != nil && !hasSuffix(redirect.Op, "&") {
+                       return true
+               }
+       }
+
+       cmdName := simple.Name.MkText
+       switch cmdName {
+       case "${ECHO_MSG}", "${PHASE_MSG}", "${STEP_MSG}",
+               "${INFO_MSG}", "${WARNING_MSG}", "${ERROR_MSG}",
+               "${WARNING_CAT}", "${ERROR_CAT}",
+               "${DO_NADA}":
+               return false
+       case "${FAIL_MSG}":
+               return true
+       case "set":
+       }
+
+       tool, _ := G.Tool(cmdName, RunTime)
+       if tool == nil {
+               return true
+       }
+
+       toolName := tool.Name
+       args := simple.Args
+       argc := len(args)
+       switch toolName {
+       case "echo", "printf", "tr":
+               return false
+       case "sed", "gsed":
+               if argc == 2 && args[0].MkText == "-e" {
+                       return false
+               }
+               return argc != 1
+       case "grep", "ggrep":
+               return argc != 1
+       }
+
+       return true
+}
+
+func (spc *ShellProgramChecker) checkSetE(list *MkShList, index int, andor *MkShAndOr) {
        if trace.Tracing {
                defer trace.Call()()
        }
 
-       // Disabled until the shell parser can recognize "command || exit 1" reliably.
-       if false && G.opts.WarnExtra && !*eflag && "the current token" == ";" {
-               *eflag = true
-               spc.shline.mkline.Warnf("Please switch to \"set -e\" mode before using a semicolon (the one after %q) to separate commands.", "previous token")
-               Explain(
-                       "Normally, when a shell command fails (returns non-zero), the",
-                       "remaining commands are still executed.  For example, the following",
-                       "commands would remove all files from the HOME directory:",
-                       "",
-                       "\tcd \"$HOME\"; cd /nonexistent; rm -rf *",
-                       "",
-                       "To fix this warning, you can:",
-                       "",
-                       "* insert ${RUN} at the beginning of the line",
-                       "  (which among other things does \"set -e\")",
-                       "* insert \"set -e\" explicitly at the beginning of the line",
-                       "* use \"&&\" instead of \";\" to separate the commands")
+       command := list.AndOrs[index-1].Pipes[0].Cmds[0]
+       if command.Simple == nil || !spc.canFail(command) {
+               return
        }
+
+       line := spc.shline.mkline.Line
+       if !line.FirstTime("switch to set -e") {
+               return
+       }
+
+       line.Warnf("Please switch to \"set -e\" mode before using a semicolon (after %q) to separate commands.",
+               NewStrCommand(command.Simple).String())
+       Explain(
+               "Normally, when a shell command fails (returns non-zero), the",
+               "remaining commands are still executed.  For example, the following",
+               "commands would remove all files from the HOME directory:",
+               "",
+               "\tcd \"$HOME\"; cd /nonexistent; rm -rf *",
+               "",
+               "In \"set -e\" mode, the shell stops when a command fails.",
+               "",
+               "To fix this warning, you can:",
+               "",
+               "* insert ${RUN} at the beginning of the line",
+               "  (which among other things does \"set -e\")",
+               "* insert \"set -e\" explicitly at the beginning of the line",
+               "* use \"&&\" instead of \";\" to separate the commands")
 }
 
 // Some shell commands should not be used in the install phase.
@@ -920,6 +967,7 @@ func splitIntoShellTokens(line Line, tex
        }
 
        word := ""
+       rest = text
        p := NewShTokenizer(line, text, false)
        emit := func() {
                if word != "" {

Index: pkgsrc/pkgtools/pkglint/files/files.go
diff -u pkgsrc/pkgtools/pkglint/files/files.go:1.18 pkgsrc/pkgtools/pkglint/files/files.go:1.19
--- pkgsrc/pkgtools/pkglint/files/files.go:1.18 Thu Aug 16 20:41:42 2018
+++ pkgsrc/pkgtools/pkglint/files/files.go      Wed Oct  3 22:27:53 2018
@@ -16,6 +16,11 @@ const (
 )
 
 func Load(fileName string, options LoadOptions) []Line {
+       fromCache := G.fileCache.Get(fileName, options)
+       if fromCache != nil {
+               return fromCache
+       }
+
        rawBytes, err := ioutil.ReadFile(fileName)
        if err != nil {
                switch {
@@ -42,7 +47,9 @@ func Load(fileName string, options LoadO
                G.loaded.Add(path.Clean(fileName), 1)
        }
 
-       return convertToLogicalLines(fileName, rawText, options&Makefile != 0)
+       result := convertToLogicalLines(fileName, rawText, options&Makefile != 0)
+       G.fileCache.Put(fileName, options, result)
+       return result
 }
 
 func LoadMk(fileName string, options LoadOptions) *MkLines {
Index: pkgsrc/pkgtools/pkglint/files/mklinechecker.go
diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.18 pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.19
--- pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.18 Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/mklinechecker.go      Wed Oct  3 22:27:53 2018
@@ -18,7 +18,7 @@ func (ck MkLineChecker) Check() {
        mkline := ck.MkLine
 
        CheckLineTrailingWhitespace(mkline.Line)
-       CheckLineValidCharacters(mkline.Line, `[\t -~]`)
+       CheckLineValidCharacters(mkline.Line)
 
        switch {
        case mkline.IsVarassign():
@@ -379,15 +379,11 @@ func (ck MkLineChecker) CheckVaruse(varu
        ck.checkVarusePermissions(varname, vartype, vuc)
 
        if varname == "LOCALBASE" && !G.Infrastructure {
-               ck.WarnVaruseLocalbase()
+               ck.MkLine.Warnf("Please use PREFIX instead of LOCALBASE.")
        }
 
        needsQuoting := mkline.VariableNeedsQuoting(varname, vartype, vuc)
 
-       if vuc.quoting == vucQuotFor {
-               ck.checkVaruseFor(varname, vartype, needsQuoting)
-       }
-
        if G.opts.WarnQuoting && vuc.quoting != vucQuotUnknown && needsQuoting != nqDontKnow {
                ck.CheckVaruseShellword(varname, vartype, vuc, varuse.Mod(), needsQuoting)
        }
@@ -538,57 +534,6 @@ func (ck MkLineChecker) checkVaruseLoadT
        }
 }
 
-func (ck MkLineChecker) WarnVaruseLocalbase() {
-       ck.MkLine.Warnf("Please use PREFIX instead of LOCALBASE.")
-       Explain(
-               // from jlam via private mail.
-               "Currently, LOCALBASE is typically used in these cases:",
-               "",
-               "(1) To locate a file or directory from another package.",
-               "(2) To refer to own files after installation.",
-               "",
-               "Example for (1):",
-               "",
-               "       STRLIST=        ${LOCALBASE}/bin/strlist",
-               "",
-               "       do-build:",
-               "               cd ${WRKSRC} && ${STRLIST} *.str",
-               "",
-               "This should better be:",
-               "",
-               "       EVAL_PREFIX=    STRLIST_PREFIX=strlist",
-               "       STRLIST=        ${STRLIST_PREFIX}/bin/strlist",
-               "",
-               "       do-build:",
-               "               cd ${WRKSRC} && ${STRLIST} *.str",
-               "",
-               "Example for (2):",
-               "",
-               "       CONFIGURE_ENV+= --with-datafiles=${LOCALBASE}/share/pkgbase",
-               "",
-               "This should better be:",
-               "",
-               "       CONFIGURE_ENV+= --with-datafiles=${PREFIX}/share/pkgbase")
-}
-
-func (ck MkLineChecker) checkVaruseFor(varname string, vartype *Vartype, needsQuoting NeedsQuoting) {
-       if trace.Tracing {
-               defer trace.Call(varname, vartype, needsQuoting)()
-       }
-
-       if false && // Too many false positives
-               vartype != nil &&
-               vartype.kindOfList != lkSpace &&
-               needsQuoting != nqDoesntMatter {
-               ck.MkLine.Warnf("The variable %s should not be used in .for loops.", varname)
-               Explain(
-                       "The .for loop splits its argument at sequences of white-space, as",
-                       "opposed to all other places in make(1), which act like the shell.",
-                       "Therefore only variables that are split at whitespace or don't",
-                       "contain any special characters should be used here.")
-       }
-}
-
 // CheckVaruseShellword checks whether a variable use of the form ${VAR}
 // or ${VAR:Modifier} is allowed in a certain context.
 func (ck MkLineChecker) CheckVaruseShellword(varname string, vartype *Vartype, vuc *VarUseContext, mod string, needsQuoting NeedsQuoting) {
@@ -601,7 +546,7 @@ func (ck MkLineChecker) CheckVaruseShell
        // configure scripts.
        //
        // When doing checks outside a package, the :M* operator is needed for safety.
-       needMstar := matches(varname, `^(?:.*_)?(?:CFLAGS||CPPFLAGS|CXXFLAGS|FFLAGS|LDFLAGS|LIBS)$`) &&
+       needMstar := matches(varname, `^(?:.*_)?(?:CFLAGS|CPPFLAGS|CXXFLAGS|FFLAGS|LDFLAGS|LIBS)$`) &&
                (G.Pkg == nil || G.Pkg.vars.Defined("GNU_CONFIGURE"))
 
        strippedMod := mod
@@ -759,23 +704,12 @@ func (ck MkLineChecker) checkVarassign()
 
        ck.checkVarassignSpecific()
 
-       if varname == "EVAL_PREFIX" {
-               if m, evalVarname := match1(value, `^([\w_]+)=`); m {
-
-                       // The variables mentioned in EVAL_PREFIX will later be
-                       // defined by find-prefix.mk. Therefore, they are marked
-                       // as known in the current file.
-                       G.Mk.vars.Define(evalVarname, mkline)
-               }
-       }
-
        if fix := G.Pkgsrc.Deprecated[varname]; fix != "" {
                mkline.Warnf("Definition of %s is deprecated. %s", varname, fix)
-       } else if fix := G.Pkgsrc.Deprecated[varcanon]; fix != "" {
+       } else if fix = G.Pkgsrc.Deprecated[varcanon]; fix != "" {
                mkline.Warnf("Definition of %s is deprecated. %s", varname, fix)
        }
 
-       ck.checkVarassignPlistComment(varname, value)
        ck.checkVarassignVaruse()
 }
 
@@ -839,10 +773,10 @@ func (ck MkLineChecker) checkVarassignVa
        mkline := ck.MkLine
        atoms := NewShTokenizer(mkline.Line, mkline.Value(), false).ShAtoms()
        for i, atom := range atoms {
-               if atom.Type == shtVaruse {
+               if varuse := atom.VarUse(); varuse != nil {
                        isWordPart := isWordPart(atoms, i)
                        vuc := &VarUseContext{vartype, time, atom.Quoting.ToVarUseContext(), isWordPart}
-                       ck.CheckVaruse(atom.Data.(*MkVarUse), vuc)
+                       ck.CheckVaruse(varuse, vuc)
                }
        }
 }
@@ -922,34 +856,6 @@ func (ck MkLineChecker) checkVarassignBs
                "bsd.prefs.mk file, which will take care of everything.")
 }
 
-func (ck MkLineChecker) checkVarassignPlistComment(varname, value string) {
-       if false && // This is currently neither correct nor helpful
-               contains(value, "@comment") && !matches(value, `="@comment "`) {
-               ck.MkLine.Warnf("Please don't use @comment in %s.", varname)
-               Explain(
-                       "If you are defining a PLIST condition here, use one of the",
-                       "following patterns instead:",
-                       "",
-                       "1. The direct way, without intermediate variable",
-                       "",
-                       "\tPLIST_SUBST+=\tMY_VAR=\"@comment \"",
-                       "",
-                       "2. The indirect way, with a separate variable",
-                       "",
-                       "\tPLIST_VARS+=\tMY_VAR",
-                       "\t.if ...",
-                       "\tMY_VAR?=\tyes",
-                       "\t.endif")
-       }
-
-       // Mark the variable as PLIST condition. This is later used in checkfile_PLIST.
-       if G.Pkg != nil {
-               if m, plistVarname := match1(value, `(.+)=.*@comment.*`); m {
-                       G.Pkg.plistSubstCond[plistVarname] = true
-               }
-       }
-}
-
 func (ck MkLineChecker) CheckVartype(varname string, op MkOperator, value, comment string) {
        if trace.Tracing {
                defer trace.Call(varname, op, value, comment)()
@@ -1040,7 +946,7 @@ func (ck MkLineChecker) checkText(text s
 
        rest := text
        for {
-               m, r := regex.ReplaceFirst(rest, `(?:^|[^$])\$\{([-A-Z0-9a-z_]+)(\.[\-0-9A-Z_a-z]+)?(?::[^\}]+)?\}`, "")
+               m, r := G.res.ReplaceFirst(rest, `(?:^|[^$])\$\{([-A-Z0-9a-z_]+)(\.[\-0-9A-Z_a-z]+)?(?::[^\}]+)?\}`, "")
                if m == nil {
                        break
                }
@@ -1145,7 +1051,7 @@ func (ck MkLineChecker) checkCompareVarS
 
 func (ck MkLineChecker) CheckValidCharactersInValue(reValid regex.Pattern) {
        mkline := ck.MkLine
-       rest := regex.Compile(reValid).ReplaceAllString(mkline.Value(), "")
+       rest := replaceAll(mkline.Value(), reValid, "")
        if rest != "" {
                uni := ""
                for _, c := range rest {

Index: pkgsrc/pkgtools/pkglint/files/licenses.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses.go:1.14 pkgsrc/pkgtools/pkglint/files/licenses.go:1.15
--- pkgsrc/pkgtools/pkglint/files/licenses.go:1.14      Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/licenses.go   Wed Oct  3 22:27:53 2018
@@ -30,7 +30,7 @@ type LicenseChecker struct {
 
 func (lc *LicenseChecker) Check(value string, op MkOperator) {
        expanded := resolveVariableRefs(value) // For ${PERL5_LICENSE}
-       licenses := licenses.Parse(ifelseStr(op == opAssignAppend, "append-placeholder ", "") + expanded)
+       licenses := licenses.Parse(ifelseStr(op == opAssignAppend, "append-placeholder ", "")+expanded, &G.res)
 
        if licenses == nil {
                if op == opAssign {
Index: pkgsrc/pkgtools/pkglint/files/logging.go
diff -u pkgsrc/pkgtools/pkglint/files/logging.go:1.14 pkgsrc/pkgtools/pkglint/files/logging.go:1.15
--- pkgsrc/pkgtools/pkglint/files/logging.go:1.14       Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/logging.go    Wed Oct  3 22:27:53 2018
@@ -113,13 +113,16 @@ func Explain(explanation ...string) {
                for _, s := range explanation {
                        if l := tabWidth(s); l > 68 && contains(s, " ") {
                                lastSpace := strings.LastIndexByte(s[:68], ' ')
-                               G.logErr.Write(fmt.Sprintf("Long explanation line: %s\nBreak after: %s\n", s, s[:lastSpace]))
+                               G.logErr.Printf("Long explanation line: %s\nBreak after: %s\n", s, s[:lastSpace])
                        }
                        if m, before := match1(s, `(.+)\. [^ ]`); m {
                                if !matches(before, `\d$|e\.g`) {
-                                       G.logErr.Write(fmt.Sprintf("Short space after period: %s\n", s))
+                                       G.logErr.Printf("Short space after period: %s\n", s)
                                }
                        }
+                       if hasSuffix(s, " ") || hasSuffix(s, "\t") {
+                               G.logErr.Printf("Trailing whitespace: %q\n", s)
+                       }
                }
        }
 
@@ -174,8 +177,10 @@ func (wr *SeparatorWriter) Write(text st
                io.WriteString(wr.out, "\n")
                wr.needSeparator = false
        }
-       io.WriteString(wr.out, text)
-       wr.wroteSomething = true
+       n, err := io.WriteString(wr.out, text)
+       if err == nil && n > 0 {
+               wr.wroteSomething = true
+       }
 }
 
 func (wr *SeparatorWriter) Printf(format string, args ...interface{}) {
Index: pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.14 pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.15
--- pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.14    Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go Wed Oct  3 22:27:53 2018
@@ -2,6 +2,131 @@ package main
 
 import "gopkg.in/check.v1"
 
+func (s *Suite) Test_MkLineChecker_Check__url2pkg(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupVartypes()
+
+       mkline := t.NewMkLine("fname.mk", 1, "# url2pkg-marker")
+
+       MkLineChecker{mkline}.Check()
+
+       t.CheckOutputLines(
+               "ERROR: fname.mk:1: This comment indicates unfinished work (url2pkg).")
+}
+
+func (s *Suite) Test_MkLineChecker_Check__buildlink3_include_prefs(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupVartypes()
+
+       t.CreateFileLines("mk/bsd.prefs.mk")
+       mklines := t.SetupFileMkLines("category/package/buildlink3.mk",
+               ".include \"../../mk/bsd.prefs.mk\"")
+       // If the buildlink3.mk file is not actually created, resolving the
+       // relative path fails since that depends on the actual file system,
+       // not on syntactical paths; see os.Stat in CheckRelativePath.
+
+       MkLineChecker{mklines.mklines[0]}.Check()
+
+       t.CheckOutputLines(
+               "NOTE: ~/category/package/buildlink3.mk:1: For efficiency reasons, please include bsd.fast.prefs.mk instead of bsd.prefs.mk.")
+}
+
+func (s *Suite) Test_MkLineChecker_checkInclude(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupVartypes()
+
+       t.CreateFileLines("pkgtools/x11-links/buildlink3.mk")
+       t.CreateFileLines("graphics/jpeg/buildlink3.mk")
+       t.CreateFileLines("devel/intltool/buildlink3.mk")
+       t.CreateFileLines("devel/intltool/builtin.mk")
+       mklines := t.SetupFileMkLines("category/package/fname.mk",
+               MkRcsID,
+               "",
+               ".include \"../../pkgtools/x11-links/buildlink3.mk\"",
+               ".include \"../../graphics/jpeg/buildlink3.mk\"",
+               ".include \"../../devel/intltool/buildlink3.mk\"",
+               ".include \"../../devel/intltool/builtin.mk\"")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/fname.mk:3: ../../pkgtools/x11-links/buildlink3.mk must not be included directly. "+
+                       "Include \"../../mk/x11.buildlink3.mk\" instead.",
+               "ERROR: ~/category/package/fname.mk:4: ../../graphics/jpeg/buildlink3.mk must not be included directly. "+
+                       "Include \"../../mk/jpeg.buildlink3.mk\" instead.",
+               "WARN: ~/category/package/fname.mk:5: Please write \"USE_TOOLS+= intltool\" instead of this line.",
+               "ERROR: ~/category/package/fname.mk:6: ../../devel/intltool/builtin.mk must not be included directly. "+
+                       "Include \"../../devel/intltool/buildlink3.mk\" instead.")
+}
+
+func (s *Suite) Test_MkLineChecker_checkDirective(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupVartypes()
+
+       mklines := t.NewMkLines("category/package/fname.mk",
+               MkRcsID,
+               "",
+               ".for",
+               ".endfor",
+               "",
+               ".if",
+               ".else don't",
+               ".endif invalid-arg",
+               "",
+               ".ifdef FNAME_MK",
+               ".endif",
+               ".ifndef FNAME_MK",
+               ".endif",
+               "",
+               ".for var in a b c",
+               ".endfor",
+               ".undef var",
+               "",
+               ".for VAR in a b c",
+               ".endfor",
+               "",
+               ".for $ in a b c",
+               ".endfor")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "ERROR: category/package/fname.mk:3: \".for\" requires arguments.",
+               "ERROR: category/package/fname.mk:6: \".if\" requires arguments.",
+               "ERROR: category/package/fname.mk:7: \".else\" does not take arguments. If you meant \"else if\", use \".elif\".",
+               "ERROR: category/package/fname.mk:8: \".endif\" does not take arguments.",
+               "WARN: category/package/fname.mk:10: The \".ifdef\" directive is deprecated. Please use \".if defined(FNAME_MK)\" instead.",
+               "WARN: category/package/fname.mk:12: The \".ifndef\" directive is deprecated. Please use \".if !defined(FNAME_MK)\" instead.",
+               "NOTE: category/package/fname.mk:17: Using \".undef\" after a \".for\" loop is unnecessary.",
+               "WARN: category/package/fname.mk:19: .for variable names should not contain uppercase letters.",
+               "ERROR: category/package/fname.mk:22: Invalid variable name \"$\".")
+}
+
+func (s *Suite) Test_MkLineChecker_checkDependencyRule(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupVartypes()
+
+       mklines := t.NewMkLines("category/package/fname.mk",
+               MkRcsID,
+               "",
+               ".PHONY: target-1",
+               "target-2: .PHONY",
+               ".ORDER: target-1 target-2",
+               "target-1:",
+               "target-2:",
+               "target-3:")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: category/package/fname.mk:8: Unusual target \"target-3\".")
+}
+
 func (s *Suite) Test_MkLineChecker_CheckVartype__simple_type(c *check.C) {
        t := s.Init(c)
 
@@ -36,6 +161,30 @@ func (s *Suite) Test_MkLineChecker_Check
        t.CheckOutputEmpty()
 }
 
+func (s *Suite) Test_MkLineChecker_CheckVartype__skip(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wno-types")
+       t.SetupVartypes()
+       mkline := t.NewMkLine("fname", 1, "DISTNAME=invalid:::distname")
+
+       MkLineChecker{mkline}.Check()
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkLineChecker_CheckVartype__append_to_non_list(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupVartypes()
+       mkline := t.NewMkLine("fname", 1, "DISTNAME+=suffix")
+
+       MkLineChecker{mkline}.Check()
+
+       t.CheckOutputLines(
+               "WARN: fname:1: The \"+=\" operator should only be used with lists.")
+}
+
 // Pkglint once interpreted all lists as consisting of shell tokens,
 // splitting this URL at the ampersand.
 func (s *Suite) Test_MkLineChecker_checkVarassign__URL_with_shell_special_characters(c *check.C) {
@@ -138,34 +287,39 @@ func (s *Suite) Test_MkLineChecker_check
 func (s *Suite) Test_MkLineChecker_checkVarassignPermissions(c *check.C) {
        t := s.Init(c)
 
-       t.SetupCommandLine("-Wall")
+       t.SetupCommandLine("-Wall,no-space")
        t.SetupVartypes()
-       mkline := t.NewMkLine("options.mk", 2, "PKG_DEVELOPER?=\tyes")
+       mklines := t.NewMkLines("options.mk",
+               MkRcsID,
+               "PKG_DEVELOPER?= yes",
+               "BUILD_DEFS?=    VARBASE")
 
-       MkLineChecker{mkline}.checkVarassignPermissions()
+       mklines.Check()
 
        t.CheckOutputLines(
-               "WARN: options.mk:2: The variable PKG_DEVELOPER may not be given a default value by any package.")
+               "WARN: options.mk:2: The variable PKG_DEVELOPER may not be given a default value by any package.",
+               "WARN: options.mk:2: Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".",
+               "WARN: options.mk:3: The variable BUILD_DEFS may not be given a default value (only appended to) in this file.")
 }
 
 // Don't check the permissions for infrastructure files since they have their own rules.
-func (s *Suite) Test_MkLineChecker_checkVarassignDefPermissions__infrastructure(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkVarassignPermissions__infrastructure(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
        t.SetupVartypes()
-       t.SetupFileMkLines("mk/infra.mk",
+       t.CreateFileLines("mk/infra.mk",
                MkRcsID,
                "",
                "PKG_DEVELOPER?=\tyes")
-       t.SetupFileMkLines("mk/bsd.pkg.mk")
+       t.CreateFileLines("mk/bsd.pkg.mk")
 
        G.CheckDirent(t.File("mk/infra.mk"))
 
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_MkLineChecker_CheckVarusePermissions(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkVarusePermissions(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -186,7 +340,7 @@ func (s *Suite) Test_MkLineChecker_Check
                "NOTE: options.mk:4: This variable value should be aligned to column 17.")
 }
 
-func (s *Suite) Test_MkLineChecker_CheckVarusePermissions__load_time(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkVarusePermissions__load_time(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -202,12 +356,13 @@ func (s *Suite) Test_MkLineChecker_Check
                "NOTE: options.mk:2: This variable value should be aligned to column 17.")
 }
 
-func (s *Suite) Test_MkLineChecker_WarnVaruseLocalbase(c *check.C) {
+func (s *Suite) Test_MkLineChecker__warn_varuse_LOCALBASE(c *check.C) {
        t := s.Init(c)
 
+       t.SetupVartypes()
        mkline := t.NewMkLine("options.mk", 56, "PKGNAME=${LOCALBASE}")
 
-       MkLineChecker{mkline}.WarnVaruseLocalbase()
+       MkLineChecker{mkline}.Check()
 
        t.CheckOutputLines(
                "WARN: options.mk:56: Please use PREFIX instead of LOCALBASE.")
@@ -256,7 +411,7 @@ func (s *Suite) Test_MkLineChecker__Varu
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_MkLineChecker_CheckCond__comparison_with_shell_command(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkDirectiveCond__comparison_with_shell_command(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -273,7 +428,7 @@ func (s *Suite) Test_MkLineChecker_Check
                "WARN: security/openssl/Makefile:2: Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.")
 }
 
-func (s *Suite) Test_MkLine_CheckCond_comparing_PKGSRC_COMPILER_with_eqeq(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkDirectiveCond__comparing_PKGSRC_COMPILER_with_eqeq(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -313,7 +468,7 @@ func (s *Suite) Test_MkLineChecker_Check
 // See PR 46570, Ctrl+F "4. Shell quoting".
 // Pkglint is correct, since the shell sees this definition for
 // CPPFLAGS as three words, not one word.
-func (s *Suite) Test_MkLineChecker_CheckVartype_CFLAGS(c *check.C) {
+func (s *Suite) Test_MkLineChecker_CheckVartype__CFLAGS(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -330,7 +485,7 @@ func (s *Suite) Test_MkLineChecker_Check
 
 // Up to 2018-01-28, pkglint applied the autofix also to the continuation
 // lines, which is incorrect. It replaced the dot in "4.*" with spaces.
-func (s *Suite) Test_MkLineChecker_checkDirectiveIndentation_autofix(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkDirectiveIndentation__autofix(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall", "--autofix")
@@ -382,6 +537,64 @@ func (s *Suite) Test_MkLineChecker_Check
                "WARN: ~/options.mk:4: The variable PATH should be quoted as part of a shell word.")
 }
 
+func (s *Suite) Test_MkLineChecker_CheckVaruseShellword__mstar(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall,no-space")
+       t.SetupVartypes()
+       mklines := t.SetupFileMkLines("options.mk",
+               MkRcsID,
+               "CONFIGURE_ARGS+=        ${CFLAGS:Q}",
+               "CONFIGURE_ARGS+=        ${CFLAGS:M*:Q}",
+               "CONFIGURE_ARGS+=        ${ADA_FLAGS:Q}",
+               "CONFIGURE_ARGS+=        ${ADA_FLAGS:M*:Q}",
+               "CONFIGURE_ENV+=         ${CFLAGS:Q}",
+               "CONFIGURE_ENV+=         ${CFLAGS:M*:Q}",
+               "CONFIGURE_ENV+=         ${ADA_FLAGS:Q}",
+               "CONFIGURE_ENV+=         ${ADA_FLAGS:M*:Q}")
+
+       mklines.Check()
+
+       // FIXME: There should be some notes and warnings; prevented by the PERL5 case in VariableNeedsQuoting.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_MkLineChecker_CheckVaruseShellword__mstar_not_needed(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall,no-space")
+       pkg := t.SetupPackage("category/package",
+               "MAKE_FLAGS+=\tCFLAGS=${CFLAGS:M*:Q}",
+               "MAKE_FLAGS+=\tLFLAGS=${LDFLAGS:M*:Q}")
+       G.Pkgsrc.LoadInfrastructure()
+       // FIXME: It is too easy to forget this important call.
+
+       // This package is guaranteed to not use GNU_CONFIGURE.
+       // Since the :M* hack is only needed for GNU_CONFIGURE, it is not necessary here.
+       G.CheckDirent(pkg)
+
+       // FIXME: Duplicate diagnostics.
+       t.CheckOutputLines(
+               "NOTE: ~/category/package/Makefile:20: The :M* modifier is not needed here.",
+               "NOTE: ~/category/package/Makefile:20: The :M* modifier is not needed here.",
+               "NOTE: ~/category/package/Makefile:21: The :M* modifier is not needed here.",
+               "NOTE: ~/category/package/Makefile:21: The :M* modifier is not needed here.")
+}
+
+func (s *Suite) Test_MkLineChecker_CheckVaruseShellword__q_not_needed(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall")
+       pkg := t.SetupPackage("category/package",
+               "MASTER_SITES=\t${HOMEPAGE:Q}")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:5: The :Q operator should not be used for ${HOMEPAGE} here.")
+}
+
 // The ${VARNAME:=suffix} expression should only be used with lists.
 // It typically appears in MASTER_SITE definitions.
 func (s *Suite) Test_MkLineChecker_CheckVaruse__eq_nonlist(c *check.C) {
@@ -455,7 +668,10 @@ func (s *Suite) Test_MkLineChecker_check
                "_TOOLS_VARNAME.sed=     SED",
                "DIST_SUBDIR=            ${PKGNAME}",
                "WRKSRC=                 ${PKGNAME}",
-               "SITES_distfile.tar.gz=  ${MASTER_SITES_GITHUB:=user/}")
+               "SITES_distfile.tar.gz=  ${MASTER_SITES_GITHUB:=user/}",
+               // TODO: The first of the below assignments should be flagged as redundant by RedundantScope.
+               "PYTHON_VERSIONS_ACCEPTED= -13",
+               "PYTHON_VERSIONS_ACCEPTED= 27 36")
 
        mklines.Check()
 
@@ -466,7 +682,14 @@ func (s *Suite) Test_MkLineChecker_check
                "WARN: ~/module.mk:4: PKGNAME should not be used in DIST_SUBDIR, as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.",
                "WARN: ~/module.mk:5: PKGNAME should not be used in WRKSRC, as it includes the PKGREVISION. Please use PKGNAME_NOREV instead.",
                "WARN: ~/module.mk:6: SITES_distfile.tar.gz is defined but not used.",
-               "WARN: ~/module.mk:6: SITES_* is deprecated. Please use SITES.* instead.")
+               "WARN: ~/module.mk:6: SITES_* is deprecated. Please use SITES.* instead.",
+               "WARN: ~/module.mk:7: The variable PYTHON_VERSIONS_ACCEPTED may not be set "+
+                       "(only given a default value, appended to) in this file; it would be ok in Makefile, Makefile.common, options.mk.",
+               "WARN: ~/module.mk:7: Invalid version number \"-13\".",
+               "ERROR: ~/module.mk:7: All values for PYTHON_VERSIONS_ACCEPTED must be positive integers.",
+               "WARN: ~/module.mk:8: The variable PYTHON_VERSIONS_ACCEPTED may not be set "+
+                       "(only given a default value, appended to) in this file; it would be ok in Makefile, Makefile.common, options.mk.",
+               "WARN: ~/module.mk:8: The values for PYTHON_VERSIONS_ACCEPTED should be in decreasing order.")
 }
 
 func (s *Suite) Test_MkLineChecker_checkText(c *check.C) {
Index: pkgsrc/pkgtools/pkglint/files/mkparser_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.14 pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.15
--- pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.14 Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/mkparser_test.go      Wed Oct  3 22:27:53 2018
@@ -104,7 +104,9 @@ func (s *Suite) Test_MkParser_MkTokens(c
        check("${${${PKG_INFO} -E ${d} || echo:L:sh}:L:C/[^[0-9]]*/ /g:[1..3]:ts.}",
                varuse("${${PKG_INFO} -E ${d} || echo:L:sh}", "L", "C/[^[0-9]]*/ /g", "[1..3]", "ts."))
 
-       check("${VAR:S/-//S/.//}", varuseText("${VAR:S/-//S/.//}", "VAR", "S/-//", "S/.//")) // For :S and :C, the colon can be left out.
+       // For :S and :C, the colon can be left out.
+       check("${VAR:S/-//S/.//}",
+               varuseText("${VAR:S/-//S/.//}", "VAR", "S/-//", "S/.//"))
 
        check("${VAR:ts}", varuse("VAR", "ts"))                 // The separator character can be left out.
        check("${VAR:ts\\000012}", varuse("VAR", "ts\\000012")) // The separator character can be a long octal number.
@@ -130,12 +132,21 @@ func (s *Suite) Test_MkParser_MkTokens(c
        checkRest("hello, ${W:L:tl}orld", []*MkToken{
                literal("hello, "),
                varuse("W", "L", "tl"),
-               literal("orld")}, "")
+               literal("orld")},
+               "")
        checkRest("ftp://${PKGNAME}/ ${MASTER_SITES:=subdir/}", []*MkToken{
                literal("ftp://";),
                varuse("PKGNAME"),
                literal("/ "),
-               varuse("MASTER_SITES", "=subdir/")}, "")
+               varuse("MASTER_SITES", "=subdir/")},
+               "")
+
+       // FIXME: Text must match modifiers.
+       checkRest("${VAR:S,a,b,c,d,e,f}",
+               []*MkToken{{
+                       Text:   "${VAR:S,a,b,c,d,e,f}",
+                       Varuse: &MkVarUse{varname: "VAR", modifiers: []string{"S,a,b,"}}}},
+               "")
 }
 
 func (s *Suite) Test_MkParser_MkCond(c *check.C) {
Index: pkgsrc/pkgtools/pkglint/files/substcontext_test.go
diff -u pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.14 pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.15
--- pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.14     Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/substcontext_test.go  Wed Oct  3 22:27:53 2018
@@ -241,37 +241,18 @@ func (s *Suite) Test_SubstContext__pre_c
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall,no-space")
-       t.SetupPkgsrc()
-       G.Pkgsrc.LoadInfrastructure()
-       t.CreateFileLines("category/Makefile")
-       t.CreateFileLines("licenses/2-clause-bsd")
-
-       t.Chdir("category/package")
-       t.CreateFileLines("PLIST",
-               PlistRcsID,
-               "bin/program")
-       t.CreateFileLines("Makefile",
-               MkRcsID,
-               "",
-               "CATEGORIES=             category",
-               "",
-               "COMMENT=                Comment",
-               "LICENSE=                2-clause-bsd",
-               "",
+       pkg := t.SetupPackage("category/package",
                "SUBST_CLASSES+=         os",
                "SUBST_STAGE.os=         pre-configure",
                "SUBST_FILES.os=         guess-os.h",
                "SUBST_SED.os=           -e s,@OPSYS@,Darwin,",
                "",
-               "NO_CHECKSUM=            yes",
-               "NO_CONFIGURE=           yes",
-               "",
-               ".include \"../../mk/bsd.pkg.mk\"")
+               "NO_CONFIGURE=           yes")
 
-       G.checkdirPackage(".")
+       G.CheckDirent(pkg)
 
        t.CheckOutputLines(
-               "WARN: Makefile:9: SUBST_STAGE pre-configure has no effect when NO_CONFIGURE is set (in line 14).")
+               "WARN: ~/category/package/Makefile:21: SUBST_STAGE pre-configure has no effect when NO_CONFIGURE is set (in line 25).")
 }
 
 func (s *Suite) Test_SubstContext__adjacent(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/licenses_test.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.15 pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.16
--- pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.15 Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/licenses_test.go      Wed Oct  3 22:27:53 2018
@@ -4,10 +4,10 @@ import (
        "gopkg.in/check.v1"
 )
 
-func (s *Suite) Test_checklineLicense(c *check.C) {
+func (s *Suite) Test_LicenseChecker_Check(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("licenses/gnu-gpl-v2",
+       t.CreateFileLines("licenses/gnu-gpl-v2",
                "Most software \u2026")
        mkline := t.NewMkLine("Makefile", 7, "LICENSE=dummy")
 
@@ -48,15 +48,15 @@ func (s *Suite) Test_checkToplevelUnused
        t := s.Init(c)
 
        t.SetupPkgsrc()
-       t.SetupFileLines("mk/misc/category.mk")
-       t.SetupFileLines("licenses/2-clause-bsd")
-       t.SetupFileLines("licenses/gnu-gpl-v3")
+       t.CreateFileLines("mk/misc/category.mk")
+       t.CreateFileLines("licenses/2-clause-bsd")
+       t.CreateFileLines("licenses/gnu-gpl-v3")
 
-       t.SetupFileLines("Makefile",
+       t.CreateFileLines("Makefile",
                MkRcsID,
                "SUBDIR+=\tcategory")
 
-       t.SetupFileLines("category/Makefile",
+       t.CreateFileLines("category/Makefile",
                MkRcsID,
                "COMMENT=\tExample category",
                "",
@@ -64,14 +64,14 @@ func (s *Suite) Test_checkToplevelUnused
                "",
                ".include \"../mk/misc/category.mk\"")
 
-       t.SetupFileLines("category/package/Makefile",
+       t.CreateFileLines("category/package/Makefile",
                MkRcsID,
                "CATEGORIES=\tcategory",
                "",
                "COMMENT=Example package",
                "LICENSE=\t2-clause-bsd",
                "NO_CHECKSUM=\tyes")
-       t.SetupFileLines("category/package/PLIST",
+       t.CreateFileLines("category/package/PLIST",
                PlistRcsID,
                "bin/program")
 
@@ -88,9 +88,9 @@ func (s *Suite) Test_LicenseChecker_chec
 
        t.SetupPkgsrc()
        t.SetupCommandLine("-Wno-space")
-       t.SetupFileLines("category/package/DESCR",
+       t.CreateFileLines("category/package/DESCR",
                "Package description")
-       t.SetupFileMkLines("category/package/Makefile",
+       t.CreateFileLines("category/package/Makefile",
                MkRcsID,
                "",
                "CATEGORIES=     chinese",
@@ -102,10 +102,10 @@ func (s *Suite) Test_LicenseChecker_chec
                "NO_CHECKSUM=    yes",
                "",
                ".include \"../../mk/bsd.pkg.mk\"")
-       t.SetupFileLines("category/package/PLIST",
+       t.CreateFileLines("category/package/PLIST",
                PlistRcsID,
                "bin/program")
-       t.SetupFileLines("category/package/my-license",
+       t.CreateFileLines("category/package/my-license",
                "An individual license file.")
 
        G.Main("pkglint", t.File("category/package"))
@@ -113,5 +113,6 @@ func (s *Suite) Test_LicenseChecker_chec
        // FIXME: It should be allowed to place a license file directly into
        // the package directory.
        t.CheckOutputLines(
-               "WARN: ~/category/package/my-license: Unexpected file found.", "0 errors and 1 warning found.")
+               "WARN: ~/category/package/my-license: Unexpected file found.",
+               "0 errors and 1 warning found.")
 }

Index: pkgsrc/pkgtools/pkglint/files/linechecker.go
diff -u pkgsrc/pkgtools/pkglint/files/linechecker.go:1.7 pkgsrc/pkgtools/pkglint/files/linechecker.go:1.8
--- pkgsrc/pkgtools/pkglint/files/linechecker.go:1.7    Sat Mar 24 14:32:49 2018
+++ pkgsrc/pkgtools/pkglint/files/linechecker.go        Wed Oct  3 22:27:53 2018
@@ -19,8 +19,8 @@ func CheckLineAbsolutePathname(line Line
        //
        // Another context where absolute pathnames usually appear is in
        // assignments like "bindir=/bin".
-       if m, path := regex.Match1(text, `(?:^|\$[{(]DESTDIR[)}]|[\w_]+\s*=\s*)(/(?:[^"'\s]|"[^"*]"|'[^']*')*)`); m {
-               if regex.Matches(path, `^/\w`) {
+       if m, path := match1(text, `(?:^|\s|\$[{(]DESTDIR[)}]|[\w_]+\s*=\s*)(/(?:[^"'\s\\]|"[^"*]"|'[^']*')*)`); m {
+               if matches(path, `^/\w`) {
                        CheckwordAbsolutePathname(line, path)
                }
        }
@@ -36,13 +36,14 @@ func CheckLineLength(line Line, maxlengt
        }
 }
 
-func CheckLineValidCharacters(line Line, reChar regex.Pattern) {
-       rest := regex.Compile(reChar).ReplaceAllString(line.Text, "")
-       if rest != "" {
-               uni := ""
-               for _, c := range rest {
-                       uni += fmt.Sprintf(" %U", c)
+func CheckLineValidCharacters(line Line) {
+       uni := ""
+       for _, r := range line.Text {
+               if r != '\t' && !(' ' <= r && r <= '~') {
+                       uni += fmt.Sprintf(" %U", r)
                }
+       }
+       if uni != "" {
                line.Warnf("Line contains invalid characters (%s).", uni[1:])
        }
 }
@@ -64,7 +65,7 @@ func CheckLineRcsid(line Line, prefixRe 
                defer trace.Call(prefixRe, suggestedPrefix)()
        }
 
-       if regex.Matches(line.Text, `^`+prefixRe+`\$`+`NetBSD(?::[^\$]+)?\$$`) {
+       if matches(line.Text, `^`+prefixRe+`\$`+`NetBSD(?::[^\$]+)?\$$`) {
                return true
        }
 
@@ -86,13 +87,16 @@ func CheckwordAbsolutePathname(line Line
        }
 
        switch {
-       case regex.Matches(word, `^/dev/(?:null|tty|zero)$`):
-       // These are defined by POSIX.
+       case matches(word, `^/dev/(?:null|tty|zero)$`):
+               // These are defined by POSIX.
+
        case word == "/bin/sh":
-       // This is usually correct, although on Solaris, it's pretty feature-crippled.
-       case regex.Matches(word, `^/s\W`):
-       // Probably a sed(1) command
-       case regex.Matches(word, `^/(?:[a-z]|\$[({])`):
+               // This is usually correct, although on Solaris, it's pretty feature-crippled.
+
+       case matches(word, `/s\W`):
+               // Probably a sed(1) command, e.g. /find/s,replace,with,
+
+       case matches(word, `^/(?:[a-z]|\$[({])`):
                // Absolute paths probably start with a lowercase letter.
                line.Warnf("Found absolute pathname: %s", word)
                if contains(line.Text, "DESTDIR") {
Index: pkgsrc/pkgtools/pkglint/files/linechecker_test.go
diff -u pkgsrc/pkgtools/pkglint/files/linechecker_test.go:1.7 pkgsrc/pkgtools/pkglint/files/linechecker_test.go:1.8
--- pkgsrc/pkgtools/pkglint/files/linechecker_test.go:1.7       Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/linechecker_test.go   Wed Oct  3 22:27:53 2018
@@ -4,22 +4,30 @@ import (
        "gopkg.in/check.v1"
 )
 
-func (s *Suite) Test_LineChecker_CheckAbsolutePathname(c *check.C) {
+func (s *Suite) Test_CheckLineAbsolutePathname(c *check.C) {
        t := s.Init(c)
 
        line := t.NewLine("Makefile", 1, "# dummy")
 
        CheckLineAbsolutePathname(line, "bindir=/bin")
        CheckLineAbsolutePathname(line, "bindir=/../lib")
-       CheckLineAbsolutePathname(line, "cat /dev/null")   // FIXME: Not classified as absolute path.
-       CheckLineAbsolutePathname(line, "cat /dev//tty")   // FIXME: Not classified as absolute patFIXMEh.
-       CheckLineAbsolutePathname(line, "cat /dev/zero")   // FIXME: Not classified as absolute path.
-       CheckLineAbsolutePathname(line, "cat /dev/stdin")  // FIXME: Not classified as absolute path.
-       CheckLineAbsolutePathname(line, "cat /dev/stdout") // FIXME: Not classified as absolute path.
-       CheckLineAbsolutePathname(line, "cat /dev/stderr") // FIXME: Not classified as absolute path.
+       CheckLineAbsolutePathname(line, "cat /dev/null")
+       CheckLineAbsolutePathname(line, "cat /dev/tty")
+       CheckLineAbsolutePathname(line, "cat /dev/zero")
+       CheckLineAbsolutePathname(line, "cat /dev/stdin")
+       CheckLineAbsolutePathname(line, "cat /dev/stdout")
+       CheckLineAbsolutePathname(line, "cat /dev/stderr")
+       CheckLineAbsolutePathname(line, "printf '#! /bin/sh\\nexit 0'")
+
+       // This is not a file name at all, but certainly looks like one.
+       // Nevertheless, pkglint doesn't fall into the trap.
+       CheckLineAbsolutePathname(line, "sed -e /usr/s/usr/var/g")
 
        t.CheckOutputLines(
-               "WARN: Makefile:1: Found absolute pathname: /bin")
+               "WARN: Makefile:1: Found absolute pathname: /bin",
+               "WARN: Makefile:1: Found absolute pathname: /dev/stdin",
+               "WARN: Makefile:1: Found absolute pathname: /dev/stdout",
+               "WARN: Makefile:1: Found absolute pathname: /dev/stderr")
 }
 
 func (s *Suite) Test_CheckLineTrailingWhitespace(c *check.C) {
Index: pkgsrc/pkgtools/pkglint/files/mkshtypes.go
diff -u pkgsrc/pkgtools/pkglint/files/mkshtypes.go:1.7 pkgsrc/pkgtools/pkglint/files/mkshtypes.go:1.8
--- pkgsrc/pkgtools/pkglint/files/mkshtypes.go:1.7      Mon Jan  1 18:04:15 2018
+++ pkgsrc/pkgtools/pkglint/files/mkshtypes.go  Wed Oct  3 22:27:53 2018
@@ -1,14 +1,16 @@
 package main
 
 import (
-       "fmt"
        "netbsd.org/pkglint/regex"
+       "strings"
 )
 
+// MkShList is a list of shell commands, separated by newlines or semicolons.
+//
 // Example: cd $dir && echo "In $dir"; cd ..; ls -l
 type MkShList struct {
        AndOrs     []*MkShAndOr
-       Separators []MkShSeparator
+       Separators []MkShSeparator // One less entry than in AndOrs.
 }
 
 func NewMkShList() *MkShList {
@@ -25,6 +27,9 @@ func (list *MkShList) AddSeparator(separ
        return list
 }
 
+// MkShAndOr is a group of commands that are connected with && or ||
+// conditions.
+//
 // Example: cd $dir && echo "In $dir" || echo "Cannot cd into $dir"
 type MkShAndOr struct {
        Pipes []*MkShPipeline
@@ -41,6 +46,8 @@ func (andor *MkShAndOr) Add(op string, p
        return andor
 }
 
+// MkShPipeline is a group of commands, connected by pipelines.
+//
 // Example: grep word file | sed s,^,---,
 type MkShPipeline struct {
        Negated bool
@@ -56,6 +63,8 @@ func (pipe *MkShPipeline) Add(cmd *MkShC
        return pipe
 }
 
+// MkShCommand is a simple or compound shell command.
+//
 // Example: LC_ALL=C sort */*.c > sorted
 // Example: dir() { ls -l "$@"; }
 // Example: { echo "first"; echo "second"; }
@@ -66,6 +75,8 @@ type MkShCommand struct {
        Redirects []*MkShRedirection // For Compound and FuncDef
 }
 
+// MkShCompoundCommand is a group of commands.
+//
 // Example: { echo "first"; echo "second"; }
 // Example: for f in *.c; do compile "$f"; done
 // Example: if [ -f "$file" ]; then echo "It exists"; fi
@@ -79,6 +90,8 @@ type MkShCompoundCommand struct {
        Loop     *MkShLoopClause
 }
 
+// MkShForClause is a "for" loop.
+//
 // Example: for f in *.c; do compile "$f"; done
 type MkShForClause struct {
        Varname string
@@ -86,12 +99,16 @@ type MkShForClause struct {
        Body    *MkShList
 }
 
+// MkShCaseClause is a "case" statement, including all its branches.
+//
 // Example: case $filename in *.c) echo "C source" ;; esac
 type MkShCaseClause struct {
        Word  *ShToken
        Cases []*MkShCaseItem
 }
 
+// MkShCaseItem is one branch of a "case" statement.
+//
 // Example: *.c) echo "C source" ;;
 type MkShCaseItem struct {
        Patterns  []*ShToken
@@ -99,6 +116,9 @@ type MkShCaseItem struct {
        Separator MkShSeparator
 }
 
+// MkShIfClause is a conditional statement, possibly having
+// many branches.
+//
 // Example: if [ -f "$file" ]; then echo "It exists"; fi
 type MkShIfClause struct {
        Conds   []*MkShList
@@ -111,6 +131,8 @@ func (cl *MkShIfClause) Prepend(cond *Mk
        cl.Actions = append([]*MkShList{action}, cl.Actions...)
 }
 
+// MkShLoopClause is a "while" or "until" loop.
+//
 // Example: while sleep 1; do printf .; done
 type MkShLoopClause struct {
        Cond   *MkShList
@@ -118,12 +140,17 @@ type MkShLoopClause struct {
        Until  bool
 }
 
+// MkShFunctionDefinition is the definition of a shell function.
+//
 // Example: dir() { ls -l "$@"; }
 type MkShFunctionDefinition struct {
        Name string
        Body *MkShCompoundCommand
 }
 
+// MkShSimpleCommand is a shell command that does not involve any
+// pipeline or conditionals.
+//
 // Example: LC_ALL=C sort */*.c > sorted
 type MkShSimpleCommand struct {
        Assignments  []*ShToken
@@ -149,9 +176,9 @@ func NewStrCommand(cmd *MkShSimpleComman
        return strcmd
 }
 
-// Similar to MkShSimpleCommand, but all components are converted
-// to strings to allow for simpler checks, especially for analyzing
-// command line options.
+// StrCommand is structurally similar to MkShSimpleCommand, but all
+// components are converted to strings to allow for simpler checks,
+// especially for analyzing command line options.
 //
 // Example: LC_ALL=C sort */*.c > sorted
 type StrCommand struct {
@@ -160,6 +187,7 @@ type StrCommand struct {
        Args        []string
 }
 
+// HasOption checks whether one of the arguments is exactly the given opt.
 func (c *StrCommand) HasOption(opt string) bool {
        for _, arg := range c.Args {
                if arg == opt {
@@ -179,9 +207,21 @@ func (c *StrCommand) AnyArgMatches(patte
 }
 
 func (c *StrCommand) String() string {
-       return fmt.Sprintf("%v %v %v", c.Assignments, c.Name, c.Args)
+       var strs []string
+       for _, assignment := range c.Assignments {
+               strs = append(strs, assignment)
+       }
+       if c.Name != "" {
+               strs = append(strs, c.Name)
+       }
+       for _, arg := range c.Args {
+               strs = append(strs, arg)
+       }
+       return strings.Join(strs, " ")
 }
 
+// MkShRedirection is a single file descriptor redirection.
+//
 // Example: > sorted
 // Example: 2>&1
 type MkShRedirection struct {
Index: pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.7 pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.8
--- pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.7    Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go        Wed Oct  3 22:27:53 2018
@@ -61,22 +61,22 @@ func (s *Suite) Test_Pkgsrc_parseSuggest
 func (s *Suite) Test_Pkgsrc_loadTools(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("mk/tools/bsd.tools.mk",
+       t.CreateFileLines("mk/tools/bsd.tools.mk",
                ".include \"flex.mk\"",
                ".include \"gettext.mk\"",
                ".include \"strip.mk\"",
                ".include \"replace.mk\"")
-       t.SetupFileLines("mk/tools/defaults.mk",
+       t.CreateFileLines("mk/tools/defaults.mk",
                "_TOOLS_VARNAME.chown=CHOWN",
                "_TOOLS_VARNAME.gawk=AWK",
                "_TOOLS_VARNAME.mv=MV",
                "_TOOLS_VARNAME.pwd=PWD")
-       t.SetupFileLines("mk/tools/flex.mk",
+       t.CreateFileLines("mk/tools/flex.mk",
                "# empty")
-       t.SetupFileLines("mk/tools/gettext.mk",
+       t.CreateFileLines("mk/tools/gettext.mk",
                "USE_TOOLS+=msgfmt",
                "TOOLS_CREATE+=msgfmt")
-       t.SetupFileLines("mk/tools/strip.mk",
+       t.CreateFileLines("mk/tools/strip.mk",
                ".if defined(_INSTALL_UNSTRIPPED) || !defined(TOOLS_PLATFORM.strip)",
                "TOOLS_NOOP+=            strip",
                ".else",
@@ -84,14 +84,14 @@ func (s *Suite) Test_Pkgsrc_loadTools(c 
                "TOOLS_PATH.strip=       ${TOOLS_PLATFORM.strip}",
                ".endif",
                "STRIP?=         strip")
-       t.SetupFileLines("mk/tools/replace.mk",
+       t.CreateFileLines("mk/tools/replace.mk",
                "_TOOLS.bzip2=\tbzip2 bzcat",
                "#TOOLS_CREATE+=commented out",
                "_UNRELATED_VAR=\t# empty")
-       t.SetupFileLines("mk/bsd.prefs.mk",
+       t.CreateFileLines("mk/bsd.prefs.mk",
                "USE_TOOLS+=\tpwd",
                "USE_TOOLS+=\tm4:pkgsrc")
-       t.SetupFileLines("mk/bsd.pkg.mk",
+       t.CreateFileLines("mk/bsd.pkg.mk",
                "USE_TOOLS+=\tmv")
 
        G.Pkgsrc.loadTools()
@@ -119,17 +119,46 @@ func (s *Suite) Test_Pkgsrc_loadTools(c 
                "TRACE: - (*Tools).Trace(\"Pkgsrc\")")
 }
 
+// As a side-benefit, loadTools also loads the _BUILD_DEFS.
+func (s *Suite) Test_Pkgsrc_loadTools__BUILD_DEFS(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall")
+       t.SetupToolUsable("echo", "ECHO")
+       pkg := t.SetupPackage("category/package",
+               "pre-configure:",
+               "\t@${ECHO} ${PKG_SYSCONFDIR} ${VARBASE}")
+       t.CreateFileLines("mk/bsd.pkg.mk",
+               MkRcsID,
+               "_BUILD_DEFS+=\tPKG_SYSCONFBASEDIR PKG_SYSCONFDIR")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.CheckDirent(pkg)
+
+       c.Check(G.Pkgsrc.IsBuildDef("PKG_SYSCONFDIR"), equals, true)
+       c.Check(G.Pkgsrc.IsBuildDef("VARBASE"), equals, false)
+
+       // FIXME: There should be a warning for VARBASE, but G.Pkgsrc.UserDefinedVars
+       // does not contain anything at mklinechecker.go:/UserDefinedVars/.
+       t.CheckOutputLines()
+}
+
 func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("doc/CHANGES-2018",
+       t.CreateFileLines("doc/CHANGES-2018",
                "\tAdded category/package version 1.0 [author1 2015-01-01]", // Wrong year
                "\tUpdated category/package to 1.5 [author2 2018-01-02]",
                "\tRenamed category/package to category/pkg [author3 2018-01-03]",
                "\tMoved category/package to other/package [author4 2018-01-04]",
                "\tRemoved category/package [author5 2018-01-09]", // Too far in the future
                "\tRemoved category/package successor category/package2 [author6 2018-01-06]",
-               "\tDowngraded category/package to 1.2 [author7 2018-01-07]")
+               "\tDowngraded category/package to 1.2 [author7 2018-01-07]",
+               "",
+               "\ttoo few fields",
+               "\ttoo many many many many many fields",
+               "\tmissing brackets around author",
+               "\tAdded another [new package]")
 
        changes := G.Pkgsrc.loadDocChangesFromFile(t.File("doc/CHANGES-2018"))
 
@@ -144,22 +173,68 @@ func (s *Suite) Test_Pkgsrc_loadDocChang
 
        t.CheckOutputLines(
                "WARN: ~/doc/CHANGES-2018:1: Year 2015 for category/package does not match the file name ~/doc/CHANGES-2018.",
-               "WARN: ~/doc/CHANGES-2018:6: Date 2018-01-06 for category/package is earlier than 2018-01-09 for category/package.")
+               "WARN: ~/doc/CHANGES-2018:6: Date 2018-01-06 for category/package is earlier than 2018-01-09 for category/package.",
+               "WARN: ~/doc/CHANGES-2018:12: Unknown doc/CHANGES line: \tAdded another [new package]")
+}
+
+func (s *Suite) Test_Pkgsrc_loadDocChanges__not_found(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupPkgsrc()
+       t.Remove("doc/CHANGES-2018")
+       t.Remove("doc/TODO")
+       t.Remove("doc")
+
+       t.ExpectFatal(
+               G.Pkgsrc.loadDocChanges,
+               "FATAL: ~/doc: Cannot be read.")
 }
 
-func (s *Suite) Test_Pkgsrc_deprecated(c *check.C) {
+func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile__not_found(c *check.C) {
+       t := s.Init(c)
+
+       t.ExpectFatal(
+               func() { G.Pkgsrc.loadDocChangesFromFile(t.File("doc/CHANGES-2018")) },
+               "FATAL: ~/doc/CHANGES-2018: Cannot be read.")
+}
+
+func (s *Suite) Test_Pkgsrc_loadDocChangesFromFile__wip(c *check.C) {
+       t := s.Init(c)
+
+       pkg := t.SetupPackage("wip/package",
+               "DISTNAME=\tpackage-1.11",
+               "CATEGORIES=\tlocal")
+       t.CreateFileLines("wip/TODO",
+               RcsID,
+               "",
+               "Suggested package updates",
+               "",
+               "\to package-1.13 [cool new features]")
+       G.Pkgsrc.LoadInfrastructure()
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputLines(
+               "WARN: ~/wip/package/Makefile:3: This package should be updated to 1.13 ([cool new features]).")
+}
+
+func (s *Suite) Test_Pkgsrc__deprecated(c *check.C) {
        t := s.Init(c)
 
        G.Pkgsrc.initDeprecatedVars()
-       mkline := t.NewMkLine("Makefile", 5, "USE_PERL5=\tyes")
+       mklines := t.NewMkLines("Makefile",
+               "USE_PERL5=\tyes",
+               "SUBST_POSTCMD.class=${ECHO}")
 
-       MkLineChecker{mkline}.checkVarassign()
+       MkLineChecker{mklines.mklines[0]}.checkVarassign()
+       MkLineChecker{mklines.mklines[1]}.checkVarassign()
 
        t.CheckOutputLines(
-               "WARN: Makefile:5: Definition of USE_PERL5 is deprecated. Use USE_TOOLS+=perl or USE_TOOLS+=perl:run instead.")
+               "WARN: Makefile:1: Definition of USE_PERL5 is deprecated. Use USE_TOOLS+=perl or USE_TOOLS+=perl:run instead.",
+               "WARN: Makefile:2: Definition of SUBST_POSTCMD.class is deprecated. Has been removed, as it seemed unused.")
 }
 
-func (s *Suite) Test_Pkgsrc_Latest_no_basedir(c *check.C) {
+func (s *Suite) Test_Pkgsrc_Latest__no_basedir(c *check.C) {
        t := s.Init(c)
 
        latest := G.Pkgsrc.Latest("lang", `^python[0-9]+$`, "../../lang/$0")
@@ -169,10 +244,10 @@ func (s *Suite) Test_Pkgsrc_Latest_no_ba
                "ERROR: Cannot find latest version of \"^python[0-9]+$\" in \"~/lang\".")
 }
 
-func (s *Suite) Test_Pkgsrc_Latest_no_subdirs(c *check.C) {
+func (s *Suite) Test_Pkgsrc_Latest__no_subdirs(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("lang/Makefile")
+       t.CreateFileLines("lang/Makefile")
 
        latest := G.Pkgsrc.Latest("lang", `^python[0-9]+$`, "../../lang/$0")
 
@@ -181,46 +256,88 @@ func (s *Suite) Test_Pkgsrc_Latest_no_su
                "ERROR: Cannot find latest version of \"^python[0-9]+$\" in \"~/lang\".")
 }
 
-func (s *Suite) Test_Pkgsrc_Latest_single(c *check.C) {
+func (s *Suite) Test_Pkgsrc_Latest__single(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("lang/Makefile")
-       t.SetupFileLines("lang/python27/Makefile")
+       t.CreateFileLines("lang/Makefile")
+       t.CreateFileLines("lang/python27/Makefile")
 
        latest := G.Pkgsrc.Latest("lang", `^python[0-9]+$`, "../../lang/$0")
 
        c.Check(latest, equals, "../../lang/python27")
+
+       cached := G.Pkgsrc.Latest("lang", `^python[0-9]+$`, "../../lang/$0")
+
+       c.Check(cached, equals, "../../lang/python27")
 }
 
-func (s *Suite) Test_Pkgsrc_Latest_multi(c *check.C) {
+func (s *Suite) Test_Pkgsrc_Latest__multi(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("lang/Makefile")
-       t.SetupFileLines("lang/python27/Makefile")
-       t.SetupFileLines("lang/python35/Makefile")
+       t.CreateFileLines("lang/Makefile")
+       t.CreateFileLines("lang/python27/Makefile")
+       t.CreateFileLines("lang/python35/Makefile")
 
        latest := G.Pkgsrc.Latest("lang", `^python[0-9]+$`, "../../lang/$0")
 
        c.Check(latest, equals, "../../lang/python35")
 }
 
-func (s *Suite) Test_Pkgsrc_Latest_numeric(c *check.C) {
+func (s *Suite) Test_Pkgsrc_Latest__numeric(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("databases/postgresql95/Makefile")
-       t.SetupFileLines("databases/postgresql97/Makefile")
-       t.SetupFileLines("databases/postgresql100/Makefile")
-       t.SetupFileLines("databases/postgresql104/Makefile")
+       t.CreateFileLines("databases/postgresql95/Makefile")
+       t.CreateFileLines("databases/postgresql97/Makefile")
+       t.CreateFileLines("databases/postgresql100/Makefile")
+       t.CreateFileLines("databases/postgresql104/Makefile")
 
        latest := G.Pkgsrc.Latest("databases", `^postgresql[0-9]+$`, "$0")
 
        c.Check(latest, equals, "postgresql104")
 }
 
+func (s *Suite) Test_Pkgsrc_Latest__numeric_multiple_numbers(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("emulators/suse_131_32_gtk2/Makefile")
+       t.CreateFileLines("emulators/suse_131_32_qt5/Makefile")
+       t.CreateFileLines("emulators/suse_131_gtk2/Makefile")
+       t.CreateFileLines("emulators/suse_131_qt5/Makefile")
+
+       latest := G.Pkgsrc.Latest("emulators", `^suse_(\d+).*$`, "$1")
+
+       c.Check(latest, equals, "131")
+}
+
+// In 2017, PostgreSQL changed their versioning scheme to SemVer,
+// and since the pkgsrc directory contains the major version,
+// without any separating dots, the case of version 10 being
+// later than 95 needs to be handled specially.
+func (s *Suite) Test_Pkgsrc_Latest__postgresql(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("databases/postgresql95/Makefile")
+       t.CreateFileLines("databases/postgresql97/Makefile")
+       t.CreateFileLines("databases/postgresql10/Makefile")
+       t.CreateFileLines("databases/postgresql11/Makefile")
+
+       latest := G.Pkgsrc.Latest("databases", `^postgresql[0-9]+$`, "$0")
+
+       c.Check(latest, equals, "postgresql11")
+}
+
+func (s *Suite) Test_Pkgsrc_Latest__invalid_argument(c *check.C) {
+       t := s.Init(c)
+
+       t.ExpectFatal(
+               func() { G.Pkgsrc.Latest("databases", `postgresql[0-9]+`, "$0") },
+               "FATAL: Pkglint internal error: Regular expression \"postgresql[0-9]+\" must be anchored at both ends.")
+}
+
 func (s *Suite) Test_Pkgsrc_loadPkgOptions(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("mk/defaults/options.description",
+       t.CreateFileLines("mk/defaults/options.description",
                "option-name      Description of the option",
                "<<<<< Merge conflict",
                "===== Merge conflict",
@@ -228,7 +345,7 @@ func (s *Suite) Test_Pkgsrc_loadPkgOptio
 
        t.ExpectFatal(
                G.Pkgsrc.loadPkgOptions,
-               "FATAL: ~/mk/defaults/options.description:2: Unknown line format.")
+               "FATAL: ~/mk/defaults/options.description:2: Unknown line format: <<<<< Merge conflict")
 }
 
 func (s *Suite) Test_Pkgsrc_loadTools__no_tools_found(c *check.C) {
Index: pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.7 pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.8
--- pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.7       Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go   Wed Oct  3 22:27:53 2018
@@ -1,10 +1,15 @@
 package main
 
-import "gopkg.in/check.v1"
+import (
+       "gopkg.in/check.v1"
+       "strings"
+)
 
 func (s *Suite) Test_ShTokenizer_ShAtom(c *check.C) {
        t := s.Init(c)
 
+       // checkRest ensures that the given string is parsed to the expected
+       // atoms, and returns the remaining text.
        checkRest := func(s string, expected ...*ShAtom) string {
                p := NewShTokenizer(dummyLine, s, false)
                q := shqPlain
@@ -14,328 +19,371 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                }
                return p.Rest()
        }
+
+       // check ensures that the given string is parsed to the expected
+       // atoms, and that the text is completely consumed by the parser.
        check := func(str string, expected ...*ShAtom) {
                rest := checkRest(str, expected...)
                c.Check(rest, equals, "")
                t.CheckOutputEmpty()
        }
 
-       token := func(typ ShAtomType, text string, quoting ShQuoting) *ShAtom {
-               return &ShAtom{typ, text, quoting, nil}
+       atom := func(typ ShAtomType, text string) *ShAtom {
+               return &ShAtom{typ, text, shqPlain, nil}
        }
-       word := func(s string) *ShAtom { return token(shtWord, s, shqPlain) }
-       dquot := func(s string) *ShAtom { return token(shtWord, s, shqDquot) }
-       squot := func(s string) *ShAtom { return token(shtWord, s, shqSquot) }
-       backt := func(s string) *ShAtom { return token(shtWord, s, shqBackt) }
-       operator := func(s string) *ShAtom { return token(shtOperator, s, shqPlain) }
-       varuse := func(varname string, modifiers ...string) *ShAtom {
+
+       operator := func(s string) *ShAtom { return atom(shtOperator, s) }
+       comment := func(s string) *ShAtom { return atom(shtComment, s) }
+       mkvar := func(varname string, modifiers ...string) *ShAtom {
                text := "${" + varname
                for _, modifier := range modifiers {
-                       text += ":" + modifier
+                       text += ":" + strings.Replace(strings.Replace(modifier, "\\", "\\\\", -1), ":", "\\:", -1)
                }
                text += "}"
                varuse := &MkVarUse{varname: varname, modifiers: modifiers}
                return &ShAtom{shtVaruse, text, shqPlain, varuse}
        }
-       q := func(q ShQuoting, token *ShAtom) *ShAtom {
-               return &ShAtom{token.Type, token.MkText, q, token.Data}
-       }
-       whitespace := func(s string) *ShAtom { return token(shtSpace, s, shqPlain) }
-       space := token(shtSpace, " ", shqPlain)
+       text := func(s string) *ShAtom { return atom(shtWord, s) }
+       whitespace := func(s string) *ShAtom { return atom(shtSpace, s) }
+
+       space := whitespace(" ")
        semicolon := operator(";")
        pipe := operator("|")
+       subshell := atom(shtSubshell, "$$(")
+
+       q := func(q ShQuoting, atom *ShAtom) *ShAtom {
+               return &ShAtom{atom.Type, atom.MkText, q, atom.data}
+       }
+       backt := func(atom *ShAtom) *ShAtom { return q(shqBackt, atom) }
+       dquot := func(atom *ShAtom) *ShAtom { return q(shqDquot, atom) }
+       squot := func(atom *ShAtom) *ShAtom { return q(shqSquot, atom) }
+       subsh := func(atom *ShAtom) *ShAtom { return q(shqSubsh, atom) }
+       backtDquot := func(atom *ShAtom) *ShAtom { return q(shqBacktDquot, atom) }
+       backtSquot := func(atom *ShAtom) *ShAtom { return q(shqBacktSquot, atom) }
+       dquotBackt := func(atom *ShAtom) *ShAtom { return q(shqDquotBackt, atom) }
+       subshDquot := func(atom *ShAtom) *ShAtom { return q(shqSubshDquot, atom) }
+       subshSquot := func(atom *ShAtom) *ShAtom { return q(shqSubshSquot, atom) }
+       dquotBacktDquot := func(atom *ShAtom) *ShAtom { return q(shqDquotBacktDquot, atom) }
+       dquotBacktSquot := func(atom *ShAtom) *ShAtom { return q(shqDquotBacktSquot, atom) }
+
+       // Ignore unused functions; useful for deleting some of the tests during debugging.
+       use := func(args ...interface{}) {}
+       use(checkRest, check)
+       use(operator, comment, mkvar, text, whitespace)
+       use(space, semicolon, pipe, subshell)
+       use(backt, dquot, squot, subsh)
+       use(backtDquot, backtSquot, dquotBackt, subshDquot, subshSquot)
+       use(dquotBacktDquot, dquotBacktSquot)
 
        check("" /* none */)
 
        check("$$var",
-               word("$$var"))
+               text("$$var"))
 
        check("$$var$$var",
-               word("$$var$$var"))
+               text("$$var$$var"))
 
        check("$$var;;",
-               word("$$var"),
+               text("$$var"),
                operator(";;"))
 
        check("'single-quoted'",
-               q(shqSquot, word("'")),
-               q(shqSquot, word("single-quoted")),
-               q(shqPlain, word("'")))
+               squot(text("'")),
+               squot(text("single-quoted")),
+               text("'"))
 
        rest := checkRest("\"" /* none */)
        c.Check(rest, equals, "\"")
 
        check("$${file%.c}.o",
-               word("$${file%.c}.o"))
+               text("$${file%.c}.o"))
 
        check("hello",
-               word("hello"))
+               text("hello"))
 
        check("hello, world",
-               word("hello,"),
+               text("hello,"),
                space,
-               word("world"))
+               text("world"))
 
        check("\"",
-               dquot("\""))
+               dquot(text("\"")))
 
        check("`",
-               backt("`"))
+               backt(text("`")))
 
        check("`cat fname`",
-               backt("`"),
-               backt("cat"),
-               token(shtSpace, " ", shqBackt),
-               backt("fname"),
-               word("`"))
+               backt(text("`")),
+               backt(text("cat")),
+               backt(space),
+               backt(text("fname")),
+               text("`"))
 
        check("hello, \"world\"",
-               word("hello,"),
+               text("hello,"),
                space,
-               dquot("\""),
-               dquot("world"),
-               word("\""))
+               dquot(text("\"")),
+               dquot(text("world")),
+               text("\""))
 
        check("set -e;",
-               word("set"),
+               text("set"),
                space,
-               word("-e"),
+               text("-e"),
                semicolon)
 
        check("cd ${WRKSRC}/doc/man/man3; PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\";",
-               word("cd"),
+               text("cd"),
                space,
-               varuse("WRKSRC"),
-               word("/doc/man/man3"),
+               mkvar("WRKSRC"),
+               text("/doc/man/man3"),
                semicolon,
                space,
-               word("PAGES="),
-               dquot("\""),
-               q(shqDquotBackt, word("`")),
-               q(shqDquotBackt, word("ls")),
-               q(shqDquotBackt, space),
-               q(shqDquotBackt, word("-1")),
-               q(shqDquotBackt, space),
-               q(shqDquotBackt, operator("|")),
-               q(shqDquotBackt, space),
-               q(shqDquotBackt, varuse("SED")),
-               q(shqDquotBackt, space),
-               q(shqDquotBackt, word("-e")),
-               q(shqDquotBackt, space),
-               q(shqDquotBacktSquot, word("'")),
-               q(shqDquotBacktSquot, word("s,3qt$$,3,")),
-               q(shqDquotBackt, word("'")),
-               q(shqDquot, word("`")),
-               q(shqPlain, word("\"")),
+               text("PAGES="),
+               dquot(text("\"")),
+               dquotBackt(text("`")),
+               dquotBackt(text("ls")),
+               dquotBackt(space),
+               dquotBackt(text("-1")),
+               dquotBackt(space),
+               dquotBackt(operator("|")),
+               dquotBackt(space),
+               dquotBackt(mkvar("SED")),
+               dquotBackt(space),
+               dquotBackt(text("-e")),
+               dquotBackt(space),
+               dquotBacktSquot(text("'")),
+               dquotBacktSquot(text("s,3qt$$,3,")),
+               dquotBackt(text("'")),
+               dquot(text("`")),
+               text("\""),
                semicolon)
 
        check("ls -1 | ${SED} -e 's,3qt$$,3,'",
-               word("ls"), space, word("-1"), space,
+               text("ls"), space, text("-1"), space,
                pipe, space,
-               varuse("SED"), space, word("-e"), space,
-               squot("'"), squot("s,3qt$$,3,"), word("'"))
+               mkvar("SED"), space, text("-e"), space,
+               squot(text("'")), squot(text("s,3qt$$,3,")), text("'"))
 
        check("(for PAGE in $$PAGES; do ",
-               &ShAtom{shtOperator, "(", shqPlain, nil},
-               word("for"),
+               operator("("),
+               text("for"),
                space,
-               word("PAGE"),
+               text("PAGE"),
                space,
-               word("in"),
+               text("in"),
                space,
-               word("$$PAGES"),
+               text("$$PAGES"),
                semicolon,
                space,
-               word("do"),
+               text("do"),
                space)
 
        check("    ${ECHO} installing ${DESTDIR}${QTPREFIX}/man/man3/$${PAGE}; ",
                whitespace("    "),
-               varuse("ECHO"),
+               mkvar("ECHO"),
                space,
-               word("installing"),
+               text("installing"),
                space,
-               varuse("DESTDIR"),
-               varuse("QTPREFIX"),
-               word("/man/man3/$${PAGE}"),
+               mkvar("DESTDIR"),
+               mkvar("QTPREFIX"),
+               text("/man/man3/$${PAGE}"),
                semicolon,
                space)
 
        check("    set - X `head -1 $${PAGE}qt`; ",
                whitespace("    "),
-               word("set"),
+               text("set"),
                space,
-               word("-"),
+               text("-"),
                space,
-               word("X"),
+               text("X"),
                space,
-               backt("`"),
-               backt("head"),
-               q(shqBackt, space),
-               backt("-1"),
-               q(shqBackt, space),
-               backt("$${PAGE}qt"),
-               word("`"),
+               backt(text("`")),
+               backt(text("head")),
+               backt(space),
+               backt(text("-1")),
+               backt(space),
+               backt(text("$${PAGE}qt")),
+               text("`"),
                semicolon,
                space)
 
        check("`\"one word\"`",
-               backt("`"),
-               q(shqBacktDquot, word("\"")),
-               q(shqBacktDquot, word("one word")),
-               q(shqBackt, word("\"")),
-               word("`"))
+               backt(text("`")),
+               backtDquot(text("\"")),
+               backtDquot(text("one word")),
+               backt(text("\"")),
+               text("`"))
 
        check("$$var \"$$var\" '$$var' `$$var`",
-               word("$$var"),
+               text("$$var"),
                space,
-               dquot("\""),
-               dquot("$$var"),
-               word("\""),
-               space,
-               squot("'"),
-               squot("$$var"),
-               word("'"),
-               space,
-               backt("`"),
-               backt("$$var"),
-               word("`"))
+               dquot(text("\"")),
+               dquot(text("$$var")),
+               text("\""),
+               space,
+               squot(text("'")),
+               squot(text("$$var")),
+               text("'"),
+               space,
+               backt(text("`")),
+               backt(text("$$var")),
+               text("`"))
 
        check("\"`'echo;echo'`\"",
-               q(shqDquot, word("\"")),
-               q(shqDquotBackt, word("`")),
-               q(shqDquotBacktSquot, word("'")),
-               q(shqDquotBacktSquot, word("echo;echo")),
-               q(shqDquotBackt, word("'")),
-               q(shqDquot, word("`")),
-               q(shqPlain, word("\"")))
+               dquot(text("\"")),
+               dquotBackt(text("`")),
+               dquotBacktSquot(text("'")),
+               dquotBacktSquot(text("echo;echo")),
+               dquotBackt(text("'")),
+               dquot(text("`")),
+               text("\""))
 
        check("cat<file",
-               word("cat"),
+               text("cat"),
                operator("<"),
-               word("file"))
+               text("file"))
 
        check("-e \"s,\\$$sysconfdir/jabberd,\\$$sysconfdir,g\"",
-               word("-e"),
+               text("-e"),
                space,
-               dquot("\""),
-               dquot("s,\\$$sysconfdir/jabberd,\\$$sysconfdir,g"),
-               word("\""))
+               dquot(text("\"")),
+               dquot(text("s,\\$$sysconfdir/jabberd,\\$$sysconfdir,g")),
+               text("\""))
 
        check("echo $$, $$- $$/ $$; $$| $$,$$/$$;$$-",
-               word("echo"),
+               text("echo"),
                space,
-               word("$$,"),
+               text("$$,"),
                space,
-               word("$$-"),
+               text("$$-"),
                space,
-               word("$$/"),
+               text("$$/"),
                space,
-               word("$$"),
+               text("$$"),
                semicolon,
                space,
-               word("$$"),
+               text("$$"),
                pipe,
                space,
-               word("$$,$$/$$"),
+               text("$$,$$/$$"),
                semicolon,
-               word("$$-"))
+               text("$$-"))
 
        rest = checkRest("COMMENT=\t\\Make $$$$ fast\"",
-               word("COMMENT="),
+               text("COMMENT="),
                whitespace("\t"),
-               word("\\Make"),
+               text("\\Make"),
                space,
-               word("$$$$"),
+               text("$$$$"),
                space,
-               word("fast"))
+               text("fast"))
        c.Check(rest, equals, "\"")
 
        check("var=`echo;echo|echo&echo||echo&&echo>echo`",
-               q(shqPlain, word("var=")),
-               q(shqBackt, word("`")),
-               q(shqBackt, word("echo")),
-               q(shqBackt, semicolon),
-               q(shqBackt, word("echo")),
-               q(shqBackt, operator("|")),
-               q(shqBackt, word("echo")),
-               q(shqBackt, operator("&")),
-               q(shqBackt, word("echo")),
-               q(shqBackt, operator("||")),
-               q(shqBackt, word("echo")),
-               q(shqBackt, operator("&&")),
-               q(shqBackt, word("echo")),
-               q(shqBackt, operator(">")),
-               q(shqBackt, word("echo")),
-               q(shqPlain, word("`")))
+               text("var="),
+               backt(text("`")),
+               backt(text("echo")),
+               backt(semicolon),
+               backt(text("echo")),
+               backt(operator("|")),
+               backt(text("echo")),
+               backt(operator("&")),
+               backt(text("echo")),
+               backt(operator("||")),
+               backt(text("echo")),
+               backt(operator("&&")),
+               backt(text("echo")),
+               backt(operator(">")),
+               backt(text("echo")),
+               text("`"))
 
        check("# comment",
-               token(shtComment, "# comment", shqPlain))
+               comment("# comment"))
        check("no#comment",
-               word("no#comment"))
+               text("no#comment"))
        check("`# comment`continue",
-               token(shtWord, "`", shqBackt),
-               token(shtComment, "# comment", shqBackt),
-               token(shtWord, "`", shqPlain),
-               token(shtWord, "continue", shqPlain))
+               backt(text("`")),
+               backt(comment("# comment")),
+               text("`"),
+               text("continue"))
        check("`no#comment`continue",
-               token(shtWord, "`", shqBackt),
-               token(shtWord, "no#comment", shqBackt),
-               token(shtWord, "`", shqPlain),
-               token(shtWord, "continue", shqPlain))
+               backt(text("`")),
+               backt(text("no#comment")),
+               text("`"),
+               text("continue"))
 
        check("var=`tr 'A-Z' 'a-z'`",
-               token(shtWord, "var=", shqPlain),
-               token(shtWord, "`", shqBackt),
-               token(shtWord, "tr", shqBackt),
-               token(shtSpace, " ", shqBackt),
-               token(shtWord, "'", shqBacktSquot),
-               token(shtWord, "A-Z", shqBacktSquot),
-               token(shtWord, "'", shqBackt),
-               token(shtSpace, " ", shqBackt),
-               token(shtWord, "'", shqBacktSquot),
-               token(shtWord, "a-z", shqBacktSquot),
-               token(shtWord, "'", shqBackt),
-               token(shtWord, "`", shqPlain))
+               text("var="),
+               backt(text("`")),
+               backt(text("tr")),
+               backt(space),
+               backtSquot(text("'")),
+               backtSquot(text("A-Z")),
+               backt(text("'")),
+               backt(space),
+               backtSquot(text("'")),
+               backtSquot(text("a-z")),
+               backt(text("'")),
+               text("`"))
 
        check("var=\"`echo \"\\`echo foo\\`\"`\"",
-               token(shtWord, "var=", shqPlain),
-               token(shtWord, "\"", shqDquot),
-               token(shtWord, "`", shqDquotBackt),
-               token(shtWord, "echo", shqDquotBackt),
-               token(shtSpace, " ", shqDquotBackt),
-               token(shtWord, "\"", shqDquotBacktDquot),
-               token(shtWord, "\\`echo foo\\`", shqDquotBacktDquot), // One token, since it doesn't influence parsing.
-               token(shtWord, "\"", shqDquotBackt),
-               token(shtWord, "`", shqDquot),
-               token(shtWord, "\"", shqPlain))
+               text("var="),
+               dquot(text("\"")),
+               dquotBackt(text("`")),
+               dquotBackt(text("echo")),
+               dquotBackt(space),
+               dquotBacktDquot(text("\"")),
+               dquotBacktDquot(text("\\`echo foo\\`")), // One atom, since it doesn't influence parsing.
+               dquotBackt(text("\"")),
+               dquot(text("`")),
+               text("\""))
 
        check("if cond1; then action1; elif cond2; then action2; else action3; fi",
-               word("if"), space, word("cond1"), semicolon, space,
-               word("then"), space, word("action1"), semicolon, space,
-               word("elif"), space, word("cond2"), semicolon, space,
-               word("then"), space, word("action2"), semicolon, space,
-               word("else"), space, word("action3"), semicolon, space,
-               word("fi"))
-
-       if false {
-               check("$$(cat)",
-                       token(shtWord, "$$(", shqSubsh),
-                       token(shtWord, "cat", shqSubsh),
-                       token(shtWord, ")", shqPlain))
-
-               check("$$(cat 'file')",
-                       token(shtWord, "$$(", shqSubsh),
-                       token(shtWord, "cat", shqSubsh),
-                       token(shtSpace, " ", shqSubsh),
-                       token(shtWord, "'", shqSubshSquot),
-                       token(shtWord, "file", shqSubshSquot),
-                       token(shtWord, "'", shqSubsh),
-                       token(shtWord, ")", shqPlain))
-       }
+               text("if"), space, text("cond1"), semicolon, space,
+               text("then"), space, text("action1"), semicolon, space,
+               text("elif"), space, text("cond2"), semicolon, space,
+               text("then"), space, text("action2"), semicolon, space,
+               text("else"), space, text("action3"), semicolon, space,
+               text("fi"))
+
+       check("$$(cat)",
+               subsh(subshell),
+               subsh(text("cat")),
+               text(")"))
+
+       check("$$(cat 'file')",
+               subsh(subshell),
+               subsh(text("cat")),
+               subsh(space),
+               subshSquot(text("'")),
+               subshSquot(text("file")),
+               subsh(text("'")),
+               text(")"))
+
+       check("$$(# comment) arg",
+               subsh(subshell),
+               subsh(comment("# comment")),
+               text(")"),
+               space,
+               text("arg"))
+
+       check("$$(echo \"first\" 'second')",
+               subsh(subshell),
+               subsh(text("echo")),
+               subsh(space),
+               subshDquot(text("\"")),
+               subshDquot(text("first")),
+               subsh(text("\"")),
+               subsh(space),
+               subshSquot(text("'")),
+               subshSquot(text("second")),
+               subsh(text("'")),
+               text(")"))
 }
 
-func (s *Suite) Test_Shtokenizer_ShAtom__quoting(c *check.C) {
+func (s *Suite) Test_ShTokenizer_ShAtom__quoting(c *check.C) {
        checkQuotingChange := func(input, expectedOutput string) {
                p := NewShTokenizer(dummyLine, input, false)
                q := shqPlain
@@ -372,105 +420,158 @@ func (s *Suite) Test_Shtokenizer_ShAtom_
 func (s *Suite) Test_ShTokenizer_ShToken(c *check.C) {
        t := s.Init(c)
 
-       check := func(str string, expected ...*ShToken) {
+       // testRest ensures that the given string is parsed to the expected
+       // tokens, and returns the remaining text.
+       testRest := func(str string, expected ...string) string {
+               p := NewShTokenizer(dummyLine, str, false)
+               for _, exp := range expected {
+                       c.Check(p.ShToken().MkText, equals, exp)
+               }
+               return p.Rest()
+       }
+       test := func(str string, expected ...string) {
                p := NewShTokenizer(dummyLine, str, false)
                for _, exp := range expected {
-                       c.Check(p.ShToken(), deepEquals, exp)
+                       c.Check(p.ShToken().MkText, equals, exp)
                }
                c.Check(p.Rest(), equals, "")
                t.CheckOutputEmpty()
        }
+       checkNil := func(str string) {
+               p := NewShTokenizer(dummyLine, str, false)
+               c.Check(p.ShToken(), check.IsNil)
+               c.Check(p.Rest(), equals, "")
+               t.CheckOutputEmpty()
+       }
 
-       check("",
-               nil)
+       checkNil("")
+       checkNil(" ")
+       rest := testRest("\t\t\t\n\n\n\n\t ",
+               "\n",
+               "\n", // TODO: Why three separators? One should be enough. What does the grammar say?
+               "\n")
+       c.Check(rest, equals, "\n\t ") // TODO: Why is the newline still here?
+
+       test("echo",
+               "echo")
+
+       test("`cat file`",
+               "`cat file`")
+
+       test("PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\"",
+               "PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\"")
+
+       test("echo hello, world",
+               "echo",
+               "hello,",
+               "world")
+
+       test("if cond1; then action1; elif cond2; then action2; else action3; fi",
+               "if", "cond1", ";", "then",
+               "action1", ";",
+               "elif", "cond2", ";", "then",
+               "action2", ";",
+               "else", "action3", ";",
+               "fi")
+
+       test("PATH=/nonexistent env PATH=${PATH:Q} true",
+               "PATH=/nonexistent",
+               "env",
+               "PATH=${PATH:Q}",
+               "true")
 
-       check("echo",
-               NewShToken("echo",
-                       NewShAtom(shtWord, "echo", shqPlain)))
-
-       check("`cat file`",
-               NewShToken("`cat file`",
-                       NewShAtom(shtWord, "`", shqBackt),
-                       NewShAtom(shtWord, "cat", shqBackt),
-                       NewShAtom(shtSpace, " ", shqBackt),
-                       NewShAtom(shtWord, "file", shqBackt),
-                       NewShAtom(shtWord, "`", shqPlain)))
-
-       check("PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\"",
-               NewShToken("PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\"",
-                       NewShAtom(shtWord, "PAGES=", shqPlain),
-                       NewShAtom(shtWord, "\"", shqDquot),
-                       NewShAtom(shtWord, "`", shqDquotBackt),
-                       NewShAtom(shtWord, "ls", shqDquotBackt),
-                       NewShAtom(shtSpace, " ", shqDquotBackt),
-                       NewShAtom(shtWord, "-1", shqDquotBackt),
-                       NewShAtom(shtSpace, " ", shqDquotBackt),
-                       NewShAtom(shtOperator, "|", shqDquotBackt),
-                       NewShAtom(shtSpace, " ", shqDquotBackt),
-                       NewShAtomVaruse("${SED}", shqDquotBackt, "SED"),
-                       NewShAtom(shtSpace, " ", shqDquotBackt),
-                       NewShAtom(shtWord, "-e", shqDquotBackt),
-                       NewShAtom(shtSpace, " ", shqDquotBackt),
-                       NewShAtom(shtWord, "'", shqDquotBacktSquot),
-                       NewShAtom(shtWord, "s,3qt$$,3,", shqDquotBacktSquot),
-                       NewShAtom(shtWord, "'", shqDquotBackt),
-                       NewShAtom(shtWord, "`", shqDquot),
-                       NewShAtom(shtWord, "\"", shqPlain)))
-
-       check("echo hello, world",
-               NewShToken("echo",
-                       NewShAtom(shtWord, "echo", shqPlain)),
-               NewShToken("hello,",
-                       NewShAtom(shtWord, "hello,", shqPlain)),
-               NewShToken("world",
-                       NewShAtom(shtWord, "world", shqPlain)))
+       test("id=$$(${AWK} '{print}' < ${WRKSRC}/idfile)",
+               "id=$$(${AWK} '{print}' < ${WRKSRC}/idfile)")
 
-       check("if cond1; then action1; elif cond2; then action2; else action3; fi",
-               NewShToken("if", NewShAtom(shtWord, "if", shqPlain)),
-               NewShToken("cond1", NewShAtom(shtWord, "cond1", shqPlain)),
-               NewShToken(";", NewShAtom(shtOperator, ";", shqPlain)),
-               NewShToken("then", NewShAtom(shtWord, "then", shqPlain)),
-               NewShToken("action1", NewShAtom(shtWord, "action1", shqPlain)),
-               NewShToken(";", NewShAtom(shtOperator, ";", shqPlain)),
-               NewShToken("elif", NewShAtom(shtWord, "elif", shqPlain)),
-               NewShToken("cond2", NewShAtom(shtWord, "cond2", shqPlain)),
-               NewShToken(";", NewShAtom(shtOperator, ";", shqPlain)),
-               NewShToken("then", NewShAtom(shtWord, "then", shqPlain)),
-               NewShToken("action2", NewShAtom(shtWord, "action2", shqPlain)),
-               NewShToken(";", NewShAtom(shtOperator, ";", shqPlain)),
-               NewShToken("else", NewShAtom(shtWord, "else", shqPlain)),
-               NewShToken("action3", NewShAtom(shtWord, "action3", shqPlain)),
-               NewShToken(";", NewShAtom(shtOperator, ";", shqPlain)),
-               NewShToken("fi", NewShAtom(shtWord, "fi", shqPlain)))
-
-       check("PATH=/nonexistent env PATH=${PATH:Q} true",
-               NewShToken("PATH=/nonexistent", NewShAtom(shtWord, "PATH=/nonexistent", shqPlain)),
-               NewShToken("env", NewShAtom(shtWord, "env", shqPlain)),
-               NewShToken("PATH=${PATH:Q}",
-                       NewShAtom(shtWord, "PATH=", shqPlain),
-                       NewShAtomVaruse("${PATH:Q}", shqPlain, "PATH", "Q")),
-               NewShToken("true", NewShAtom(shtWord, "true", shqPlain)))
-
-       if false { // Don't know how to tokenize this correctly.
-               check("id=$$(${AWK} '{print}' < ${WRKSRC}/idfile)",
-                       NewShToken("id=$$(${AWK} '{print}' < ${WRKSRC}/idfile)",
-                               NewShAtom(shtWord, "id=", shqPlain),
-                               NewShAtom(shtWord, "$$(", shqPlain),
-                               NewShAtomVaruse("${AWK}", shqPlain, "AWK")))
+       test("id=`${AWK} '{print}' < ${WRKSRC}/idfile`",
+               "id=`${AWK} '{print}' < ${WRKSRC}/idfile`")
+}
+
+func (s *Suite) Test_ShTokenizer__examples_from_fuzzing(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("fuzzing.mk",
+               MkRcsID,
+               "",
+               "pre-configure:",
+
+               // Covers shAtomBacktDquot: return nil.
+               // These are nested backticks with double quotes,
+               // which should be avoided since POSIX marks them as unspecified.
+               "\t"+"`\"`",
+
+               // Covers shAtomBacktSquot: return nil
+               "\t"+"`'$`",
+
+               // Covers shAtomDquotBacktSquot: return nil
+               "\t"+"\"`'`y",
+
+               // Covers shAtomDquotBackt: return nil
+               // FIXME: Pkglint must parse unescpaed dollar in the same way, everywhere.
+               "\t"+"\"`$|",
+
+               // Covers shAtomDquotBacktDquot: return nil
+               // FIXME: Pkglint must support unlimited nesting.
+               "\t"+"\"`\"`",
+
+               // Covers shAtomSubshDquot: return nil
+               "\t"+"$$(\"'",
+
+               // Covers shAtomSubsh: case repl.AdvanceStr("`")
+               "\t"+"$$(`",
+
+               // Covers shAtomSubshSquot: return nil
+               "\t"+"$$('$)",
+
+               // Covers shAtomDquotBackt: case repl.AdvanceRegexp("^#[^`]*")
+               "\t"+"\"`# comment")
+
+       mklines.Check()
+
+       // Just good that these redundant error messages don't occur every day.
+       t.CheckOutputLines(
+               "WARN: fuzzing.mk:4: Pkglint parse error in ShTokenizer.ShAtom at \"`\" (quoting=bd).",
+               "WARN: fuzzing.mk:4: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}",
+
+               "WARN: fuzzing.mk:5: Pkglint parse error in ShTokenizer.ShAtom at \"$`\" (quoting=bs).",
+               "WARN: fuzzing.mk:5: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}",
+               "WARN: fuzzing.mk:5: Pkglint parse error in MkLine.Tokenize at \"$`\".",
+
+               "WARN: fuzzing.mk:6: Pkglint parse error in ShTokenizer.ShAtom at \"`y\" (quoting=dbs).",
+               "WARN: fuzzing.mk:6: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}",
+
+               "WARN: fuzzing.mk:7: Pkglint parse error in ShTokenizer.ShAtom at \"$|\" (quoting=db).",
+               "WARN: fuzzing.mk:7: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}",
+               "WARN: fuzzing.mk:7: Pkglint parse error in MkLine.Tokenize at \"$|\".",
+
+               "WARN: fuzzing.mk:8: Pkglint parse error in ShTokenizer.ShAtom at \"`\" (quoting=dbd).",
+               "WARN: fuzzing.mk:8: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}",
+
+               "WARN: fuzzing.mk:9: Pkglint parse error in ShTokenizer.ShAtom at \"'\" (quoting=Sd).",
+               "WARN: fuzzing.mk:9: Invoking subshells via $(...) is not portable enough.",
+
+               "WARN: fuzzing.mk:10: Pkglint parse error in ShTokenizer.ShAtom at \"`\" (quoting=S).",
+               "WARN: fuzzing.mk:10: Invoking subshells via $(...) is not portable enough.",
+
+               "WARN: fuzzing.mk:11: Pkglint parse error in ShTokenizer.ShAtom at \"$)\" (quoting=Ss).",
+               "WARN: fuzzing.mk:11: Invoking subshells via $(...) is not portable enough.",
+               "WARN: fuzzing.mk:11: Pkglint parse error in MkLine.Tokenize at \"$)\".",
+
+               "WARN: fuzzing.mk:12: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}")
+}
+
+func (s *Suite) Test_ShTokenizer__fuzzing(c *check.C) {
+       t := s.Init(c)
+
+       fuzzer := NewFuzzer()
+       fuzzer.Char("\"'`$();|_#", 10)
+       fuzzer.Range('a', 'z', 5)
+
+       defer fuzzer.CheckOk()
+       for i := 0; i < 1000; i++ {
+               tokenizer := NewShTokenizer(dummyLine, fuzzer.Generate(50), false)
+               tokenizer.ShAtoms()
+               t.Output() // Discard the output, only react on fatal errors.
        }
-       check("id=`${AWK} '{print}' < ${WRKSRC}/idfile`",
-               NewShToken("id=`${AWK} '{print}' < ${WRKSRC}/idfile`",
-                       NewShAtom(shtWord, "id=", shqPlain),
-                       NewShAtom(shtWord, "`", shqBackt),
-                       NewShAtomVaruse("${AWK}", shqBackt, "AWK"),
-                       NewShAtom(shtSpace, " ", shqBackt),
-                       NewShAtom(shtWord, "'", shqBacktSquot),
-                       NewShAtom(shtWord, "{print}", shqBacktSquot),
-                       NewShAtom(shtWord, "'", shqBackt),
-                       NewShAtom(shtSpace, " ", shqBackt),
-                       NewShAtom(shtOperator, "<", shqBackt),
-                       NewShAtom(shtSpace, " ", shqBackt),
-                       NewShAtomVaruse("${WRKSRC}", shqBackt, "WRKSRC"),
-                       NewShAtom(shtWord, "/idfile", shqBackt),
-                       NewShAtom(shtWord, "`", shqPlain)))
+       fuzzer.Ok()
 }
Index: pkgsrc/pkgtools/pkglint/files/vartype_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.7 pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.8
--- pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.7   Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/vartype_test.go       Wed Oct  3 22:27:53 2018
@@ -23,7 +23,7 @@ func (s *Suite) Test_Vartype_EffectivePe
        }
 }
 
-func (s *Suite) Test_VarChecker_HasEnum(c *check.C) {
+func (s *Suite) Test_BasicType_HasEnum(c *check.C) {
        vc := enum("catinstall middle maninstall")
 
        c.Check(vc.HasEnum("catinstall"), equals, true)
@@ -31,7 +31,7 @@ func (s *Suite) Test_VarChecker_HasEnum(
        c.Check(vc.HasEnum("maninstall"), equals, true)
 }
 
-func (s *Suite) Test_AclPermissions_Contains(c *check.C) {
+func (s *Suite) Test_ACLPermissions_Contains(c *check.C) {
        perms := aclpAllRuntime
 
        c.Check(perms.Contains(aclpAllRuntime), equals, true)
@@ -39,7 +39,7 @@ func (s *Suite) Test_AclPermissions_Cont
        c.Check(perms.Contains(aclpUseLoadtime), equals, false)
 }
 
-func (s *Suite) Test_AclPermissions_String(c *check.C) {
+func (s *Suite) Test_ACLPermissions_String(c *check.C) {
        c.Check(ACLPermissions(0).String(), equals, "none")
        c.Check(aclpAll.String(), equals, "set, set-default, append, use-loadtime, use")
        c.Check(aclpUnknown.String(), equals, "unknown")

Index: pkgsrc/pkgtools/pkglint/files/mkline.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline.go:1.37 pkgsrc/pkgtools/pkglint/files/mkline.go:1.38
--- pkgsrc/pkgtools/pkglint/files/mkline.go:1.37        Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/mkline.go     Wed Oct  3 22:27:53 2018
@@ -4,7 +4,6 @@ package main
 
 import (
        "fmt"
-       "netbsd.org/pkglint/regex"
        "netbsd.org/pkglint/trace"
        "path"
        "strings"
@@ -327,12 +326,12 @@ func (mkline *MkLineImpl) ValueTokens() 
 func (mkline *MkLineImpl) WithoutMakeVariables(value string) string {
        valueNovar := value
        for {
-               var m []string
                // TODO: properly parse nested variables
-               m, valueNovar = regex.ReplaceFirst(valueNovar, `\$\{[^{}]*\}`, "")
-               if m == nil {
+               replaced := replaceFirst(valueNovar, `\$\{[^{}]*\}`, "")
+               if replaced == valueNovar {
                        return valueNovar
                }
+               valueNovar = replaced
        }
 }
 
@@ -357,7 +356,7 @@ func (mkline *MkLineImpl) ResolveVarsInR
                tmp = strings.Replace(tmp, "${PHPPKGSRCDIR}", G.Pkgsrc.Latest("lang", `^php[0-9]+$`, "../../lang/$0"), -1)
        }
        if contains(tmp, "${SUSE_DIR_PREFIX}") {
-               suseDirPrefix := G.Pkgsrc.Latest("emulators", `^(suse[0-9]+)_base`, "$1")
+               suseDirPrefix := G.Pkgsrc.Latest("emulators", `^(suse[0-9]+)_base$`, "$1")
                tmp = strings.Replace(tmp, "${SUSE_DIR_PREFIX}", suseDirPrefix, -1)
        }
        if contains(tmp, "${PYPKGSRCDIR}") {
@@ -372,14 +371,14 @@ func (mkline *MkLineImpl) ResolveVarsInR
        }
 
        if adjustDepth {
-               if m, pkgpath := match1(tmp, `^\.\./\.\./([^.].*)$`); m {
-                       tmp = pkgsrcdir + "/" + pkgpath
+               if hasPrefix(tmp, "../../") && !hasPrefix(tmp[6:], ".") {
+                       tmp = pkgsrcdir + "/" + tmp[6:]
                }
        }
 
        tmp = cleanpath(tmp)
 
-       if trace.Tracing {
+       if trace.Tracing && relativePath != tmp {
                trace.Step2("resolveVarsInRelativePath: %q => %q", relativePath, tmp)
        }
        return tmp
@@ -584,7 +583,7 @@ func (mkline *MkLineImpl) VariableNeedsQ
 
 // TODO: merge with determineUsedVariables
 func (mkline *MkLineImpl) ExtractUsedVariables(text string) []string {
-       re := regex.Compile(`^(?:[^\$]+|\$[\$*<>?@]|\$\{([.0-9A-Z_a-z]+)(?::(?:[^\${}]|\$[^{])+)?\})`)
+       re := G.res.Compile(`^(?:[^\$]+|\$[\$*<>?@]|\$\{([.0-9A-Z_a-z]+)(?::(?:[^\${}]|\$[^{])+)?\})`)
        rest := text
        var result []string
        for {
@@ -635,7 +634,7 @@ func (mkline *MkLineImpl) DetermineUsedV
                }
                rest = rest[min:]
 
-               m := regex.Compile(`(?:\$\{|\$\(|defined\(|empty\()([*+\-.0-9A-Z_a-z]+)[:})]`).FindStringSubmatchIndex(rest)
+               m := G.res.Compile(`(?:\$\{|\$\(|defined\(|empty\()([*+\-.0-9A-Z_a-z]+)[:})]`).FindStringSubmatchIndex(rest)
                if m == nil {
                        return
                }
@@ -918,9 +917,17 @@ func (ind *Indentation) TrackAfter(mklin
        switch directive {
        case "if":
                // For multiple-inclusion guards, the indentation stays at the same level.
-               if m, varname := match1(args, `^!defined\(([\w]+_MK)\)$`); m {
-                       ind.AddVar(varname)
-               } else {
+               guard := false
+               if hasPrefix(args, "!defined") && hasSuffix(args, "_MK)") {
+                       if hasPrefix(args, "!defined(") && hasSuffix(args, ")") {
+                               varname := args[9 : len(args)-1]
+                               if varname != "" && isalnum(varname) {
+                                       ind.AddVar(varname)
+                                       guard = true
+                               }
+                       }
+               }
+               if !guard {
                        ind.top().depth += 2
                }
 
@@ -1055,5 +1062,27 @@ func MatchVarassign(text string) (m, com
 }
 
 func MatchMkInclude(text string) (m bool, indentation, directive, filename string) {
-       return match3(text, `^\.(\s*)(s?include)\s+\"([^\"]+)\"\s*(?:#.*)?$`)
+       repl := G.NewPrefixReplacer(text)
+       if repl.AdvanceStr(".") {
+               if repl.AdvanceHspace() {
+                       indentation = repl.Str()
+               }
+               if repl.AdvanceStr("include") || repl.AdvanceStr("sinclude") {
+                       directive = repl.Str()
+                       repl.AdvanceHspace()
+                       if repl.AdvanceByte('"') {
+                               if repl.AdvanceBytesFunc(func(c byte) bool { return c != '"' }) {
+                                       filename = repl.Str()
+                                       if repl.AdvanceByte('"') {
+                                               repl.AdvanceHspace()
+                                               if repl.EOF() || repl.PeekByte() == '#' {
+                                                       m = true
+                                                       return
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+       return false, "", "", ""
 }
Index: pkgsrc/pkgtools/pkglint/files/pkglint.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint.go:1.37 pkgsrc/pkgtools/pkglint/files/pkglint.go:1.38
--- pkgsrc/pkgtools/pkglint/files/pkglint.go:1.37       Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/pkglint.go    Wed Oct  3 22:27:53 2018
@@ -5,6 +5,7 @@ import (
        "netbsd.org/pkglint/getopt"
        "netbsd.org/pkglint/histogram"
        "netbsd.org/pkglint/regex"
+       "netbsd.org/pkglint/textproc"
        "netbsd.org/pkglint/trace"
        "os"
        "os/user"
@@ -49,8 +50,16 @@ type Pkglint struct {
        logOut                *SeparatorWriter
        logErr                *SeparatorWriter
 
-       loghisto *histogram.Histogram
-       loaded   *histogram.Histogram
+       loghisto  *histogram.Histogram
+       loaded    *histogram.Histogram
+       res       regex.Registry
+       fileCache *FileCache
+}
+
+func NewPkglint() Pkglint {
+       return Pkglint{
+               res:       regex.NewRegistry(),
+               fileCache: NewFileCache(100)}
 }
 
 type CmdOpts struct {
@@ -106,13 +115,15 @@ type Hash struct {
 
 // G is the abbreviation for "global state";
 // it is the only global variable in this Go package
-var G Pkglint
+var G = NewPkglint()
+
+var exit = os.Exit // Indirect access, to allow main() to be tested.
 
 func main() {
        G.logOut = NewSeparatorWriter(os.Stdout)
        G.logErr = NewSeparatorWriter(os.Stderr)
        trace.Out = os.Stdout
-       os.Exit(G.Main(os.Args...))
+       exit(G.Main(os.Args...))
 }
 
 // Main runs the main program with the given arguments.
@@ -139,21 +150,22 @@ func (pkglint *Pkglint) Main(argv ...str
        if pkglint.opts.Profiling {
                f, err := os.Create("pkglint.pprof")
                if err != nil {
-                       dummyLine.Fatalf("Cannot create profiling file: %s", err)
+                       G.Panicf("Cannot create profiling file: %s", err)
                }
                defer f.Close()
 
                pprof.StartCPUProfile(f)
                defer pprof.StopCPUProfile()
 
-               regex.Profiling = true
+               pkglint.res.Profiling()
                pkglint.loghisto = histogram.New()
                pkglint.loaded = histogram.New()
                defer func() {
                        pkglint.logOut.Write("")
                        pkglint.loghisto.PrintStats("loghisto", pkglint.logOut.out, -1)
-                       regex.PrintStats(pkglint.logOut.out)
+                       G.res.PrintStats(pkglint.logOut.out)
                        pkglint.loaded.PrintStats("loaded", pkglint.logOut.out, 10)
+                       pkglint.logOut.WriteLine(fmt.Sprintf("fileCache: %d hits, %d misses", pkglint.fileCache.hits, pkglint.fileCache.misses))
                }()
        }
 
@@ -170,7 +182,7 @@ func (pkglint *Pkglint) Main(argv ...str
        }
        relTopdir := findPkgsrcTopdir(firstArg)
        if relTopdir == "" {
-               dummyLine.Fatalf("%q is not inside a pkgsrc tree.", firstArg)
+               G.Panicf("%q is not inside a pkgsrc tree.", firstArg)
        }
 
        pkglint.Pkgsrc = NewPkgsrc(firstArg + "/" + relTopdir)
@@ -179,7 +191,7 @@ func (pkglint *Pkglint) Main(argv ...str
        currentUser, err := user.Current()
        if err == nil {
                // On Windows, this is `Computername\Username`.
-               pkglint.CurrentUsername = regex.Compile(`^.*\\`).ReplaceAllString(currentUser.Username, "")
+               pkglint.CurrentUsername = replaceAll(currentUser.Username, `^.*\\`, "")
        }
 
        for len(pkglint.Todo) != 0 {
@@ -332,6 +344,27 @@ func (pkglint *Pkglint) CheckDirent(fnam
        }
 }
 
+func (pkglint *Pkglint) Panicf(format string, args ...interface{}) {
+       prefix := ifelseStr(G.opts.GccOutput, llFatal.GccName, llFatal.TraditionalName)
+       pkglint.logErr.Write(prefix + ": " + fmt.Sprintf(format, args...) + "\n")
+       panic(pkglintFatal{})
+}
+
+// Assertf checks that the condition is true. Otherwise it terminates the
+// process with a fatal error message, prefixed with "Pkglint internal error".
+//
+// This method must only be used for programming errors.
+// For runtime errors, use Panicf.
+func (pkglint *Pkglint) Assertf(cond bool, format string, args ...interface{}) {
+       if !cond {
+               pkglint.Panicf("Pkglint internal error: "+format, args...)
+       }
+}
+
+func (pkglint *Pkglint) NewPrefixReplacer(s string) *textproc.PrefixReplacer {
+       return textproc.NewPrefixReplacer(s, &pkglint.res)
+}
+
 // Returns the pkgsrc top-level directory, relative to the given file or directory.
 func findPkgsrcTopdir(fname string) string {
        for _, dir := range [...]string{".", "..", "../..", "../../.."} {
@@ -342,9 +375,9 @@ func findPkgsrcTopdir(fname string) stri
        return ""
 }
 
-func resolveVariableRefs(text string) string {
-       if trace.Tracing {
-               defer trace.Call1(text)()
+func resolveVariableRefs(text string) (resolved string) {
+       if !contains(text, "${") {
+               return text
        }
 
        visited := make(map[string]bool) // To prevent endless loops
@@ -354,14 +387,8 @@ func resolveVariableRefs(text string) st
                if !visited[varname] {
                        visited[varname] = true
                        if G.Pkg != nil {
-                               switch varname {
-                               case "KRB5_TYPE":
-                                       return "heimdal"
-                               case "PGSQL_VERSION":
-                                       return "95"
-                               }
-                               if mkline := G.Pkg.vars.FirstDefinition(varname); mkline != nil {
-                                       return mkline.Value()
+                               if value, ok := G.Pkg.vars.Value(varname); ok {
+                                       return value
                                }
                        }
                        if G.Mk != nil {
@@ -375,8 +402,11 @@ func resolveVariableRefs(text string) st
 
        str := text
        for {
-               replaced := regex.Compile(`\$\{([\w.]+)\}`).ReplaceAllStringFunc(str, replacer)
+               replaced := replaceAllFunc(str, `\$\{([\w.]+)\}`, replacer)
                if replaced == str {
+                       if trace.Tracing && str != text {
+                               trace.Stepf("resolveVariableRefs %q => %q", text, replaced)
+                       }
                        return replaced
                }
                str = replaced
@@ -401,7 +431,7 @@ func ChecklinesDescr(lines []Line) {
        for _, line := range lines {
                CheckLineLength(line, 80)
                CheckLineTrailingWhitespace(line)
-               CheckLineValidCharacters(line, `[\t -~]`)
+               CheckLineValidCharacters(line)
                if contains(line.Text, "${") {
                        line.Notef("Variables are not expanded in the DESCR file.")
                }
@@ -453,7 +483,7 @@ func ChecklinesMessage(lines []Line) {
        for _, line := range lines {
                CheckLineLength(line, 80)
                CheckLineTrailingWhitespace(line)
-               CheckLineValidCharacters(line, `[\t -~]`)
+               CheckLineValidCharacters(line)
        }
        if lastLine := lines[len(lines)-1]; lastLine.Text != hline {
                fix := lastLine.Autofix()
@@ -516,10 +546,16 @@ func (pkglint *Pkglint) Checkfile(fname 
                return
        }
 
-       pkglint.checkExecutable(st, fname)
+       pkglint.checkExecutable(st)
+       pkglint.checkMode(fname, st.Mode())
+}
 
+// checkMode checks a directory entry based on its file name and its mode
+// (regular file, directory, symlink).
+func (pkglint *Pkglint) checkMode(fname string, mode os.FileMode) {
+       basename := path.Base(fname)
        switch {
-       case st.Mode().IsDir():
+       case mode.IsDir():
                switch {
                case basename == "files" || basename == "patches" || isIgnoredFilename(basename):
                        // Ok
@@ -529,12 +565,12 @@ func (pkglint *Pkglint) Checkfile(fname 
                        NewLineWhole(fname).Warnf("Unknown directory name.")
                }
 
-       case st.Mode()&os.ModeSymlink != 0:
+       case mode&os.ModeSymlink != 0:
                if !hasPrefix(basename, "work") {
                        NewLineWhole(fname).Warnf("Unknown symlink name.")
                }
 
-       case !st.Mode().IsRegular():
+       case !mode.IsRegular():
                NewLineWhole(fname).Errorf("Only files and directories are allowed in pkgsrc.")
 
        case basename == "ALTERNATIVES":
@@ -609,9 +645,6 @@ func (pkglint *Pkglint) Checkfile(fname 
                        }
                }
 
-       case basename == "TODO" || basename == "README":
-               // Ok
-
        case hasPrefix(basename, "CHANGES-"):
                // This only checks the file, but doesn't register the changes globally.
                _ = pkglint.Pkgsrc.loadDocChangesFromFile(fname)
@@ -620,7 +653,9 @@ func (pkglint *Pkglint) Checkfile(fname 
                // Skip
 
        case basename == "spec":
-               // Ok in regression tests
+               if !hasPrefix(G.Pkgsrc.ToRel(fname), "regress/") {
+                       NewLineWhole(fname).Warnf("Only packages in regress/ may have spec files.")
+               }
 
        default:
                NewLineWhole(fname).Warnf("Unexpected file found.")
@@ -630,7 +665,8 @@ func (pkglint *Pkglint) Checkfile(fname 
        }
 }
 
-func (pkglint *Pkglint) checkExecutable(st os.FileInfo, fname string) {
+func (pkglint *Pkglint) checkExecutable(st os.FileInfo) {
+       fname := st.Name()
        if st.Mode().IsRegular() && st.Mode().Perm()&0111 != 0 && !isCommitted(fname) {
                line := NewLine(fname, 0, "", nil)
                fix := line.Autofix()
@@ -643,7 +679,7 @@ func (pkglint *Pkglint) checkExecutable(
                fix.Custom(func(printAutofix, autofix bool) {
                        fix.Describef(0, "Clearing executable bits")
                        if autofix {
-                               if err := os.Chmod(line.Filename, st.Mode()&^0111); err != nil {
+                               if err := os.Chmod(fname, st.Mode()&^0111); err != nil {
                                        line.Errorf("Cannot clear executable bits: %s", err)
                                }
                        }
@@ -714,6 +750,11 @@ func (pkglint *Pkglint) Tool(command str
        return
 }
 
+// ToolByVarname looks up the tool by its variable name, e.g. "SED".
+//
+// The returned tool may come either from the current Makefile or the
+// current package. It is not guaranteed to be usable; that must be
+// checked by the calling code.
 func (pkglint *Pkglint) ToolByVarname(varname string, time ToolTime) *Tool {
 
        var tool *Tool

Index: pkgsrc/pkgtools/pkglint/files/mkline_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.41 pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.42
--- pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.41   Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/mkline_test.go        Wed Oct  3 22:27:53 2018
@@ -2,7 +2,7 @@ package main
 
 import "gopkg.in/check.v1"
 
-func (s *Suite) Test_VaralignBlock_Check_autofix(c *check.C) {
+func (s *Suite) Test_VaralignBlock_Check__autofix(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wspace", "--show-autofix")
@@ -68,7 +68,7 @@ func (s *Suite) Test_VaralignBlock_Check
                "NOTE: file.mk:3: This variable value should be aligned to column 9.")
 }
 
-func (s *Suite) Test_VaralignBlock_Check_longest_line_no_space(c *check.C) {
+func (s *Suite) Test_VaralignBlock_Check__longest_line_no_space(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wspace")
@@ -91,7 +91,7 @@ func (s *Suite) Test_VaralignBlock_Check
                "NOTE: file.mk:4: This variable value should be aligned to column 33.")
 }
 
-func (s *Suite) Test_VaralignBlock_Check_only_spaces(c *check.C) {
+func (s *Suite) Test_VaralignBlock_Check__only_spaces(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wspace")
@@ -219,7 +219,7 @@ func (s *Suite) Test_NewMkLine__autofix_
 }
 
 // Guessing the variable type works for both plain and parameterized variable names.
-func (s *Suite) Test_MkLine_VariableType_varparam(c *check.C) {
+func (s *Suite) Test_Pkgsrc_VariableType__varparam(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -242,13 +242,13 @@ func (s *Suite) Test_VarUseContext_Strin
        vartype := G.Pkgsrc.VariableType("PKGNAME")
        vuc := &VarUseContext{vartype, vucTimeUnknown, vucQuotBackt, false}
 
-       c.Check(vuc.String(), equals, "(PkgName time:unknown quoting:backt wordpart:false)")
+       c.Check(vuc.String(), equals, "(Pkgname time:unknown quoting:backt wordpart:false)")
 }
 
 // In variable assignments, a plain '#' introduces a line comment, unless
 // it is escaped by a backslash. In shell commands, on the other hand, it
 // is interpreted literally.
-func (s *Suite) Test_NewMkLine_numbersign(c *check.C) {
+func (s *Suite) Test_NewMkLine__number_sign(c *check.C) {
        t := s.Init(c)
 
        mklineVarassignEscaped := t.NewMkLine("fname", 1, "SED_CMD=\t's,\\#,hash,g'")
@@ -273,7 +273,7 @@ func (s *Suite) Test_NewMkLine_numbersig
                "WARN: fname:1: The # character starts a comment.")
 }
 
-func (s *Suite) Test_NewMkLine_leading_space(c *check.C) {
+func (s *Suite) Test_NewMkLine__leading_space(c *check.C) {
        t := s.Init(c)
 
        _ = t.NewMkLine("rubyversion.mk", 427, " _RUBYVER=\t2.15")
@@ -311,7 +311,7 @@ func (s *Suite) Test_MkLines_Check__extr
                "NOTE: options.mk:11: You can use \"../build\" instead of \"${WRKSRC}/../build\".")
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__unknown_rhs(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__unknown_rhs(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("fname", 1, "PKGNAME := ${UNKNOWN}")
@@ -323,7 +323,7 @@ func (s *Suite) Test_MkLine_variableNeed
        c.Check(nq, equals, nqDontKnow)
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__append_URL_to_list_of_URLs(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__append_URL_to_list_of_URLs(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -341,7 +341,7 @@ func (s *Suite) Test_MkLine_variableNeed
        t.CheckOutputEmpty() // Up to pkglint 5.3.6, it warned about a missing :Q here, which was wrong.
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__append_list_to_list(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__append_list_to_list(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -355,7 +355,7 @@ func (s *Suite) Test_MkLine_variableNeed
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__eval_shell(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__eval_shell(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -369,7 +369,7 @@ func (s *Suite) Test_MkLine_variableNeed
                "NOTE: builtin.mk:3: The :Q operator isn't necessary for ${BUILTIN_PKG.Xfixes} here.")
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__command_in_single_quotes(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_in_single_quotes(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -382,7 +382,7 @@ func (s *Suite) Test_MkLine_variableNeed
                "WARN: Makefile:3: Please use ${INSTALL:Q} instead of ${INSTALL} and make sure the variable appears outside of any quoting characters.")
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__command_in_command(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_in_command(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -401,7 +401,7 @@ func (s *Suite) Test_MkLine_variableNeed
                "WARN: Makefile:2: The exitcode of \"${FIND}\" at the left of the | operator is ignored.")
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__word_as_part_of_word(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__word_as_part_of_word(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -421,7 +421,7 @@ func (s *Suite) Test_MkLine_variableNeed
 // therefore no warning is issued in both these cases.
 //
 // Based on graphics/circos/Makefile.
-func (s *Suite) Test_MkLine_variableNeedsQuoting__command_as_command_argument(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_as_command_argument(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -441,7 +441,7 @@ func (s *Suite) Test_MkLine_variableNeed
 }
 
 // Based on mail/mailfront/Makefile.
-func (s *Suite) Test_MkLine_variableNeedsQuoting__URL_as_part_of_word_in_list(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__URL_as_part_of_word_in_list(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -460,7 +460,7 @@ func (s *Suite) Test_MkLine_variableNeed
 // modifier.
 //
 // Based on www/firefox31/xpi.mk.
-func (s *Suite) Test_MkLine_variableNeedsQuoting__command_in_subshell(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_in_subshell(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -483,7 +483,7 @@ func (s *Suite) Test_MkLine_variableNeed
 // LDFLAGS (and even more so CPPFLAGS and CFLAGS) may contain special
 // shell characters like quotes or backslashes. Therefore, quoting them
 // correctly is more tricky than with other variables.
-func (s *Suite) Test_MkLine_variableNeedsQuoting__LDFLAGS_in_single_quotes(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__LDFLAGS_in_single_quotes(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -506,7 +506,7 @@ func (s *Suite) Test_MkLine_variableNeed
 // requires the variable to be declared as "lkSpace".
 // In this case it doesn't matter though since each option is an identifier,
 // and these do not pose any quoting or escaping problems.
-func (s *Suite) Test_MkLine_variableNeedsQuoting__package_options(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__package_options(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -542,7 +542,7 @@ func (s *Suite) Test_MkLines_Check__MAST
                "WARN: devel/catch/Makefile:5: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__tool_in_quotes_in_subshell_in_shellwords(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__tool_in_quotes_in_subshell_in_shellwords(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -559,7 +559,7 @@ func (s *Suite) Test_MkLine_variableNeed
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__LDADD_in_BUILDLINK_TRANSFORM(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__LDADD_in_BUILDLINK_TRANSFORM(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -574,7 +574,7 @@ func (s *Suite) Test_MkLine_variableNeed
                "WARN: x11/qt5-qtbase/Makefile.common:1: Please use ${BUILDLINK_LDADD.dl:Q} instead of ${BUILDLINK_LDADD.dl:M*}.")
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__command_in_message(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_in_message(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -588,7 +588,7 @@ func (s *Suite) Test_MkLine_variableNeed
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__guessed_list_variable_in_quotes(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__guessed_list_variable_in_quotes(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -604,7 +604,7 @@ func (s *Suite) Test_MkLine_variableNeed
                "WARN: audio/jack-rack/Makefile:3: The variable LADSPA_PLUGIN_PATH should be quoted as part of a shell word.")
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__list_in_list(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__list_in_list(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -618,7 +618,7 @@ func (s *Suite) Test_MkLine_variableNeed
        t.CheckOutputEmpty() // Don't warn about missing :Q modifiers.
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__PKGNAME_and_URL_list_in_URL_list(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__PKGNAME_and_URL_list_in_URL_list(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -633,7 +633,7 @@ func (s *Suite) Test_MkLine_variableNeed
        t.CheckOutputEmpty() // Don't warn about missing :Q modifiers.
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__tool_in_CONFIGURE_ENV(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__tool_in_CONFIGURE_ENV(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -654,7 +654,7 @@ func (s *Suite) Test_MkLine_variableNeed
                "NOTE: Makefile:3: The :Q operator isn't necessary for ${TOOLS_TAR} here.")
 }
 
-func (s *Suite) Test_MkLine_variableNeedsQuoting__backticks(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__backticks(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -682,7 +682,7 @@ func (s *Suite) Test_MkLine_variableNeed
 // the :Q modifier can be safely removed since pkgsrc will never support
 // having special characters in these directory names.
 // For guessed variable types be cautious and don't autofix them.
-func (s *Suite) Test_MkLine_variableNeedsQuoting__only_remove_known(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__only_remove_known(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall", "--autofix")
@@ -709,7 +709,7 @@ func (s *Suite) Test_MkLine_variableNeed
 
 // TODO: COMPILER_RPATH_FLAG and LINKER_RPATH_FLAG have different types
 // defined in vardefs.go; examine why.
-func (s *Suite) Test_MkLine_variableNeedsQuoting__shellword_part(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__shellword_part(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall,no-space")
@@ -729,7 +729,7 @@ func (s *Suite) Test_MkLine_variableNeed
 }
 
 // Tools, when used in a shell command, must not be quoted.
-func (s *Suite) Test_MkLine_variableNeedsQuoting__tool_in_shell_command(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__tool_in_shell_command(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall,no-space")
@@ -747,7 +747,7 @@ func (s *Suite) Test_MkLine_variableNeed
 }
 
 // These examples from real pkgsrc end up in the final nqDontKnow case.
-func (s *Suite) Test_MkLine_variableNeedsQuoting__uncovered_cases(c *check.C) {
+func (s *Suite) Test_MkLine_VariableNeedsQuoting__uncovered_cases(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall,no-space")
@@ -770,7 +770,7 @@ func (s *Suite) Test_MkLine_variableNeed
                "WARN: ~/Makefile:4: LINKER_RPATH_FLAG should not be evaluated at load time.")
 }
 
-func (s *Suite) Test_MkLine_Pkgmandir(c *check.C) {
+func (s *Suite) Test_ShellLine_CheckWord__PKGMANDIR(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -820,7 +820,7 @@ func (s *Suite) Test_MkLines_Check__shel
                "WARN: x11/lablgtk1/Makefile:2: Please use ${CC:Q} instead of ${CC}.")
 }
 
-func (s *Suite) Test_MkLine_shell_varuse_in_backt_dquot(c *check.C) {
+func (s *Suite) Test_MkLine__shell_varuse_in_backt_dquot(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -838,7 +838,7 @@ func (s *Suite) Test_MkLine_shell_varuse
 }
 
 // See PR 46570, Ctrl+F "3. In lang/perl5".
-func (s *Suite) Test_MkLine_VariableType(c *check.C) {
+func (s *Suite) Test_Pkgsrc_VariableType(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()

Index: pkgsrc/pkgtools/pkglint/files/mklines.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines.go:1.31 pkgsrc/pkgtools/pkglint/files/mklines.go:1.32
--- pkgsrc/pkgtools/pkglint/files/mklines.go:1.31       Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/mklines.go    Wed Oct  3 22:27:53 2018
@@ -112,8 +112,8 @@ func (mklines *MkLines) Check() {
 
                        switch mkline.Varcanon() {
                        case "PLIST_VARS":
-                               value := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "")
-                               for _, id := range value {
+                               ids := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "")
+                               for _, id := range ids {
                                        if !mklines.plistVarSkip && mklines.plistVarSet[id] == nil {
                                                mkline.Warnf("%q is added to PLIST_VARS, but PLIST.%s is not defined in this file.", id, id)
                                        }
@@ -220,8 +220,8 @@ func (mklines *MkLines) DetermineDefined
                        }
 
                case "PLIST_VARS":
-                       value := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "")
-                       for _, id := range value {
+                       ids := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "")
+                       for _, id := range ids {
                                if trace.Tracing {
                                        trace.Step1("PLIST.%s is added to PLIST_VARS.", id)
                                }
@@ -255,8 +255,8 @@ func (mklines *MkLines) collectPlistVars
                if mkline.IsVarassign() {
                        switch mkline.Varcanon() {
                        case "PLIST_VARS":
-                               value := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "")
-                               for _, id := range value {
+                               ids := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "")
+                               for _, id := range ids {
                                        if containsVarRef(id) {
                                                mklines.plistVarSkip = true
                                        } else {
@@ -310,9 +310,13 @@ func (mklines *MkLines) determineDocumen
 
        for _, mkline := range mklines.mklines {
                text := mkline.Text
-               words := splitOnSpace(text)
+               switch {
+               case hasPrefix(text, "#"):
+                       words := splitOnSpace(text)
+                       if len(words) <= 1 {
+                               break
+                       }
 
-               if 1 < len(words) && words[0] == "#" {
                        commentLines++
 
                        parser := NewMkParser(mkline.Line, words[1], false)
@@ -330,9 +334,8 @@ func (mklines *MkLines) determineDocumen
                        if 1 < len(words) && words[1] == "Copyright" {
                                relevant = false
                        }
-               }
 
-               if text == "" {
+               case mkline.IsEmpty():
                        finish()
                }
        }
Index: pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.31 pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.32
--- pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.31     Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go  Wed Oct  3 22:27:53 2018
@@ -48,9 +48,9 @@ func (s *Suite) Test_VartypeCheck_Catego
        t := s.Init(c)
        vt := NewVartypeCheckTester(t, (*VartypeCheck).Category)
 
-       t.SetupFileLines("filesyscategory/Makefile",
+       t.CreateFileLines("filesyscategory/Makefile",
                "# empty")
-       t.SetupFileLines("wip/Makefile",
+       t.CreateFileLines("wip/Makefile",
                "# empty")
 
        vt.Varname("CATEGORIES")
@@ -669,7 +669,7 @@ func (s *Suite) Test_VartypeCheck_Perms(
 }
 
 func (s *Suite) Test_VartypeCheck_Pkgname(c *check.C) {
-       vt := NewVartypeCheckTester(s.Init(c), (*VartypeCheck).PkgName)
+       vt := NewVartypeCheckTester(s.Init(c), (*VartypeCheck).Pkgname)
 
        vt.Varname("PKGNAME")
        vt.Values(
@@ -754,7 +754,8 @@ func (s *Suite) Test_VartypeCheck_Machin
                "NetBSD-1.6.2-i386",
                "FreeBSD*",
                "FreeBSD-*",
-               "${LINUX}")
+               "${LINUX}",
+               "NetBSD-[0-1]*-*")
 
        vt.Output(
                "WARN: fname:1: \"linux-i386\" is not a valid platform pattern.",
@@ -780,7 +781,8 @@ func (s *Suite) Test_VartypeCheck_Machin
                        "i386 i586 i686 ia64 m68000 m68k m88k mips mips64 mips64eb mips64el mipseb mipsel mipsn32 "+
                        "mlrisc ns32k pc532 pmax powerpc powerpc64 rs6000 s390 sh3eb sh3el sparc sparc64 vax x86_64 "+
                        "} for the hardware architecture part of ONLY_FOR_PLATFORM.",
-               "WARN: fname:5: \"FreeBSD*\" is not a valid platform pattern.")
+               "WARN: fname:5: \"FreeBSD*\" is not a valid platform pattern.",
+               "WARN: fname:8: Please use \"[0-1].*\" instead of \"[0-1]*\" as the version pattern.")
 }
 
 func (s *Suite) Test_VartypeCheck_PythonDependency(c *check.C) {
@@ -1015,10 +1017,16 @@ func (s *Suite) Test_VartypeCheck_Versio
        vt.Values(
                "a*",
                "1.2/456",
+               "4*",
+               "?.??",
+               "1.[234]*",
+               "1.[2-7].*",
                "[0-9]*")
        vt.Output(
                "WARN: fname:11: Invalid version number pattern \"a*\".",
-               "WARN: fname:12: Invalid version number pattern \"1.2/456\".")
+               "WARN: fname:12: Invalid version number pattern \"1.2/456\".",
+               "WARN: fname:13: Please use \"4.*\" instead of \"4*\" as the version pattern.",
+               "WARN: fname:15: Please use \"1.[234].*\" instead of \"1.[234]*\" as the version pattern.")
 }
 
 func (s *Suite) Test_VartypeCheck_WrapperReorder(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/mklines_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.27 pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.28
--- pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.27  Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/mklines_test.go       Wed Oct  3 22:27:53 2018
@@ -64,7 +64,7 @@ func (s *Suite) Test_MkLineChecker_check
                "ERROR: ~/Makefile:2: Other Makefiles must not be included directly.")
 }
 
-func (s *Suite) Test_MkLines_quoting_LDFLAGS_for_GNU_configure(c *check.C) {
+func (s *Suite) Test_MkLines__quoting_LDFLAGS_for_GNU_configure(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -386,7 +386,7 @@ func (s *Suite) Test_MkLines_DetermineUs
        c.Check(mklines.vars.FirstUse("outer.*"), equals, mkline)
 }
 
-func (s *Suite) Test_MkLines_PrivateTool_Undefined(c *check.C) {
+func (s *Suite) Test_MkLines__private_tool_undefined(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -402,7 +402,7 @@ func (s *Suite) Test_MkLines_PrivateTool
                "WARN: fname:3: Unknown shell command \"md5sum\".")
 }
 
-func (s *Suite) Test_MkLines_PrivateTool_Defined(c *check.C) {
+func (s *Suite) Test_MkLines__private_tool_defined(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -420,7 +420,7 @@ func (s *Suite) Test_MkLines_PrivateTool
                "WARN: fname:4: The \"md5sum\" tool is used but not added to USE_TOOLS.")
 }
 
-func (s *Suite) Test_MkLines_Check_indentation(c *check.C) {
+func (s *Suite) Test_MkLines_Check__indentation(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -519,7 +519,7 @@ func (s *Suite) Test_MkLines_Check__unba
 // Demonstrates how to define your own make(1) targets for creating
 // files in the current directory. The pkgsrc-wip category Makefile
 // does this, while all other categories don't need any custom code.
-func (s *Suite) Test_MkLines_wip_category_Makefile(c *check.C) {
+func (s *Suite) Test_MkLines__wip_category_Makefile(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall", "--explain")

Index: pkgsrc/pkgtools/pkglint/files/mkparser.go
diff -u pkgsrc/pkgtools/pkglint/files/mkparser.go:1.16 pkgsrc/pkgtools/pkglint/files/mkparser.go:1.17
--- pkgsrc/pkgtools/pkglint/files/mkparser.go:1.16      Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/mkparser.go   Wed Oct  3 22:27:53 2018
@@ -6,6 +6,8 @@ import (
        "strings"
 )
 
+// MkParser wraps a Parser and provides methods for parsing
+// things related to Makefiles.
 type MkParser struct {
        *Parser
 }
@@ -193,6 +195,7 @@ loop:
                }
 
                repl.Reset(modifierMark)
+               // FIXME: Why AdvanceRegexp? This accepts :S,a,b,c,d,e,f but shouldn't.
                for p.VarUse() != nil || repl.AdvanceRegexp(regex.Pattern(`^([^:$`+closing+`]|\$\$)+`)) {
                }
                if suffixSubst := repl.Since(modifierMark); contains(suffixSubst, "=") {
Index: pkgsrc/pkgtools/pkglint/files/vartype.go
diff -u pkgsrc/pkgtools/pkglint/files/vartype.go:1.16 pkgsrc/pkgtools/pkglint/files/vartype.go:1.17
--- pkgsrc/pkgtools/pkglint/files/vartype.go:1.16       Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/vartype.go    Wed Oct  3 22:27:53 2018
@@ -232,7 +232,7 @@ var (
        BtPathname               = &BasicType{"Pathname", (*VartypeCheck).Pathname}
        BtPerl5Packlist          = &BasicType{"Perl5Packlist", (*VartypeCheck).Perl5Packlist}
        BtPerms                  = &BasicType{"Perms", (*VartypeCheck).Perms}
-       BtPkgName                = &BasicType{"PkgName", (*VartypeCheck).PkgName}
+       BtPkgName                = &BasicType{"Pkgname", (*VartypeCheck).Pkgname}
        BtPkgPath                = &BasicType{"PkgPath", (*VartypeCheck).PkgPath}
        BtPkgOptionsVar          = &BasicType{"PkgOptionsVar", (*VartypeCheck).PkgOptionsVar}
        BtPkgRevision            = &BasicType{"PkgRevision", (*VartypeCheck).PkgRevision}

Index: pkgsrc/pkgtools/pkglint/files/mkshparser_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkshparser_test.go:1.6 pkgsrc/pkgtools/pkglint/files/mkshparser_test.go:1.7
--- pkgsrc/pkgtools/pkglint/files/mkshparser_test.go:1.6        Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/mkshparser_test.go    Wed Oct  3 22:27:53 2018
@@ -27,7 +27,15 @@ type ShSuite struct {
 
 var _ = check.Suite(&ShSuite{})
 
-func (s *ShSuite) Test_ShellParser_program(c *check.C) {
+func (s *ShSuite) SetUpTest(c *check.C) {
+       G = NewPkglint()
+}
+
+func (s *ShSuite) TearDownTest(c *check.C) {
+       G = Pkglint{} // Make it unusable
+}
+
+func (s *ShSuite) Test_ShellParser__program(c *check.C) {
        b := s.init(c)
 
        s.test("",
@@ -103,7 +111,7 @@ func (s *ShSuite) Test_ShellParser_progr
                        b.List().AddCommand(b.SimpleCommand("action2")).AddSemicolon())))
 }
 
-func (s *ShSuite) Test_ShellParser_list(c *check.C) {
+func (s *ShSuite) Test_ShellParser__list(c *check.C) {
        b := s.init(c)
 
        s.test("echo1 && echo2",
@@ -125,7 +133,7 @@ func (s *ShSuite) Test_ShellParser_list(
                        AddBackground())
 }
 
-func (s *ShSuite) Test_ShellParser_and_or(c *check.C) {
+func (s *ShSuite) Test_ShellParser__and_or(c *check.C) {
        b := s.init(c)
 
        s.test("echo1 | echo2",
@@ -154,7 +162,7 @@ func (s *ShSuite) Test_ShellParser_and_o
                                b.SimpleCommand("echo4")))))
 }
 
-func (s *ShSuite) Test_ShellParser_pipeline(c *check.C) {
+func (s *ShSuite) Test_ShellParser__pipeline(c *check.C) {
        b := s.init(c)
 
        s.test("command1 | command2",
@@ -168,7 +176,7 @@ func (s *ShSuite) Test_ShellParser_pipel
                        b.SimpleCommand("command2")))))
 }
 
-func (s *ShSuite) Test_ShellParser_pipe_sequence(c *check.C) {
+func (s *ShSuite) Test_ShellParser__pipe_sequence(c *check.C) {
        b := s.init(c)
 
        s.test("command1 | if true ; then : ; fi",
@@ -179,7 +187,7 @@ func (s *ShSuite) Test_ShellParser_pipe_
                                b.List().AddCommand(b.SimpleCommand(":")).AddSemicolon())))))
 }
 
-func (s *ShSuite) Test_ShellParser_command(c *check.C) {
+func (s *ShSuite) Test_ShellParser__command(c *check.C) {
        b := s.init(c)
 
        s.test("simple_command",
@@ -205,7 +213,7 @@ func (s *ShSuite) Test_ShellParser_comma
                        b.Redirection(2, ">&", "1"))))
 }
 
-func (s *ShSuite) Test_ShellParser_compound_command(c *check.C) {
+func (s *ShSuite) Test_ShellParser__compound_command(c *check.C) {
        b := s.init(c)
 
        s.test("{ brace ; }",
@@ -228,7 +236,7 @@ func (s *ShSuite) Test_ShellParser_compo
 
 }
 
-func (s *ShSuite) Test_ShellParser_subshell(c *check.C) {
+func (s *ShSuite) Test_ShellParser__subshell(c *check.C) {
        b := s.init(c)
 
        sub3 := b.Subshell(b.List().AddCommand(b.SimpleCommand("sub3")))
@@ -237,7 +245,7 @@ func (s *ShSuite) Test_ShellParser_subsh
        s.test("( ( ( sub3 ) ; sub2 ) ; sub1 )", b.List().AddCommand(sub1))
 }
 
-func (s *ShSuite) Test_ShellParser_compound_list(c *check.C) {
+func (s *ShSuite) Test_ShellParser__compound_list(c *check.C) {
        b := s.init(c)
 
        s.test("( \n echo )",
@@ -245,13 +253,13 @@ func (s *ShSuite) Test_ShellParser_compo
                        b.List().AddCommand(b.SimpleCommand("echo")))))
 }
 
-func (s *ShSuite) Test_ShellParser_term(c *check.C) {
+func (s *ShSuite) Test_ShellParser__term(c *check.C) {
        b := s.init(c)
 
        _ = b
 }
 
-func (s *ShSuite) Test_ShellParser_for_clause(c *check.C) {
+func (s *ShSuite) Test_ShellParser__for_clause(c *check.C) {
        b := s.init(c)
 
        s.test("for var do echo $var ; done",
@@ -296,7 +304,7 @@ func (s *ShSuite) Test_ShellParser_for_c
                                b.List().AddCommand(b.SimpleCommand("echo", "$$i$$j")).AddSemicolon())))))
 }
 
-func (s *ShSuite) Test_ShellParser_case_clause(c *check.C) {
+func (s *ShSuite) Test_ShellParser__case_clause(c *check.C) {
        b := s.init(c)
 
        s.test("case $var in esac",
@@ -335,7 +343,7 @@ func (s *ShSuite) Test_ShellParser_case_
 
 }
 
-func (s *ShSuite) Test_ShellParser_if_clause(c *check.C) {
+func (s *ShSuite) Test_ShellParser__if_clause(c *check.C) {
        b := s.init(c)
 
        s.test(
@@ -354,7 +362,7 @@ func (s *ShSuite) Test_ShellParser_if_cl
                                b.List().AddCommand(b.SimpleCommand("action")).AddSemicolon())))))
 }
 
-func (s *ShSuite) Test_ShellParser_while_clause(c *check.C) {
+func (s *ShSuite) Test_ShellParser__while_clause(c *check.C) {
        b := s.init(c)
 
        s.test("while condition ; do action ; done",
@@ -363,7 +371,7 @@ func (s *ShSuite) Test_ShellParser_while
                        b.List().AddCommand(b.SimpleCommand("action")).AddSemicolon())))
 }
 
-func (s *ShSuite) Test_ShellParser_until_clause(c *check.C) {
+func (s *ShSuite) Test_ShellParser__until_clause(c *check.C) {
        b := s.init(c)
 
        s.test("until condition ; do action ; done",
@@ -372,13 +380,13 @@ func (s *ShSuite) Test_ShellParser_until
                        b.List().AddCommand(b.SimpleCommand("action")).AddSemicolon())))
 }
 
-func (s *ShSuite) Test_ShellParser_function_definition(c *check.C) {
+func (s *ShSuite) Test_ShellParser__function_definition(c *check.C) {
        b := s.init(c)
 
        _ = b
 }
 
-func (s *ShSuite) Test_ShellParser_brace_group(c *check.C) {
+func (s *ShSuite) Test_ShellParser__brace_group(c *check.C) {
        b := s.init(c)
 
        // No semicolon necessary after the closing brace.
@@ -389,7 +397,7 @@ func (s *ShSuite) Test_ShellParser_brace
                                b.List().AddCommand(b.SimpleCommand("echo", "yes")).AddSemicolon())))))
 }
 
-func (s *ShSuite) Test_ShellParser_simple_command(c *check.C) {
+func (s *ShSuite) Test_ShellParser__simple_command(c *check.C) {
        b := s.init(c)
 
        s.test(
@@ -427,7 +435,7 @@ func (s *ShSuite) Test_ShellParser_simpl
                b.List().AddCommand(b.SimpleCommand("{OpenGrok", "args")))
 }
 
-func (s *ShSuite) Test_ShellParser_io_redirect(c *check.C) {
+func (s *ShSuite) Test_ShellParser__io_redirect(c *check.C) {
        b := s.init(c)
 
        s.test("echo >> ${PLIST_SRC}",
@@ -472,7 +480,7 @@ func (s *ShSuite) Test_ShellParser_io_re
                                {-1, ">", b.Token("/dev/stderr")}}}}))
 }
 
-func (s *ShSuite) Test_ShellParser_io_here(c *check.C) {
+func (s *ShSuite) Test_ShellParser__io_here(c *check.C) {
        b := s.init(c)
 
        _ = b

Index: pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go:1.3 pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go:1.4
--- pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go:1.3        Sun Aug 12 16:31:56 2018
+++ pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go    Wed Oct  3 22:27:53 2018
@@ -26,12 +26,16 @@ func (s *Suite) Test_MkShWalker_Walk(c *
                        commands = append(commands, fmt.Sprintf("%16s %s", kind, detail))
                }
 
-               callback := NewMkShWalkCallback()
+               walker := NewMkShWalker()
+               callback := &walker.Callback
                callback.List = func(list *MkShList) { add("List", "with %d andOrs", len(list.AndOrs)) }
                callback.AndOr = func(andor *MkShAndOr) { add("AndOr", "with %d pipelines", len(andor.Pipes)) }
                callback.Pipeline = func(pipeline *MkShPipeline) { add("Pipeline", "with %d commands", len(pipeline.Cmds)) }
                callback.Command = func(command *MkShCommand) { add("Command", "") }
-               callback.SimpleCommand = func(command *MkShSimpleCommand) { add("SimpleCommand", "%s", NewStrCommand(command).String()) }
+               callback.SimpleCommand = func(command *MkShSimpleCommand) {
+                       add("SimpleCommand", "%s", NewStrCommand(command).String())
+                       add("Path", "%s", walker.Path())
+               }
                callback.CompoundCommand = func(command *MkShCompoundCommand) { add("CompoundCommand", "") }
                callback.Case = func(caseClause *MkShCaseClause) { add("Case", "with %d items", len(caseClause.Cases)) }
                callback.CaseItem = func(caseItem *MkShCaseItem) { add("CaseItem", "with %d patterns", len(caseItem.Patterns)) }
@@ -45,7 +49,7 @@ func (s *Suite) Test_MkShWalker_Walk(c *
                callback.For = func(forClause *MkShForClause) { add("For", "variable %s", forClause.Varname) }
                callback.Varname = func(varname string) { add("Varname", "%s", varname) }
 
-               NewMkShWalker().Walk(list, callback)
+               walker.Walk(list)
 
                c.Check(commands, deepEquals, []string{
                        "            List with 5 andOrs",
@@ -58,13 +62,15 @@ func (s *Suite) Test_MkShWalker_Walk(c *
                        "           AndOr with 1 pipelines",
                        "        Pipeline with 1 commands",
                        "         Command ",
-                       "   SimpleCommand [] condition []",
+                       "   SimpleCommand condition",
+                       "            Path List.AndOr[0].Pipeline[0].Command[0].CompoundCommand.IfClause.List[0].AndOr[0].Pipeline[0].Command[0].SimpleCommand",
                        "            Word condition",
                        "            List with 1 andOrs",
                        "           AndOr with 1 pipelines",
                        "        Pipeline with 1 commands",
                        "         Command ",
-                       "   SimpleCommand [] action []",
+                       "   SimpleCommand action",
+                       "            Path List.AndOr[0].Pipeline[0].Command[0].CompoundCommand.IfClause.List[1].AndOr[0].Pipeline[0].Command[0].SimpleCommand",
                        "            Word action",
                        "            List with 1 andOrs",
                        "           AndOr with 1 pipelines",
@@ -80,19 +86,22 @@ func (s *Suite) Test_MkShWalker_Walk(c *
                        "           AndOr with 1 pipelines",
                        "        Pipeline with 1 commands",
                        "         Command ",
-                       "   SimpleCommand [] case-item-action []",
+                       "   SimpleCommand case-item-action",
+                       "            Path 
List.AndOr[0].Pipeline[0].Command[0].CompoundCommand.IfClause.List[2].AndOr[0].Pipeline[0].Command[0].CompoundCommand.CaseClause.CaseItem[0].List[1].AndOr[0].Pipeline[0].Command[0].SimpleCommand",
                        "            Word case-item-action",
                        "           AndOr with 1 pipelines",
                        "        Pipeline with 1 commands",
                        "         Command ",
-                       "   SimpleCommand [] set [-e]",
+                       "   SimpleCommand set -e",
+                       "            Path List.AndOr[1].Pipeline[0].Command[0].SimpleCommand",
                        "            Word set",
                        "           Words with 1 words",
                        "            Word -e",
                        "           AndOr with 1 pipelines",
                        "        Pipeline with 1 commands",
                        "         Command ",
-                       "   SimpleCommand [] cd [${WRKSRC}/locale]",
+                       "   SimpleCommand cd ${WRKSRC}/locale",
+                       "            Path List.AndOr[2].Pipeline[0].Command[0].SimpleCommand",
                        "            Word cd",
                        "           Words with 1 words",
                        "            Word ${WRKSRC}/locale",
@@ -108,7 +117,8 @@ func (s *Suite) Test_MkShWalker_Walk(c *
                        "           AndOr with 2 pipelines",
                        "        Pipeline with 1 commands",
                        "         Command ",
-                       "   SimpleCommand [] [ [\"$${lang}\" = \"wxstd.po\" ]]",
+                       "   SimpleCommand [ \"$${lang}\" = \"wxstd.po\" ]",
+                       "            Path List.AndOr[3].Pipeline[0].Command[0].CompoundCommand.ForClause.List.AndOr[0].Pipeline[0].Command[0].SimpleCommand",
                        "            Word [",
                        "           Words with 4 words",
                        "            Word \"$${lang}\"",
@@ -117,12 +127,14 @@ func (s *Suite) Test_MkShWalker_Walk(c *
                        "            Word ]",
                        "        Pipeline with 1 commands",
                        "         Command ",
-                       "   SimpleCommand [] continue []",
+                       "   SimpleCommand continue",
+                       "            Path List.AndOr[3].Pipeline[0].Command[0].CompoundCommand.ForClause.List.AndOr[0].Pipeline[1].Command[0].SimpleCommand",
                        "            Word continue",
                        "           AndOr with 1 pipelines",
                        "        Pipeline with 1 commands",
                        "         Command ",
-                       "   SimpleCommand [] ${TOOLS_PATH.msgfmt} [-c -o \"$${lang%.po}.mo\" \"$${lang}\"]",
+                       "   SimpleCommand ${TOOLS_PATH.msgfmt} -c -o \"$${lang%.po}.mo\" \"$${lang}\"",
+                       "            Path List.AndOr[3].Pipeline[0].Command[0].CompoundCommand.ForClause.List.AndOr[1].Pipeline[0].Command[0].SimpleCommand",
                        "            Word ${TOOLS_PATH.msgfmt}",
                        "           Words with 4 words",
                        "            Word -c",
@@ -138,7 +150,8 @@ func (s *Suite) Test_MkShWalker_Walk(c *
                        "           AndOr with 1 pipelines",
                        "        Pipeline with 1 commands",
                        "         Command ",
-                       "   SimpleCommand [] : []",
+                       "   SimpleCommand :",
+                       "            Path List.AndOr[4].Pipeline[0].Command[0].CompoundCommand.LoopClause.List[0].AndOr[0].Pipeline[0].Command[0].SimpleCommand",
                        "            Word :",
                        "            List with 1 andOrs",
                        "           AndOr with 1 pipelines",
@@ -150,10 +163,15 @@ func (s *Suite) Test_MkShWalker_Walk(c *
                        "           AndOr with 1 pipelines",
                        "        Pipeline with 1 commands",
                        "         Command ",
-                       "   SimpleCommand [] : []",
+                       "   SimpleCommand :",
+                       "            Path 
List.AndOr[4].Pipeline[0].Command[0].CompoundCommand.LoopClause.List[1].AndOr[0].Pipeline[0].Command[0].FunctionDefinition.CompoundCommand.List.AndOr[0].Pipeline[0].Command[0].SimpleCommand",
                        "            Word :",
                        "       Redirects with 1 redirects",
                        "        Redirect >&",
                        "            Word 2"})
+
+               // After parsing, there is not a single level of indentation,
+               // therefore even Parent(0) returns nil.
+               c.Check(walker.Parent(0), equals, nil)
        }
 }
Index: pkgsrc/pkgtools/pkglint/files/vardefs_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.3 pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.4
--- pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.3   Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/vardefs_test.go       Wed Oct  3 22:27:53 2018
@@ -2,21 +2,21 @@ package main
 
 import "gopkg.in/check.v1"
 
-func (s *Suite) Test_InitVartypes__enumFrom(c *check.C) {
+func (s *Suite) Test_Pkgsrc_InitVartypes__enumFrom(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileMkLines("editors/emacs/modules.mk",
+       t.CreateFileLines("editors/emacs/modules.mk",
                MkRcsID,
                "",
                "_EMACS_VERSIONS_ALL=    emacs31",
                "_EMACS_VERSIONS_ALL+=   emacs29")
-       t.SetupFileMkLines("mk/java-vm.mk",
+       t.CreateFileLines("mk/java-vm.mk",
                MkRcsID,
                "",
                "_PKG_JVMS.8=            openjdk8 oracle-jdk8",
                "_PKG_JVMS.7=            ${_PKG_JVMS.8} openjdk7 sun-jdk7",
                "_PKG_JVMS.6=            ${_PKG_JVMS.7} sun-jdk6 jdk16")
-       t.SetupFileMkLines("mk/compiler.mk",
+       t.CreateFileLines("mk/compiler.mk",
                MkRcsID,
                "",
                "_COMPILERS=             gcc ido mipspro-ucode \\",
@@ -41,3 +41,23 @@ func (s *Suite) Test_InitVartypes__enumF
        checkEnumValues("USE_LANGUAGES", "ShellList of enum: ada c c++ c++03 c++0x c++11 c++14 c99 fortran fortran77 gnu++03 gnu++0x gnu++11 gnu++14 java obj-c++ objc ")
        checkEnumValues("PKGSRC_COMPILER", "ShellList of enum: ccache distcc f2c g95 gcc ido mipspro-ucode sunpro ")
 }
+
+func (s *Suite) Test_parseACLEntries(c *check.C) {
+       t := s.Init(c)
+
+       t.ExpectFatal(
+               func() { parseACLEntries("VARNAME", "buildlink3.mk: *; *: *") },
+               "FATAL: Invalid ACL permission \"*\" for \"VARNAME\".")
+
+       t.ExpectFatal(
+               func() { parseACLEntries("VARNAME", "buildlink3.mk: use; *: use") },
+               "FATAL: Repeated permissions \"use\" for \"VARNAME\".")
+
+       t.ExpectFatal(
+               func() { parseACLEntries("VARNAME", "*.txt: use") },
+               "FATAL: Invalid ACL glob \"*.txt\" for \"VARNAME\".")
+
+       t.ExpectFatal(
+               func() { parseACLEntries("VARNAME", "*.mk: use; buildlink3.mk: append") },
+               "FATAL: Ineffective ACL glob \"buildlink3.mk\" for \"VARNAME\".")
+}

Index: pkgsrc/pkgtools/pkglint/files/package.go
diff -u pkgsrc/pkgtools/pkglint/files/package.go:1.35 pkgsrc/pkgtools/pkglint/files/package.go:1.36
--- pkgsrc/pkgtools/pkglint/files/package.go:1.35       Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/package.go    Wed Oct  3 22:27:53 2018
@@ -31,7 +31,6 @@ type Package struct {
 
        vars                  Scope
        bl3                   map[string]Line // buildlink3.mk name => line; contains only buildlink3.mk files that are directly included.
-       plistSubstCond        map[string]bool // varname => true; all variables that are used as conditions (@comment or nothing) in PLISTs.
        included              map[string]Line // fname => line
        seenMakefileCommon    bool            // Does the package have any .includes?
        conditionalIncludes   map[string]MkLine
@@ -52,24 +51,33 @@ func NewPackage(dir string) *Package {
                Pkgdir:                ".",
                Filesdir:              "files",
                Patchdir:              "patches",
-               DistinfoFile:          "distinfo",
+               DistinfoFile:          "${PKGDIR}/distinfo",
                PlistDirs:             make(map[string]bool),
                PlistFiles:            make(map[string]bool),
                vars:                  NewScope(),
                bl3:                   make(map[string]Line),
-               plistSubstCond:        make(map[string]bool),
                included:              make(map[string]Line),
                conditionalIncludes:   make(map[string]MkLine),
                unconditionalIncludes: make(map[string]MkLine),
        }
        pkg.vars.DefineAll(G.Pkgsrc.UserDefinedVars)
+
+       pkg.vars.Fallback("PKGDIR", ".")
+       pkg.vars.Fallback("DISTINFO_FILE", "${PKGDIR}/distinfo")
+       pkg.vars.Fallback("FILESDIR", "files")
+       pkg.vars.Fallback("PATCHDIR", "patches")
+       pkg.vars.Fallback("KRB5_TYPE", "heimdal")
+       pkg.vars.Fallback("PGSQL_VERSION", "95")
+       pkg.vars.Fallback(".CURDIR", ".") // FIXME: In reality, this is an absolute pathname.
+
        return pkg
 }
 
 // File returns the (possibly absolute) path to relativeFilename,
 // as resolved from the package's directory.
+// Variables that are known in the package are resolved, e.g. ${PKGDIR}.
 func (pkg *Package) File(relativeFilename string) string {
-       return cleanpath(pkg.dir + "/" + relativeFilename)
+       return cleanpath(resolveVariableRefs(pkg.dir + "/" + relativeFilename))
 }
 
 func (pkg *Package) checkPossibleDowngrade() {
@@ -93,7 +101,7 @@ func (pkg *Package) checkPossibleDowngra
        }
 
        if change.Action == "Updated" {
-               changeVersion := regex.Compile(`nb\d+$`).ReplaceAllString(change.Version, "")
+               changeVersion := replaceAll(change.Version, `nb\d+$`, "")
                if pkgver.Compare(pkgversion, changeVersion) < 0 {
                        mkline.Warnf("The package is being downgraded from %s (see %s) to %s.", change.Version, change.Line.ReferenceFrom(mkline.Line), pkgversion)
                        Explain(
@@ -158,7 +166,7 @@ func (pkglint *Pkglint) checkdirPackage(
                files = append(files, dirglob(pkg.File(pkg.Filesdir))...)
        }
        files = append(files, dirglob(pkg.File(pkg.Patchdir))...)
-       if pkg.DistinfoFile != "distinfo" && pkg.DistinfoFile != "./distinfo" {
+       if pkg.DistinfoFile != pkg.vars.fallback["DISTINFO_FILE"] {
                files = append(files, pkg.File(pkg.DistinfoFile))
        }
        haveDistinfo := false
@@ -181,11 +189,14 @@ func (pkglint *Pkglint) checkdirPackage(
 
        for _, fname := range files {
                if containsVarRef(fname) {
+                       if trace.Tracing {
+                               trace.Stepf("Skipping file %q because the name contains an unresolved variable.", fname)
+                       }
                        continue
                }
                if fname == pkg.File("Makefile") {
                        if st, err := os.Lstat(fname); err == nil {
-                               pkglint.checkExecutable(st, fname)
+                               pkglint.checkExecutable(st)
                        }
                        if G.opts.CheckMakefile {
                                pkg.checkfilePackageMakefile(fname, lines)
@@ -201,7 +212,7 @@ func (pkglint *Pkglint) checkdirPackage(
                pkg.checkLocallyModified(fname)
        }
 
-       if G.opts.CheckDistinfo && G.opts.CheckPatches {
+       if pkg.Pkgdir == "." && G.opts.CheckDistinfo && G.opts.CheckPatches {
                if havePatches && !haveDistinfo {
                        NewLineWhole(pkg.File(pkg.DistinfoFile)).Warnf("File not found. Please run \"%s makepatchsum\".", confMake)
                }
@@ -214,10 +225,6 @@ func (pkglint *Pkglint) checkdirPackage(
                        }
                }
        }
-
-       if !isEmptyDir(pkg.File("scripts")) {
-               NewLineWhole(pkg.File("scripts")).Warnf("This directory and its contents are deprecated! Please call the script(s) explicitly from the corresponding target(s) in the pkg's Makefile.")
-       }
 }
 
 func (pkg *Package) loadPackageMakefile() *MkLines {
@@ -227,7 +234,8 @@ func (pkg *Package) loadPackageMakefile(
        }
 
        mainLines, allLines := NewMkLines(nil), NewMkLines(nil)
-       if !pkg.readMakefile(fname, mainLines, allLines, "") {
+       if _, result := pkg.readMakefile(fname, mainLines, allLines, ""); !result {
+               LoadMk(fname, NotEmpty|LogErrors) // Just for the LogErrors.
                return nil
        }
 
@@ -241,10 +249,10 @@ func (pkg *Package) loadPackageMakefile(
        allLines.DetermineUsedVariables()
        allLines.CheckRedundantVariables()
 
-       pkg.Pkgdir = pkg.expandVariableWithDefault("PKGDIR", ".")
-       pkg.DistinfoFile = pkg.expandVariableWithDefault("DISTINFO_FILE", "distinfo")
-       pkg.Filesdir = pkg.expandVariableWithDefault("FILESDIR", "files")
-       pkg.Patchdir = pkg.expandVariableWithDefault("PATCHDIR", "patches")
+       pkg.Pkgdir, _ = pkg.vars.Value("PKGDIR")
+       pkg.DistinfoFile, _ = pkg.vars.Value("DISTINFO_FILE")
+       pkg.Filesdir, _ = pkg.vars.Value("FILESDIR")
+       pkg.Patchdir, _ = pkg.vars.Value("PATCHDIR")
 
        // See lang/php/ext.mk
        if varIsDefined("PHPEXT_MK") {
@@ -271,19 +279,20 @@ func (pkg *Package) loadPackageMakefile(
        return mainLines
 }
 
-func (pkg *Package) readMakefile(fname string, mainLines *MkLines, allLines *MkLines, includingFnameForUsedCheck string) bool {
+func (pkg *Package) readMakefile(fname string, mainLines *MkLines, allLines *MkLines, includingFnameForUsedCheck string) (exists bool, result bool) {
        if trace.Tracing {
                defer trace.Call1(fname)()
        }
 
-       fileMklines := LoadMk(fname, NotEmpty|LogErrors)
+       fileMklines := LoadMk(fname, NotEmpty)
        if fileMklines == nil {
-               return false
+               return false, false
        }
+       exists = true
 
        isMainMakefile := len(mainLines.mklines) == 0
 
-       result := true
+       result = true
        lineAction := func(mkline MkLine) bool {
                if isMainMakefile {
                        mainLines.mklines = append(mainLines.mklines, mkline)
@@ -308,7 +317,7 @@ func (pkg *Package) readMakefile(fname s
                if includeFile != "" {
                        if path.Base(fname) != "buildlink3.mk" {
                                if m, bl3File := match1(includeFile, `^\.\./\.\./(.*)/buildlink3\.mk$`); m {
-                                       G.Pkg.bl3[bl3File] = mkline.Line
+                                       pkg.bl3[bl3File] = mkline.Line
                                        if trace.Tracing {
                                                trace.Step1("Buildlink3 file in package: %q", bl3File)
                                        }
@@ -316,8 +325,8 @@ func (pkg *Package) readMakefile(fname s
                        }
                }
 
-               if includeFile != "" && G.Pkg.included[includeFile] == nil {
-                       G.Pkg.included[includeFile] = mkline.Line
+               if includeFile != "" && pkg.included[includeFile] == nil {
+                       pkg.included[includeFile] = mkline.Line
 
                        if matches(includeFile, `^\.\./[^./][^/]*/[^/]+`) {
                                mkline.Warnf("References to other packages should look like \"../../category/package\", not \"../package\".")
@@ -328,7 +337,7 @@ func (pkg *Package) readMakefile(fname s
                                if trace.Tracing {
                                        trace.Step1("Including %q sets seenMakefileCommon.", includeFile)
                                }
-                               G.Pkg.seenMakefileCommon = true
+                               pkg.seenMakefileCommon = true
                        }
 
                        skip := contains(fname, "/mk/") || hasSuffix(includeFile, "/bsd.pkg.mk") || IsPrefs(includeFile)
@@ -336,30 +345,37 @@ func (pkg *Package) readMakefile(fname s
                                dirname, _ := path.Split(fname)
                                dirname = cleanpath(dirname)
 
-                               // Only look in the directory relative to the
-                               // current file and in the current working directory.
-                               // Pkglint doesn't have an include dir list, like make(1) does.
-                               if !fileExists(dirname + "/" + includeFile) {
+                               fullIncluded := dirname + "/" + includeFile
+                               if trace.Tracing {
+                                       trace.Step1("Including %q.", fullIncluded)
+                               }
+                               fullIncluding := ifelseStr(incBase == "Makefile.common" && incDir != "", fname, "")
+                               innerExists, innerResult := pkg.readMakefile(fullIncluded, mainLines, allLines, fullIncluding)
 
+                               if !innerExists {
                                        if fileMklines.indentation.IsCheckedFile(includeFile) {
                                                return true // See https://github.com/rillig/pkglint/issues/1
+                                       }
 
-                                       } else if dirname != pkg.File(".") { // Prevent unnecessary syscalls
-                                               dirname = pkg.File(".")
-                                               if !fileExists(dirname + "/" + includeFile) {
-                                                       mkline.Errorf("Cannot read %q.", dirname+"/"+includeFile)
-                                                       result = false
-                                                       return false
-                                               }
+                                       // Only look in the directory relative to the
+                                       // current file and in the package directory.
+                                       // Make(1) has a list of include directories, but pkgsrc
+                                       // doesn't make use of that, so pkglint also doesn't
+                                       // need this extra complexity.
+                                       pkgBasedir := pkg.File(".")
+                                       if dirname != pkgBasedir { // Prevent unnecessary syscalls
+                                               dirname = pkgBasedir
+
+                                               fullIncludedFallback := dirname + "/" + includeFile
+                                               innerExists, innerResult = pkg.readMakefile(fullIncludedFallback, mainLines, allLines, fullIncluding)
                                        }
-                               }
 
-                               if trace.Tracing {
-                                       trace.Step1("Including %q.", dirname+"/"+includeFile)
+                                       if !innerExists {
+                                               mkline.Errorf("Cannot read %q.", includeFile)
+                                       }
                                }
-                               absIncluding := ifelseStr(incBase == "Makefile.common" && incDir != "", fname, "")
-                               absIncluded := dirname + "/" + includeFile
-                               if !pkg.readMakefile(absIncluded, mainLines, allLines, absIncluding) {
+
+                               if !innerResult {
                                        result = false
                                        return false
                                }
@@ -369,11 +385,11 @@ func (pkg *Package) readMakefile(fname s
                if mkline.IsVarassign() {
                        varname, op, value := mkline.Varname(), mkline.Op(), mkline.Value()
 
-                       if op != opAssignDefault || !G.Pkg.vars.Defined(varname) {
+                       if op != opAssignDefault || !pkg.vars.Defined(varname) {
                                if trace.Tracing {
                                        trace.Stepf("varassign(%q, %q, %q)", varname, op, value)
                                }
-                               G.Pkg.vars.Define(varname, mkline)
+                               pkg.vars.Define(varname, mkline)
                        }
                }
                return true
@@ -385,7 +401,7 @@ func (pkg *Package) readMakefile(fname s
                fileMklines.checkForUsedComment(G.Pkgsrc.ToRel(includingFnameForUsedCheck))
        }
 
-       return result
+       return
 }
 
 func (pkg *Package) checkfilePackageMakefile(fname string, mklines *MkLines) {
@@ -413,28 +429,14 @@ func (pkg *Package) checkfilePackageMake
        }
 
        if perlLine, noconfLine := vars.FirstDefinition("REPLACE_PERL"), vars.FirstDefinition("NO_CONFIGURE"); perlLine != nil && noconfLine != nil {
-               perlLine.Warnf("REPLACE_PERL is ignored when NO_CONFIGURE is set (in %s)", noconfLine.ReferenceFrom(perlLine.Line))
+               perlLine.Warnf("REPLACE_PERL is ignored when NO_CONFIGURE is set (in %s).", noconfLine.ReferenceFrom(perlLine.Line))
        }
 
        if !vars.Defined("LICENSE") && !vars.Defined("META_PACKAGE") && pkg.once.FirstTime("LICENSE") {
                NewLineWhole(fname).Errorf("Each package must define its LICENSE.")
        }
 
-       if gnuLine, useLine := vars.FirstDefinition("GNU_CONFIGURE"), vars.FirstDefinition("USE_LANGUAGES"); gnuLine != nil && useLine != nil {
-               if matches(useLine.VarassignComment(), `(?-i)\b(?:c|empty|none)\b`) {
-                       // Don't emit a warning, since the comment
-                       // probably contains a statement that C is
-                       // really not needed.
-
-               } else if !G.Infrastructure && useLine.Filename == "../../mk/compiler.mk" {
-                       // Ignore this one
-
-               } else if !matches(useLine.Value(), `(?:^|\s+)(?:c|c99|objc)(?:\s+|$)`) {
-                       gnuLine.Warnf("GNU_CONFIGURE almost always needs a C compiler, but \"c\" is not added to USE_LANGUAGES in %s.",
-                               useLine.ReferenceFrom(gnuLine.Line))
-               }
-       }
-
+       pkg.checkGnuConfigureUseLanguages()
        pkg.determineEffectivePkgVars()
        pkg.checkPossibleDowngrade()
 
@@ -454,6 +456,22 @@ func (pkg *Package) checkfilePackageMake
        SaveAutofixChanges(mklines.lines)
 }
 
+func (pkg *Package) checkGnuConfigureUseLanguages() {
+       vars := pkg.vars
+
+       if gnuLine, useLine := vars.FirstDefinition("GNU_CONFIGURE"), vars.FirstDefinition("USE_LANGUAGES"); gnuLine != nil && useLine != nil {
+               if matches(useLine.VarassignComment(), `(?-i)\b(?:c|empty|none)\b`) {
+                       // Don't emit a warning, since the comment
+                       // probably contains a statement that C is
+                       // really not needed.
+
+               } else if !matches(useLine.Value(), `(?:^|\s+)(?:c|c99|objc)(?:\s+|$)`) {
+                       gnuLine.Warnf("GNU_CONFIGURE almost always needs a C compiler, but \"c\" is not added to USE_LANGUAGES in %s.",
+                               useLine.ReferenceFrom(gnuLine.Line))
+               }
+       }
+}
+
 func (pkg *Package) getNbpart() string {
        pkgrevision, _ := pkg.vars.Value("PKGREVISION")
        if rev, err := strconv.Atoi(pkgrevision); err == nil {
@@ -514,19 +532,17 @@ func (pkg *Package) determineEffectivePk
 func (pkg *Package) pkgnameFromDistname(pkgname, distname string) string {
        tokens := NewMkParser(dummyLine, pkgname, false).MkTokens()
 
-       subst := func(str, smod string) (result string) {
-               if trace.Tracing {
-                       defer trace.Call(str, smod, trace.Result(&result))()
-               }
+       // Example:
+       //  subst("distname-1.0", "S,name,file,g") => "distfile-1.0"
+       subst := func(str, smod string) string {
                qsep := regexp.QuoteMeta(smod[1:2])
-               if m, left, from, right, to, flags := regex.Match5(smod, regex.Pattern(`^S`+qsep+`(\^?)([^:]*?)(\$?)`+qsep+`([^:]*)`+qsep+`([1g]*)$`)); m {
-                       result := mkopSubst(str, left != "", from, right != "", to, flags)
-                       if trace.Tracing {
-                               trace.Stepf("subst %q %q => %q", str, smod, result)
-                       }
-                       return result
+               m, left, from, right, to, flags := match5(smod, regex.Pattern(`^S`+qsep+`(\^?)([^:]*?)(\$?)`+qsep+`([^:]*)`+qsep+`([1g]*)$`))
+               G.Assertf(m, "pkgnameFromDistname %q", smod)
+               result := mkopSubst(str, left != "", from, right != "", to, flags)
+               if trace.Tracing && result != str {
+                       trace.Stepf("pkgnameFromDistname.subst: %q %q => %q", str, smod, result)
                }
-               return str
+               return result
        }
 
        result := ""
@@ -551,23 +567,6 @@ func (pkg *Package) pkgnameFromDistname(
        return result
 }
 
-func (pkg *Package) expandVariableWithDefault(varname, defaultValue string) string {
-       mkline := G.Pkg.vars.FirstDefinition(varname)
-       if mkline == nil {
-               return defaultValue
-       }
-
-       value := mkline.Value()
-       value = mkline.ResolveVarsInRelativePath(value, true)
-       if containsVarRef(value) {
-               value = resolveVariableRefs(value)
-       }
-       if trace.Tracing {
-               trace.Step2("Expanded %q to %q", varname, value)
-       }
-       return value
-}
-
 func (pkg *Package) checkUpdate() {
        if pkg.EffectivePkgbase != "" {
                for _, sugg := range G.Pkgsrc.GetSuggestedPackageUpdates() {
@@ -848,10 +847,7 @@ func (pkg *Package) checkLocallyModified
 
        owner, _ := pkg.vars.Value("OWNER")
        maintainer, _ := pkg.vars.Value("MAINTAINER")
-       if containsVarRef(owner) {
-               owner = ""
-       }
-       if containsVarRef(maintainer) || maintainer == "pkgsrc-users%NetBSD.org@localhost" {
+       if maintainer == "pkgsrc-users%NetBSD.org@localhost" {
                maintainer = ""
        }
        if owner == "" && maintainer == "" {

Index: pkgsrc/pkgtools/pkglint/files/package_test.go
diff -u pkgsrc/pkgtools/pkglint/files/package_test.go:1.29 pkgsrc/pkgtools/pkglint/files/package_test.go:1.30
--- pkgsrc/pkgtools/pkglint/files/package_test.go:1.29  Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/package_test.go       Wed Oct  3 22:27:53 2018
@@ -2,6 +2,40 @@ package main
 
 import "gopkg.in/check.v1"
 
+func (s *Suite) Test_Package_checklinesBuildlink3Inclusion__file_but_not_package(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("category/dependency/buildlink3.mk")
+       G.Pkg = NewPackage(t.File("category/package"))
+       mklines := t.NewMkLines("category/package/buildlink3.mk",
+               MkRcsID,
+               "",
+               ".include \"../../category/dependency/buildlink3.mk\"")
+
+       G.Pkg.checklinesBuildlink3Inclusion(mklines)
+
+       t.CheckOutputLines(
+               "WARN: category/package/buildlink3.mk:3: category/dependency/buildlink3.mk is included by this file but not by the package.")
+}
+
+func (s *Suite) Test_Package_checklinesBuildlink3Inclusion__package_but_not_file(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("category/dependency/buildlink3.mk")
+       G.Pkg = NewPackage(t.File("category/package"))
+       G.Pkg.bl3["../../category/dependency/buildlink3.mk"] = t.NewLine("fname", 1, "")
+       mklines := t.NewMkLines("category/package/buildlink3.mk",
+               MkRcsID)
+
+       t.EnableTracingToLog()
+       G.Pkg.checklinesBuildlink3Inclusion(mklines)
+
+       t.CheckOutputLines(
+               "TRACE: + (*Package).checklinesBuildlink3Inclusion()",
+               "TRACE: 1   ../../category/dependency/buildlink3.mk/buildlink3.mk is included by the package but not by the buildlink3.mk file.",
+               "TRACE: - (*Package).checklinesBuildlink3Inclusion()")
+}
+
 func (s *Suite) Test_Package_pkgnameFromDistname(c *check.C) {
        t := s.Init(c)
 
@@ -18,6 +52,9 @@ func (s *Suite) Test_Package_pkgnameFrom
        c.Check(pkg.pkgnameFromDistname("${DISTNAME:C/beta/.0./}", "fspanel-0.8beta1"), equals, "${DISTNAME:C/beta/.0./}")
        c.Check(pkg.pkgnameFromDistname("${DISTNAME:S/-0$/.0/1}", "aspell-af-0.50-0"), equals, "aspell-af-0.50.0")
 
+       // FIXME: Should produce a parse error since the :S modifier is malformed; see Test_MkParser_MkTokens.
+       c.Check(pkg.pkgnameFromDistname("${DISTNAME:S,a,b,c,d}", "aspell-af-0.50-0"), equals, "bspell-af-0.50-0")
+
        t.CheckOutputEmpty()
 }
 
@@ -87,7 +124,7 @@ func (s *Suite) Test_Package_CheckVarord
                "DISTNAME=\tdistname-1.0",
                "CATEGORIES=\tsysutils",
                "",
-               "MAINTAINER=\tpkgsrc-users%pkgsrc.org@localhost",
+               "MAINTAINER=\tpkgsrc-users%NetBSD.org@localhost",
                "# comment",
                "COMMENT=\tComment",
                "LICENSE=\tgnu-gpl-v2"))
@@ -109,7 +146,7 @@ func (s *Suite) Test_Package_CheckVarord
                "CATEGORIES=\tsysutils",
                "",
                ".if ${DISTNAME:Mdistname-*}",
-               "MAINTAINER=\tpkgsrc-users%pkgsrc.org@localhost",
+               "MAINTAINER=\tpkgsrc-users%NetBSD.org@localhost",
                ".endif",
                "LICENSE=\tgnu-gpl-v2"))
 
@@ -118,7 +155,7 @@ func (s *Suite) Test_Package_CheckVarord
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_Package_CheckVarorder_GitHub(c *check.C) {
+func (s *Suite) Test_Package_CheckVarorder__GitHub(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Worder")
@@ -139,7 +176,7 @@ func (s *Suite) Test_Package_CheckVarord
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_Package_varorder_license(c *check.C) {
+func (s *Suite) Test_Package_CheckVarorder__license(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Worder")
@@ -276,6 +313,35 @@ func (s *Suite) Test_Package_determineEf
        c.Check(pkg.EffectivePkgversion, equals, "1.0")
 }
 
+func (s *Suite) Test_Package_determineEffectivePkgVars__same(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall,no-order")
+       pkg := t.SetupPackage("category/package",
+               "DISTNAME=\tdistname-1.0",
+               "PKGNAME=\tdistname-1.0")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputLines(
+               "NOTE: ~/category/package/Makefile:20: " +
+                       "PKGNAME is ${DISTNAME} by default. You probably don't need to define PKGNAME.")
+}
+
+func (s *Suite) Test_Package_determineEffectivePkgVars__invalid_DISTNAME(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall,no-order")
+       pkg := t.SetupPackage("category/package",
+               "DISTNAME=\tpkgname-version")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:3: " +
+                       "As DISTNAME is not a valid package name, please define the PKGNAME explicitly.")
+}
+
 func (s *Suite) Test_Package_checkPossibleDowngrade(c *check.C) {
        t := s.Init(c)
 
@@ -300,11 +366,48 @@ func (s *Suite) Test_Package_checkPossib
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_checkdirPackage(c *check.C) {
+func (s *Suite) Test_Package_loadPackageMakefile__dump(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("--dumpmakefile")
+       t.SetupVartypes()
+       t.SetupPkgsrc()
+       t.CreateFileLines("category/Makefile")
+       t.CreateFileLines("category/package/PLIST",
+               PlistRcsID,
+               "bin/program")
+       t.CreateFileLines("category/package/distinfo",
+               RcsID,
+               "",
+               "SHA1 (distfile-1.0.tar.gz) = 12341234...",
+               "RMD160 (distfile-1.0.tar.gz) = 12341234...",
+               "SHA512 (distfile-1.0.tar.gz) = 12341234...",
+               "Size (distfile-1.0.tar.gz) = 12341234...")
+       t.CreateFileLines("category/package/Makefile",
+               MkRcsID,
+               "",
+               "CATEGORIES=category",
+               "",
+               "COMMENT=\tComment",
+               "LICENSE=\t2-clause-bsd")
+
+       G.checkdirPackage(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "Whole Makefile (with all included files) follows:",
+               "~/category/package/Makefile:1: # $NetBSD: package_test.go,v 1.30 2018/10/03 22:27:53 rillig Exp $",
+               "~/category/package/Makefile:2: ",
+               "~/category/package/Makefile:3: CATEGORIES=category",
+               "~/category/package/Makefile:4: ",
+               "~/category/package/Makefile:5: COMMENT=\tComment",
+               "~/category/package/Makefile:6: LICENSE=\t2-clause-bsd")
+}
+
+func (s *Suite) Test_Pkglint_checkdirPackage(c *check.C) {
        t := s.Init(c)
 
        t.Chdir("category/package")
-       t.SetupFileLines("Makefile",
+       t.CreateFileLines("Makefile",
                MkRcsID)
 
        G.checkdirPackage(".")
@@ -316,6 +419,56 @@ func (s *Suite) Test_checkdirPackage(c *
                "WARN: Makefile: No COMMENT given.")
 }
 
+func (s *Suite) Test_Pkglint_checkdirPackage__PKGDIR(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupVartypes()
+       t.SetupPkgsrc()
+       t.CreateFileLines("category/Makefile")
+       t.CreateFileLines("other/package/Makefile",
+               MkRcsID)
+       t.CreateFileLines("other/package/PLIST",
+               PlistRcsID,
+               "bin/program")
+       t.CreateFileLines("other/package/distinfo",
+               RcsID,
+               "",
+               "SHA1 (patch-aa) = da39a3ee5e6b4b0d3255bfef95601890afd80709")
+       t.CreateFileLines("category/package/patches/patch-aa",
+               RcsID)
+       t.Chdir("category/package")
+       t.CreateFileLines("Makefile",
+               MkRcsID,
+               "",
+               "CATEGORIES=category",
+               "",
+               "COMMENT=\tComment",
+               "LICENSE=\t2-clause-bsd",
+               "PKGDIR=\t../../other/package")
+
+       // DISTINFO_FILE is resolved relative to PKGDIR, the other places
+       // are resolved relative to the package base directory.
+       G.checkdirPackage(".")
+
+       t.CheckOutputLines(
+               "ERROR: patches/patch-aa:1: Patch files must not be empty.")
+}
+
+func (s *Suite) Test_Pkglint_checkdirPackage__patch_without_distinfo(c *check.C) {
+       t := s.Init(c)
+
+       pkg := t.SetupPackage("category/package")
+       t.CreateFileDummyPatch("category/package/patches/patch-aa")
+       t.Remove("category/package/distinfo")
+
+       G.CheckDirent(pkg)
+
+       // FIXME: One of the below warnings is redundant.
+       t.CheckOutputLines(
+               "WARN: ~/category/package/distinfo: File not found. Please run \""+confMake+" makesum\" or define NO_CHECKSUM=yes in the package Makefile.",
+               "WARN: ~/category/package/distinfo: File not found. Please run \""+confMake+" makepatchsum\".")
+}
+
 func (s *Suite) Test_Pkglint_checkdirPackage__meta_package_without_license(c *check.C) {
        t := s.Init(c)
 
@@ -414,7 +567,7 @@ func (s *Suite) Test_Package__varuse_at_
 func (s *Suite) Test_Package_loadPackageMakefile(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("category/package/Makefile",
+       t.CreateFileLines("category/package/Makefile",
                MkRcsID,
                "",
                "PKGNAME=pkgname-1.67",
@@ -433,7 +586,34 @@ func (s *Suite) Test_Package_loadPackage
                "NOTE: ~/category/package/Makefile:4: Definition of DISTNAME is redundant because of Makefile:4.")
 }
 
-func (s *Suite) Test_Package_conditionalAndUnconditionalInclude(c *check.C) {
+func (s *Suite) Test_Package_loadPackageMakefile__PECL_VERSION(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("lang/php/ext.mk",
+               MkRcsID,
+               "",
+               "PHPEXT_MK=      # defined",
+               "PHPPKGSRCDIR=   ../../lang/php72",
+               "LICENSE?=        unknown-license",
+               "COMMENT?=       Some PHP package",
+               "GENERATE_PLIST+=# none",
+               "",
+               ".if !defined(PECL_VERSION)",
+               "DISTINFO_FILE=  ${.CURDIR}/${PHPPKGSRCDIR}/distinfo",
+               ".endif",
+               ".if defined(USE_PHP_EXT_PATCHES)",
+               "PATCHDIR=       ${.CURDIR}/${PHPPKGSRCDIR}/patches",
+               ".endif")
+       pkg := t.SetupPackage("category/package",
+               "PECL_VERSION=\t1.1.2",
+               ".include \"../../lang/php/ext.mk\"")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputLines()
+}
+
+func (s *Suite) Test_Package_CheckInclude__conditional_and_unconditional_include(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -479,7 +659,7 @@ func (s *Suite) Test_Package_conditional
 }
 
 // See https://github.com/rillig/pkglint/issues/1
-func (s *Suite) Test_Package_includeWithoutExists(c *check.C) {
+func (s *Suite) Test_Package__include_without_exists(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -494,11 +674,11 @@ func (s *Suite) Test_Package_includeWith
        G.checkdirPackage(t.File("category/package"))
 
        t.CheckOutputLines(
-               "ERROR: ~/category/package/options.mk: Cannot be read.")
+               "ERROR: ~/category/package/Makefile:3: Cannot read \"options.mk\".")
 }
 
 // See https://github.com/rillig/pkglint/issues/1
-func (s *Suite) Test_Package_includeAfterExists(c *check.C) {
+func (s *Suite) Test_Package__include_after_exists(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -524,7 +704,7 @@ func (s *Suite) Test_Package_includeAfte
 }
 
 // See https://github.com/rillig/pkglint/issues/1
-func (s *Suite) Test_Package_includeOtherAfterExists(c *check.C) {
+func (s *Suite) Test_Package_readMakefile__include_other_after_exists(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -541,7 +721,7 @@ func (s *Suite) Test_Package_includeOthe
        G.checkdirPackage(t.File("category/package"))
 
        t.CheckOutputLines(
-               "ERROR: ~/category/package/another.mk: Cannot be read.")
+               "ERROR: ~/category/package/Makefile:4: Cannot read \"another.mk\".")
 }
 
 // See https://mail-index.netbsd.org/tech-pkg/2018/07/22/msg020092.html
@@ -650,3 +830,232 @@ func (s *Suite) Test_NewPackage(c *check
                check.PanicMatches,
                `Package directory "category" must be two subdirectories below the pkgsrc root ".*".`)
 }
+
+// Before 2018-09-09, the .CURDIR variable did not have a fallback value.
+// When resolving the relative path x11/gst-x11/${.CURDIR}/../../multimedia/gst-base/distinfo,
+// "gst-x11/${.CURDIR}" was interpreted as "category/package", and the whole
+// path was resolved to "x11/multimedia/gst-base/distinfo, which of course
+// could not be found.
+func (s *Suite) Test__distinfo_from_other_package(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall,no-space")
+       t.SetupPkgsrc()
+       t.Chdir(".")
+       t.CreateFileLines("x11/gst-x11/Makefile",
+               MkRcsID,
+               ".include \"../../multimedia/gst-base/Makefile.common\"",
+               ".include \"../../mk/bsd.pkg.mk\"")
+       t.CreateFileLines("multimedia/gst-base/Makefile.common",
+               MkRcsID,
+               ".include \"plugins.mk\"")
+       t.CreateFileLines("multimedia/gst-base/plugins.mk",
+               MkRcsID,
+               "DISTINFO_FILE=\t${.CURDIR}/../../multimedia/gst-base/distinfo")
+       t.CreateFileLines("multimedia/gst-base/distinfo",
+               RcsID,
+               "",
+               "SHA1 (patch-aa) = 1234")
+
+       G.CheckDirent("x11/gst-x11")
+
+       t.CheckOutputLines(
+               "WARN: x11/gst-x11/Makefile: Neither PLIST nor PLIST.common exist, and PLIST_SRC is unset.",
+               "ERROR: x11/gst-x11/Makefile: Each package must define its LICENSE.",
+               "WARN: x11/gst-x11/Makefile: No COMMENT given.",
+               "WARN: x11/gst-x11/../../multimedia/gst-base/distinfo:3: Patch file \"patch-aa\" does not exist in directory \"../../x11/gst-x11/patches\".")
+}
+
+func (s *Suite) Test_Package_checkfilePackageMakefile__GNU_CONFIGURE(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall,no-space")
+       pkg := t.SetupPackage("category/package",
+               "GNU_CONFIGURE=\tyes",
+               "USE_LANGUAGES=\t#")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:20: GNU_CONFIGURE almost always needs a C compiler, but \"c\" is not added to USE_LANGUAGES in line 21.")
+}
+
+func (s *Suite) Test_Package_checkfilePackageMakefile__GNU_CONFIGURE_ok(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall,no-space")
+       pkg := t.SetupPackage("category/package",
+               "GNU_CONFIGURE=\tyes",
+               "USE_LANGUAGES=\t# none, really")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_Package_checkfilePackageMakefile__REPLACE_PERL(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall,no-space")
+       pkg := t.SetupPackage("category/package",
+               "REPLACE_PERL=\t*.pl",
+               "NO_CONFIGURE=\tyes")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:20: REPLACE_PERL is ignored when NO_CONFIGURE is set (in line 21).")
+}
+
+func (s *Suite) Test_Package_checkfilePackageMakefile__META_PACKAGE_with_distinfo(c *check.C) {
+       t := s.Init(c)
+
+       pkg := t.SetupPackage("category/package",
+               "META_PACKAGE=\tyes")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/distinfo: " +
+                       "This file should not exist if NO_CHECKSUM or META_PACKAGE is set.")
+}
+
+func (s *Suite) Test_Package_checkfilePackageMakefile__USE_IMAKE_and_USE_X11(c *check.C) {
+       t := s.Init(c)
+
+       pkg := t.SetupPackage("category/package",
+               "USE_X11=\tyes",
+               "USE_IMAKE=\tyes")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputLines(
+               "NOTE: ~/category/package/Makefile:21: USE_IMAKE makes USE_X11 in line 20 superfluous.")
+}
+
+func (s *Suite) Test_Package_readMakefile__skipping(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall,no-space")
+       pkg := t.SetupPackage("category/package",
+               ".include \"${MYSQL_PKGSRCDIR:S/-client$/-server/}/buildlink3.mk\"")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputLines(
+               "NOTE: ~/category/package/Makefile:20: " +
+                       "Skipping include file \"${MYSQL_PKGSRCDIR:S/-client$/-server/}/buildlink3.mk\". " +
+                       "This may result in false warnings.")
+}
+
+func (s *Suite) Test_Package_readMakefile__not_found(c *check.C) {
+       t := s.Init(c)
+
+       pkg := t.SetupPackage("category/package",
+               ".include \"../../devel/zlib/buildlink3.mk\"")
+       t.CreateFileLines("devel/zlib/buildlink3.mk",
+               ".include \"../../enoent/enoent/buildlink3.mk\"")
+
+       G.checkdirPackage(pkg)
+
+       t.CheckOutputLines(
+               "ERROR: ~/devel/zlib/buildlink3.mk:1: Cannot read \"../../enoent/enoent/buildlink3.mk\".")
+}
+
+func (s *Suite) Test_Package_readMakefile__relative(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("category/package/extra.mk",
+               MkRcsID)
+       pkg := t.SetupPackage("category/package",
+               ".include \"../package/extra.mk\"")
+
+       G.CheckDirent(pkg)
+
+       // FIXME: One of the below warnings is redundant.
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile:20: References to other packages should look like \"../../category/package\", not \"../package\".",
+               "WARN: ~/category/package/Makefile:20: Invalid relative path \"../package/extra.mk\".")
+}
+
+func (s *Suite) Test_Package_checkLocallyModified(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall,no-order")
+       G.CurrentUsername = "example-user"
+       t.CreateFileLines("category/package/CVS/Entries",
+               "/Makefile//modified//")
+
+       // Since MAINTAINER= pkgsrc-users%NetBSD.org@localhost, everyone may commit changes.
+
+       pkg := t.SetupPackage("category/package")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputEmpty()
+
+       // A package with a MAINTAINER may be edited with care.
+
+       t.CreateFileLines("category/package/Makefile",
+               MkRcsID,
+               "",
+               "DISTNAME=\tdistname-1.0",
+               "CATEGORIES=\tcategory",
+               "MASTER_SITES=\t# none",
+               "",
+               "MAINTAINER=\tmaintainer%example.org@localhost", // Different from default value
+               "HOMEPAGE=\t# none",
+               "COMMENT=\tDummy package",
+               "LICENSE=\t2-clause-bsd",
+               "",
+               ".include \"../../mk/bsd.pkg.mk\"")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputLines(
+               "NOTE: ~/category/package/Makefile: " +
+                       "Please only commit changes that maintainer%example.org@localhost would approve.")
+
+       // A package with an OWNER may NOT be edited by others.
+
+       pkg = t.SetupPackage("category/package",
+               "OWNER=\towner%example.org@localhost")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile: " +
+                       "Don't commit changes to this file without asking the OWNER, owner%example.org@localhost.")
+
+       // ... unless you are the owner, of course.
+
+       G.CurrentUsername = "owner"
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_Pkglint_checkdirPackage__filename_with_variable(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall,no-order")
+       pkg := t.SetupPackage("category/package",
+               ".include \"../../mk/bsd.prefs.mk\"",
+               "",
+               "RUBY_VERSIONS_ACCEPTED=\t22 23 24 25", // As of 2018.
+               ".for rv in ${RUBY_VERSIONS_ACCEPTED}",
+               "RUBY_VER?=\t\t${rv}",
+               ".endfor",
+               "",
+               "RUBY_PKGDIR=\t../../lang/ruby-${RUBY_VER}-base",
+               "DISTINFO_FILE=\t${RUBY_PKGDIR}/distinfo")
+
+       // Pkglint cannot currently resolve the location of DISTINFO_FILE completely
+       // because the variable \"rv\" comes from a .for loop.
+       //
+       // TODO: resolve variables in simple .for loops like the above.
+       G.CheckDirent(pkg)
+
+       t.CheckOutputEmpty()
+}

Index: pkgsrc/pkgtools/pkglint/files/patches_test.go
diff -u pkgsrc/pkgtools/pkglint/files/patches_test.go:1.20 pkgsrc/pkgtools/pkglint/files/patches_test.go:1.21
--- pkgsrc/pkgtools/pkglint/files/patches_test.go:1.20  Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/patches_test.go       Wed Oct  3 22:27:53 2018
@@ -42,7 +42,7 @@ func (s *Suite) Test_ChecklinesPatch__wi
                "-old line",
                "+new line",
                " context after")
-       t.SetupFileLines("distinfo",
+       t.CreateFileLines("distinfo",
                RcsID,
                "",
                // The hash is taken from a breakpoint at the beginning of AutofixDistinfo, oldSha1
@@ -691,8 +691,8 @@ func (s *Suite) Test_PatchChecker_checkt
        ChecklinesPatch(lines)
 
        t.CheckOutputLines(
-               "WARN: ~/patch-aa:7: Found RCS tag \"$Id: patches_test.go,v 1.20 2018/09/05 17:56:22 rillig Exp $\". Please remove it.",
-               "WARN: ~/patch-aa:8: Found RCS tag \"$Id: patches_test.go,v 1.20 2018/09/05 17:56:22 rillig Exp $\". Please remove it by reducing the number of context lines using pkgdiff or \"diff 
-U[210]\".",
+               "WARN: ~/patch-aa:7: Found RCS tag \"$Id: patches_test.go,v 1.21 2018/10/03 22:27:53 rillig Exp $\". Please remove it.",
+               "WARN: ~/patch-aa:8: Found RCS tag \"$Id: patches_test.go,v 1.21 2018/10/03 22:27:53 rillig Exp $\". Please remove it by reducing the number of context lines using pkgdiff or \"diff 
-U[210]\".",
                "WARN: ~/patch-aa:11: Found RCS tag \"$Author: rillig $\". Please remove it by reducing the number of context lines using pkgdiff or \"diff -U[210]\".")
 }
 

Index: pkgsrc/pkgtools/pkglint/files/pkglint_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.24 pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.25
--- pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.24  Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/pkglint_test.go       Wed Oct  3 22:27:53 2018
@@ -3,13 +3,14 @@ package main
 import (
        "io/ioutil"
        "strings"
+       "time"
 
        "gopkg.in/check.v1"
        "netbsd.org/pkglint/trace"
        "os"
 )
 
-func (s *Suite) Test_Pkglint_Main_help(c *check.C) {
+func (s *Suite) Test_Pkglint_Main__help(c *check.C) {
        t := s.Init(c)
 
        exitcode := G.Main("pkglint", "-h")
@@ -18,7 +19,7 @@ func (s *Suite) Test_Pkglint_Main_help(c
        c.Check(t.Output(), check.Matches, `^\Qusage: pkglint [options] dir...\E\n(?s).+`)
 }
 
-func (s *Suite) Test_Pkglint_Main_version(c *check.C) {
+func (s *Suite) Test_Pkglint_Main__version(c *check.C) {
        t := s.Init(c)
 
        exitcode := G.Main("pkglint", "--version")
@@ -28,7 +29,7 @@ func (s *Suite) Test_Pkglint_Main_versio
                confVersion)
 }
 
-func (s *Suite) Test_Pkglint_Main_no_args(c *check.C) {
+func (s *Suite) Test_Pkglint_Main__no_args(c *check.C) {
        t := s.Init(c)
 
        exitcode := G.Main("pkglint")
@@ -115,6 +116,18 @@ func (s *Suite) Test_Pkglint_Main__unkno
                "  (Prefix a flag with \"no-\" to disable it.)")
 }
 
+func (s *Suite) Test_Pkglint_Main__panic(c *check.C) {
+       t := s.Init(c)
+
+       pkg := t.SetupPackage("category/package")
+
+       G.logOut = nil // Force an error that cannot happen in practice.
+
+       c.Check(
+               func() { G.Main("pkglint", pkg) },
+               check.PanicMatches, `(?s).*\bnil pointer\b.*`)
+}
+
 // Demonstrates which infrastructure files are necessary to actually run
 // pkglint in a realistic scenario.
 // For most tests, this setup is too much work, therefore they
@@ -129,7 +142,7 @@ func (s *Suite) Test_Pkglint_Main__compl
 
        // FIXME: pkglint should warn that the latest version in this file
        // (1.10) doesn't match the current version in the package (1.11).
-       t.SetupFileLines("doc/CHANGES-2018",
+       t.CreateFileLines("doc/CHANGES-2018",
                RcsID,
                "",
                "Changes to the packages collection and infrastructure in 2018:",
@@ -137,7 +150,7 @@ func (s *Suite) Test_Pkglint_Main__compl
                "\tUpdated sysutils/checkperms to 1.10 [rillig 2018-01-05]")
 
        // See Pkgsrc.loadSuggestedUpdates.
-       t.SetupFileLines("doc/TODO",
+       t.CreateFileLines("doc/TODO",
                RcsID,
                "",
                "Suggested package updates",
@@ -145,19 +158,19 @@ func (s *Suite) Test_Pkglint_Main__compl
                "\to checkperms-1.13 [supports more file formats]")
 
        // The LICENSE in the package Makefile is searched here.
-       t.SetupFileLines("licenses/bsd-2",
+       t.CreateFileLines("licenses/bsd-2",
                "# dummy")
 
        // The MASTER_SITES in the package Makefile are searched here.
        // See Pkgsrc.loadMasterSites.
-       t.SetupFileMkLines("mk/fetch/sites.mk",
+       t.CreateFileLines("mk/fetch/sites.mk",
                MkRcsID,
                "",
                "MASTER_SITE_GITHUB+=\thttps://github.com/";)
 
        // The existence of this file makes the category "sysutils" valid.
        // The category "tools" on the other hand is not valid.
-       t.SetupFileMkLines("sysutils/Makefile",
+       t.CreateFileLines("sysutils/Makefile",
                MkRcsID)
 
        // The package Makefile is quite simple, containing just the
@@ -165,21 +178,21 @@ func (s *Suite) Test_Pkglint_Main__compl
        // values is partly defined in the pkgsrc infrastructure files
        // (as defined in the previous lines), and partly in the pkglint
        // code directly. Many details can be found in vartypecheck.go.
-       t.SetupFileMkLines("sysutils/checkperms/Makefile",
+       t.CreateFileLines("sysutils/checkperms/Makefile",
                MkRcsID,
                "",
                "DISTNAME=\tcheckperms-1.11",
                "CATEGORIES=\tsysutils tools",
                "MASTER_SITES=\t${MASTER_SITE_GITHUB:=rillig/}",
                "",
-               "MAINTAINER=\tpkgsrc-users%pkgsrc.org@localhost",
+               "MAINTAINER=\tpkgsrc-users%NetBSD.org@localhost",
                "HOMEPAGE=\thttps://github.com/rillig/checkperms/";,
                "COMMENT=\tCheck file permissions",
                "LICENSE=\tbsd-2",
                "",
                ".include \"../../mk/bsd.pkg.mk\"")
 
-       t.SetupFileLines("sysutils/checkperms/MESSAGE",
+       t.CreateFileLines("sysutils/checkperms/MESSAGE",
                "===========================================================================",
                RcsID,
                "",
@@ -187,18 +200,18 @@ func (s *Suite) Test_Pkglint_Main__compl
                "",
                "===========================================================================")
 
-       t.SetupFileLines("sysutils/checkperms/PLIST",
+       t.CreateFileLines("sysutils/checkperms/PLIST",
                PlistRcsID,
                "bin/checkperms",
                "man/man1/checkperms.1")
 
-       t.SetupFileLines("sysutils/checkperms/README",
+       t.CreateFileLines("sysutils/checkperms/README",
                "When updating this package, test the pkgsrc bootstrap.")
 
-       t.SetupFileLines("sysutils/checkperms/TODO",
+       t.CreateFileLines("sysutils/checkperms/TODO",
                "Make the package work on MS-DOS")
 
-       t.SetupFileLines("sysutils/checkperms/patches/patch-checkperms.c",
+       t.CreateFileLines("sysutils/checkperms/patches/patch-checkperms.c",
                RcsID,
                "",
                "A simple patch demonstrating that pkglint checks for missing",
@@ -211,7 +224,7 @@ func (s *Suite) Test_Pkglint_Main__compl
                "+// Header 1",
                "+// Header 2",
                "+// Header 3")
-       t.SetupFileLines("sysutils/checkperms/distinfo",
+       t.CreateFileLines("sysutils/checkperms/distinfo",
                RcsID,
                "",
                "SHA1 (checkperms-1.12.tar.gz) = 34c084b4d06bcd7a8bba922ff57677e651eeced5",
@@ -242,7 +255,7 @@ func (s *Suite) Test_Pkglint_Main__compl
 // pkgsrcdir=...
 // env PKGLINT_TESTCMDLINE="$pkgsrcdir -r" ./pkglint.test -test.coverprofile pkglint.cov
 // go tool cover -html=pkglint.cov -o coverage.html
-func (s *Suite) Test_Pkglint_coverage(c *check.C) {
+func (s *Suite) Test_Pkglint__coverage(c *check.C) {
        cmdline := os.Getenv("PKGLINT_TESTCMDLINE")
        if cmdline != "" {
                G.logOut, G.logErr, trace.Out = NewSeparatorWriter(os.Stdout), NewSeparatorWriter(os.Stderr), os.Stdout
@@ -253,7 +266,7 @@ func (s *Suite) Test_Pkglint_coverage(c 
 func (s *Suite) Test_Pkglint_CheckDirent__outside(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("empty")
+       t.CreateFileLines("empty")
 
        G.CheckDirent(t.File("."))
 
@@ -261,13 +274,55 @@ func (s *Suite) Test_Pkglint_CheckDirent
                "ERROR: ~: Cannot determine the pkgsrc root directory for \"~\".")
 }
 
+func (s *Suite) Test_Pkglint_CheckDirent__empty_directory(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupPkgsrc()
+       t.CreateFileLines("category/package/CVS/Entries")
+
+       G.CheckDirent(t.File("category/package"))
+
+       // Empty directories are silently skipped.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_Pkglint_CheckDirent__files_directory(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupPkgsrc()
+       t.CreateFileLines("category/package/files/README.md")
+
+       G.CheckDirent(t.File("category/package/files"))
+
+       // This diagnostic is not really correct, but it's an edge case anyway.
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/files: Cannot check directories outside a pkgsrc tree.")
+}
+
+func (s *Suite) Test_Pkglint_CheckDirent__manual_patch(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupPkgsrc()
+       t.CreateFileLines("category/package/patches/manual-configure")
+       t.CreateFileLines("category/package/Makefile",
+               MkRcsID)
+
+       G.CheckDirent(t.File("category/package"))
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/Makefile: Neither PLIST nor PLIST.common exist, and PLIST_SRC is unset.",
+               "WARN: ~/category/package/distinfo: File not found. Please run \""+confMake+" makesum\" or define NO_CHECKSUM=yes in the package Makefile.",
+               "ERROR: ~/category/package/Makefile: Each package must define its LICENSE.",
+               "WARN: ~/category/package/Makefile: No COMMENT given.")
+}
+
 func (s *Suite) Test_Pkglint_CheckDirent(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("mk/bsd.pkg.mk")
-       t.SetupFileLines("category/package/Makefile")
-       t.SetupFileLines("category/Makefile")
-       t.SetupFileLines("Makefile")
+       t.CreateFileLines("mk/bsd.pkg.mk")
+       t.CreateFileLines("category/package/Makefile")
+       t.CreateFileLines("category/Makefile")
+       t.CreateFileLines("Makefile")
 
        G.CheckDirent(t.File("."))
 
@@ -434,10 +489,11 @@ func (s *Suite) Test_Pkglint__profiling(
        t := s.Init(c)
 
        t.SetupPkgsrc()
-       G.Main("pkglint", "--profiling", t.File("."))
+       t.Chdir(".")
+
+       G.Main("pkglint", "--profiling")
 
        // Pkglint always writes the profiling data into the current directory.
-       // Luckily, this directory is usually writable.
        c.Check(fileExists("pkglint.pprof"), equals, true)
 
        err := os.Remove("pkglint.pprof")
@@ -447,7 +503,20 @@ func (s *Suite) Test_Pkglint__profiling(
        // or not interesting enough, since that info includes the exact timing
        // that the top time-consuming regular expressions took.
        firstOutput := strings.Split(t.Output(), "\n")[0]
-       c.Check(firstOutput, equals, "ERROR: ~/Makefile: Cannot be read.")
+       c.Check(firstOutput, equals, "ERROR: Makefile: Cannot be read.")
+}
+
+func (s *Suite) Test_Pkglint__profiling_error(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupPkgsrc()
+       t.Chdir(".")
+       t.CreateFileLines("pkglint.pprof/file")
+
+       exitcode := G.Main("pkglint", "--profiling")
+
+       c.Check(exitcode, equals, 1)
+       c.Check(t.Output(), check.Matches, `^FATAL: Cannot create profiling file: open pkglint\.pprof: .*\n$`)
 }
 
 func (s *Suite) Test_Pkglint_Checkfile__in_current_working_directory(c *check.C) {
@@ -594,35 +663,17 @@ func (s *Suite) Test_Pkglint_ToolByVarna
        c.Check(G.ToolByVarname("TOOL", RunTime), equals, global)
 }
 
-func (s *Suite) Test_Pkglint_Checkfile__CheckExtra(c *check.C) {
+func (s *Suite) Test_CheckfileExtra(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Call", "-Wall,no-space")
-       t.SetupPkgsrc()
-       G.Pkgsrc.LoadInfrastructure()
-       t.CreateFileLines("licenses/gnu-gpl-2.0")
-       t.CreateFileLines("category/Makefile")
-       t.CreateFileLines("category/package/Makefile",
-               MkRcsID,
-               "",
-               "DISTNAME=       pkgname-1.0",
-               "CATEGORIES=     category",
-               "",
-               "COMMENT=        Comment",
-               "LICENSE=        gnu-gpl-2.0",
-               "",
-               "NO_CHECKSUM=    yes",
-               "",
-               ".include \"../../mk/bsd.pkg.mk\"")
-       t.CreateFileLines("category/package/PLIST",
-               PlistRcsID,
-               "bin/program")
+       pkg := t.SetupPackage("category/package")
        t.CreateFileLines("category/package/INSTALL",
                "#! /bin/sh")
        t.CreateFileLines("category/package/DEINSTALL",
                "#! /bin/sh")
 
-       G.CheckDirent(t.File("category/package"))
+       G.CheckDirent(pkg)
 
        t.CheckOutputEmpty()
 }
@@ -631,31 +682,13 @@ func (s *Suite) Test_Pkglint_Checkfile__
        t := s.Init(c)
 
        t.SetupCommandLine("-Call", "-Wall,no-space", "--import")
-       t.SetupPkgsrc()
-       G.Pkgsrc.LoadInfrastructure()
-       t.CreateFileLines("licenses/gnu-gpl-2.0")
-       t.CreateFileLines("category/Makefile")
-       t.CreateFileLines("category/package/Makefile",
-               MkRcsID,
-               "",
-               "DISTNAME=       pkgname-1.0",
-               "CATEGORIES=     category",
-               "",
-               "COMMENT=        Comment",
-               "LICENSE=        gnu-gpl-2.0",
-               "",
-               "NO_CHECKSUM=    yes",
-               "",
-               ".include \"../../mk/bsd.pkg.mk\"")
-       t.CreateFileLines("category/package/PLIST",
-               PlistRcsID,
-               "bin/program")
+       pkg := t.SetupPackage("category/package")
        t.CreateFileLines("category/package/work/log")
        t.CreateFileLines("category/package/Makefile~")
        t.CreateFileLines("category/package/Makefile.orig")
        t.CreateFileLines("category/package/Makefile.rej")
 
-       G.CheckDirent(t.File("category/package"))
+       G.CheckDirent(pkg)
 
        t.CheckOutputLines(
                "ERROR: ~/category/package/Makefile.orig: Must be cleaned up before committing the package.",
@@ -775,3 +808,145 @@ func (s *Suite) Test_Pkglint_Checkfile__
                "ERROR: wip/package/TODO: Must be cleaned up before committing the package.",
                "4 errors and 0 warnings found.")
 }
+
+func (s *Suite) Test_Pkglint_Checkfile__unknown_file_in_patches(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileDummyPatch("category/Makefile/patches/index")
+
+       G.Checkfile(t.File("category/Makefile/patches/index"))
+
+       t.CheckOutputLines(
+               "WARN: ~/category/Makefile/patches/index: " +
+                       "Patch files should be named \"patch-\", followed by letters, '-', '_', '.', and digits only.")
+}
+
+func (s *Suite) Test_Pkglint_Checkfile__file_in_files(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("category/package/files/index")
+
+       G.Checkfile(t.File("category/package/files/index"))
+
+       // These files are ignored since they could contain anything.
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_Pkglint_Checkfile__spec(c *check.C) {
+       t := s.Init(c)
+
+       t.CreateFileLines("category/package/spec")
+       t.CreateFileLines("regress/package/spec")
+
+       G.Checkfile(t.File("category/package/spec"))
+       G.Checkfile(t.File("regress/package/spec"))
+
+       t.CheckOutputLines(
+               "WARN: ~/category/package/spec: Only packages in regress/ may have spec files.")
+}
+
+func (s *Suite) Test_Pkglint_checkMode__skipped(c *check.C) {
+       t := s.Init(c)
+
+       G.checkMode("work", os.ModeSymlink)
+       G.checkMode("work.i386", os.ModeSymlink)
+       G.checkMode("work.hostname", os.ModeSymlink)
+       G.checkMode("other", os.ModeSymlink)
+
+       G.checkMode("device", os.ModeDevice)
+
+       t.CheckOutputLines(
+               "WARN: other: Unknown symlink name.",
+               "ERROR: device: Only files and directories are allowed in pkgsrc.")
+}
+
+func (s *Suite) Test_Pkglint_checkdirPackage__ALTERNATIVES(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall,no-space")
+       pkg := t.SetupPackage("category/package")
+       t.CreateFileLines("category/package/ALTERNATIVES",
+               "bin/wrapper bin/wrapper-impl")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputLines(
+               "ERROR: ~/category/package/ALTERNATIVES:1: " +
+                       "Alternative implementation \"bin/wrapper-impl\" must appear in the PLIST.")
+}
+
+func (s *Suite) Test_CheckfileMk__enoent(c *check.C) {
+       t := s.Init(c)
+
+       CheckfileMk(t.File("fname.mk"))
+
+       t.CheckOutputLines(
+               "ERROR: ~/fname.mk: Cannot be read.")
+}
+
+func (s *Suite) Test_Pkglint_checkExecutable(c *check.C) {
+       t := s.Init(c)
+
+       G.checkExecutable(ExecutableFileInfo{t.File("fname.mk")})
+
+       t.CheckOutputLines(
+               "WARN: ~/fname.mk: Should not be executable.")
+
+       t.SetupCommandLine("--autofix")
+
+       G.checkExecutable(ExecutableFileInfo{t.File("fname.mk")})
+
+       // FIXME: The error message "Cannot clear executable bits" is swallowed.
+       t.CheckOutputLines(
+               "AUTOFIX: ~/fname.mk: Clearing executable bits")
+}
+
+func (s *Suite) Test_main(c *check.C) {
+       t := s.Init(c)
+
+       out, err := os.Create(t.CreateFileLines("out"))
+       c.Check(err, check.IsNil)
+
+       pkg := t.SetupPackage("category/package")
+
+       func() {
+               args := os.Args
+               stdout := os.Stdout
+               stderr := os.Stderr
+               prevExit := exit
+               defer func() {
+                       os.Stderr = stderr
+                       os.Stdout = stdout
+                       os.Args = args
+                       exit = prevExit
+               }()
+               os.Args = []string{"pkglint", pkg}
+               os.Stdout = out
+               os.Stderr = out
+               exit = func(code int) {
+                       c.Check(code, equals, 0)
+               }
+
+               main()
+       }()
+
+       err = out.Close()
+       c.Check(err, check.IsNil)
+
+       t.CheckOutputEmpty()
+       t.CheckFileLines("out",
+               "Looks fine.")
+}
+
+// ExecutableFileInfo mocks a FileInfo because on Windows,
+// regular files don't have the executable bit.
+type ExecutableFileInfo struct {
+       name string
+}
+
+func (i ExecutableFileInfo) Name() string       { return i.name }
+func (i ExecutableFileInfo) Size() int64        { return 13 }
+func (i ExecutableFileInfo) Mode() os.FileMode  { return 0777 }
+func (i ExecutableFileInfo) ModTime() time.Time { return time.Unix(0, 0) }
+func (i ExecutableFileInfo) IsDir() bool        { return false }
+func (i ExecutableFileInfo) Sys() interface{}   { return nil }

Index: pkgsrc/pkgtools/pkglint/files/plist.go
diff -u pkgsrc/pkgtools/pkglint/files/plist.go:1.28 pkgsrc/pkgtools/pkglint/files/plist.go:1.29
--- pkgsrc/pkgtools/pkglint/files/plist.go:1.28 Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/plist.go      Wed Oct  3 22:27:53 2018
@@ -1,7 +1,6 @@
 package main
 
 import (
-       "netbsd.org/pkglint/regex"
        "netbsd.org/pkglint/trace"
        "path"
        "sort"
@@ -300,7 +299,7 @@ func (ck *PlistChecker) checkpathLib(pli
 }
 
 func (ck *PlistChecker) checkpathMan(pline *PlistLine) {
-       m, catOrMan, section, manpage, ext, gz := regex.Match5(pline.text, `^man/(cat|man)(\w+)/(.*?)\.(\w+)(\.gz)?$`)
+       m, catOrMan, section, manpage, ext, gz := match5(pline.text, `^man/(cat|man)(\w+)/(.*?)\.(\w+)(\.gz)?$`)
        if !m {
                // maybe: line.Warnf("Invalid filename %q for manual page.", text)
                return
Index: pkgsrc/pkgtools/pkglint/files/util.go
diff -u pkgsrc/pkgtools/pkglint/files/util.go:1.28 pkgsrc/pkgtools/pkglint/files/util.go:1.29
--- pkgsrc/pkgtools/pkglint/files/util.go:1.28  Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/util.go       Wed Oct  3 22:27:53 2018
@@ -13,6 +13,7 @@ import (
        "strconv"
        "strings"
        "time"
+       "unicode"
 )
 
 type YesNoUnknown uint8
@@ -38,19 +39,32 @@ func hasSuffix(s, suffix string) bool {
        return strings.HasSuffix(s, suffix)
 }
 func matches(s string, re regex.Pattern) bool {
-       return regex.Matches(s, re)
+       return G.res.Matches(s, re)
 }
 func match1(s string, re regex.Pattern) (matched bool, m1 string) {
-       return regex.Match1(s, re)
+       return G.res.Match1(s, re)
 }
 func match2(s string, re regex.Pattern) (matched bool, m1, m2 string) {
-       return regex.Match2(s, re)
+       return G.res.Match2(s, re)
 }
 func match3(s string, re regex.Pattern) (matched bool, m1, m2, m3 string) {
-       return regex.Match3(s, re)
+       return G.res.Match3(s, re)
 }
 func match4(s string, re regex.Pattern) (matched bool, m1, m2, m3, m4 string) {
-       return regex.Match4(s, re)
+       return G.res.Match4(s, re)
+}
+func match5(s string, re regex.Pattern) (matched bool, m1, m2, m3, m4, m5 string) {
+       return G.res.Match5(s, re)
+}
+func replaceFirst(s string, re regex.Pattern, repl string) string {
+       m, replaced := G.res.ReplaceFirst(s, re, repl)
+       return ifelseStr(m != nil, replaced, s)
+}
+func replaceAll(s string, re regex.Pattern, repl string) string {
+       return G.res.Compile(re).ReplaceAllString(s, repl)
+}
+func replaceAllFunc(s string, re regex.Pattern, repl func(string) string) string {
+       return G.res.Compile(re).ReplaceAllStringFunc(s, repl)
 }
 
 func ifelseStr(cond bool, a, b string) string {
@@ -77,7 +91,7 @@ func imax(a, b int) int {
 }
 
 func mustMatch(s string, re regex.Pattern) []string {
-       if m := regex.Match(s, re); m != nil {
+       if m := G.res.Match(s, re); m != nil {
                return m
        }
        panic(fmt.Sprintf("mustMatch %q %q", s, re))
@@ -138,12 +152,9 @@ func isCommitted(fname string) bool {
 }
 
 func isLocallyModified(fname string) bool {
-       lines := loadCvsEntries(fname)
-       if len(lines) == 0 {
-               return false
-       }
-
        baseName := path.Base(fname)
+
+       lines := loadCvsEntries(fname)
        for _, line := range lines {
                fields := strings.Split(line.Text, "/")
                if 3 < len(fields) && fields[1] == baseName {
@@ -156,7 +167,7 @@ func isLocallyModified(fname string) boo
                        cvsModTime := fields[3]
                        fsModTime := st.ModTime().Format(time.ANSIC)
                        if trace.Tracing {
-                               trace.Stepf("cvs.time=%q fs.time=%q", cvsModTime, st.ModTime())
+                               trace.Stepf("cvs.time=%q fs.time=%q", cvsModTime, fsModTime)
                        }
 
                        return cvsModTime != fsModTime
@@ -220,21 +231,21 @@ func shorten(s string, maxChars int) str
 
 func varnameBase(varname string) string {
        dot := strings.IndexByte(varname, '.')
-       if dot != -1 {
+       if dot > 0 {
                return varname[:dot]
        }
        return varname
 }
 func varnameCanon(varname string) string {
        dot := strings.IndexByte(varname, '.')
-       if dot != -1 {
+       if dot > 0 {
                return varname[:dot] + ".*"
        }
        return varname
 }
 func varnameParam(varname string) string {
        dot := strings.IndexByte(varname, '.')
-       if dot != -1 {
+       if dot > 0 {
                return varname[dot+1:]
        }
        return ""
@@ -267,7 +278,28 @@ func varIsUsed(varname string) bool {
 }
 
 func splitOnSpace(s string) []string {
-       return regex.Compile(`\S+`).FindAllString(s, -1)
+       i := 0
+       n := len(s)
+
+       for i < n && unicode.IsSpace(rune(s[i])) {
+               i++
+       }
+
+       var parts []string
+       for i < n {
+               start := i
+               for i < n && !unicode.IsSpace(rune(s[i])) {
+                       i++
+               }
+               if start != i {
+                       parts = append(parts, s[start:i])
+               }
+               for i < n && unicode.IsSpace(rune(s[i])) {
+                       i++
+               }
+       }
+
+       return parts
 }
 
 func fileExists(fname string) bool {
@@ -317,7 +349,7 @@ func mkopSubst(s string, left bool, from
        re := regex.Pattern(ifelseStr(left, "^", "") + regexp.QuoteMeta(from) + ifelseStr(right, "$", ""))
        done := false
        gflag := contains(flags, "g")
-       return regex.Compile(re).ReplaceAllStringFunc(s, func(match string) string {
+       return replaceAllFunc(s, re, func(match string) string {
                if gflag || !done {
                        done = !gflag
                        return to
@@ -328,13 +360,10 @@ func mkopSubst(s string, left bool, from
 
 // relpath returns the relative path from `from` to `to`.
 func relpath(from, to string) string {
-       absFrom, err1 := filepath.Abs(from)
-       absTo, err2 := filepath.Abs(to)
-       rel, err3 := filepath.Rel(absFrom, absTo)
-       if err1 != nil || err2 != nil || err3 != nil {
-               trace.Stepf("relpath.panic", from, to, err1, err2, err3)
-               panic(fmt.Sprintf("relpath %q, %q", from, to))
-       }
+       absFrom := abspath(from)
+       absTo := abspath(to)
+       rel, err := filepath.Rel(absFrom, absTo)
+       G.Assertf(err == nil, "relpath %q %q.", from, to)
        result := filepath.ToSlash(rel)
        if trace.Tracing {
                trace.Stepf("relpath from %q to %q = %q", from, to, result)
@@ -344,9 +373,7 @@ func relpath(from, to string) string {
 
 func abspath(fname string) string {
        abs, err := filepath.Abs(fname)
-       if err != nil {
-               NewLineWhole(fname).Fatalf("Cannot determine absolute path.")
-       }
+       G.Assertf(err == nil, "abspath %q.", fname)
        return filepath.ToSlash(abs)
 }
 
@@ -367,7 +394,24 @@ func cleanpath(fname string) string {
        for contains(tmp, "//") {
                tmp = strings.Replace(tmp, "//", "/", -1)
        }
-       tmp = reReplaceRepeatedly(tmp, `/[^.][^/]*/[^.][^/]*/\.\./\.\./`, "/")
+
+       // Repeatedly replace `/[^.][^/]*/[^.][^/]*/\.\./\.\./` with "/"
+again:
+       slash0 := -1
+       slash1 := -1
+       slash2 := -1
+       for i, ch := range []byte(tmp) {
+               if ch == '/' {
+                       slash0 = slash1
+                       slash1 = slash2
+                       slash2 = i
+                       if slash0 != -1 && tmp[slash0+1:slash1] != ".." && tmp[slash1+1:slash2] != ".." && hasPrefix(tmp[i:], "/../../") {
+                               tmp = tmp[:slash0] + tmp[i+6:]
+                               goto again
+                       }
+               }
+       }
+
        tmp = strings.TrimSuffix(tmp, "/")
        return tmp
 }
@@ -376,14 +420,6 @@ func containsVarRef(s string) bool {
        return contains(s, "${")
 }
 
-func reReplaceRepeatedly(from string, re regex.Pattern, to string) string {
-       replaced := regex.Compile(re).ReplaceAllString(from, to)
-       if replaced != from {
-               return reReplaceRepeatedly(replaced, re, to)
-       }
-       return replaced
-}
-
 func hasAlnumPrefix(s string) bool {
        if s == "" {
                return false
@@ -412,12 +448,13 @@ func (o *Once) FirstTime(what string) bo
 // Scope remembers which variables are defined and which are used
 // in a certain scope, such as a package or a file.
 type Scope struct {
-       defined map[string]MkLine
-       used    map[string]MkLine
+       defined  map[string]MkLine
+       fallback map[string]string
+       used     map[string]MkLine
 }
 
 func NewScope() Scope {
-       return Scope{make(map[string]MkLine), make(map[string]MkLine)}
+       return Scope{make(map[string]MkLine), make(map[string]string), make(map[string]MkLine)}
 }
 
 // Define marks the variable and its canonicalized form as defined.
@@ -437,6 +474,10 @@ func (s *Scope) Define(varname string, m
        }
 }
 
+func (s *Scope) Fallback(varname string, value string) {
+       s.fallback[varname] = value
+}
+
 // Use marks the variable and its canonicalized form as used.
 func (s *Scope) Use(varname string, line MkLine) {
        if s.used[varname] == nil {
@@ -514,12 +555,21 @@ func (s *Scope) Value(varname string) (v
        if mkline != nil {
                return mkline.Value(), true
        }
+       if fallback, ok := s.fallback[varname]; ok {
+               return fallback, true
+       }
        return "", false
 }
 
 func (s *Scope) DefineAll(other Scope) {
-       for varname, mkline := range other.defined {
-               s.Define(varname, mkline)
+       var varnames []string
+       for varname := range other.defined {
+               varnames = append(varnames, varname)
+       }
+       sort.Strings(varnames)
+
+       for _, varname := range varnames {
+               s.Define(varname, other.defined[varname])
        }
 }
 
@@ -690,3 +740,102 @@ func IsPrefs(fileName string) bool {
        }
        return false
 }
+
+func isalnum(s string) bool {
+       for _, ch := range []byte(s) {
+               if !('0' <= ch && ch <= '9' || 'A' <= ch && ch <= 'Z' || ch == '_' || 'a' <= ch && ch <= 'z') {
+                       return false
+               }
+       }
+       return true
+}
+
+// FileCache reduces the IO load for commonly loaded files by about 50%,
+// especially for buildlink3.mk and *.buildlink3.mk files.
+type FileCache struct {
+       table   []*fileCacheEntry
+       mapping map[string]*fileCacheEntry // Pointers into FileCache.table
+       hits    int
+       misses  int
+}
+
+type fileCacheEntry struct {
+       count    int
+       fileName string
+       options  LoadOptions
+       lines    []Line
+}
+
+func NewFileCache(size int) *FileCache {
+       return &FileCache{
+               make([]*fileCacheEntry, 0, size),
+               make(map[string]*fileCacheEntry),
+               0,
+               0}
+}
+
+func (c *FileCache) Put(fileName string, options LoadOptions, lines []Line) {
+       key := c.key(fileName)
+
+       entry := c.mapping[key]
+       if entry == nil {
+               if len(c.table) == cap(c.table) {
+                       sort.Slice(c.table, func(i, j int) bool { return c.table[j].count < c.table[i].count })
+                       minCount := c.table[len(c.table)-1].count
+                       newLen := len(c.table)
+                       for newLen > 0 && c.table[newLen-1].count == minCount {
+                               delete(c.mapping, c.key(c.table[newLen-1].fileName))
+                               newLen--
+                       }
+                       c.table = c.table[0:newLen]
+
+                       // To avoid files from getting stuck in the cache.
+                       for _, e := range c.table {
+                               e.count /= 2
+                       }
+               }
+
+               entry = &fileCacheEntry{0, fileName, options, lines}
+               c.table = append(c.table, entry)
+               c.mapping[key] = entry
+       }
+
+       entry.count = 0
+       entry.fileName = fileName
+       entry.options = options
+       entry.lines = lines
+}
+
+func (c *FileCache) Get(fileName string, options LoadOptions) []Line {
+       key := c.key(fileName)
+       entry, found := c.mapping[key]
+       if found && entry.options == options {
+               c.hits++
+               entry.count++
+
+               lines := make([]Line, len(entry.lines))
+               for i, line := range entry.lines {
+                       lines[i] = NewLineMulti(fileName, int(line.firstLine), int(line.lastLine), line.Text, line.raw)
+               }
+               return lines
+       }
+       c.misses++
+       return nil
+}
+
+func (c *FileCache) Evict(fileName string) {
+       key := c.key(fileName)
+       entry, found := c.mapping[key]
+       if found {
+               delete(c.mapping, key)
+
+               sort.Slice(c.table, func(i, j int) bool {
+                       return c.table[j] == entry && c.table[i] != entry
+               })
+               c.table = c.table[0 : len(c.table)-1]
+       }
+}
+
+func (c *FileCache) key(fileName string) string {
+       return path.Clean(fileName)
+}

Index: pkgsrc/pkgtools/pkglint/files/shell_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shell_test.go:1.30 pkgsrc/pkgtools/pkglint/files/shell_test.go:1.31
--- pkgsrc/pkgtools/pkglint/files/shell_test.go:1.30    Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/shell_test.go Wed Oct  3 22:27:53 2018
@@ -2,7 +2,6 @@ package main
 
 import (
        "gopkg.in/check.v1"
-       "netbsd.org/pkglint/textproc"
 )
 
 func (s *Suite) Test_splitIntoShellTokens__line_continuation(c *check.C) {
@@ -27,7 +26,7 @@ func (s *Suite) Test_splitIntoShellToken
 func (s *Suite) Test_splitIntoShellTokens__dollar_subshell(c *check.C) {
        words, rest := splitIntoShellTokens(dummyLine, "id=$$(${AWK} '{print}' < ${WRKSRC}/idfile) && echo \"$$id\"")
 
-       c.Check(words, deepEquals, []string{"id=", "$$(", "${AWK}", "'{print}'", "<", "${WRKSRC}/idfile", ")", "&&", "echo", "\"$$id\""})
+       c.Check(words, deepEquals, []string{"id=$$(${AWK} '{print}' < ${WRKSRC}/idfile)", "&&", "echo", "\"$$id\""})
        c.Check(rest, equals, "")
 }
 
@@ -138,6 +137,10 @@ func (s *Suite) Test_ShellLine_CheckShel
 
        t.SetupCommandLine("-Wall")
        t.SetupVartypes()
+       t.SetupToolUsable("awk", "AWK")
+       t.SetupToolUsable("cp", "CP")
+       t.SetupToolUsable("mkdir", "MKDIR") // This is actually "mkdir -p".
+       t.SetupToolUsable("unzip", "UNZIP_CMD")
 
        checkShellCommandLine := func(shellCommand string) {
                G.Mk = t.NewMkLines("fname",
@@ -157,6 +160,7 @@ func (s *Suite) Test_ShellLine_CheckShel
 
        t.CheckOutputLines(
                "WARN: fname:1: Unknown shell command \"uname\".",
+               "WARN: fname:1: Please switch to \"set -e\" mode before using a semicolon (after \"uname=`uname`\") to separate commands.",
                "WARN: fname:1: Unknown shell command \"echo\".",
                "WARN: fname:1: Unknown shell command \"echo\".")
 
@@ -216,9 +220,7 @@ func (s *Suite) Test_ShellLine_CheckShel
        checkShellCommandLine("${RUN} subdir=\"`unzip -c \"$$e\" install.rdf | awk '/re/ { print \"hello\" }'`\"")
 
        t.CheckOutputLines(
-               "WARN: fname:1: The exitcode of \"unzip\" at the left of the | operator is ignored.",
-               "WARN: fname:1: Unknown shell command \"unzip\".",
-               "WARN: fname:1: Unknown shell command \"awk\".")
+               "WARN: fname:1: The exitcode of \"unzip\" at the left of the | operator is ignored.")
 
        // From mail/thunderbird/Makefile, rev. 1.159
        checkShellCommandLine("" +
@@ -232,12 +234,7 @@ func (s *Suite) Test_ShellLine_CheckShel
 
        t.CheckOutputLines(
                "WARN: fname:1: XPI_FILES is used but not defined.",
-               "WARN: fname:1: The exitcode of \"${UNZIP_CMD}\" at the left of the | operator is ignored.",
-               "WARN: fname:1: UNZIP_CMD is used but not defined.",
-               "WARN: fname:1: Unknown shell command \"awk\".",
-               "WARN: fname:1: Unknown shell command \"${MKDIR}\".",
-               "WARN: fname:1: MKDIR is used but not defined.",
-               "WARN: fname:1: UNZIP_CMD is used but not defined.")
+               "WARN: fname:1: The exitcode of \"${UNZIP_CMD}\" at the left of the | operator is ignored.")
 
        // From x11/wxGTK28/Makefile
        checkShellCommandLine("" +
@@ -255,8 +252,18 @@ func (s *Suite) Test_ShellLine_CheckShel
        checkShellCommandLine("@cp from to")
 
        t.CheckOutputLines(
-               "WARN: fname:1: The shell command \"cp\" should not be hidden.",
-               "WARN: fname:1: Unknown shell command \"cp\".")
+               "WARN: fname:1: The shell command \"cp\" should not be hidden.")
+
+       checkShellCommandLine("-cp from to")
+
+       t.CheckOutputLines(
+               "WARN: fname:1: Using a leading \"-\" to suppress errors is deprecated.")
+
+       checkShellCommandLine("-${MKDIR} deeply/nested/subdir")
+
+       t.CheckOutputLines(
+               "NOTE: fname:1: You don't need to use \"-\" before \"${MKDIR} deeply/nested/subdir\".",
+               "WARN: fname:1: Using a leading \"-\" to suppress errors is deprecated.")
 
        G.Pkg = NewPackage(t.File("category/pkgbase"))
        G.Pkg.PlistDirs["share/pkgbase"] = true
@@ -283,7 +290,7 @@ func (s *Suite) Test_ShellLine_CheckShel
        t.CheckOutputEmpty() // No warning about missing error checking.
 }
 
-func (s *Suite) Test_ShellLine_CheckShellCommandLine_strip(c *check.C) {
+func (s *Suite) Test_ShellLine_CheckShellCommandLine__strip(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -344,8 +351,7 @@ func (s *Suite) Test_ShellLine_CheckShel
                "AUTOFIX: Makefile:1: Replacing \"${PKGNAME:Q}\" with \"${PKGNAME}\".")
 }
 
-// Simple commands like echo(1) or printf(1) are assumed to never fail.
-func (s *Suite) Test_ShellLine_CheckShellCommandLine__exitcode(c *check.C) {
+func (s *Suite) Test_ShellProgramChecker_checkPipeExitcode(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wall")
@@ -363,7 +369,10 @@ func (s *Suite) Test_ShellLine_CheckShel
                "\t cat | echo | right-side",
                "\t echo | cat | right-side",
                "\t sed s,s,s, filename | right-side",
-               "\t sed s,s,s < input | right-side")
+               "\t sed s,s,s < input | right-side",
+               "\t ./unknown | right-side",
+               "\t var=value | right-side",
+               "\t if :; then :; fi | right-side")
 
        for _, mkline := range G.Mk.mklines {
                shline := NewShellLine(mkline)
@@ -375,7 +384,9 @@ func (s *Suite) Test_ShellLine_CheckShel
                "WARN: Makefile:5: The exitcode of \"cat\" at the left of the | operator is ignored.",
                "WARN: Makefile:6: The exitcode of \"cat\" at the left of the | operator is ignored.",
                "WARN: Makefile:7: The exitcode of \"sed\" at the left of the | operator is ignored.",
-               "WARN: Makefile:8: The exitcode of \"sed\" at the left of the | operator is ignored.")
+               "WARN: Makefile:8: The exitcode of \"sed\" at the left of the | operator is ignored.",
+               "WARN: Makefile:9: The exitcode of \"./unknown\" at the left of the | operator is ignored.",
+               "WARN: Makefile:11: The exitcode of the command at the left of the | operator is ignored.")
 }
 
 func (s *Suite) Test_ShellLine_CheckShellCommandLine__autofix(c *check.C) {
@@ -423,7 +434,7 @@ func (s *Suite) Test_ShellLine_CheckShel
                "WARN: fname:1: Unknown shell command \"echo\".")
 }
 
-func (s *Suite) Test_ShellLine_CheckShelltext__dollar_without_variable(c *check.C) {
+func (s *Suite) Test_ShellLine_CheckShellCommandLine__dollar_without_variable(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -511,6 +522,44 @@ func (s *Suite) Test_ShellLine_CheckWord
        t.CheckOutputEmpty()
 }
 
+func (s *Suite) Test_ShellLine_CheckWord__backslash_plus(c *check.C) {
+       t := s.Init(c)
+
+       shline := t.NewShellLine("fname", 1, "\tfind . -exec rm -rf {} \\+")
+
+       shline.CheckShellCommandLine(shline.mkline.ShellCommand())
+
+       // FIXME: A backslash before any other character than "\` keeps its original meaning.
+       t.CheckOutputLines(
+               "WARN: fname:1: Pkglint parse error in ShellLine.CheckWord at \"\\\\+\" (quoting=plain), rest: \\+")
+}
+
+func (s *Suite) Test_ShellLine_CheckWord__squot_dollar(c *check.C) {
+       t := s.Init(c)
+
+       shline := t.NewShellLine("fname", 1, "\t'$")
+
+       shline.CheckWord(shline.mkline.ShellCommand(), false, RunTime)
+
+       // FIXME: Should be parsed correctly. Make passes the dollar through (probably),
+       // and the shell parser should complain about the unfinished string literal.
+       t.CheckOutputLines(
+               "WARN: fname:1: Pkglint parse error in ShellLine.CheckWord at \"'$\" (quoting=s), rest: $")
+}
+
+func (s *Suite) Test_ShellLine_CheckWord__dquot_dollar(c *check.C) {
+       t := s.Init(c)
+
+       shline := t.NewShellLine("fname", 1, "\t\"$")
+
+       shline.CheckWord(shline.mkline.ShellCommand(), false, RunTime)
+
+       // FIXME: Should be parsed correctly. Make passes the dollar through (probably),
+       // and the shell parser should complain about the unfinished string literal.
+       t.CheckOutputLines(
+               "WARN: fname:1: Pkglint parse error in ShellLine.CheckWord at \"\\\"$\" (quoting=d), rest: $")
+}
+
 func (s *Suite) Test_ShellLine_CheckWord__dollar_subshell(c *check.C) {
        t := s.Init(c)
 
@@ -522,6 +571,67 @@ func (s *Suite) Test_ShellLine_CheckWord
                "WARN: fname:1: Invoking subshells via $(...) is not portable enough.")
 }
 
+func (s *Suite) Test_ShellLine_unescapeBackticks__unfinished(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall")
+       mklines := t.NewMkLines("fname.mk",
+               MkRcsID,
+               "",
+               "pre-configure:",
+               "\t`${VAR}",      // Error in first shell word
+               "\techo `${VAR}") // Error after first shell word
+
+       // Breakpoint in ShellLine.CheckShellCommand
+       // Breakpoint in ShellLine.CheckToken
+       // Breakpoint in ShellLine.unescapeBackticks
+       mklines.Check()
+
+       // FIXME: Mention the unfinished backquote.
+       t.CheckOutputLines(
+               "WARN: fname.mk:4: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}",
+               "WARN: fname.mk:5: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"echo\"}")
+}
+
+func (s *Suite) Test_ShellLine_unescapeBackticks__unfinished_direct(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall")
+
+       // This call is unrealistic. It doesn't happen in practice, and this
+       // direct, forcing test is only to reach the code coverage.
+       NewShellLine(dummyMkLine).unescapeBackticks(
+               "dummy",
+               G.NewPrefixReplacer(""),
+               shqBackt)
+
+       t.CheckOutputLines(
+               "ERROR: Unfinished backquotes: ")
+}
+
+func (s *Suite) Test_ShellLine_variableNeedsQuoting(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall")
+       t.SetupVartypes()
+       t.SetupToolUsable("cp", "")
+       mklines := t.NewMkLines("fname.mk",
+               MkRcsID,
+               "",
+               // It's a bit silly to use shell variables in CONFIGURE_ARGS,
+               // but currently that's the only way to run ShellLine.variableNeedsQuoting.
+               "CONFIGURE_ARGS+=\t; cp $$dir $$\\# $$target",
+               "pre-configure:",
+               "\tcp $$dir $$\\# $$target")
+
+       mklines.Check()
+
+       // Quoting check is currently disabled for real shell commands.
+       // See ShellLine.CheckShellCommand, spc.checkWord.
+       t.CheckOutputLines(
+               "WARN: fname.mk:3: Unquoted shell variable \"target\".")
+}
+
 func (s *Suite) Test_ShellLine_CheckShellCommandLine__echo(c *check.C) {
        t := s.Init(c)
 
@@ -689,7 +799,7 @@ func (s *Suite) Test_ShellLine_unescapeB
        shline := t.NewShellLine("dummy.mk", 13, "# dummy")
        // foobar="`echo \"foo   bar\" "\ " "three"`"
        text := "foobar=\"`echo \\\"foo   bar\\\" \"\\ \" \"three\"`\""
-       repl := textproc.NewPrefixReplacer(text)
+       repl := G.NewPrefixReplacer(text)
        repl.AdvanceStr("foobar=\"`")
 
        backtCommand, newQuoting := shline.unescapeBackticks(text, repl, shqDquotBackt)
@@ -761,6 +871,64 @@ func (s *Suite) Test_ShellLine_CheckShel
                "WARN: Makefile:3: Found absolute pathname: /etc/passwd")
 }
 
+func (s *Suite) Test_ShellLine_CheckShellCommand__subshell(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("Makefile",
+               MkRcsID,
+               "",
+               "pre-configure:",
+               "\t@(echo ok)",
+               "\techo $$(uname -r); echo $$(expr 4 '*' $$(echo 1024))",
+               "\t@(echo nb$$(uname -r) $$(${EXPR} 4 \\* $$(echo 1024)))")
+
+       mklines.Check()
+
+       // FIXME: Fix the parse errors (nested subshells).
+       // FIXME: Fix the duplicate diagnostic in line 6.
+       // FIXME: "(" is not a shell command, it's an operator.
+       t.CheckOutputLines(
+               "WARN: Makefile:4: The shell command \"(\" should not be hidden.",
+               "WARN: Makefile:5: Pkglint parse error in ShTokenizer.ShAtom at \"$$(echo 1024))\" (quoting=S).",
+               "WARN: Makefile:5: Invoking subshells via $(...) is not portable enough.",
+               "WARN: Makefile:6: Pkglint parse error in ShTokenizer.ShAtom at \"$$(echo 1024)))\" (quoting=S).",
+               "WARN: Makefile:6: The shell command \"(\" should not be hidden.",
+               "WARN: Makefile:6: Pkglint parse error in ShTokenizer.ShAtom at \"$$(echo 1024)))\" (quoting=S).",
+               "WARN: Makefile:6: Invoking subshells via $(...) is not portable enough.")
+}
+
+func (s *Suite) Test_ShellLine_CheckShellCommand__case_patterns_from_variable(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("Makefile",
+               MkRcsID,
+               "",
+               "pre-configure:",
+               "\tcase $$file in ${CHECK_PERMS_SKIP:@pattern@${pattern}) ;;@} *) continue; esac")
+
+       mklines.Check()
+
+       // FIXME: Support the above variable expansion.
+       t.CheckOutputLines(
+               "WARN: Makefile:4: Pkglint ShellLine.CheckShellCommand: " +
+                       "parse error at []string{\"*\", \")\", \"continue\", \";\", \"esac\"}")
+}
+
+func (s *Suite) Test_ShellLine_checkHiddenAndSuppress(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("Makefile",
+               MkRcsID,
+               "",
+               "show-all-targets: .PHONY",
+               "\t@echo 'hello'",
+               "\t@ls -l")
+
+       mklines.Check()
+
+       t.CheckOutputEmpty()
+}
+
 func (s *Suite) Test_SimpleCommandChecker_handleForbiddenCommand(c *check.C) {
        t := s.Init(c)
 
@@ -779,6 +947,66 @@ func (s *Suite) Test_SimpleCommandChecke
                "ERROR: Makefile:3: \"truss\" must not be used in Makefiles.")
 }
 
+func (s *Suite) Test_SimpleCommandChecker_handleCommandVariable(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupToolUsable("perl", "PERL5")
+       t.SetupTool("perl6", "PERL6")
+       mklines := t.NewMkLines("Makefile",
+               MkRcsID,
+               "",
+               "PERL5_VARS_CMD=\t${PERL5:Q}",
+               "PERL5_VARS_CMD=\t${PERL6:Q}")
+
+       mklines.Check()
+
+       // FIXME: Warn about using _PERL5_VARS because it starts with an underscore.
+       // FIXME: Omit the redundant PERL5_VARS_CMD warning in line 4.
+       // FIXME: In PERL5:Q and PERL6:Q, the :Q is wrong.
+       t.CheckOutputLines(
+               "WARN: Makefile:3: PERL5_VARS_CMD is defined but not used.",
+               "WARN: Makefile:4: The \"perl6\" tool is used but not added to USE_TOOLS.",
+               "WARN: Makefile:4: PERL5_VARS_CMD is defined but not used.")
+}
+
+// The package Makefile and other .mk files in a package directory
+// may use any shell commands defined by any included files.
+// But only if the package is checked as a whole.
+//
+// On the contrary, when pkglint checks a single .mk file, these
+// commands are not guaranteed to be defined, not even when the
+// .mk file includes the file defining the command.
+// FIXME: This paragraph sounds wrong. All commands from included files should be valid.
+//
+// The variable must not be called *_CMD, or another code path is taken.
+func (s *Suite) Test_SimpleCommandChecker_handleCommandVariable__from_package(c *check.C) {
+       t := s.Init(c)
+
+       pkg := t.SetupPackage("category/package",
+               "post-install:",
+               "\t${PYTHON_BIN}",
+               "",
+               ".include \"extra.mk\"")
+       t.CreateFileLines("category/package/extra.mk",
+               MkRcsID,
+               "PYTHON_BIN= my_cmd")
+
+       G.CheckDirent(pkg)
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_SimpleCommandChecker_handleComment(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("file.mk", 3, "\t# comment; continuation")
+
+       MkLineChecker{mkline}.Check()
+
+       t.CheckOutputLines(
+               "WARN: file.mk:3: A shell comment should not contain semicolons.")
+}
+
 func (s *Suite) Test_SimpleCommandChecker_checkPaxPe(c *check.C) {
        t := s.Init(c)
 
@@ -813,16 +1041,122 @@ func (s *Suite) Test_SimpleCommandChecke
                "WARN: Makefile:4: Please use ${ECHO_N} instead of \"echo -n\".")
 }
 
-func (s *Suite) Test_SimpleCommandChecker_checkConditionalCd(c *check.C) {
+func (s *Suite) Test_ShellProgramChecker_checkConditionalCd(c *check.C) {
        t := s.Init(c)
 
        mklines := t.NewMkLines("Makefile",
                MkRcsID,
                "pre-configure:",
-               "\t${RUN} while cd ..; do printf .; done")
+               "\t${RUN} while cd ..; do printf .; done",
+               // TODO: "\t${RUN} if ls | tr -d $$; then :; fi",
+               "\t${RUN} if ls | tr -d shell$$; then :; fi")
+
+       mklines.Check()
+
+       // FIXME: Fix the parse error.
+       t.CheckOutputLines(
+               "ERROR: Makefile:3: The Solaris /bin/sh cannot handle \"cd\" inside conditionals.",
+               "WARN: Pkglint parse error in ShTokenizer.ShAtom at \"$$\" (quoting=plain).")
+}
+
+func (s *Suite) Test_SimpleCommandChecker_checkRegexReplace(c *check.C) {
+       t := s.Init(c)
+
+       mklines := t.NewMkLines("Makefile",
+               MkRcsID,
+               "pre-configure:",
+               "\t${PAX} -s s,.*,, src dst",
+               "\tpax -s s,.*,, src dst",
+               "\t${SED} -e s,.*,, src dst",
+               "\tsed -e s,.*,, src dst",
+               "\tpax -s s,\\.orig,, src dst",
+               "\tsed -e s,a,b,g src dst")
+
+       mklines.Check()
+
+       // FIXME: warn for "pax -s".
+       // FIXME: warn for "sed -e".
+       // TODO: don't warn for "pax .orig".
+       // TODO: don't warn for "s,a,b,g".
+       t.CheckOutputLines(
+               "WARN: Makefile:3: Substitution commands like \"s,.*,,\" should always be quoted.",
+               "WARN: Makefile:5: Substitution commands like \"s,.*,,\" should always be quoted.")
+
+}
+
+func (s *Suite) Test_ShellProgramChecker_checkSetE__simple_commands(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall")
+       t.SetupToolUsable("echo", "")
+       t.SetupToolUsable("rm", "")
+       t.SetupToolUsable("touch", "")
+       mklines := t.NewMkLines("Makefile",
+               MkRcsID,
+               "pre-configure:",
+               "\techo 1; echo 2; echo 3",
+               "\techo 1; touch file; rm file",
+               "\techo 1; var=value; echo 3")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: Makefile:4: Please switch to \"set -e\" mode before using a semicolon (after \"touch file\") to separate commands.")
+}
+
+func (s *Suite) Test_ShellProgramChecker_checkSetE__compound_commands(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall")
+       t.SetupToolUsable("echo", "")
+       t.SetupToolUsable("touch", "")
+       mklines := t.NewMkLines("Makefile",
+               MkRcsID,
+               "pre-configure:",
+               "\ttouch file; for f in file; do echo \"$$f\"; done",
+               "\tfor f in file; do echo \"$$f\"; done; touch file")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: Makefile:3: Please switch to \"set -e\" mode before using a semicolon (after \"touch file\") to separate commands.")
+}
+
+func (s *Suite) Test_ShellProgramChecker_canFail(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall")
+       t.SetupVartypes()
+       t.SetupToolUsable("echo", "")
+       t.SetupToolUsable("grep", "GREP")
+       t.SetupToolUsable("sed", "")
+       t.SetupToolUsable("touch", "")
+       t.SetupToolUsable("tr", "tr")
+       t.SetupToolUsable("true", "TRUE")
+       mklines := t.NewMkLines("Makefile",
+               MkRcsID,
+               "pre-configure:",
+               "\tsocklen=`${GREP} 'expr' ${WRKSRC}/config.h`; echo 'done.'",
+               "\tsocklen=`${GREP} 'expr' ${WRKSRC}/config.h || ${TRUE}`; echo 'done.'",
+               "\t${ECHO_MSG} \"Message\"; echo 'done.'",
+               "\t${FAIL_MSG} \"Failure\"; echo 'done.'",
+               "\tset -x; echo 'done.'",
+               "\techo 'input' | sed -e s,in,out,; echo 'done.'",
+               "\tsed -e s,in,out,; echo 'done.'",
+               "\tsed s,in,out,; echo 'done.'",
+               "\tgrep input; echo 'done.'",
+               "\ttouch file; echo 'done.'",
+               "\techo 'starting'; echo 'done.'",
+               "\techo 'logging' > log; echo 'done.'",
+               "\techo 'to stderr' 1>&2; echo 'done.'",
+               "\techo 'hello' | tr -d 'aeiou'")
 
        mklines.Check()
 
        t.CheckOutputLines(
-               "ERROR: Makefile:3: The Solaris /bin/sh cannot handle \"cd\" inside conditionals.")
+               "WARN: Makefile:3: Please switch to \"set -e\" mode before using a semicolon (after \"socklen=`${GREP} 'expr' ${WRKSRC}/config.h`\") to separate commands.",
+               "WARN: Makefile:6: Please switch to \"set -e\" mode before using a semicolon (after \"${FAIL_MSG} \\\"Failure\\\"\") to separate commands.",
+               "WARN: Makefile:7: Please switch to \"set -e\" mode before using a semicolon (after \"set -x\") to separate commands.",
+               "WARN: Makefile:12: Please switch to \"set -e\" mode before using a semicolon (after \"touch file\") to separate commands.",
+               "WARN: Makefile:14: Please switch to \"set -e\" mode before using a semicolon (after \"echo 'logging'\") to separate commands.")
 }

Index: pkgsrc/pkgtools/pkglint/files/util_test.go
diff -u pkgsrc/pkgtools/pkglint/files/util_test.go:1.13 pkgsrc/pkgtools/pkglint/files/util_test.go:1.14
--- pkgsrc/pkgtools/pkglint/files/util_test.go:1.13     Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/util_test.go  Wed Oct  3 22:27:53 2018
@@ -2,8 +2,6 @@ package main
 
 import (
        "gopkg.in/check.v1"
-       "netbsd.org/pkglint/regex"
-       "netbsd.org/pkglint/textproc"
        "os"
        "runtime"
        "testing"
@@ -16,36 +14,36 @@ func (s *Suite) Test_YesNoUnknown_String
        c.Check(unknown.String(), equals, "unknown")
 }
 
-func (s *Suite) Test_MkopSubst__middle(c *check.C) {
+func (s *Suite) Test_mkopSubst__middle(c *check.C) {
        c.Check(mkopSubst("pkgname", false, "kgna", false, "ri", ""), equals, "prime")
        c.Check(mkopSubst("pkgname", false, "pkgname", false, "replacement", ""), equals, "replacement")
        c.Check(mkopSubst("aaaaaaa", false, "a", false, "b", ""), equals, "baaaaaa")
 }
 
-func (s *Suite) Test_MkopSubst__left(c *check.C) {
+func (s *Suite) Test_mkopSubst__left(c *check.C) {
        c.Check(mkopSubst("pkgname", true, "kgna", false, "ri", ""), equals, "pkgname")
        c.Check(mkopSubst("pkgname", true, "pkgname", false, "replacement", ""), equals, "replacement")
 }
 
-func (s *Suite) Test_MkopSubst__right(c *check.C) {
+func (s *Suite) Test_mkopSubst__right(c *check.C) {
        c.Check(mkopSubst("pkgname", false, "kgna", true, "ri", ""), equals, "pkgname")
        c.Check(mkopSubst("pkgname", false, "pkgname", true, "replacement", ""), equals, "replacement")
 }
 
-func (s *Suite) Test_MkopSubst__leftRight(c *check.C) {
+func (s *Suite) Test_mkopSubst__left_and_right(c *check.C) {
        c.Check(mkopSubst("pkgname", true, "kgna", true, "ri", ""), equals, "pkgname")
        c.Check(mkopSubst("pkgname", false, "pkgname", false, "replacement", ""), equals, "replacement")
 }
 
-func (s *Suite) Test_MkopSubst__gflag(c *check.C) {
+func (s *Suite) Test_mkopSubst__gflag(c *check.C) {
        c.Check(mkopSubst("aaaaa", false, "a", false, "b", "g"), equals, "bbbbb")
        c.Check(mkopSubst("aaaaa", true, "a", false, "b", "g"), equals, "baaaa")
        c.Check(mkopSubst("aaaaa", false, "a", true, "b", "g"), equals, "aaaab")
        c.Check(mkopSubst("aaaaa", true, "a", true, "b", "g"), equals, "aaaaa")
 }
 
-func (s *Suite) Test_replaceFirst(c *check.C) {
-       m, rest := regex.ReplaceFirst("a+b+c+d", `(\w)(.)(\w)`, "X")
+func (s *Suite) Test__regex_ReplaceFirst(c *check.C) {
+       m, rest := G.res.ReplaceFirst("a+b+c+d", `(\w)(.)(\w)`, "X")
 
        c.Assert(m, check.NotNil)
        c.Check(m, check.DeepEquals, []string{"a+b", "a", "+", "b"})
@@ -65,7 +63,7 @@ func (s *Suite) Test_shorten(c *check.C)
        c.Check(shorten("aaa", 5), equals, "aaa")
 }
 
-func (s *Suite) Test_tabLength(c *check.C) {
+func (s *Suite) Test_tabWidth(c *check.C) {
        c.Check(tabWidth("12345"), equals, 5)
        c.Check(tabWidth("\t"), equals, 8)
        c.Check(tabWidth("123\t"), equals, 8)
@@ -81,13 +79,18 @@ func (s *Suite) Test_cleanpath(c *check.
        c.Check(cleanpath("dir/../dir/../dir/../dir/subdir/../../Makefile"), equals, "dir/../dir/../dir/../Makefile")
        c.Check(cleanpath("dir/multi/././/file"), equals, "dir/multi/file")
        c.Check(cleanpath("111/222/../../333/444/../../555/666/../../777/888/9"), equals, "111/222/../../777/888/9")
+       c.Check(cleanpath("1/2/3/../../4/5/6/../../7/8/9/../../../../10"), equals, "1/10")
        c.Check(cleanpath("cat/pkg.v1/../../cat/pkg.v2/Makefile"), equals, "cat/pkg.v1/../../cat/pkg.v2/Makefile")
        c.Check(cleanpath("dir/"), equals, "dir")
 }
 
 func (s *Suite) Test_relpath(c *check.C) {
+       t := s.Init(c)
+
        if runtime.GOOS == "windows" {
-               c.Check(func() { relpath("c:/", "d:/") }, check.Panics, "relpath \"c:/\", \"d:/\"")
+               t.ExpectFatal(
+                       func() { relpath("c:/", "d:/") },
+                       "FATAL: Pkglint internal error: relpath \"c:/\" \"d:/\".")
        }
 }
 
@@ -97,21 +100,21 @@ func (s *Suite) Test_abspath(c *check.C)
        if runtime.GOOS == "windows" {
                t.ExpectFatal(
                        func() { abspath("file\u0000name") },
-                       "FATAL: file\x00name: Cannot determine absolute path.")
+                       "FATAL: Pkglint internal error: abspath \"file\\x00name\".")
        }
 }
 
-func (s *Suite) Test_isEmptyDir_and_getSubdirs(c *check.C) {
+func (s *Suite) Test_isEmptyDir__and_getSubdirs(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("CVS/Entries",
+       t.CreateFileLines("CVS/Entries",
                "dummy")
 
        if dir := t.File("."); true {
                c.Check(isEmptyDir(dir), equals, true)
                c.Check(getSubdirs(dir), check.DeepEquals, []string(nil))
 
-               t.SetupFileLines("somedir/file")
+               t.CreateFileLines("somedir/file")
 
                c.Check(isEmptyDir(dir), equals, false)
                c.Check(getSubdirs(dir), check.DeepEquals, []string{"somedir"})
@@ -130,7 +133,7 @@ func (s *Suite) Test_isEmptyDir_and_getS
 func (s *Suite) Test_isEmptyDir__empty_subdir(c *check.C) {
        t := s.Init(c)
 
-       t.SetupFileLines("CVS/Entries",
+       t.CreateFileLines("CVS/Entries",
                "dummy")
        t.CreateFileLines("subdir/CVS/Entries",
                "dummy")
@@ -138,8 +141,8 @@ func (s *Suite) Test_isEmptyDir__empty_s
        c.Check(isEmptyDir(t.File(".")), equals, true)
 }
 
-func (s *Suite) Test_PrefixReplacer_Since(c *check.C) {
-       repl := textproc.NewPrefixReplacer("hello, world")
+func (s *Suite) Test__PrefixReplacer_Since(c *check.C) {
+       repl := G.NewPrefixReplacer("hello, world")
        mark := repl.Mark()
        repl.AdvanceRegexp(`^\w+`)
        c.Check(repl.Since(mark), equals, "hello")
@@ -168,7 +171,7 @@ func Benchmark_match3_bsd_pkg_mk(b *test
        }
 }
 
-func Benchmark_match3_samedir(b *testing.B) {
+func Benchmark_match3_same_dir(b *testing.B) {
        for i := 0; i < b.N; i++ {
                match3(".include \"options.mk\"", reMkIncludeBenchmark)
        }
@@ -192,7 +195,7 @@ func Benchmark_match3_bsd_pkg_mk_positiv
        }
 }
 
-func Benchmark_match3_samedir_positive(b *testing.B) {
+func Benchmark_match3_same_dir_positive(b *testing.B) {
        for i := 0; i < b.N; i++ {
                match3(".include \"options.mk\"", reMkIncludeBenchmarkPositive)
        }
@@ -305,4 +308,95 @@ func (s *Suite) Test_naturalLess(c *chec
        c.Check(naturalLess("000", "0000"), equals, true)
        c.Check(naturalLess("0000", "000"), equals, false)
        c.Check(naturalLess("000", "000"), equals, false)
+       c.Check(naturalLess("00011", "000111"), equals, true)
+       c.Check(naturalLess("00011", "00012"), equals, true)
+}
+
+func (s *Suite) Test_varnameBase(c *check.C) {
+       c.Check(varnameBase("VAR"), equals, "VAR")
+       c.Check(varnameBase("VAR.param"), equals, "VAR")
+       c.Check(varnameBase(".CURDIR"), equals, ".CURDIR")
+}
+
+func (s *Suite) Test_varnameParam(c *check.C) {
+       c.Check(varnameParam("VAR"), equals, "")
+       c.Check(varnameParam("VAR.param"), equals, "param")
+       c.Check(varnameParam(".CURDIR"), equals, "")
+}
+
+func (s *Suite) Test_varnameCanon(c *check.C) {
+       c.Check(varnameCanon("VAR"), equals, "VAR")
+       c.Check(varnameCanon("VAR.param"), equals, "VAR.*")
+       c.Check(varnameCanon(".CURDIR"), equals, ".CURDIR")
+}
+
+func (s *Suite) Test_isalnum(c *check.C) {
+       c.Check(isalnum(""), equals, true)
+       c.Check(isalnum("/"), equals, false)
+       c.Check(isalnum("0"), equals, true)
+       c.Check(isalnum("9"), equals, true)
+       c.Check(isalnum(":"), equals, false)
+       c.Check(isalnum("@"), equals, false)
+       c.Check(isalnum("A"), equals, true)
+       c.Check(isalnum("Z"), equals, true)
+       c.Check(isalnum("["), equals, false)
+       c.Check(isalnum("_"), equals, true)
+       c.Check(isalnum("`"), equals, false)
+       c.Check(isalnum("a"), equals, true)
+       c.Check(isalnum("z"), equals, true)
+       c.Check(isalnum("{"), equals, false)
+       c.Check(isalnum("Hello_world005"), equals, true)
+       c.Check(isalnum("Hello,world005"), equals, false)
+}
+
+func (s *Suite) Test_FileCache(c *check.C) {
+       t := s.Init(c)
+
+       cache := NewFileCache(3)
+
+       lines := t.NewLines("Makefile",
+               MkRcsID,
+               "# line 2")
+
+       c.Check(cache.Get("Makefile", 0), check.IsNil)
+       c.Check(cache.hits, equals, 0)
+       c.Check(cache.misses, equals, 1)
+
+       cache.Put("Makefile", 0, lines)
+       c.Check(cache.Get("Makefile", MustSucceed|LogErrors), check.IsNil) // Wrong LoadOptions.
+
+       linesFromCache := cache.Get("Makefile", 0)
+       c.Check(linesFromCache, check.HasLen, 2)
+       c.Check(linesFromCache[1].Filename, equals, "Makefile")
+
+       // Cache keys are normalized using path.Clean.
+       linesFromCache2 := cache.Get("./Makefile", 0)
+       c.Check(linesFromCache2, check.HasLen, 2)
+       c.Check(linesFromCache2[1].Filename, equals, "./Makefile")
+
+       cache.Put("file1.mk", 0, lines)
+       cache.Put("file2.mk", 0, lines)
+
+       // Now the cache is full. All three entries can be retrieved.
+       c.Check(cache.Get("Makefile", 0), check.NotNil)
+       c.Check(cache.Get("file1.mk", 0), check.NotNil)
+       c.Check(cache.Get("file2.mk", 0), check.NotNil)
+
+       // Adding another entry removes all entries with minimum count,
+       // which currently are file1.mk and file2.mk.
+       // Makefile is still in the cache because it was accessed once.
+       cache.Put("file3.mk", 0, lines)
+
+       c.Check(cache.Get("Makefile", 0), check.NotNil)
+       c.Check(cache.Get("file1.mk", 0), check.IsNil)
+       c.Check(cache.Get("file2.mk", 0), check.IsNil)
+       c.Check(cache.Get("file3.mk", 0), check.NotNil)
+
+       cache.Evict("Makefile")
+
+       c.Check(cache.Get("Makefile", 0), check.IsNil)
+       c.Check(cache.table, check.HasLen, 1)
+       c.Check(cache.mapping, check.HasLen, 1)
+       c.Check(cache.hits, equals, 7)
+       c.Check(cache.misses, equals, 5)
 }

Index: pkgsrc/pkgtools/pkglint/files/vardefs.go
diff -u pkgsrc/pkgtools/pkglint/files/vardefs.go:1.45 pkgsrc/pkgtools/pkglint/files/vardefs.go:1.46
--- pkgsrc/pkgtools/pkglint/files/vardefs.go:1.45       Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/vardefs.go    Wed Oct  3 22:27:53 2018
@@ -1,7 +1,6 @@
 package main
 
 import (
-       "fmt"
        "netbsd.org/pkglint/trace"
        "path"
        "strings"
@@ -26,9 +25,6 @@ func (src *Pkgsrc) InitVartypes() {
 
                vartype := &Vartype{kindOfList, checker, parseACLEntries(varname, aclEntries), false}
 
-               if src.vartypes == nil {
-                       src.vartypes = make(map[string]*Vartype)
-               }
                if varparam == "" || varparam == "*" {
                        src.vartypes[varbase] = vartype
                }
@@ -247,203 +243,209 @@ func (src *Pkgsrc) InitVartypes() {
        usr("BIN_INSTALL_FLAGS", lkShell, BtShellWord)
        usr("LOCALPATCHES", lkNone, BtPathname)
 
-       if false {
-               // The remaining variables from mk/defaults/mk.conf may be overridden by packages.
-               // Therefore they need a separate definition of "user-settable".
-               usr := func(varname string, kindOfList KindOfList, checker *BasicType) {
-                       acl(varname, kindOfList, checker, ""+
-                               "Makefile: set, use; "+
-                               "buildlink3.mk, builtin.mk:; "+
-                               "Makefile.*, *.mk: default, set, use; "+
-                               "*: use-loadtime, use")
-               }
-               usr("ACROREAD_FONTPATH", lkNone, BtPathlist)
-               usr("AMANDA_USER", lkNone, BtUserGroupName)
-               usr("AMANDA_TMP", lkNone, BtPathname)
-               usr("AMANDA_VAR", lkNone, BtPathname)
-               usr("APACHE_USER", lkNone, BtUserGroupName)
-               usr("APACHE_GROUP", lkNone, BtUserGroupName)
-               usr("APACHE_SUEXEC_CONFIGURE_ARGS", lkShell, BtShellWord)
-               usr("APACHE_SUEXEC_DOCROOT", lkShell, BtPathname)
-               usr("ARLA_CACHE", lkNone, BtPathname)
-               usr("BIND_DIR", lkNone, BtPathname)
-               usr("BIND_GROUP", lkNone, BtUserGroupName)
-               usr("BIND_USER", lkNone, BtUserGroupName)
-               usr("CACTI_GROUP", lkNone, BtUserGroupName)
-               usr("CACTI_USER", lkNone, BtUserGroupName)
-               usr("CANNA_GROUP", lkNone, BtUserGroupName)
-               usr("CANNA_USER", lkNone, BtUserGroupName)
-               usr("CDRECORD_CONF", lkNone, BtPathname)
-               usr("CLAMAV_GROUP", lkNone, BtUserGroupName)
-               usr("CLAMAV_USER", lkNone, BtUserGroupName)
-               usr("CLAMAV_DBDIR", lkNone, BtPathname)
-               usr("CONSERVER_DEFAULTHOST", lkNone, BtIdentifier)
-               usr("CONSERVER_DEFAULTPORT", lkNone, BtInteger)
-               usr("CUPS_GROUP", lkNone, BtUserGroupName)
-               usr("CUPS_USER", lkNone, BtUserGroupName)
-               usr("CUPS_SYSTEM_GROUPS", lkShell, BtUserGroupName)
-               usr("CYRUS_IDLE", lkNone, enum("poll idled no"))
-               usr("CYRUS_GROUP", lkNone, BtUserGroupName)
-               usr("CYRUS_USER", lkNone, BtUserGroupName)
-               usr("DBUS_GROUP", lkNone, BtUserGroupName)
-               usr("DBUS_USER", lkNone, BtUserGroupName)
-               usr("DEFANG_GROUP", lkNone, BtUserGroupName)
-               usr("DEFANG_USER", lkNone, BtUserGroupName)
-               usr("DEFANG_SPOOLDIR", lkNone, BtPathname)
-               usr("DEFAULT_IRC_SERVER", lkNone, BtIdentifier)
-               usr("DEFAULT_SERIAL_DEVICE", lkNone, BtPathname)
-               usr("DIALER_GROUP", lkNone, BtUserGroupName)
-               usr("DT_LAYOUT", lkNone, enum("US FI FR GER DV"))
-               usr("ELK_GUI", lkShell, enum("none xaw motif"))
-               usr("EMACS_TYPE", lkNone, enum("emacs25 emacs25nox emacs21 emacs21nox emacs20 xemacs214 xemacs215"))
-               usr("EXIM_GROUP", lkNone, BtUserGroupName)
-               usr("EXIM_USER", lkNone, BtUserGroupName)
-               usr("FLUXBOX_USE_XINERAMA", lkNone, enum("YES NO"))
-               usr("FLUXBOX_USE_KDE", lkNone, enum("YES NO"))
-               usr("FLUXBOX_USE_GNOME", lkNone, enum("YES NO"))
-               usr("FLUXBOX_USE_XFT", lkNone, enum("YES NO"))
-               usr("FOX_USE_XUNICODE", lkNone, enum("YES NO"))
-               usr("FREEWNN_USER", lkNone, BtUserGroupName)
-               usr("FREEWNN_GROUP", lkNone, BtUserGroupName)
-               usr("GAMES_USER", lkNone, BtUserGroupName)
-               usr("GAMES_GROUP", lkNone, BtUserGroupName)
-               usr("GAMEMODE", lkNone, BtFileMode)
-               usr("GAMEDIRMODE", lkNone, BtFileMode)
-               usr("GAMEDATAMODE", lkNone, BtFileMode)
-               usr("GAMEGRP", lkNone, BtUserGroupName)
-               usr("GAMEOWN", lkNone, BtUserGroupName)
-               usr("GRUB_NETWORK_CARDS", lkNone, BtIdentifier)
-               usr("GRUB_PRESET_COMMAND", lkNone, enum("bootp dhcp rarp"))
-               usr("GRUB_SCAN_ARGS", lkShell, BtShellWord)
-               usr("HASKELL_COMPILER", lkNone, enum("ghc"))
-               usr("HOWL_GROUP", lkNone, BtUserGroupName)
-               usr("HOWL_USER", lkNone, BtUserGroupName)
-               usr("ICECAST_CHROOTDIR", lkNone, BtPathname)
-               usr("ICECAST_CHUNKLEN", lkNone, BtInteger)
-               usr("ICECAST_SOURCE_BUFFSIZE", lkNone, BtInteger)
-               usr("IMAP_UW_CCLIENT_MBOX_FMT", lkNone, enum("mbox mbx mh mmdf mtx mx news phile tenex unix"))
-               usr("IMAP_UW_MAILSPOOLHOME", lkNone, BtFilename)
-               usr("IMDICTDIR", lkNone, BtPathname)
-               usr("INN_DATA_DIR", lkNone, BtPathname)
-               usr("INN_USER", lkNone, BtUserGroupName)
-               usr("INN_GROUP", lkNone, BtUserGroupName)
-               usr("IRCD_HYBRID_NICLEN", lkNone, BtInteger)
-               usr("IRCD_HYBRID_TOPICLEN", lkNone, BtInteger)
-               usr("IRCD_HYBRID_SYSLOG_EVENTS", lkNone, BtUnknown)
-               usr("IRCD_HYBRID_SYSLOG_FACILITY", lkNone, BtIdentifier)
-               usr("IRCD_HYBRID_MAXCONN", lkNone, BtInteger)
-               usr("IRCD_HYBRID_IRC_USER", lkNone, BtUserGroupName)
-               usr("IRCD_HYBRID_IRC_GROUP", lkNone, BtUserGroupName)
-               usr("IRRD_USE_PGP", lkNone, enum("5 2"))
-               usr("JABBERD_USER", lkNone, BtUserGroupName)
-               usr("JABBERD_GROUP", lkNone, BtUserGroupName)
-               usr("JABBERD_LOGDIR", lkNone, BtPathname)
-               usr("JABBERD_SPOOLDIR", lkNone, BtPathname)
-               usr("JABBERD_PIDDIR", lkNone, BtPathname)
-               usr("JAKARTA_HOME", lkNone, BtPathname)
-               usr("KERBEROS", lkNone, BtYes)
-               usr("KERMIT_SUID_UUCP", lkNone, BtYes)
-               usr("KJS_USE_PCRE", lkNone, BtYes)
-               usr("KNEWS_DOMAIN_FILE", lkNone, BtPathname)
-               usr("KNEWS_DOMAIN_NAME", lkNone, BtIdentifier)
-               usr("LIBDVDCSS_HOMEPAGE", lkNone, BtHomepage)
-               usr("LIBDVDCSS_MASTER_SITES", lkShell, BtFetchURL)
-               usr("LATEX2HTML_ICONPATH", lkNone, BtURL)
-               usr("LEAFNODE_DATA_DIR", lkNone, BtPathname)
-               usr("LEAFNODE_USER", lkNone, BtUserGroupName)
-               usr("LEAFNODE_GROUP", lkNone, BtUserGroupName)
-               usr("LINUX_LOCALES", lkShell, BtIdentifier)
-               usr("MAILAGENT_DOMAIN", lkNone, BtIdentifier)
-               usr("MAILAGENT_EMAIL", lkNone, BtMailAddress)
-               usr("MAILAGENT_FQDN", lkNone, BtIdentifier)
-               usr("MAILAGENT_ORGANIZATION", lkNone, BtUnknown)
-               usr("MAJORDOMO_HOMEDIR", lkNone, BtPathname)
-               usr("MAKEINFO_ARGS", lkShell, BtShellWord)
-               usr("MECAB_CHARSET", lkNone, BtIdentifier)
-               usr("MEDIATOMB_GROUP", lkNone, BtUserGroupName)
-               usr("MEDIATOMB_USER", lkNone, BtUserGroupName)
-               usr("MLDONKEY_GROUP", lkNone, BtUserGroupName)
-               usr("MLDONKEY_HOME", lkNone, BtPathname)
-               usr("MLDONKEY_USER", lkNone, BtUserGroupName)
-               usr("MONOTONE_GROUP", lkNone, BtUserGroupName)
-               usr("MONOTONE_USER", lkNone, BtUserGroupName)
-               usr("MOTIF_TYPE", lkNone, enum("motif openmotif lesstif dt"))
-               usr("MOTIF_TYPE_DEFAULT", lkNone, enum("motif openmotif lesstif dt"))
-               usr("MTOOLS_ENABLE_FLOPPYD", lkNone, BtYesNo)
-               usr("MYSQL_USER", lkNone, BtUserGroupName)
-               usr("MYSQL_GROUP", lkNone, BtUserGroupName)
-               usr("MYSQL_DATADIR", lkNone, BtPathname)
-               usr("MYSQL_CHARSET", lkNone, BtIdentifier)
-               usr("MYSQL_EXTRA_CHARSET", lkShell, BtIdentifier)
-               usr("NAGIOS_GROUP", lkNone, BtUserGroupName)
-               usr("NAGIOS_USER", lkNone, BtUserGroupName)
-               usr("NAGIOSCMD_GROUP", lkNone, BtUserGroupName)
-               usr("NAGIOSDIR", lkNone, BtPathname)
-               usr("NBPAX_PROGRAM_PREFIX", lkNone, BtIdentifier)
-               usr("NMH_EDITOR", lkNone, BtIdentifier)
-               usr("NMH_MTA", lkNone, enum("smtp sendmail"))
-               usr("NMH_PAGER", lkNone, BtIdentifier)
-               usr("NS_PREFERRED", lkNone, enum("communicator navigator mozilla"))
-               usr("OPENSSH_CHROOT", lkNone, BtPathname)
-               usr("OPENSSH_USER", lkNone, BtUserGroupName)
-               usr("OPENSSH_GROUP", lkNone, BtUserGroupName)
-               usr("P4USER", lkNone, BtUserGroupName)
-               usr("P4GROUP", lkNone, BtUserGroupName)
-               usr("P4ROOT", lkNone, BtPathname)
-               usr("P4PORT", lkNone, BtInteger)
-               usr("PALMOS_DEFAULT_SDK", lkNone, enum("1 2 3.1 3.5"))
-               usr("PAPERSIZE", lkNone, enum("A4 Letter"))
-               usr("PGGROUP", lkNone, BtUserGroupName)
-               usr("PGUSER", lkNone, BtUserGroupName)
-               usr("PGHOME", lkNone, BtPathname)
-               usr("PILRC_USE_GTK", lkNone, BtYesNo)
-               usr("PKG_JVM_DEFAULT", lkNone, jvms)
-               usr("POPTOP_USE_MPPE", lkNone, BtYes)
-               usr("PROCMAIL_MAILSPOOLHOME", lkNone, BtFilename)
-               usr("PROCMAIL_TRUSTED_IDS", lkShell, BtIdentifier)
-               usr("PVM_SSH", lkNone, BtPathname)
-               usr("QMAILDIR", lkNone, BtPathname)
-               usr("QMAIL_QFILTER_TMPDIR", lkNone, BtPathname)
-               usr("QMAIL_QUEUE_DIR", lkNone, BtPathname)
-               usr("QMAIL_QUEUE_EXTRA", lkNone, BtMailAddress)
-               usr("QPOPPER_FAC", lkNone, BtIdentifier)
-               usr("QPOPPER_USER", lkNone, BtUserGroupName)
-               usr("QPOPPER_SPOOL_DIR", lkNone, BtPathname)
-               usr("RASMOL_DEPTH", lkNone, enum("8 16 32"))
-               usr("RELAY_CTRL_DIR", lkNone, BtPathname)
-               usr("RPM_DB_PREFIX", lkNone, BtPathname)
-               usr("RSSH_SCP_PATH", lkNone, BtPathname)
-               usr("RSSH_SFTP_SERVER_PATH", lkNone, BtPathname)
-               usr("RSSH_CVS_PATH", lkNone, BtPathname)
-               usr("RSSH_RDIST_PATH", lkNone, BtPathname)
-               usr("RSSH_RSYNC_PATH", lkNone, BtPathname)
-               usr("SAWFISH_THEMES", lkShell, BtFilename)
-               usr("SCREWS_GROUP", lkNone, BtUserGroupName)
-               usr("SCREWS_USER", lkNone, BtUserGroupName)
-               usr("SDIST_PAWD", lkNone, enum("pawd pwd"))
-               usr("SERIAL_DEVICES", lkShell, BtPathname)
-               usr("SILC_CLIENT_WITH_PERL", lkNone, BtYesNo)
-               usr("SSH_SUID", lkNone, BtYesNo)
-               usr("SSYNC_PAWD", lkNone, enum("pawd pwd"))
-               usr("SUSE_PREFER", lkNone, enum("13.1 12.1 10.0"))
-               usr("TEXMFSITE", lkNone, BtPathname)
-               usr("THTTPD_LOG_FACILITY", lkNone, BtIdentifier)
-               usr("UNPRIVILEGED", lkNone, BtYesNo)
-               usr("USE_CROSS_COMPILE", lkNone, BtYesNo)
-               usr("USERPPP_GROUP", lkNone, BtUserGroupName)
-               usr("UUCP_GROUP", lkNone, BtUserGroupName)
-               usr("UUCP_USER", lkNone, BtUserGroupName)
-               usr("VIM_EXTRA_OPTS", lkShell, BtShellWord)
-               usr("WCALC_HTMLDIR", lkNone, BtPathname)
-               usr("WCALC_HTMLPATH", lkNone, BtPathname) // URL path
-               usr("WCALC_CGIDIR", lkNone, BtPrefixPathname)
-               usr("WCALC_CGIPATH", lkNone, BtPathname) // URL path
-               usr("WDM_MANAGERS", lkShell, BtIdentifier)
-               usr("X10_PORT", lkNone, BtPathname)
-               usr("XAW_TYPE", lkNone, enum("standard 3d xpm neXtaw"))
-               usr("XLOCK_DEFAULT_MODE", lkNone, BtIdentifier)
-               usr("ZSH_STATIC", lkNone, BtYes)
+       // The remaining variables from mk/defaults/mk.conf may be overridden by packages.
+       // Therefore they need a separate definition of "user-settable".
+       //
+       // It is debatable whether packages should be allowed to override these
+       // variables at all since then there are two competing sources for the
+       // default values. Current practice is to have exactly this ambiguity,
+       // combined with some package Makefiles including bsd.prefs.mk and others
+       // omitting this necessary inclusion.
+       //
+       // TODO: parse all the below information directly from mk/defaults/mk.conf.
+       usrpkg := func(varname string, kindOfList KindOfList, checker *BasicType) {
+               acl(varname, kindOfList, checker, ""+
+                       "Makefile: default, set, use, use-loadtime; "+
+                       "buildlink3.mk, builtin.mk:; "+
+                       "Makefile.*, *.mk: default, set, use, use-loadtime; "+
+                       "*: use-loadtime, use")
        }
+       usrpkg("ACROREAD_FONTPATH", lkNone, BtPathlist)
+       usrpkg("AMANDA_USER", lkNone, BtUserGroupName)
+       usrpkg("AMANDA_TMP", lkNone, BtPathname)
+       usrpkg("AMANDA_VAR", lkNone, BtPathname)
+       usrpkg("APACHE_USER", lkNone, BtUserGroupName)
+       usrpkg("APACHE_GROUP", lkNone, BtUserGroupName)
+       usrpkg("APACHE_SUEXEC_CONFIGURE_ARGS", lkShell, BtShellWord)
+       usrpkg("APACHE_SUEXEC_DOCROOT", lkShell, BtPathname)
+       usrpkg("ARLA_CACHE", lkNone, BtPathname)
+       usrpkg("BIND_DIR", lkNone, BtPathname)
+       usrpkg("BIND_GROUP", lkNone, BtUserGroupName)
+       usrpkg("BIND_USER", lkNone, BtUserGroupName)
+       usrpkg("CACTI_GROUP", lkNone, BtUserGroupName)
+       usrpkg("CACTI_USER", lkNone, BtUserGroupName)
+       usrpkg("CANNA_GROUP", lkNone, BtUserGroupName)
+       usrpkg("CANNA_USER", lkNone, BtUserGroupName)
+       usrpkg("CDRECORD_CONF", lkNone, BtPathname)
+       usrpkg("CLAMAV_GROUP", lkNone, BtUserGroupName)
+       usrpkg("CLAMAV_USER", lkNone, BtUserGroupName)
+       usrpkg("CLAMAV_DBDIR", lkNone, BtPathname)
+       usrpkg("CONSERVER_DEFAULTHOST", lkNone, BtIdentifier)
+       usrpkg("CONSERVER_DEFAULTPORT", lkNone, BtInteger)
+       usrpkg("CUPS_GROUP", lkNone, BtUserGroupName)
+       usrpkg("CUPS_USER", lkNone, BtUserGroupName)
+       usrpkg("CUPS_SYSTEM_GROUPS", lkShell, BtUserGroupName)
+       usrpkg("CYRUS_IDLE", lkNone, enum("poll idled no"))
+       usrpkg("CYRUS_GROUP", lkNone, BtUserGroupName)
+       usrpkg("CYRUS_USER", lkNone, BtUserGroupName)
+       usrpkg("DBUS_GROUP", lkNone, BtUserGroupName)
+       usrpkg("DBUS_USER", lkNone, BtUserGroupName)
+       usrpkg("DEFANG_GROUP", lkNone, BtUserGroupName)
+       usrpkg("DEFANG_USER", lkNone, BtUserGroupName)
+       usrpkg("DEFANG_SPOOLDIR", lkNone, BtPathname)
+       usrpkg("DEFAULT_IRC_SERVER", lkNone, BtIdentifier)
+       usrpkg("DEFAULT_SERIAL_DEVICE", lkNone, BtPathname)
+       usrpkg("DIALER_GROUP", lkNone, BtUserGroupName)
+       usrpkg("DT_LAYOUT", lkNone, enum("US FI FR GER DV"))
+       usrpkg("ELK_GUI", lkShell, enum("none xaw motif"))
+       usrpkg("EMACS_TYPE", lkNone, emacsVersions)
+       usrpkg("EXIM_GROUP", lkNone, BtUserGroupName)
+       usrpkg("EXIM_USER", lkNone, BtUserGroupName)
+       usrpkg("FLUXBOX_USE_XINERAMA", lkNone, enum("YES NO"))
+       usrpkg("FLUXBOX_USE_KDE", lkNone, enum("YES NO"))
+       usrpkg("FLUXBOX_USE_GNOME", lkNone, enum("YES NO"))
+       usrpkg("FLUXBOX_USE_XFT", lkNone, enum("YES NO"))
+       usrpkg("FOX_USE_XUNICODE", lkNone, enum("YES NO"))
+       usrpkg("FREEWNN_USER", lkNone, BtUserGroupName)
+       usrpkg("FREEWNN_GROUP", lkNone, BtUserGroupName)
+       usrpkg("GAMES_USER", lkNone, BtUserGroupName)
+       usrpkg("GAMES_GROUP", lkNone, BtUserGroupName)
+       usrpkg("GAMEMODE", lkNone, BtFileMode)
+       usrpkg("GAMEDIRMODE", lkNone, BtFileMode)
+       usrpkg("GAMEDATAMODE", lkNone, BtFileMode)
+       usrpkg("GAMEGRP", lkNone, BtUserGroupName)
+       usrpkg("GAMEOWN", lkNone, BtUserGroupName)
+       usrpkg("GRUB_NETWORK_CARDS", lkNone, BtIdentifier)
+       usrpkg("GRUB_PRESET_COMMAND", lkNone, enum("bootp dhcp rarp"))
+       usrpkg("GRUB_SCAN_ARGS", lkShell, BtShellWord)
+       usrpkg("HASKELL_COMPILER", lkNone, enum("ghc"))
+       usrpkg("HOWL_GROUP", lkNone, BtUserGroupName)
+       usrpkg("HOWL_USER", lkNone, BtUserGroupName)
+       usrpkg("ICECAST_CHROOTDIR", lkNone, BtPathname)
+       usrpkg("ICECAST_CHUNKLEN", lkNone, BtInteger)
+       usrpkg("ICECAST_SOURCE_BUFFSIZE", lkNone, BtInteger)
+       usrpkg("IMAP_UW_CCLIENT_MBOX_FMT", lkNone, enum("mbox mbx mh mmdf mtx mx news phile tenex unix"))
+       usrpkg("IMAP_UW_MAILSPOOLHOME", lkNone, BtFilename)
+       usrpkg("IMDICTDIR", lkNone, BtPathname)
+       usrpkg("INN_DATA_DIR", lkNone, BtPathname)
+       usrpkg("INN_USER", lkNone, BtUserGroupName)
+       usrpkg("INN_GROUP", lkNone, BtUserGroupName)
+       usrpkg("IRCD_HYBRID_NICLEN", lkNone, BtInteger)
+       usrpkg("IRCD_HYBRID_TOPICLEN", lkNone, BtInteger)
+       usrpkg("IRCD_HYBRID_SYSLOG_EVENTS", lkNone, BtUnknown)
+       usrpkg("IRCD_HYBRID_SYSLOG_FACILITY", lkNone, BtIdentifier)
+       usrpkg("IRCD_HYBRID_MAXCONN", lkNone, BtInteger)
+       usrpkg("IRCD_HYBRID_IRC_USER", lkNone, BtUserGroupName)
+       usrpkg("IRCD_HYBRID_IRC_GROUP", lkNone, BtUserGroupName)
+       usrpkg("IRRD_USE_PGP", lkNone, enum("5 2"))
+       usrpkg("JABBERD_USER", lkNone, BtUserGroupName)
+       usrpkg("JABBERD_GROUP", lkNone, BtUserGroupName)
+       usrpkg("JABBERD_LOGDIR", lkNone, BtPathname)
+       usrpkg("JABBERD_SPOOLDIR", lkNone, BtPathname)
+       usrpkg("JABBERD_PIDDIR", lkNone, BtPathname)
+       usrpkg("JAKARTA_HOME", lkNone, BtPathname)
+       usrpkg("KERBEROS", lkNone, BtYes)
+       usrpkg("KERMIT_SUID_UUCP", lkNone, BtYes)
+       usrpkg("KJS_USE_PCRE", lkNone, BtYes)
+       usrpkg("KNEWS_DOMAIN_FILE", lkNone, BtPathname)
+       usrpkg("KNEWS_DOMAIN_NAME", lkNone, BtIdentifier)
+       usrpkg("LIBDVDCSS_HOMEPAGE", lkNone, BtHomepage)
+       usrpkg("LIBDVDCSS_MASTER_SITES", lkShell, BtFetchURL)
+       usrpkg("LATEX2HTML_ICONPATH", lkNone, BtURL)
+       usrpkg("LEAFNODE_DATA_DIR", lkNone, BtPathname)
+       usrpkg("LEAFNODE_USER", lkNone, BtUserGroupName)
+       usrpkg("LEAFNODE_GROUP", lkNone, BtUserGroupName)
+       usrpkg("LINUX_LOCALES", lkShell, BtIdentifier)
+       usrpkg("MAILAGENT_DOMAIN", lkNone, BtIdentifier)
+       usrpkg("MAILAGENT_EMAIL", lkNone, BtMailAddress)
+       usrpkg("MAILAGENT_FQDN", lkNone, BtIdentifier)
+       usrpkg("MAILAGENT_ORGANIZATION", lkNone, BtUnknown)
+       usrpkg("MAJORDOMO_HOMEDIR", lkNone, BtPathname)
+       usrpkg("MAKEINFO_ARGS", lkShell, BtShellWord)
+       usrpkg("MECAB_CHARSET", lkNone, BtIdentifier)
+       usrpkg("MEDIATOMB_GROUP", lkNone, BtUserGroupName)
+       usrpkg("MEDIATOMB_USER", lkNone, BtUserGroupName)
+       usrpkg("MLDONKEY_GROUP", lkNone, BtUserGroupName)
+       usrpkg("MLDONKEY_HOME", lkNone, BtPathname)
+       usrpkg("MLDONKEY_USER", lkNone, BtUserGroupName)
+       usrpkg("MONOTONE_GROUP", lkNone, BtUserGroupName)
+       usrpkg("MONOTONE_USER", lkNone, BtUserGroupName)
+       usrpkg("MOTIF_TYPE", lkNone, enum("motif openmotif lesstif dt"))
+       usrpkg("MOTIF_TYPE_DEFAULT", lkNone, enum("motif openmotif lesstif dt"))
+       usrpkg("MTOOLS_ENABLE_FLOPPYD", lkNone, BtYesNo)
+       usrpkg("MYSQL_USER", lkNone, BtUserGroupName)
+       usrpkg("MYSQL_GROUP", lkNone, BtUserGroupName)
+       usrpkg("MYSQL_DATADIR", lkNone, BtPathname)
+       usrpkg("MYSQL_CHARSET", lkNone, BtIdentifier)
+       usrpkg("MYSQL_EXTRA_CHARSET", lkShell, BtIdentifier)
+       usrpkg("NAGIOS_GROUP", lkNone, BtUserGroupName)
+       usrpkg("NAGIOS_USER", lkNone, BtUserGroupName)
+       usrpkg("NAGIOSCMD_GROUP", lkNone, BtUserGroupName)
+       usrpkg("NAGIOSDIR", lkNone, BtPathname)
+       usrpkg("NBPAX_PROGRAM_PREFIX", lkNone, BtUnknown)
+       usrpkg("NMH_EDITOR", lkNone, BtIdentifier)
+       usrpkg("NMH_MTA", lkNone, enum("smtp sendmail"))
+       usrpkg("NMH_PAGER", lkNone, BtIdentifier)
+       usrpkg("NS_PREFERRED", lkNone, enum("communicator navigator mozilla"))
+       usrpkg("OPENSSH_CHROOT", lkNone, BtPathname)
+       usrpkg("OPENSSH_USER", lkNone, BtUserGroupName)
+       usrpkg("OPENSSH_GROUP", lkNone, BtUserGroupName)
+       usrpkg("P4USER", lkNone, BtUserGroupName)
+       usrpkg("P4GROUP", lkNone, BtUserGroupName)
+       usrpkg("P4ROOT", lkNone, BtPathname)
+       usrpkg("P4PORT", lkNone, BtInteger)
+       usrpkg("PALMOS_DEFAULT_SDK", lkNone, enum("1 2 3.1 3.5"))
+       usrpkg("PAPERSIZE", lkNone, enum("A4 Letter"))
+       usrpkg("PGGROUP", lkNone, BtUserGroupName)
+       usrpkg("PGUSER", lkNone, BtUserGroupName)
+       usrpkg("PGHOME", lkNone, BtPathname)
+       usrpkg("PILRC_USE_GTK", lkNone, BtYesNo)
+       usrpkg("PKG_JVM_DEFAULT", lkNone, jvms)
+       usrpkg("POPTOP_USE_MPPE", lkNone, BtYes)
+       usrpkg("PROCMAIL_MAILSPOOLHOME", lkNone, BtFilename)
+       usrpkg("PROCMAIL_TRUSTED_IDS", lkShell, BtUnknown) // Comma-separated list of string or integer literals.
+       usrpkg("PVM_SSH", lkNone, BtPathname)
+       usrpkg("QMAILDIR", lkNone, BtPathname)
+       usrpkg("QMAIL_QFILTER_TMPDIR", lkNone, BtPathname)
+       usrpkg("QMAIL_QUEUE_DIR", lkNone, BtPathname)
+       usrpkg("QMAIL_QUEUE_EXTRA", lkNone, BtMailAddress)
+       usrpkg("QPOPPER_FAC", lkNone, BtIdentifier)
+       usrpkg("QPOPPER_USER", lkNone, BtUserGroupName)
+       usrpkg("QPOPPER_SPOOL_DIR", lkNone, BtPathname)
+       usrpkg("RASMOL_DEPTH", lkNone, enum("8 16 32"))
+       usrpkg("RELAY_CTRL_DIR", lkNone, BtPathname)
+       usrpkg("RPM_DB_PREFIX", lkNone, BtPathname)
+       usrpkg("RSSH_SCP_PATH", lkNone, BtPathname)
+       usrpkg("RSSH_SFTP_SERVER_PATH", lkNone, BtPathname)
+       usrpkg("RSSH_CVS_PATH", lkNone, BtPathname)
+       usrpkg("RSSH_RDIST_PATH", lkNone, BtPathname)
+       usrpkg("RSSH_RSYNC_PATH", lkNone, BtPathname)
+       usrpkg("SAWFISH_THEMES", lkShell, BtFilename)
+       usrpkg("SCREWS_GROUP", lkNone, BtUserGroupName)
+       usrpkg("SCREWS_USER", lkNone, BtUserGroupName)
+       usrpkg("SDIST_PAWD", lkNone, enum("pawd pwd"))
+       usrpkg("SERIAL_DEVICES", lkShell, BtPathname)
+       usrpkg("SILC_CLIENT_WITH_PERL", lkNone, BtYesNo)
+       usrpkg("SSH_SUID", lkNone, BtYesNo)
+       usrpkg("SSYNC_PAWD", lkNone, enum("pawd pwd"))
+       usrpkg("SUSE_PREFER", lkNone, enum("13.1 12.1 10.0"))
+       usrpkg("TEXMFSITE", lkNone, BtPathname)
+       usrpkg("THTTPD_LOG_FACILITY", lkNone, BtIdentifier)
+       usrpkg("UNPRIVILEGED", lkNone, BtYesNo)
+       usrpkg("USE_CROSS_COMPILE", lkNone, BtYesNo)
+       usrpkg("USERPPP_GROUP", lkNone, BtUserGroupName)
+       usrpkg("UUCP_GROUP", lkNone, BtUserGroupName)
+       usrpkg("UUCP_USER", lkNone, BtUserGroupName)
+       usrpkg("VIM_EXTRA_OPTS", lkShell, BtShellWord)
+       usrpkg("WCALC_HTMLDIR", lkNone, BtPathname)
+       usrpkg("WCALC_HTMLPATH", lkNone, BtPathname) // URL path
+       usrpkg("WCALC_CGIDIR", lkNone, BtPrefixPathname)
+       usrpkg("WCALC_CGIPATH", lkNone, BtPathname) // URL path
+       usrpkg("WDM_MANAGERS", lkShell, BtIdentifier)
+       usrpkg("X10_PORT", lkNone, BtPathname)
+       usrpkg("XAW_TYPE", lkNone, enum("standard 3d xpm neXtaw"))
+       usrpkg("XLOCK_DEFAULT_MODE", lkNone, BtIdentifier)
+       usrpkg("ZSH_STATIC", lkNone, BtYes)
 
        // some other variables, sorted alphabetically
 
@@ -654,7 +656,6 @@ func (src *Pkgsrc) InitVartypes() {
        usr("EMUL_TYPE.*", lkNone, enum("native builtin suse suse-9.1 suse-9.x suse-10.0 suse-10.x"))
        sys("ERROR_CAT", lkNone, BtShellCommand)
        sys("ERROR_MSG", lkNone, BtShellCommand)
-       acl("EVAL_PREFIX", lkSpace, BtShellWord, "Makefile, Makefile.common: append") // XXX: Combining ShellWord with lkSpace looks weird.
        sys("EXPORT_SYMBOLS_LDFLAGS", lkShell, BtLdFlag)
        sys("EXTRACT_CMD", lkNone, BtShellCommand)
        pkg("EXTRACT_DIR", lkNone, BtPathname)
@@ -956,7 +957,7 @@ func (src *Pkgsrc) InitVartypes() {
        acl("PKG_SYSCONFVAR", lkNone, BtIdentifier, "")
        acl("PKG_UID", lkNone, BtInteger, "Makefile: set")
        acl("PKG_USERS", lkShell, BtShellWord, "Makefile: set, append")
-       pkg("PKG_USERS_VARS", lkShell, BtVariableName)
+       pkglist("PKG_USERS_VARS", lkShell, BtVariableName)
        acl("PKG_USE_KERBEROS", lkNone, BtYes, "Makefile, Makefile.common: set")
        pkg("PLIST.*", lkNone, BtYes)
        pkglist("PLIST_VARS", lkShell, BtIdentifier)
@@ -978,8 +979,8 @@ func (src *Pkgsrc) InitVartypes() {
        acl("PYPKGPREFIX", lkNone, enum("py27 py34 py35 py36"), "pyversion.mk: set; *: use-loadtime, use")
        pkg("PYTHON_FOR_BUILD_ONLY", lkNone, BtYes)
        pkglist("REPLACE_PYTHON", lkShell, BtPathmask)
-       pkg("PYTHON_VERSIONS_ACCEPTED", lkShell, BtVersion)
-       pkg("PYTHON_VERSIONS_INCOMPATIBLE", lkShell, BtVersion)
+       pkglist("PYTHON_VERSIONS_ACCEPTED", lkShell, BtVersion)
+       pkglist("PYTHON_VERSIONS_INCOMPATIBLE", lkShell, BtVersion)
        usr("PYTHON_VERSION_DEFAULT", lkNone, BtVersion)
        usr("PYTHON_VERSION_REQD", lkNone, BtVersion)
        pkglist("PYTHON_VERSIONED_DEPENDENCIES", lkShell, BtPythonDependency)
@@ -1139,9 +1140,10 @@ func parseACLEntries(varname string, acl
                        globs = strings.TrimSuffix(arg, ":")
                }
                if perms == prevperms {
-                       fmt.Printf("Repeated permissions for %s: %s\n", varname, perms)
+                       G.Panicf("Repeated permissions %q for %q.", perms, varname)
                }
                prevperms = perms
+
                var permissions ACLPermissions
                for _, perm := range strings.Split(perms, ", ") {
                        switch perm {
@@ -1158,9 +1160,10 @@ func parseACLEntries(varname string, acl
                        case "":
                                break
                        default:
-                               print(fmt.Sprintf("Invalid ACL permission %q for varname %q.\n", perm, varname))
+                               G.Panicf("Invalid ACL permission %q for %q.", perm, varname)
                        }
                }
+
                for _, glob := range strings.Split(globs, ", ") {
                        switch glob {
                        case "*",
@@ -1169,11 +1172,11 @@ func parseACLEntries(varname string, acl
                                "bsd.options.mk", "pkgconfig-builtin.mk", "pyversion.mk":
                                break
                        default:
-                               print(fmt.Sprintf("Invalid ACL glob %q for varname %q.\n", glob, varname))
+                               G.Panicf("Invalid ACL glob %q for %q.", glob, varname)
                        }
                        for _, prev := range result {
                                if matched, err := path.Match(prev.glob, glob); err != nil || matched {
-                                       print(fmt.Sprintf("Ineffective ACL glob %q for varname %q.\n", glob, varname))
+                                       G.Panicf("Ineffective ACL glob %q for %q.", glob, varname)
                                }
                        }
                        result = append(result, ACLEntry{glob, permissions})

Index: pkgsrc/pkgtools/pkglint/files/vartypecheck.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.39 pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.40
--- pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.39  Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck.go       Wed Oct  3 22:27:53 2018
@@ -1,7 +1,6 @@
 package main
 
 import (
-       "netbsd.org/pkglint/regex"
        "netbsd.org/pkglint/trace"
        "path"
        "sort"
@@ -277,7 +276,8 @@ func (cv *VartypeCheck) Dependency() {
                        "foo-* matches foo-1.2, but also foo-client-1.2 and foo-server-1.2.")
        }
 
-       if nocclasses := regex.Compile(`\[[\d-]+\]`).ReplaceAllString(wildcard, ""); contains(nocclasses, "-") {
+       withoutCharClasses := replaceAll(wildcard, `\[[\d-]+\]`, "")
+       if contains(withoutCharClasses, "-") {
                line.Warnf("The version pattern %q should not contain a hyphen.", wildcard)
                Explain(
                        "Pkgsrc interprets package names with version numbers like this:",
@@ -730,7 +730,7 @@ func (cv *VartypeCheck) Perms() {
        }
 }
 
-func (cv *VartypeCheck) PkgName() {
+func (cv *VartypeCheck) Pkgname() {
        if cv.Op != opUseMatch && cv.Value == cv.ValueNoVar && !matches(cv.Value, rePkgname) {
                cv.Line.Warnf("%q is not a valid package name. A valid package name has the form packagename-version, where version consists only of digits, letters and dots.", cv.Value)
        }
@@ -792,25 +792,34 @@ func (cv *VartypeCheck) MachinePlatformP
                pattern += "-*"
        }
 
-       if m, opsysPattern, _, archPattern := match3(pattern, reTriple); m {
+       if m, opsysPattern, versionPattern, archPattern := match3(pattern, reTriple); m {
                opsysCv := &VartypeCheck{
                        cv.MkLine,
                        cv.Line,
                        "the operating system part of " + cv.Varname,
-                       opUseMatch, // Always allow patterns, since this is a PlatformPattern.
+                       opUseMatch, // Always allow patterns, since this is a platform pattern.
                        opsysPattern,
                        opsysPattern,
                        cv.MkComment,
                        cv.Guessed}
                enumMachineOpsys.checker(opsysCv)
 
-               // no check for os_version
+               versionCv := &VartypeCheck{
+                       cv.MkLine,
+                       cv.Line,
+                       "the version part of " + cv.Varname,
+                       opUseMatch, // Always allow patterns, since this is a platform pattern.
+                       versionPattern,
+                       versionPattern,
+                       cv.MkComment,
+                       cv.Guessed}
+               versionCv.Version()
 
                archCv := &VartypeCheck{
                        cv.MkLine,
                        cv.Line,
                        "the hardware architecture part of " + cv.Varname,
-                       opUseMatch, // Always allow patterns, since this is a PlatformPattern.
+                       opUseMatch, // Always allow patterns, since this is a platform pattern.
                        archPattern,
                        archPattern,
                        cv.MkComment,
@@ -976,6 +985,7 @@ func (cv *VartypeCheck) Tool() {
        }
 }
 
+// Unknown doesn't check for anything.
 func (cv *VartypeCheck) Unknown() {
        // Do nothing.
 }
@@ -1035,12 +1045,30 @@ func (cv *VartypeCheck) VariableName() {
 }
 
 func (cv *VartypeCheck) Version() {
+       line := cv.Line
+       value := cv.Value
+
        if cv.Op == opUseMatch {
-               if !matches(cv.Value, `^[\d?\[][\w\-.*?\[\]]+$`) {
-                       cv.Line.Warnf("Invalid version number pattern %q.", cv.Value)
+               if value != "*" && !matches(value, `^[\d?\[][\w\-.*?\[\]]+$`) {
+                       line.Warnf("Invalid version number pattern %q.", value)
+                       return
+               }
+
+               const digit = `(?:\d|\[[\d-]+\])`
+               const alnum = `(?:\w|\[[\d-]+\])`
+               if m, ver, suffix := match2(value, `^(`+digit+alnum+`*(?:\.`+alnum+`+)*)(\.\*|\*|)$`); m {
+                       if suffix == "*" && ver != "[0-9]" {
+                               line.Warnf("Please use %q instead of %q as the version pattern.", ver+".*", ver+"*")
+                               Explain(
+                                       "For example, the version \"1*\" also matches \"10.0.0\", which is",
+                                       "probably not intended.")
+                       }
                }
-       } else if cv.Value == cv.ValueNoVar && !matches(cv.Value, `^\d[\w.]*$`) {
-               cv.Line.Warnf("Invalid version number %q.", cv.Value)
+               return
+       }
+
+       if value == cv.ValueNoVar && !matches(value, `^\d[\w.]*$`) {
+               line.Warnf("Invalid version number %q.", value)
        }
 }
 

Index: pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go
diff -u pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go:1.5 pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go:1.6
--- pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go:1.5     Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go Wed Oct  3 22:27:54 2018
@@ -137,3 +137,43 @@ func (s *Suite) Test_Options_Parse_strin
 
        c.Check(err.Error(), check.Equals, "progname: option requires an argument: --include")
 }
+
+func (s *Suite) Test_Options_Parse__long_flags(c *check.C) {
+       var aflag, bflag, cflag, dflag, eflag, fflag, gflag, hflag, iflag, jflag bool
+
+       opts := NewOptions()
+       opts.AddFlagVar('a', "aflag", &aflag, false, "")
+       opts.AddFlagVar('b', "bflag", &bflag, false, "")
+       opts.AddFlagVar('c', "cflag", &cflag, false, "")
+       opts.AddFlagVar('d', "dflag", &dflag, false, "")
+       opts.AddFlagVar('e', "eflag", &eflag, true, "")
+       opts.AddFlagVar('f', "fflag", &fflag, true, "")
+       opts.AddFlagVar('g', "gflag", &gflag, true, "")
+       opts.AddFlagVar('h', "hflag", &hflag, true, "")
+       opts.AddFlagVar('i', "iflag", &iflag, false, "")
+       opts.AddFlagVar('j', "jflag", &jflag, false, "")
+
+       args, err := opts.Parse([]string{"progname",
+               "--aflag=true",
+               "--bflag=on",
+               "--cflag=enabled",
+               "--dflag=1",
+               "--eflag=false",
+               "--fflag=off",
+               "--gflag=disabled",
+               "--hflag=0",
+               "--iflag",
+               "--jflag=unknown"})
+
+       c.Check(args, check.HasLen, 0)
+       c.Check(aflag, check.Equals, true)
+       c.Check(bflag, check.Equals, true)
+       c.Check(cflag, check.Equals, true)
+       c.Check(dflag, check.Equals, true)
+       c.Check(eflag, check.Equals, false)
+       c.Check(fflag, check.Equals, false)
+       c.Check(gflag, check.Equals, false)
+       c.Check(hflag, check.Equals, false)
+       c.Check(iflag, check.Equals, true)
+       c.Check(err, check.ErrorMatches, `^progname: invalid argument for option --jflag$`)
+}

Index: pkgsrc/pkgtools/pkglint/files/licenses/licenses.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses/licenses.go:1.2 pkgsrc/pkgtools/pkglint/files/licenses/licenses.go:1.3
--- pkgsrc/pkgtools/pkglint/files/licenses/licenses.go:1.2      Thu Mar 16 20:03:22 2017
+++ pkgsrc/pkgtools/pkglint/files/licenses/licenses.go  Wed Oct  3 22:27:54 2018
@@ -1,6 +1,9 @@
 package licenses
 
-import "netbsd.org/pkglint/textproc"
+import (
+       "netbsd.org/pkglint/regex"
+       "netbsd.org/pkglint/textproc"
+)
 
 // Condition describes a complex license condition.
 // It has either `Name` or `Paren` or `Children` set.
@@ -14,8 +17,8 @@ type Condition struct {
        Children []*Condition `json:",omitempty"`
 }
 
-func Parse(licenses string) *Condition {
-       lexer := &licenseLexer{repl: textproc.NewPrefixReplacer(licenses)}
+func Parse(licenses string, res *regex.Registry) *Condition {
+       lexer := &licenseLexer{repl: textproc.NewPrefixReplacer(licenses, res)}
        result := liyyNewParser().Parse(lexer)
        if result == 0 {
                return lexer.result

Index: pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go:1.1 pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go:1.2
--- pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go:1.1 Tue Jan 17 22:37:28 2017
+++ pkgsrc/pkgtools/pkglint/files/licenses/licenses_test.go     Wed Oct  3 22:27:54 2018
@@ -3,6 +3,7 @@ package licenses
 import (
        "encoding/json"
        "gopkg.in/check.v1"
+       "netbsd.org/pkglint/regex"
        "strings"
        "testing"
 )
@@ -10,11 +11,15 @@ import (
 type Suite struct{}
 
 func (s *Suite) Test_Parse(c *check.C) {
+       res := regex.NewRegistry()
        checkParse := func(cond string, expected string) {
-               c.Check(toJSON(Parse(cond)), check.Equals, expected)
+               c.Check(toJSON(Parse(cond, &res)), check.Equals, expected)
+       }
+       checkParseDeep := func(cond string, expected *Condition) {
+               c.Check(Parse(cond, &res), check.DeepEquals, expected)
        }
 
-       c.Check(Parse("gnu-gpl-v2"), check.DeepEquals, NewSingleton(NewName("gnu-gpl-v2")))
+       checkParseDeep("gnu-gpl-v2", NewSingleton(NewName("gnu-gpl-v2")))
 
        checkParse("gnu-gpl-v2", "{Children:[{Name:gnu-gpl-v2}]}")
        checkParse("a AND b", "{And:true,Children:[{Name:a},{Name:b}]}")
@@ -24,21 +29,18 @@ func (s *Suite) Test_Parse(c *check.C) {
        checkParse("(a OR b) AND c", "{And:true,Children:[{Paren:{Or:true,Children:[{Name:a},{Name:b}]}},{Name:c}]}")
 
        checkParse("a AND b AND c AND d", "{And:true,Children:[{Name:a},{Name:b},{Name:c},{Name:d}]}")
-       c.Check(
-               Parse("a AND b AND c AND d"),
-               check.DeepEquals,
+       checkParseDeep(
+               "a AND b AND c AND d",
                NewAnd(NewName("a"), NewName("b"), NewName("c"), NewName("d")))
 
        checkParse("a OR b OR c OR d", "{Or:true,Children:[{Name:a},{Name:b},{Name:c},{Name:d}]}")
-       c.Check(
-               Parse("a OR b OR c OR d"),
-               check.DeepEquals,
+       checkParseDeep(
+               "a OR b OR c OR d",
                NewOr(NewName("a"), NewName("b"), NewName("c"), NewName("d")))
 
        checkParse("(a OR b) AND (c AND d)", "{And:true,Children:[{Paren:{Or:true,Children:[{Name:a},{Name:b}]}},{Paren:{And:true,Children:[{Name:c},{Name:d}]}}]}")
-       c.Check(
-               (Parse("(a OR b) AND (c AND d)")),
-               check.DeepEquals,
+       checkParseDeep(
+               "(a OR b) AND (c AND d)",
                NewAnd(
                        NewParen(NewOr(NewName("a"), NewName("b"))),
                        NewParen(NewAnd(NewName("c"), NewName("d")))))
@@ -46,9 +48,9 @@ func (s *Suite) Test_Parse(c *check.C) {
        checkParse("a AND b OR c AND d", "{And:true,Or:true,Children:[{Name:a},{Name:b},{Name:c},{Name:d}]}")
        checkParse("((a AND (b AND c)))", "{Children:[{Paren:{Children:[{Paren:{And:true,Children:[{Name:a},{Paren:{And:true,Children:[{Name:b},{Name:c}]}}]}}]}}]}")
 
-       c.Check(Parse("a AND b OR c AND d").String(), check.Equals, "a MIXED b MIXED c MIXED d")
+       c.Check(Parse("a AND b OR c AND d", &res).String(), check.Equals, "a MIXED b MIXED c MIXED d")
 
-       c.Check(Parse("AND artistic"), check.IsNil)
+       c.Check(Parse("AND artistic", &res), check.IsNil)
 }
 
 func (s *Suite) Test_Condition_String(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/regex/regex.go
diff -u pkgsrc/pkgtools/pkglint/files/regex/regex.go:1.3 pkgsrc/pkgtools/pkglint/files/regex/regex.go:1.4
--- pkgsrc/pkgtools/pkglint/files/regex/regex.go:1.3    Thu Aug 16 20:41:42 2018
+++ pkgsrc/pkgtools/pkglint/files/regex/regex.go        Wed Oct  3 22:27:54 2018
@@ -14,97 +14,107 @@ import (
 
 type Pattern string
 
-var (
-       Profiling bool
-)
-
-var (
-       res       = make(map[Pattern]*regexp.Regexp)
-       rematch   = histogram.New()
-       renomatch = histogram.New()
-       retime    = histogram.New()
-)
+type Registry struct {
+       res       map[Pattern]*regexp.Regexp
+       rematch   *histogram.Histogram
+       renomatch *histogram.Histogram
+       retime    *histogram.Histogram
+       profiling bool
+}
+
+func NewRegistry() Registry {
+       return Registry{make(map[Pattern]*regexp.Regexp), nil, nil, nil, false}
+}
+
+func (r *Registry) Profiling() {
+       if !r.profiling {
+               r.rematch = histogram.New()
+               r.renomatch = histogram.New()
+               r.retime = histogram.New()
+               r.profiling = true
+       }
+}
 
-func Compile(re Pattern) *regexp.Regexp {
-       cre := res[re]
+func (r *Registry) Compile(re Pattern) *regexp.Regexp {
+       cre := r.res[re]
        if cre == nil {
                cre = regexp.MustCompile(string(re))
-               res[re] = cre
+               r.res[re] = cre
        }
        return cre
 }
 
-func Match(s string, re Pattern) []string {
-       if !Profiling {
-               return Compile(re).FindStringSubmatch(s)
+func (r *Registry) Match(s string, re Pattern) []string {
+       if !r.profiling {
+               return r.Compile(re).FindStringSubmatch(s)
        }
 
        before := time.Now()
        immediatelyBefore := time.Now()
-       m := Compile(re).FindStringSubmatch(s)
+       m := r.Compile(re).FindStringSubmatch(s)
        after := time.Now()
 
        delay := immediatelyBefore.UnixNano() - before.UnixNano()
        timeTaken := after.UnixNano() - immediatelyBefore.UnixNano() - delay
 
-       retime.Add(string(re), int(timeTaken))
+       r.retime.Add(string(re), int(timeTaken))
        if m != nil {
-               rematch.Add(string(re), 1)
+               r.rematch.Add(string(re), 1)
        } else {
-               renomatch.Add(string(re), 1)
+               r.renomatch.Add(string(re), 1)
        }
        return m
 }
 
-func Matches(s string, re Pattern) bool {
-       matches := Compile(re).MatchString(s)
-       if Profiling {
+func (r *Registry) Matches(s string, re Pattern) bool {
+       matches := r.Compile(re).MatchString(s)
+       if r.profiling {
                if matches {
-                       rematch.Add(string(re), 1)
+                       r.rematch.Add(string(re), 1)
                } else {
-                       renomatch.Add(string(re), 1)
+                       r.renomatch.Add(string(re), 1)
                }
        }
        return matches
 }
 
-func Match1(s string, re Pattern) (matched bool, m1 string) {
-       if m := matchn(s, re, 1); m != nil {
+func (r *Registry) Match1(s string, re Pattern) (matched bool, m1 string) {
+       if m := r.matchn(s, re, 1); m != nil {
                return true, m[1]
        }
        return
 }
 
-func Match2(s string, re Pattern) (matched bool, m1, m2 string) {
-       if m := matchn(s, re, 2); m != nil {
+func (r *Registry) Match2(s string, re Pattern) (matched bool, m1, m2 string) {
+       if m := r.matchn(s, re, 2); m != nil {
                return true, m[1], m[2]
        }
        return
 }
 
-func Match3(s string, re Pattern) (matched bool, m1, m2, m3 string) {
-       if m := matchn(s, re, 3); m != nil {
+func (r *Registry) Match3(s string, re Pattern) (matched bool, m1, m2, m3 string) {
+       if m := r.matchn(s, re, 3); m != nil {
                return true, m[1], m[2], m[3]
        }
        return
 }
 
-func Match4(s string, re Pattern) (matched bool, m1, m2, m3, m4 string) {
-       if m := matchn(s, re, 4); m != nil {
+func (r *Registry) Match4(s string, re Pattern) (matched bool, m1, m2, m3, m4 string) {
+       if m := r.matchn(s, re, 4); m != nil {
                return true, m[1], m[2], m[3], m[4]
        }
        return
 }
 
-func Match5(s string, re Pattern) (matched bool, m1, m2, m3, m4, m5 string) {
-       if m := matchn(s, re, 5); m != nil {
+func (r *Registry) Match5(s string, re Pattern) (matched bool, m1, m2, m3, m4, m5 string) {
+       if m := r.matchn(s, re, 5); m != nil {
                return true, m[1], m[2], m[3], m[4], m[5]
        }
        return
 }
 
-func ReplaceFirst(s string, re Pattern, replacement string) ([]string, string) {
-       if m := Compile(re).FindStringSubmatchIndex(s); m != nil {
+func (r *Registry) ReplaceFirst(s string, re Pattern, replacement string) ([]string, string) {
+       if m := r.Compile(re).FindStringSubmatchIndex(s); m != nil {
                replaced := s[:m[0]] + replacement + s[m[1]:]
                mm := make([]string, len(m)/2)
                for i := 0; i < len(m); i += 2 {
@@ -115,16 +125,16 @@ func ReplaceFirst(s string, re Pattern, 
        return nil, s
 }
 
-func PrintStats(out io.Writer) {
-       if Profiling {
-               rematch.PrintStats("rematch", out, 10)
-               renomatch.PrintStats("renomatch", out, 10)
-               retime.PrintStats("retime", out, 10)
+func (r *Registry) PrintStats(out io.Writer) {
+       if r.profiling {
+               r.rematch.PrintStats("rematch", out, 10)
+               r.renomatch.PrintStats("renomatch", out, 10)
+               r.retime.PrintStats("retime", out, 10)
        }
 }
 
-func matchn(s string, re Pattern, n int) []string {
-       if m := Match(s, re); m != nil {
+func (r *Registry) matchn(s string, re Pattern, n int) []string {
+       if m := r.Match(s, re); m != nil {
                if len(m) != 1+n {
                        panic(fmt.Sprintf("expected match%d, got match%d for %q", len(m)-1, n, re))
                }

Index: pkgsrc/pkgtools/pkglint/files/textproc/prefixreplacer.go
diff -u pkgsrc/pkgtools/pkglint/files/textproc/prefixreplacer.go:1.5 pkgsrc/pkgtools/pkglint/files/textproc/prefixreplacer.go:1.6
--- pkgsrc/pkgtools/pkglint/files/textproc/prefixreplacer.go:1.5        Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/textproc/prefixreplacer.go    Wed Oct  3 22:27:54 2018
@@ -17,10 +17,11 @@ type PrefixReplacer struct {
        rest string
        s    string
        m    []string
+       res  *regex.Registry
 }
 
-func NewPrefixReplacer(s string) *PrefixReplacer {
-       return &PrefixReplacer{s, "", nil}
+func NewPrefixReplacer(s string, res *regex.Registry) *PrefixReplacer {
+       return &PrefixReplacer{s, "", nil, res}
 }
 
 func (pr *PrefixReplacer) EOF() bool {
@@ -98,10 +99,10 @@ func (pr *PrefixReplacer) AdvanceRegexp(
        if !strings.HasPrefix(string(re), "^") {
                panic(fmt.Sprintf("PrefixReplacer.AdvanceRegexp: regular expression %q must have prefix %q.", re, "^"))
        }
-       if Testing && regex.Matches("", re) {
+       if Testing && pr.res.Matches("", re) {
                panic(fmt.Sprintf("PrefixReplacer.AdvanceRegexp: the empty string must not match the regular expression %q.", re))
        }
-       if m := regex.Match(pr.rest, re); m != nil {
+       if m := pr.res.Match(pr.rest, re); m != nil {
                if trace.Tracing {
                        trace.Stepf("PrefixReplacer.AdvanceRegexp(%q, %q, %q)", pr.rest, re, m[0])
                }
@@ -153,5 +154,5 @@ func (pr *PrefixReplacer) HasPrefix(str 
 }
 
 func (pr *PrefixReplacer) HasPrefixRegexp(re regex.Pattern) bool {
-       return regex.Matches(pr.rest, re)
+       return pr.res.Matches(pr.rest, re)
 }

Added files:

Index: pkgsrc/pkgtools/pkglint/files/fuzzer_test.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/fuzzer_test.go:1.1
--- /dev/null   Wed Oct  3 22:27:54 2018
+++ pkgsrc/pkgtools/pkglint/files/fuzzer_test.go        Wed Oct  3 22:27:53 2018
@@ -0,0 +1,90 @@
+package main
+
+import (
+       "gopkg.in/check.v1"
+       "math/rand"
+)
+
+type Fuzzer struct {
+       seed  int64
+       rnd   *rand.Rand
+       stock []struct {
+               r      rune
+               weight int
+       }
+       total int
+       last  string
+       ok    bool
+}
+
+func NewFuzzer(seed ...int64) *Fuzzer {
+       var actualSeed int64
+       if len(seed) != 0 {
+               actualSeed = seed[0]
+       } else {
+               actualSeed = rand.Int63()
+       }
+       return &Fuzzer{seed: actualSeed, rnd: rand.New(rand.NewSource(actualSeed))}
+}
+
+// Char randomly generates a character from the given set.
+// Each character has the given weight.
+func (f *Fuzzer) Char(set string, weight int) {
+       for _, r := range set {
+               f.addChar(r, weight)
+       }
+}
+
+// Range randomly generates a character from the given range.
+// Each character has the given weight.
+func (f *Fuzzer) Range(minIncl, maxIncl rune, weight int) {
+       for r := minIncl; r <= maxIncl; r++ {
+               f.addChar(r, weight)
+       }
+}
+
+func (f *Fuzzer) addChar(r rune, weight int) {
+       f.stock = append(f.stock, struct {
+               r      rune
+               weight int
+       }{r, weight})
+       f.total += weight
+}
+
+func (f *Fuzzer) Generate(length int) string {
+       s := ""
+       for i := 0; i < length; i++ {
+               s += string(f.randomChar())
+       }
+       f.last = s
+       return s
+}
+
+func (f *Fuzzer) randomChar() rune {
+       i := int(f.rnd.Int31n(int32(f.total)))
+       for _, entry := range f.stock {
+               i -= entry.weight
+               if i < 0 {
+                       return entry.r
+               }
+       }
+       panic("Out of stock")
+}
+
+func (f *Fuzzer) CheckOk() {
+       if !f.ok {
+               dummyLine.Errorf("Fuzzing failed with seed %d, last generated value: %s", f.seed, f.last)
+       }
+}
+
+func (f *Fuzzer) Ok() { f.ok = true }
+
+func (s *Suite) Test_Fuzzer__out_of_stock(c *check.C) {
+       fuzzer := NewFuzzer(0)
+       fuzzer.total = 1 // Trick the fuzzer to achieve full code coverage.
+
+       c.Check(
+               func() { fuzzer.Generate(1) },
+               check.Panics,
+               "Out of stock")
+}
Index: pkgsrc/pkgtools/pkglint/files/testnames_test.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/testnames_test.go:1.1
--- /dev/null   Wed Oct  3 22:27:54 2018
+++ pkgsrc/pkgtools/pkglint/files/testnames_test.go     Wed Oct  3 22:27:53 2018
@@ -0,0 +1,130 @@
+package main
+
+import (
+       "go/ast"
+       "go/parser"
+       "go/token"
+       "gopkg.in/check.v1"
+       "os"
+       "sort"
+       "strings"
+)
+
+// Ensures that all test names follow a common naming scheme:
+//
+//  Test_${Type}_${Method}__${description_using_underscores}
+func (s *Suite) Test__test_names(c *check.C) {
+
+       // addTestee adds a single type or function declaration
+       // to the testees.
+       addTestee := func(testees *[]string, decl ast.Decl) {
+               switch decl := decl.(type) {
+
+               case *ast.GenDecl:
+                       for _, spec := range decl.Specs {
+                               switch spec := spec.(type) {
+                               case *ast.TypeSpec:
+                                       *testees = append(*testees, spec.Name.Name)
+                               }
+                       }
+
+               case *ast.FuncDecl:
+                       typePrefix := ""
+                       if decl.Recv != nil {
+                               typeExpr := decl.Recv.List[0].Type.(ast.Expr)
+                               var typeName string
+                               if star, ok := typeExpr.(*ast.StarExpr); ok {
+                                       typeName = star.X.(*ast.Ident).Name
+                               } else {
+                                       typeName = typeExpr.(*ast.Ident).Name
+                               }
+                               typePrefix = strings.TrimSuffix(typeName, "Impl") + "."
+                       }
+                       *testees = append(*testees, typePrefix+decl.Name.Name)
+               }
+       }
+
+       // loadAllTestees returns all type, function and method names
+       // from the current package, in the form FunctionName or
+       // TypeName.MethodName (omitting the * from the type name).
+       loadAllTestees := func() []string {
+               fset := token.NewFileSet()
+               pkgs, err := parser.ParseDir(fset, ".", func(fi os.FileInfo) bool { return true }, 0)
+               if err != nil {
+                       panic(err)
+               }
+
+               var typesAndFunctions []string
+               for _, file := range pkgs["main"].Files {
+                       for _, decl := range file.Decls {
+                               addTestee(&typesAndFunctions, decl)
+                       }
+               }
+
+               sort.Strings(typesAndFunctions)
+               return typesAndFunctions
+       }
+
+       generateAllowedPrefixes := func(typesAndFunctions []string) map[string]bool {
+               prefixes := make(map[string]bool)
+               for _, funcName := range typesAndFunctions {
+                       prefix := strings.Replace(funcName, ".", "_", 1)
+                       prefixes[prefix] = true
+               }
+
+               // Allow some special test name prefixes.
+               prefixes["Varalign"] = true
+               prefixes["ShellParser"] = true
+               return prefixes
+       }
+
+       checkTestName := func(fullTestMethod string, testee string, descr string, prefixes map[string]bool) {
+               if !prefixes[testee] {
+                       c.Errorf("%s: Testee %q not found.\n", fullTestMethod, testee)
+               }
+               if matches(descr, `\p{Ll}\p{Lu}`) {
+                       switch descr {
+                       case "comparing_YesNo_variable_to_string",
+                               "GitHub",
+                               "enumFrom",
+                               "dquotBacktDquot",
+                               "and_getSubdirs":
+                               // These exceptions are ok.
+
+                       default:
+                               c.Errorf("%s: Test description must not use CamelCase.\n", fullTestMethod)
+                       }
+               }
+       }
+
+       checkAll := func(typesAndFunctions []string, prefixes map[string]bool) {
+               for _, funcName := range typesAndFunctions {
+                       typeAndMethod := strings.SplitN(funcName, ".", 2)
+                       if len(typeAndMethod) == 2 {
+                               method := typeAndMethod[1]
+                               switch {
+                               case !hasPrefix(method, "Test"):
+                                       // Ignore
+
+                               case hasPrefix(method, "Test__"):
+                                       // OK
+
+                               case hasPrefix(method, "Test_"):
+                                       refAndDescr := strings.SplitN(method[5:], "__", 2)
+                                       descr := ""
+                                       if len(refAndDescr) > 1 {
+                                               descr = refAndDescr[1]
+                                       }
+                                       checkTestName(funcName, refAndDescr[0], descr, prefixes)
+
+                               default:
+                                       c.Errorf("%s: Missing underscore.\n", funcName)
+                               }
+                       }
+               }
+       }
+
+       testees := loadAllTestees()
+       prefixes := generateAllowedPrefixes(testees)
+       checkAll(testees, prefixes)
+}



Home | Main Index | Thread Index | Old Index