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:           Sun Dec  2 01:57:48 UTC 2018

Modified Files:
        pkgsrc/pkgtools/pkglint: Makefile
        pkgsrc/pkgtools/pkglint/files: alternatives.go alternatives_test.go
            autofix.go autofix_test.go buildlink3.go buildlink3_test.go
            check_test.go distinfo.go distinfo_test.go files.go files_test.go
            licenses.go licenses_test.go line.go line_test.go linechecker.go
            linechecker_test.go lines.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.go
            mkshparser_test.go mkshtypes.go mkshwalker_test.go mktypes.go
            options.go package.go package_test.go parser.go parser_test.go
            patches.go patches_test.go pkglint.go pkglint_test.go pkgsrc.go
            pkgsrc_test.go plist.go plist_test.go shell.go shell.y
            shell_test.go shtokenizer.go shtokenizer_test.go shtypes.go
            shtypes_test.go substcontext.go substcontext_test.go tools.go
            tools_test.go toplevel.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.go getopt_test.go
        pkgsrc/pkgtools/pkglint/files/intqa: testnames.go
        pkgsrc/pkgtools/pkglint/files/licenses: licenses.go
        pkgsrc/pkgtools/pkglint/files/textproc: lexer.go lexer_test.go
        pkgsrc/pkgtools/pkglint/files/trace: tracing.go
Added Files:
        pkgsrc/pkgtools/pkglint/files: lines_test.go
        pkgsrc/pkgtools/pkglint/files/intqa: ideas.go
        pkgsrc/pkgtools/pkglint/files/textproc: lexer_bench_test.go

Log Message:
pkgtools/pkglint: update to 5.6.7

Changes since 5.6.6:

User-defined variables that are not yet added to BUILD_DEFS are only
reported once per file.

Unnecessary space after variable names is only worth a note instead of
a warning. Example:

        MASTER_SITES =  https://cdn.example.org/

All variable names that are defined in the pkgsrc infrastructure are
assumed to be available to the package Makefiles. This reduces the
number of wrong "used but not defined" warnings, at the expense of

Variable names that are used in other variable names are checked
whether they are defined somewhere. Example:

        CFLAGS+=        ${CFLAGS.${PARAM}}      # PARAM is now checked

In SUBST_SED, when the pattern is s,@VAR@,${VAR}, or a slight variant
thereof, pkglint suggests to define SUBST_VARS instead, which frees the
package author from thinking about how to escape special characters and
is generally easier to read. Example:

        SUBST_SED.class=        s,@VAR@,${VAR:Q},

        SUBST_VARS.class=       VAR

Directives like .if !defined(...) are now handled the same whether or
not there is a space after before the (...).

The check for locally modified files now works independently of the
timezone.

As always, lots of refactorings have happened under the hood. Many small
bugs have been discovered and fixed accordingly.


To generate a diff of this commit:
cvs rdiff -u -r1.558 -r1.559 pkgsrc/pkgtools/pkglint/Makefile
cvs rdiff -u -r1.6 -r1.7 pkgsrc/pkgtools/pkglint/files/alternatives.go \
    pkgsrc/pkgtools/pkglint/files/logging_test.go \
    pkgsrc/pkgtools/pkglint/files/mktypes.go \
    pkgsrc/pkgtools/pkglint/files/vardefs_test.go
cvs rdiff -u -r1.7 -r1.8 pkgsrc/pkgtools/pkglint/files/alternatives_test.go \
    pkgsrc/pkgtools/pkglint/files/mkshparser_test.go \
    pkgsrc/pkgtools/pkglint/files/options.go \
    pkgsrc/pkgtools/pkglint/files/tools.go \
    pkgsrc/pkgtools/pkglint/files/tools_test.go
cvs rdiff -u -r1.12 -r1.13 pkgsrc/pkgtools/pkglint/files/autofix.go \
    pkgsrc/pkgtools/pkglint/files/autofix_test.go \
    pkgsrc/pkgtools/pkglint/files/pkgsrc.go \
    pkgsrc/pkgtools/pkglint/files/shtokenizer.go
cvs rdiff -u -r1.14 -r1.15 pkgsrc/pkgtools/pkglint/files/buildlink3.go \
    pkgsrc/pkgtools/pkglint/files/line_test.go \
    pkgsrc/pkgtools/pkglint/files/toplevel.go
cvs rdiff -u -r1.20 -r1.21 pkgsrc/pkgtools/pkglint/files/buildlink3_test.go \
    pkgsrc/pkgtools/pkglint/files/vartype.go
cvs rdiff -u -r1.28 -r1.29 pkgsrc/pkgtools/pkglint/files/check_test.go \
    pkgsrc/pkgtools/pkglint/files/shell.go
cvs rdiff -u -r1.23 -r1.24 pkgsrc/pkgtools/pkglint/files/distinfo.go \
    pkgsrc/pkgtools/pkglint/files/patches_test.go
cvs rdiff -u -r1.19 -r1.20 pkgsrc/pkgtools/pkglint/files/distinfo_test.go \
    pkgsrc/pkgtools/pkglint/files/files_test.go \
    pkgsrc/pkgtools/pkglint/files/mkparser.go
cvs rdiff -u -r1.21 -r1.22 pkgsrc/pkgtools/pkglint/files/files.go
cvs rdiff -u -r1.16 -r1.17 pkgsrc/pkgtools/pkglint/files/licenses.go \
    pkgsrc/pkgtools/pkglint/files/logging.go \
    pkgsrc/pkgtools/pkglint/files/substcontext_test.go
cvs rdiff -u -r1.17 -r1.18 pkgsrc/pkgtools/pkglint/files/licenses_test.go \
    pkgsrc/pkgtools/pkglint/files/mkparser_test.go \
    pkgsrc/pkgtools/pkglint/files/util_test.go
cvs rdiff -u -r1.27 -r1.28 pkgsrc/pkgtools/pkglint/files/line.go \
    pkgsrc/pkgtools/pkglint/files/pkglint_test.go \
    pkgsrc/pkgtools/pkglint/files/plist_test.go
cvs rdiff -u -r1.9 -r1.10 pkgsrc/pkgtools/pkglint/files/linechecker.go \
    pkgsrc/pkgtools/pkglint/files/linechecker_test.go \
    pkgsrc/pkgtools/pkglint/files/mkshtypes.go \
    pkgsrc/pkgtools/pkglint/files/parser_test.go
cvs rdiff -u -r1.1 -r1.2 pkgsrc/pkgtools/pkglint/files/lines.go
cvs rdiff -u -r0 -r1.1 pkgsrc/pkgtools/pkglint/files/lines_test.go
cvs rdiff -u -r1.40 -r1.41 pkgsrc/pkgtools/pkglint/files/mkline.go \
    pkgsrc/pkgtools/pkglint/files/pkglint.go
cvs rdiff -u -r1.44 -r1.45 pkgsrc/pkgtools/pkglint/files/mkline_test.go
cvs rdiff -u -r1.22 -r1.23 pkgsrc/pkgtools/pkglint/files/mklinechecker.go
cvs rdiff -u -r1.18 -r1.19 \
    pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go
cvs rdiff -u -r1.34 -r1.35 pkgsrc/pkgtools/pkglint/files/mklines.go
cvs rdiff -u -r1.30 -r1.31 pkgsrc/pkgtools/pkglint/files/mklines_test.go
cvs rdiff -u -r1.8 -r1.9 pkgsrc/pkgtools/pkglint/files/mkshparser.go
cvs rdiff -u -r1.4 -r1.5 pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go
cvs rdiff -u -r1.38 -r1.39 pkgsrc/pkgtools/pkglint/files/package.go
cvs rdiff -u -r1.32 -r1.33 pkgsrc/pkgtools/pkglint/files/package_test.go
cvs rdiff -u -r1.11 -r1.12 pkgsrc/pkgtools/pkglint/files/parser.go
cvs rdiff -u -r1.24 -r1.25 pkgsrc/pkgtools/pkglint/files/patches.go
cvs rdiff -u -r1.10 -r1.11 pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go \
    pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go \
    pkgsrc/pkgtools/pkglint/files/shtypes.go \
    pkgsrc/pkgtools/pkglint/files/vartype_test.go
cvs rdiff -u -r1.31 -r1.32 pkgsrc/pkgtools/pkglint/files/plist.go \
    pkgsrc/pkgtools/pkglint/files/util.go
cvs rdiff -u -r1.2 -r1.3 pkgsrc/pkgtools/pkglint/files/shell.y
cvs rdiff -u -r1.33 -r1.34 pkgsrc/pkgtools/pkglint/files/shell_test.go
cvs rdiff -u -r1.5 -r1.6 pkgsrc/pkgtools/pkglint/files/shtypes_test.go
cvs rdiff -u -r1.15 -r1.16 pkgsrc/pkgtools/pkglint/files/substcontext.go
cvs rdiff -u -r1.49 -r1.50 pkgsrc/pkgtools/pkglint/files/vardefs.go
cvs rdiff -u -r1.43 -r1.44 pkgsrc/pkgtools/pkglint/files/vartypecheck.go
cvs rdiff -u -r1.35 -r1.36 pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go
cvs rdiff -u -r1.6 -r1.7 pkgsrc/pkgtools/pkglint/files/getopt/getopt.go
cvs rdiff -u -r1.8 -r1.9 pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go
cvs rdiff -u -r0 -r1.1 pkgsrc/pkgtools/pkglint/files/intqa/ideas.go
cvs rdiff -u -r1.1 -r1.2 pkgsrc/pkgtools/pkglint/files/intqa/testnames.go
cvs rdiff -u -r1.5 -r1.6 pkgsrc/pkgtools/pkglint/files/licenses/licenses.go
cvs rdiff -u -r1.1 -r1.2 pkgsrc/pkgtools/pkglint/files/textproc/lexer.go \
    pkgsrc/pkgtools/pkglint/files/textproc/lexer_test.go
cvs rdiff -u -r0 -r1.1 \
    pkgsrc/pkgtools/pkglint/files/textproc/lexer_bench_test.go
cvs rdiff -u -r1.3 -r1.4 pkgsrc/pkgtools/pkglint/files/trace/tracing.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.558 pkgsrc/pkgtools/pkglint/Makefile:1.559
--- pkgsrc/pkgtools/pkglint/Makefile:1.558      Sun Nov 11 20:55:23 2018
+++ pkgsrc/pkgtools/pkglint/Makefile    Sun Dec  2 01:57:48 2018
@@ -1,6 +1,6 @@
-# $NetBSD: Makefile,v 1.558 2018/11/11 20:55:23 rillig Exp $
+# $NetBSD: Makefile,v 1.559 2018/12/02 01:57:48 rillig Exp $
 
-PKGNAME=       pkglint-5.6.6
+PKGNAME=       pkglint-5.6.7
 CATEGORIES=    pkgtools
 DISTNAME=      tools
 MASTER_SITES=  ${MASTER_SITE_GITHUB:=golang/}

Index: pkgsrc/pkgtools/pkglint/files/alternatives.go
diff -u pkgsrc/pkgtools/pkglint/files/alternatives.go:1.6 pkgsrc/pkgtools/pkglint/files/alternatives.go:1.7
--- pkgsrc/pkgtools/pkglint/files/alternatives.go:1.6   Wed Nov  7 20:58:22 2018
+++ pkgsrc/pkgtools/pkglint/files/alternatives.go       Sun Dec  2 01:57:48 2018
@@ -2,8 +2,8 @@ package main
 
 import "strings"
 
-func CheckfileAlternatives(fileName string) {
-       lines := Load(fileName, NotEmpty|LogErrors)
+func CheckfileAlternatives(filename string) {
+       lines := Load(filename, NotEmpty|LogErrors)
        if lines == nil {
                return
        }
@@ -32,7 +32,7 @@ func CheckfileAlternatives(fileName stri
                        }
 
                        fix := line.Autofix()
-                       fix.Notef("@PREFIX@/ can be omitted from the file name.")
+                       fix.Notef("@PREFIX@/ can be omitted from the filename.")
                        fix.Explain(
                                "The alternative implementation is always interpreted relative to",
                                "${PREFIX}.")
@@ -40,7 +40,7 @@ func CheckfileAlternatives(fileName stri
                        fix.Apply()
                } else {
                        line.Errorf("Invalid ALTERNATIVES line %q.", line.Text)
-                       Explain(
+                       G.Explain(
                                sprintf("Run %q for more information.", makeHelp("alternatives")))
                }
        }
Index: pkgsrc/pkgtools/pkglint/files/logging_test.go
diff -u pkgsrc/pkgtools/pkglint/files/logging_test.go:1.6 pkgsrc/pkgtools/pkglint/files/logging_test.go:1.7
--- pkgsrc/pkgtools/pkglint/files/logging_test.go:1.6   Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/logging_test.go       Sun Dec  2 01:57:48 2018
@@ -1,6 +1,102 @@
 package main
 
-import "gopkg.in/check.v1"
+import (
+       "gopkg.in/check.v1"
+       "strings"
+)
+
+// Calling Logf without further preparation just logs the message.
+// Suppressing duplicate messages or filtering messages happens
+// in other methods of the Logger, namely Relevant, FirstTime, Diag.
+func (s *Suite) Test_Logger_Logf(c *check.C) {
+       var sw strings.Builder
+       logger := Logger{out: NewSeparatorWriter(&sw)}
+
+       logger.Logf(Error, "filename", "3", "Blue should be %s.", "Blue should be orange.")
+
+       c.Check(sw.String(), equals, ""+
+               "ERROR: filename:3: Blue should be orange.\n")
+}
+
+// Logf doesn't filter duplicates, but Diag does.
+func (s *Suite) Test_Logger_Logf__duplicates(c *check.C) {
+       var sw strings.Builder
+       logger := Logger{out: NewSeparatorWriter(&sw)}
+
+       logger.Logf(Error, "filename", "3", "Blue should be %s.", "Blue should be orange.")
+       logger.Logf(Error, "filename", "3", "Blue should be %s.", "Blue should be orange.")
+
+       c.Check(sw.String(), equals, ""+
+               "ERROR: filename:3: Blue should be orange.\n"+
+               "ERROR: filename:3: Blue should be orange.\n")
+}
+
+// Ensure that suppressing a diagnostic doesn't influence later calls to Logf.
+func (s *Suite) Test_Logger_Logf__mixed_with_Diag(c *check.C) {
+       t := s.Init(c)
+
+       var sw strings.Builder
+       logger := Logger{out: NewSeparatorWriter(&sw)}
+       line := t.NewLine("filename", 3, "Text")
+
+       logger.Logf(Error, "filename", "3", "Logf output 1.", "Logf output 1.")
+       logger.Diag(line, Error, "Diag %s.", "1")
+       logger.Logf(Error, "filename", "3", "Logf output 2.", "Logf output 2.")
+       logger.Diag(line, Error, "Diag %s.", "1") // Duplicate, therefore suppressed
+       logger.Logf(Error, "filename", "3", "Logf output 3.", "Logf output 3.")
+
+       c.Check(sw.String(), equals, ""+
+               "ERROR: filename:3: Logf output 1.\n"+
+               "ERROR: filename:3: Diag 1.\n"+
+               "ERROR: filename:3: Logf output 2.\n"+
+               "ERROR: filename:3: Logf output 3.\n")
+}
+
+// Diag filters duplicate messages, unlike Logf.
+func (s *Suite) Test_Logger_Diag__duplicates(c *check.C) {
+       t := s.Init(c)
+
+       var sw strings.Builder
+       logger := Logger{out: NewSeparatorWriter(&sw)}
+       line := t.NewLine("filename", 3, "Text")
+
+       logger.Diag(line, Error, "Blue should be %s.", "orange")
+       logger.Diag(line, Error, "Blue should be %s.", "orange")
+
+       c.Check(sw.String(), equals, ""+
+               "ERROR: filename:3: Blue should be orange.\n")
+}
+
+// Explanations are associated with their diagnostics. Therefore, when one
+// of them is suppressed, the other is suppressed, too.
+func (s *Suite) Test_Logger_Diag__explanation(c *check.C) {
+       t := s.Init(c)
+
+       var sw strings.Builder
+       logger := Logger{out: NewSeparatorWriter(&sw)}
+       logger.Opts.Explain = true
+       line := t.NewLine("filename", 3, "Text")
+
+       logger.Diag(line, Error, "Blue should be %s.", "orange")
+       logger.Explain(
+               "The colors have changed.")
+
+       logger.Diag(line, Error, "Blue should be %s.", "orange")
+       logger.Explain(
+               "The colors have changed.")
+
+       // Even when the text of the explanation is not the same, it is still
+       // suppressed since it belongs to the diagnostic.
+       logger.Diag(line, Error, "Blue should be %s.", "orange")
+       logger.Explain(
+               "The colors have further changed.")
+
+       c.Check(sw.String(), equals, ""+
+               "ERROR: filename:3: Blue should be orange.\n"+
+               "\n"+
+               "\tThe colors have changed.\n"+
+               "\n")
+}
 
 // Since the --source option generates multi-line diagnostics,
 // they are separated by an empty line.
@@ -15,7 +111,7 @@ import "gopkg.in/check.v1"
 // to first show the code and then show the diagnostic. This allows
 // the diagnostics to underline the relevant part of the source code
 // and reminds of the squiggly line used for spellchecking.
-func (s *Suite) Test__show_source_separator(c *check.C) {
+func (s *Suite) Test_Logger__show_source_separator(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--source")
@@ -47,6 +143,12 @@ func (s *Suite) Test__show_source_separa
                "WARN: ~/DESCR:3: Using \"third\" is deprecated.")
 }
 
+// When the --show-autofix option is given, the warning is shown first,
+// without the affected source, even if the --source option is also given.
+// This is because the original and the modified source are shown after
+// the "Replacing" message. Since these are shown in diff style, they
+// must be kept together. And since the "+" line must be below the "Replacing"
+// line, this order of lines seems to be the most intuitive.
 func (s *Suite) Test__show_source_separator_show_autofix(c *check.C) {
        t := s.Init(c)
 
@@ -80,6 +182,11 @@ func (s *Suite) Test__show_source_separa
                "+\tThe bronze medal line")
 }
 
+// See Test__show_source_separator_show_autofix for the ordering of the
+// output lines.
+//
+// TODO: Giving the diagnostics again would be useful, but the warning and
+// error counters should not be affected, as well as the exitcode.
 func (s *Suite) Test__show_source_separator_autofix(c *check.C) {
        t := s.Init(c)
 
@@ -111,39 +218,340 @@ func (s *Suite) Test__show_source_separa
                "+\tThe bronze medal line")
 }
 
-func (s *Suite) Test_Explain__only(c *check.C) {
+func (s *Suite) Test_Logger_Explain__only(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--only", "interesting", "--explain")
        line := t.NewLine("Makefile", 27, "The old song")
 
-       line.Warnf("Filtered warning.")               // Is not logged.
-       Explain("Explanation for the above warning.") // Neither is this explanation logged.
+       // Neither the warning nor the corresponding explanation are logged.
+       line.Warnf("Filtered warning.")
+       G.Explain("Explanation for the above warning.")
 
-       line.Warnf("What an interesting line.")
-       Explain("This explanation is logged.")
+       line.Notef("What an interesting line.")
+       G.Explain("This explanation is logged.")
 
        t.CheckOutputLines(
-               "WARN: Makefile:27: What an interesting line.",
+               "NOTE: Makefile:27: What an interesting line.",
                "",
                "\tThis explanation is logged.",
                "")
 }
 
-func (s *Suite) Test_logf__duplicate_messages(c *check.C) {
+func (s *Suite) Test_Logger_Explain__show_autofix(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("--explain", "--show-autofix")
+       line := t.NewLine("Makefile", 27, "The old song")
+
+       line.Warnf("Warning without fix.")
+       line.Explain(
+               "Explanation for warning without fix.")
+
+       fix := line.Autofix()
+       fix.Warnf("Warning with fix.")
+       fix.Explain(
+               "Explanation for warning with fix.")
+       fix.Replace("old", "new")
+       fix.Apply()
+
+       // Since the warning without fix doesn't fix anything, it is filtered out.
+       // So is the corresponding explanation.
+       t.CheckOutputLines(
+               "WARN: Makefile:27: Warning with fix.",
+               "AUTOFIX: Makefile:27: Replacing \"old\" with \"new\".",
+               "",
+               "\tExplanation for warning with fix.",
+               "")
+}
+
+func (s *Suite) Test_Logger_Explain__show_autofix_and_source(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("--explain", "--show-autofix", "--source")
+       line := t.NewLine("Makefile", 27, "The old song")
+
+       line.Warnf("Warning without fix.")
+       line.Explain(
+               "Explanation for warning without fix.")
+
+       fix := line.Autofix()
+       fix.Warnf("Warning with fix.")
+       fix.Explain(
+               "Explanation for warning with fix.")
+       fix.Replace("old", "new")
+       fix.Apply()
+
+       // Since the warning without fix doesn't fix anything, it is filtered out.
+       // So is the corresponding explanation.
+       t.CheckOutputLines(
+               "WARN: Makefile:27: Warning with fix.",
+               "AUTOFIX: Makefile:27: Replacing \"old\" with \"new\".",
+               "-\tThe old song",
+               "+\tThe new song",
+               "",
+               "\tExplanation for warning with fix.",
+               "")
+}
+
+// When the --autofix option is given, the warnings are not shown, therefore it doesn't
+// make sense to show the explanation for the warning.
+func (s *Suite) Test_Logger_Explain__autofix_and_source(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("--explain", "--autofix", "--source")
+       line := t.NewLine("Makefile", 27, "The old song")
+
+       line.Warnf("Warning without fix.")
+       line.Explain(
+               "Explanation for warning without fix.")
+
+       fix := line.Autofix()
+       fix.Warnf("Warning with fix.")
+       fix.Explain(
+               "Explanation for warning with fix.")
+       fix.Replace("old", "new")
+       fix.Apply()
+
+       // Since the warning without fix doesn't fix anything, it is filtered out.
+       // So is the corresponding explanation.
+       t.CheckOutputLines(
+               "AUTOFIX: Makefile:27: Replacing \"old\" with \"new\".",
+               "-\tThe old song",
+               "+\tThe new song")
+}
+
+// When an explanation consists of multiple paragraphs, it contains some empty lines.
+// When printing these lines, there is no need to write the tab that is used for indenting
+// the normal lines.
+//
+// Since pkglint likes to complain about trailing whitespace, it should not generate it itself.
+func (s *Suite) Test_Logger_Explain__empty_lines(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--explain")
-       G.Opts.LogVerbose = false
+       line := t.NewLine("Makefile", 27, "The old song")
+
+       line.Warnf("A normal warning.")
+       line.Explain(
+               "Paragraph 1 of the explanation.",
+               "",
+               "Paragraph 2 of the explanation.")
+
+       t.CheckOutputLines(
+               "WARN: Makefile:27: A normal warning.",
+               "",
+               "\tParagraph 1 of the explanation.",
+               "",
+               "\tParagraph 2 of the explanation.",
+               "")
+}
+
+func (s *Suite) Test_Logger_ShowSummary__explanations_with_only(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("--only", "interesting")
+       line := t.NewLine("Makefile", 27, "The old song")
+
+       // Neither the warning nor the corresponding explanation are logged.
+       line.Warnf("Filtered warning.")
+       G.Explain("Explanation for the above warning.")
+       G.ShowSummary()
+
+       // Since the above warning is filtered out by the --only option,
+       // adding --explain to the options would not show any explanation.
+       // Therefore, "Run \"pkglint -e\"" is not advertised in this case,
+       // but see below.
+       c.Check(G.explanationsAvailable, equals, false)
+       t.CheckOutputLines(
+               "Looks fine.")
+
+       line.Warnf("This warning is interesting.")
+       G.Explain("This explanation is available.")
+       G.ShowSummary()
+
+       c.Check(G.explanationsAvailable, equals, true)
+       t.CheckOutputLines(
+               "WARN: Makefile:27: This warning is interesting.",
+               "0 errors and 1 warning found.",
+               "(Run \"pkglint -e\" to show explanations.)")
+}
+
+func (s *Suite) Test_Logger_ShowSummary__looks_fine(c *check.C) {
+       t := s.Init(c)
+
+       logger := Logger{out: NewSeparatorWriter(&t.stdout)}
+
+       logger.ShowSummary()
+
+       t.CheckOutputLines(
+               "Looks fine.")
+}
+
+func (s *Suite) Test_Logger_ShowSummary__1_error_1_warning(c *check.C) {
+       t := s.Init(c)
+
+       logger := Logger{out: NewSeparatorWriter(&t.stdout)}
+       logger.Logf(Error, "", "", ".", ".")
+       logger.Logf(Warn, "", "", ".", ".")
+
+       logger.ShowSummary()
+
+       t.CheckOutputLines(
+               "ERROR: .",
+               "WARN: .",
+               "1 error and 1 warning found.")
+}
+
+func (s *Suite) Test_Logger_ShowSummary__2_errors_3_warnings(c *check.C) {
+       t := s.Init(c)
+
+       logger := Logger{out: NewSeparatorWriter(&t.stdout)}
+       logger.Logf(Error, "", "", "1.", "1.")
+       logger.Logf(Error, "", "", "2.", "2.")
+       logger.Logf(Warn, "", "", "3.", "3.")
+       logger.Logf(Warn, "", "", "4.", "4.")
+       logger.Logf(Warn, "", "", "5.", "5.")
+
+       logger.ShowSummary()
+
+       t.CheckOutputLines(
+               "ERROR: 1.",
+               "ERROR: 2.",
+               "WARN: 3.",
+               "WARN: 4.",
+               "WARN: 5.",
+               "2 errors and 3 warnings found.")
+}
+
+func (s *Suite) Test_Logger_ShowSummary__looks_fine_quiet(c *check.C) {
+       t := s.Init(c)
+
+       logger := Logger{out: NewSeparatorWriter(&t.stdout)}
+       logger.Opts.Quiet = true
+
+       logger.ShowSummary()
+
+       t.CheckOutputEmpty()
+}
+
+func (s *Suite) Test_Logger_ShowSummary__1_error_1_warning_quiet(c *check.C) {
+       t := s.Init(c)
+
+       logger := Logger{out: NewSeparatorWriter(&t.stdout)}
+       logger.Opts.Quiet = true
+       logger.Logf(Error, "", "", ".", ".")
+       logger.Logf(Warn, "", "", ".", ".")
+
+       logger.ShowSummary()
+
+       t.CheckOutputLines(
+               "ERROR: .",
+               "WARN: .")
+}
+
+func (s *Suite) Test_Logger_ShowSummary__explanations_available(c *check.C) {
+       t := s.Init(c)
+
+       logger := Logger{out: NewSeparatorWriter(&t.stdout)}
+       logger.Logf(Error, "", "", ".", ".")
+       logger.Explain(
+               "Explanation.")
+
+       logger.ShowSummary()
+
+       t.CheckOutputLines(
+               "ERROR: .",
+               "1 error and 0 warnings found.",
+               "(Run \"pkglint -e\" to show explanations.)")
+}
+
+func (s *Suite) Test_Logger_ShowSummary__explanations_available_in_explain_mode(c *check.C) {
+       t := s.Init(c)
+
+       logger := Logger{out: NewSeparatorWriter(&t.stdout)}
+       logger.Logf(Error, "", "", ".", ".")
+       logger.Explain(
+               "Explanation.")
+
+       // Since the --explain option is already given, it need not be advertised.
+       logger.Opts.Explain = true
+
+       logger.ShowSummary()
+
+       t.CheckOutputLines(
+               "ERROR: .",
+               "1 error and 0 warnings found.")
+}
+
+func (s *Suite) Test_Logger_ShowSummary__autofix_available(c *check.C) {
+       t := s.Init(c)
+
+       logger := Logger{out: NewSeparatorWriter(&t.stdout)}
+       logger.autofixAvailable = true // See SaveAutofixChanges
+
+       logger.ShowSummary()
+
+       t.CheckOutputLines(
+               "Looks fine.",
+               "(Run \"pkglint -fs\" to show what can be fixed automatically.)",
+               "(Run \"pkglint -F\" to automatically fix some issues.)")
+}
+
+func (s *Suite) Test_Logger_ShowSummary__autofix_available_with_show_autofix_option(c *check.C) {
+       t := s.Init(c)
+
+       logger := Logger{out: NewSeparatorWriter(&t.stdout)}
+       logger.autofixAvailable = true // See SaveAutofixChanges
+       logger.Opts.ShowAutofix = true
+
+       logger.ShowSummary()
+
+       // Since the --show-autofix option is already given, it need not be advertised.
+       // But the --autofix option is not given, therefore mention it.
+       t.CheckOutputLines(
+               "Looks fine.",
+               "(Run \"pkglint -F\" to automatically fix some issues.)")
+}
+
+func (s *Suite) Test_Logger_ShowSummary__autofix_available_with_autofix_option(c *check.C) {
+       t := s.Init(c)
+
+       logger := Logger{out: NewSeparatorWriter(&t.stdout)}
+       logger.autofixAvailable = true // See SaveAutofixChanges
+       logger.Opts.Autofix = true
+
+       logger.ShowSummary()
+
+       // Since the --autofix option is already given, it need not be advertised.
+       // Mentioning the --show-autofix option would be pointless here since the
+       // usual path goes from default mode via --show-autofix to --autofix.
+       // The usual "x warnings" would also be misleading since the warnings have just
+       // been fixed by the autofix feature. Therefore the output is completely empty.
+       t.CheckOutputEmpty()
+}
+
+// In rare cases, the explanations for the same warning may differ
+// when they appear in different contexts. In such a case, if the
+// warning is suppressed, the explanation must not appear on its own.
+//
+// An example of this was (until November 2018) DESTDIR in the check
+// for absolute pathnames.
+func (s *Suite) Test_Logger_Logf__duplicate_messages(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("--explain")
+       G.Logger.Opts.LogVerbose = false
        line := t.NewLine("README.txt", 123, "text")
 
-       // In rare cases, the explanations for the same warning may differ
-       // when they appear in different contexts. In such a case, if the
-       // warning is suppressed, the explanation must not appear on its own.
-       line.Warnf("The warning.") // Is logged
-       Explain("Explanation 1")
-       line.Warnf("The warning.") // Is suppressed
-       Explain("Explanation 2")
+       // Is logged because it is the first appearance of this warning.
+       line.Warnf("The warning.")
+       G.Explain("Explanation 1")
+
+       // Is suppressed because the warning is the same as above and LogVerbose
+       // has been set to false for this test.
+       line.Warnf("The warning.")
+       G.Explain("Explanation 2")
 
        t.CheckOutputLines(
                "WARN: README.txt:123: The warning.",
@@ -152,7 +560,7 @@ func (s *Suite) Test_logf__duplicate_mes
                "")
 }
 
-func (s *Suite) Test_logf__duplicate_explanations(c *check.C) {
+func (s *Suite) Test_Logger_Logf__duplicate_explanations(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--explain")
@@ -160,9 +568,9 @@ func (s *Suite) Test_logf__duplicate_exp
 
        // In rare cases, different diagnostics may have the same explanation.
        line.Warnf("Warning 1.")
-       Explain("Explanation")
+       G.Explain("Explanation")
        line.Warnf("Warning 2.")
-       Explain("Explanation") // Is suppressed.
+       G.Explain("Explanation") // Is suppressed.
 
        t.CheckOutputLines(
                "WARN: README.txt:123: Warning 1.",
@@ -172,13 +580,145 @@ func (s *Suite) Test_logf__duplicate_exp
                "WARN: README.txt:123: Warning 2.")
 }
 
+func (s *Suite) Test_Logger_Logf__gcc_format(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("--gcc-output-format")
+
+       G.Logf(Note, "filename", "123", "Both filename and line number.", "Both filename and line number.")
+       G.Logf(Note, "", "123", "No filename, only line number.", "No filename, only line number.")
+       G.Logf(Note, "filename", "", "Filename without line number.", "Filename without line number.")
+       G.Logf(Note, "", "", "Neither filename nor line number.", "Neither filename nor line number.")
+
+       t.CheckOutputLines(
+               "filename:123: note: Both filename and line number.",
+               "note: No filename, only line number.",
+               "filename: note: Filename without line number.",
+               "note: Neither filename nor line number.")
+}
+
+func (s *Suite) Test_Logger_Logf__traditional_format(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("--gcc-output-format=no")
+
+       G.Logf(Note, "filename", "123", "Both filename and line number.", "Both filename and line number.")
+       G.Logf(Note, "", "123", "No filename, only line number.", "No filename, only line number.")
+       G.Logf(Note, "filename", "", "Filename without line number.", "Filename without line number.")
+       G.Logf(Note, "", "", "Neither filename nor line number.", "Neither filename nor line number.")
+
+       t.CheckOutputLines(
+               "NOTE: filename:123: Both filename and line number.",
+               "NOTE: No filename, only line number.",
+               "NOTE: filename: Filename without line number.",
+               "NOTE: Neither filename nor line number.")
+}
+
+// Ensures that pkglint never destroys the terminal emulator by sending unintended escape sequences.
+func (s *Suite) Test_Logger_Logf__strange_characters(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("--gcc-output-format")
+
+       G.Logf(Note, "filename", "123", "Format.", "Unicode \U0001F645 and ANSI \x1B are never logged.")
+
+       t.CheckOutputLines(
+               "filename:123: note: Unicode U+1F645 and ANSI U+001B are never logged.")
+}
+
+func (s *Suite) Test_Logger_Diag__show_source(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("--show-autofix", "--source")
+       line := t.NewLine("filename", 123, "text")
+
+       fix := line.Autofix()
+       fix.Notef("Diagnostics can show the differences in autofix mode.")
+       fix.InsertBefore("new line before")
+       fix.InsertAfter("new line after")
+       fix.Apply()
+
+       t.CheckOutputLines(
+               "NOTE: filename:123: Diagnostics can show the differences in autofix mode.",
+               "AUTOFIX: filename:123: Inserting a line \"new line before\" before this line.",
+               "AUTOFIX: filename:123: Inserting a line \"new line after\" after this line.",
+               "+\tnew line before",
+               ">\ttext",
+               "+\tnew line after")
+}
+
+func (s *Suite) Test_Logger_Diag__show_source_with_whole_file(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("--source")
+       line := NewLineWhole("filename")
+
+       line.Warnf("This line does not have any RawLine attached.")
+
+       t.CheckOutputLines(
+               "WARN: filename: This line does not have any RawLine attached.")
+}
+
+// Ensures that when two packages produce a warning in the same file, both the
+// warning and the corresponding source code are logged only once.
+func (s *Suite) Test_Logger_Diag__source_duplicates(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupPkgsrc()
+       t.CreateFileLines("category/dependency/patches/patch-aa",
+               RcsID,
+               "",
+               "--- old file",
+               "+++ new file",
+               "@@ -1,1 +1,1 @@",
+               "-old line",
+               "+new line")
+       t.SetupPackage("category/package1",
+               "PATCHDIR=\t../../category/dependency/patches")
+       t.SetupPackage("category/package2",
+               "PATCHDIR=\t../../category/dependency/patches")
+
+       G.Main("pkglint", "--source", "-Wall", t.File("category/package1"), t.File("category/package2"))
+
+       t.CheckOutputLines(
+               "ERROR: ~/category/package1/distinfo: patch \"../dependency/patches/patch-aa\" "+
+                       "is not recorded. Run \""+confMake+" makepatchsum\".",
+               "",
+               ">\t--- old file",
+               "ERROR: ~/category/dependency/patches/patch-aa:3: Each patch must be documented.",
+               "",
+               "ERROR: ~/category/package2/distinfo: patch \"../dependency/patches/patch-aa\" "+
+                       "is not recorded. Run \""+confMake+" makepatchsum\".",
+               "",
+               "3 errors and 0 warnings found.",
+               "(Run \"pkglint -e\" to show explanations.)")
+}
+
+func (s *Suite) Test_Logger_shallBeLogged(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine( /* none */ )
+
+       c.Check(G.shallBeLogged("Options should not contain whitespace."), equals, true)
+
+       t.SetupCommandLine("--only", "whitespace")
+
+       c.Check(G.shallBeLogged("Options should not contain whitespace."), equals, true)
+       c.Check(G.shallBeLogged("Options should not contain space."), equals, false)
+
+       t.SetupCommandLine( /* none again */ )
+
+       c.Check(G.shallBeLogged("Options should not contain whitespace."), equals, true)
+       c.Check(G.shallBeLogged("Options should not contain space."), equals, true)
+}
+
 // Even if verbose logging is disabled, the "Replacing" diagnostics
 // must not be filtered for duplicates since each of them modifies the line.
-func (s *Suite) Test_logf__duplicate_autofix(c *check.C) {
+func (s *Suite) Test_Logger_Logf__duplicate_autofix(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("--explain", "--autofix")
-       G.Opts.LogVerbose = false // See SetUpTest
+       G.Logger.Opts.LogVerbose = false // See SetUpTest
        line := t.NewLine("README.txt", 123, "text")
 
        fix := line.Autofix()
@@ -191,32 +731,77 @@ func (s *Suite) Test_logf__duplicate_aut
                "AUTOFIX: README.txt:123: Replacing \"t\" with \"T\".")
 }
 
-func (s *Suite) Test_logf__panic(c *check.C) {
+func (s *Suite) Test_Logger_Logf__panic(c *check.C) {
        t := s.Init(c)
 
        t.ExpectPanic(
-               func() { logf(Error, "fileName", "13", "No period", "No period") },
+               func() { G.Logf(Error, "filename", "13", "No period", "No period") },
                "Pkglint internal error: Diagnostic format \"No period\" must end in a period.")
 }
 
-func (s *Suite) Test_Explain__long_lines(c *check.C) {
-       t := s.Init(c)
+func (s *Suite) Test_SeparatorWriter(c *check.C) {
+       var sb strings.Builder
+       wr := NewSeparatorWriter(&sb)
 
-       Explain(
-               "123456789 12345678. abcdefghi. 123456789 123456789 123456789 123456789 123456789")
+       wr.WriteLine("a")
+       wr.WriteLine("b")
 
-       t.CheckOutputLines(
-               "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")
+       c.Check(sb.String(), equals, "a\nb\n")
+
+       wr.Separate()
+
+       c.Check(sb.String(), equals, "a\nb\n")
+
+       wr.WriteLine("c")
+
+       c.Check(sb.String(), equals, "a\nb\n\nc\n")
 }
 
-func (s *Suite) Test_Explain__trailing_whitespace(c *check.C) {
-       t := s.Init(c)
+func (s *Suite) Test_SeparatorWriter_Flush(c *check.C) {
+       var sb strings.Builder
+       wr := NewSeparatorWriter(&sb)
 
-       Explain(
-               "This is a space: ")
+       wr.Write("a")
+       wr.Write("b")
 
-       t.CheckOutputLines(
-               "Trailing whitespace: \"This is a space: \"")
+       c.Check(sb.String(), equals, "")
+
+       wr.Flush()
+
+       c.Check(sb.String(), equals, "ab")
+
+       wr.Separate()
+
+       // The current line is terminated immediately by the above Separate(),
+       // but the empty line for separating two paragraphs is kept in mind.
+       // It will be added later, before the next non-newline character.
+       c.Check(sb.String(), equals, "ab\n")
+
+       wr.Write("c")
+       wr.Flush()
+
+       c.Check(sb.String(), equals, "ab\n\nc")
+}
+
+func (s *Suite) Test_SeparatorWriter_Separate(c *check.C) {
+       var sb strings.Builder
+       wr := NewSeparatorWriter(&sb)
+
+       wr.WriteLine("a")
+       wr.Separate()
+
+       c.Check(sb.String(), equals, "a\n")
+
+       // The call to Separate had requested an empty line. That empty line
+       // can either be given explicitly (like here), or it will be written
+       // implicitly before the next non-newline character.
+       wr.WriteLine("")
+       wr.Separate()
+
+       c.Check(sb.String(), equals, "a\n\n")
+
+       wr.WriteLine("c")
+       wr.Separate()
+
+       c.Check(sb.String(), equals, "a\n\nc\n")
 }
Index: pkgsrc/pkgtools/pkglint/files/mktypes.go
diff -u pkgsrc/pkgtools/pkglint/files/mktypes.go:1.6 pkgsrc/pkgtools/pkglint/files/mktypes.go:1.7
--- pkgsrc/pkgtools/pkglint/files/mktypes.go:1.6        Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/mktypes.go    Sun Dec  2 01:57:48 2018
@@ -42,7 +42,7 @@ func (m MkVarUseModifier) IsSuffixSubst(
 func (m MkVarUseModifier) MatchSubst() (ok bool, regex bool, from string, to string, options string) {
        l := textproc.NewLexer(m.Text)
        regex = l.PeekByte() == 'C'
-       if l.NextByte('S') || l.NextByte('C') {
+       if l.SkipByte('S') || l.SkipByte('C') {
                separator := l.PeekByte()
                l.Skip(1)
                if unicode.IsPunct(rune(separator)) || separator == '|' {
@@ -63,9 +63,9 @@ func (m MkVarUseModifier) MatchSubst() (
                        }
 
                        from = nextToken()
-                       if from != "" && l.NextByte(byte(separator)) {
+                       if from != "" && l.SkipByte(byte(separator)) {
                                to = nextToken()
-                               if l.NextByte(byte(separator)) {
+                               if l.SkipByte(byte(separator)) {
                                        options = l.NextBytesFunc(func(b byte) bool {
                                                return b == '1' || b == 'g' || b == 'W'
                                        })
Index: pkgsrc/pkgtools/pkglint/files/vardefs_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.6 pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.7
--- pkgsrc/pkgtools/pkglint/files/vardefs_test.go:1.6   Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/vardefs_test.go       Sun Dec  2 01:57:48 2018
@@ -48,7 +48,8 @@ func (s *Suite) Test_Pkgsrc_InitVartypes
 
        checkEnumValues("EMACS_VERSIONS_ACCEPTED", "ShellList of enum: emacs29 emacs31 ")
        checkEnumValues("PKG_JVM", "enum: jdk16 openjdk7 openjdk8 oracle-jdk8 sun-jdk6 sun-jdk7 ")
-       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("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 ")
 }
 

Index: pkgsrc/pkgtools/pkglint/files/alternatives_test.go
diff -u pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.7 pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.8
--- pkgsrc/pkgtools/pkglint/files/alternatives_test.go:1.7      Wed Nov  7 20:58:22 2018
+++ pkgsrc/pkgtools/pkglint/files/alternatives_test.go  Sun Dec  2 01:57:48 2018
@@ -22,9 +22,10 @@ func (s *Suite) Test_CheckfileAlternativ
        G.CheckDirent(".")
 
        t.CheckOutputLines(
-               "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.",
+               "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 filename.",
+               "NOTE: ALTERNATIVES:2: @PREFIX@/ can be omitted from the filename.",
                "ERROR: ALTERNATIVES:3: Alternative wrapper \"bin/echo\" must not appear in the PLIST.",
                "ERROR: ALTERNATIVES:3: Alternative implementation \"bin/gnu-echo\" must appear in the PLIST.",
                "ERROR: ALTERNATIVES:5: Invalid ALTERNATIVES line \"invalid\".")
Index: pkgsrc/pkgtools/pkglint/files/mkshparser_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkshparser_test.go:1.7 pkgsrc/pkgtools/pkglint/files/mkshparser_test.go:1.8
--- pkgsrc/pkgtools/pkglint/files/mkshparser_test.go:1.7        Wed Oct  3 22:27:53 2018
+++ pkgsrc/pkgtools/pkglint/files/mkshparser_test.go    Sun Dec  2 01:57:48 2018
@@ -262,6 +262,7 @@ func (s *ShSuite) Test_ShellParser__term
 func (s *ShSuite) Test_ShellParser__for_clause(c *check.C) {
        b := s.init(c)
 
+       // If this test fails, the cause might be in shell.y, in the for_clause rule.
        s.test("for var do echo $var ; done",
                b.List().AddCommand(b.For(
                        "var",
Index: pkgsrc/pkgtools/pkglint/files/options.go
diff -u pkgsrc/pkgtools/pkglint/files/options.go:1.7 pkgsrc/pkgtools/pkglint/files/options.go:1.8
--- pkgsrc/pkgtools/pkglint/files/options.go:1.7        Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/options.go    Sun Dec  2 01:57:48 2018
@@ -12,7 +12,7 @@ func ChecklinesOptionsMk(mklines MkLines
 
        if exp.EOF() || !(exp.CurrentMkLine().IsVarassign() && exp.CurrentMkLine().Varname() == "PKG_OPTIONS_VAR") {
                exp.CurrentLine().Warnf("Expected definition of PKG_OPTIONS_VAR.")
-               Explain(
+               G.Explain(
                        "The input variables in an options.mk file should always be",
                        "mentioned in the same order: PKG_OPTIONS_VAR,",
                        "PKG_SUPPORTED_OPTIONS, PKG_SUGGESTED_OPTIONS.  This way, the",
@@ -30,7 +30,9 @@ loop:
                mkline := exp.CurrentMkLine()
                switch {
                case mkline.IsComment():
+                       break
                case mkline.IsEmpty():
+                       break
 
                case mkline.IsVarassign():
                        switch mkline.Varcanon() {
@@ -47,14 +49,14 @@ loop:
                        // The conditionals are typically for OPSYS and MACHINE_ARCH.
 
                case mkline.IsInclude():
-                       if mkline.IncludeFile() == "../../mk/bsd.options.mk" {
+                       if mkline.IncludedFile() == "../../mk/bsd.options.mk" {
                                exp.Advance()
                                break loop
                        }
 
                default:
                        exp.CurrentLine().Warnf("Expected inclusion of \"../../mk/bsd.options.mk\".")
-                       Explain(
+                       G.Explain(
                                "After defining the input variables (PKG_OPTIONS_VAR, etc.),",
                                "bsd.options.mk should be included to do the actual processing.",
                                "No other actions should take place in this part of the file",
@@ -71,7 +73,7 @@ loop:
                                continue
                        }
 
-                       NewMkCondWalker().Walk(cond, &MkCondCallback{
+                       cond.Walk(&MkCondCallback{
                                Empty: func(varuse *MkVarUse) {
                                        if varuse.varname == "PKG_OPTIONS" && len(varuse.modifiers) == 1 {
                                                if m, positive, pattern := varuse.modifiers[0].MatchMatch(); m && positive {
@@ -86,7 +88,7 @@ loop:
 
                        if cond.Empty != nil && mkline.HasElseBranch() {
                                mkline.Notef("The positive branch of the .if/.else should be the one where the option is set.")
-                               Explain(
+                               G.Explain(
                                        "For consistency among packages, the upper branch of this",
                                        ".if/.else statement should always handle the case where the",
                                        "option is activated.  A missing exclamation mark at this",
@@ -100,13 +102,13 @@ loop:
                handled := handledOptions[option]
                if declared != nil && handled == nil {
                        declared.Warnf("Option %q should be handled below in an .if block.", option)
-                       Explain(
+                       G.Explain(
                                "If an option is not processed in this file, it may either be a",
                                "typo, or the option does not have any effect.")
                }
                if declared == nil && handled != nil {
                        handled.Warnf("Option %q is handled but not added to PKG_SUPPORTED_OPTIONS.", option)
-                       Explain(
+                       G.Explain(
                                "This block of code will never be run since PKG_OPTIONS cannot",
                                "contain this value.  This is most probably a typo.")
                }
Index: pkgsrc/pkgtools/pkglint/files/tools.go
diff -u pkgsrc/pkgtools/pkglint/files/tools.go:1.7 pkgsrc/pkgtools/pkglint/files/tools.go:1.8
--- pkgsrc/pkgtools/pkglint/files/tools.go:1.7  Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/tools.go      Sun Dec  2 01:57:48 2018
@@ -84,7 +84,16 @@ type Tools struct {
        byName    map[string]*Tool // "sed" => tool
        byVarname map[string]*Tool // "GREP_CMD" => tool
        fallback  *Tools
-       SeenPrefs bool // Determines the effect of adding the tool to USE_TOOLS
+
+       // Determines the effect of adding the tool to USE_TOOLS.
+       //
+       // As long as bsd.prefs.mk has definitely not been included by the current file,
+       // tools added to USE_TOOLS are available at load time, but only after bsd.prefs.mk
+       // has been included.
+       //
+       // Adding a tool to USE_TOOLS _after_ bsd.prefs.mk has been included, on the other
+       // hand, only makes the tool available at run time.
+       SeenPrefs bool
 }
 
 func NewTools(traceName string) *Tools {
@@ -116,14 +125,14 @@ func (tr *Tools) Define(name, varname st
 }
 
 func (tr *Tools) def(name, varname string, mustUseVarForm bool, validity Validity) *Tool {
-       fresh := &Tool{name, varname, mustUseVarForm, validity}
+       fresh := Tool{name, varname, mustUseVarForm, validity}
 
        tool := tr.byName[name]
        if tool == nil {
-               tool = fresh
+               tool = &fresh
                tr.byName[name] = tool
        } else {
-               tr.merge(tool, fresh)
+               tr.merge(tool, &fresh)
        }
 
        if tr.fallback != nil {
@@ -221,7 +230,7 @@ func (tr *Tools) ParseToolLine(mkline Mk
                }
 
        case mkline.IsInclude():
-               if IsPrefs(mkline.IncludeFile()) {
+               if IsPrefs(mkline.IncludedFile()) {
                        tr.SeenPrefs = true
                }
        }
Index: pkgsrc/pkgtools/pkglint/files/tools_test.go
diff -u pkgsrc/pkgtools/pkglint/files/tools_test.go:1.7 pkgsrc/pkgtools/pkglint/files/tools_test.go:1.8
--- pkgsrc/pkgtools/pkglint/files/tools_test.go:1.7     Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/tools_test.go Sun Dec  2 01:57:48 2018
@@ -53,18 +53,19 @@ func (s *Suite) Test_Tools_ParseToolLine
 func (s *Suite) Test_Tools_Define__invalid_tool_name(c *check.C) {
        t := s.Init(c)
 
+       mkline := t.NewMkLine("dummy.mk", 123, "DUMMY=\tvalue")
        reg := NewTools("")
 
-       reg.Define("tool_name", "", dummyMkLine)
-       reg.Define("tool:dependency", "", dummyMkLine)
-       reg.Define("tool:build", "", dummyMkLine)
+       reg.Define("tool_name", "", mkline)
+       reg.Define("tool:dependency", "", mkline)
+       reg.Define("tool:build", "", mkline)
 
        // As of October 2018, the underscore is not used in any tool name.
        // If there should ever be such a case, just use a different character for testing.
        t.CheckOutputLines(
-               "ERROR: Invalid tool name \"tool_name\".",
-               "ERROR: Invalid tool name \"tool:dependency\".",
-               "ERROR: Invalid tool name \"tool:build\".")
+               "ERROR: dummy.mk:123: Invalid tool name \"tool_name\".",
+               "ERROR: dummy.mk:123: Invalid tool name \"tool:dependency\".",
+               "ERROR: dummy.mk:123: Invalid tool name \"tool:build\".")
 }
 
 func (s *Suite) Test_Tools_Trace__coverage(c *check.C) {
@@ -108,15 +109,17 @@ func (s *Suite) Test_Tools__USE_TOOLS_pr
 // variable name. When trying to define the tool with its variable name
 // later, the existing definition is amended.
 func (s *Suite) Test_Tools__add_varname_later(c *check.C) {
+       t := s.Init(c)
 
+       mkline := t.NewMkLine("dummy.mk", 123, "DUMMY=\tvalue")
        tools := NewTools("")
-       tool := tools.Define("tool", "", dummyMkLine)
+       tool := tools.Define("tool", "", mkline)
 
        c.Check(tool.Name, equals, "tool")
        c.Check(tool.Varname, equals, "")
 
        // Updates the existing tool definition.
-       tools.Define("tool", "TOOL", dummyMkLine)
+       tools.Define("tool", "TOOL", mkline)
 
        c.Check(tool.Name, equals, "tool")
        c.Check(tool.Varname, equals, "TOOL")

Index: pkgsrc/pkgtools/pkglint/files/autofix.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix.go:1.12 pkgsrc/pkgtools/pkglint/files/autofix.go:1.13
--- pkgsrc/pkgtools/pkglint/files/autofix.go:1.12       Wed Nov  7 20:58:22 2018
+++ pkgsrc/pkgtools/pkglint/files/autofix.go    Sun Dec  2 01:57:48 2018
@@ -99,7 +99,7 @@ func (fix *Autofix) ReplaceAfter(prefix,
                if rawLine.Lineno != 0 {
                        replaced := strings.Replace(rawLine.textnl, prefix+from, prefix+to, 1)
                        if replaced != rawLine.textnl {
-                               if G.Opts.ShowAutofix || G.Opts.Autofix {
+                               if G.Logger.IsAutofix() {
                                        rawLine.textnl = replaced
                                }
                                fix.Describef(rawLine.Lineno, "Replacing %q with %q.", from, to)
@@ -136,7 +136,7 @@ func (fix *Autofix) ReplaceRegex(from re
 
                        replaced := replaceAllFunc(rawLine.textnl, from, replace)
                        if replaced != rawLine.textnl {
-                               if G.Opts.ShowAutofix || G.Opts.Autofix {
+                               if G.Logger.IsAutofix() {
                                        rawLine.textnl = replaced
                                }
                                for _, fromText := range froms {
@@ -175,7 +175,7 @@ func (fix *Autofix) Custom(fixer func(sh
                return
        }
 
-       fixer(G.Opts.ShowAutofix, G.Opts.Autofix)
+       fixer(G.Logger.Opts.ShowAutofix, G.Logger.Opts.Autofix)
 }
 
 // Describef is used while Autofix.Custom is called to remember a description
@@ -193,7 +193,7 @@ func (fix *Autofix) InsertBefore(text st
                return
        }
 
-       if G.Opts.ShowAutofix || G.Opts.Autofix {
+       if G.Logger.IsAutofix() {
                fix.linesBefore = append(fix.linesBefore, text+"\n")
        }
        fix.Describef(fix.line.raw[0].Lineno, "Inserting a line %q before this line.", text)
@@ -207,7 +207,7 @@ func (fix *Autofix) InsertAfter(text str
                return
        }
 
-       if G.Opts.ShowAutofix || G.Opts.Autofix {
+       if G.Logger.IsAutofix() {
                fix.linesAfter = append(fix.linesAfter, text+"\n")
        }
        fix.Describef(fix.line.raw[len(fix.line.raw)-1].Lineno, "Inserting a line %q after this line.", text)
@@ -223,7 +223,7 @@ func (fix *Autofix) Delete() {
        }
 
        for _, line := range fix.line.raw {
-               if G.Opts.ShowAutofix || G.Opts.Autofix {
+               if G.Logger.IsAutofix() {
                        line.textnl = ""
                }
                fix.Describef(line.Lineno, "Deleting this line.")
@@ -244,7 +244,7 @@ func (fix *Autofix) Apply() {
        // To fix this assertion, call one of Autofix.Errorf, Autofix.Warnf
        // or Autofix.Notef before calling Apply.
        G.Assertf(
-               fix.level != nil && fix.diagFormat != "",
+               fix.level != nil,
                "Each autofix must have a log level and a diagnostic.")
 
        reset := func() {
@@ -256,22 +256,23 @@ func (fix *Autofix) Apply() {
                fix.autofixShortTerm = autofixShortTerm{}
        }
 
-       G.explainNext = shallBeLogged(fix.diagFormat)
-       if !G.explainNext || len(fix.actions) == 0 {
+       if !G.Logger.Relevant(fix.diagFormat) || len(fix.actions) == 0 {
                reset()
                return
        }
 
-       logDiagnostic := (G.Opts.ShowAutofix || !G.Opts.Autofix) &&
+       logDiagnostic := (G.Logger.Opts.ShowAutofix || !G.Logger.Opts.Autofix) &&
                fix.diagFormat != SilentAutofixFormat
-       logFix := G.Opts.Autofix || G.Opts.ShowAutofix
+       logFix := G.Logger.IsAutofix()
 
        if logDiagnostic {
+               msg := fmt.Sprintf(fix.diagFormat, fix.diagArgs...)
                if !logFix {
-                       line.showSource(G.logOut)
+                       if fix.diagFormat == AutofixFormat || G.Logger.FirstTime(line.Filename, line.Linenos(), msg) {
+                               line.showSource(G.out)
+                       }
                }
-               msg := fmt.Sprintf(fix.diagFormat, fix.diagArgs...)
-               logf(fix.level, line.FileName, line.Linenos(), fix.diagFormat, msg)
+               G.Logf(fix.level, line.Filename, line.Linenos(), fix.diagFormat, msg)
        }
 
        if logFix {
@@ -280,20 +281,20 @@ func (fix *Autofix) Apply() {
                        if action.lineno != 0 {
                                lineno = strconv.Itoa(action.lineno)
                        }
-                       logf(AutofixLogLevel, line.FileName, lineno, AutofixFormat, action.description)
+                       G.Logf(AutofixLogLevel, line.Filename, lineno, AutofixFormat, action.description)
                }
        }
 
        if logDiagnostic || logFix {
                if logFix {
-                       line.showSource(G.logOut)
+                       line.showSource(G.out)
                }
                if logDiagnostic && len(fix.explanation) > 0 {
-                       Explain(fix.explanation...)
+                       G.Explain(fix.explanation...)
                }
-               if G.Opts.ShowSource {
-                       if !G.Opts.Explain || !logDiagnostic || len(fix.explanation) == 0 {
-                               G.logOut.Separate()
+               if G.Logger.Opts.ShowSource {
+                       if !G.Logger.Opts.Explain || !logDiagnostic || len(fix.explanation) == 0 {
+                               G.out.Separate()
                        }
                }
        }
@@ -348,11 +349,11 @@ func (fix *Autofix) Realign(mkline MkLin
 
        for _, rawLine := range fix.line.raw[1:] {
                _, comment, oldSpace := match2(rawLine.textnl, `^(#?)([ \t]*)`)
-               newWidth := tabWidth(oldSpace) - oldWidth + newWidth
-               newSpace := strings.Repeat("\t", newWidth/8) + strings.Repeat(" ", newWidth%8)
+               newLineWidth := tabWidth(oldSpace) - oldWidth + newWidth
+               newSpace := strings.Repeat("\t", newLineWidth/8) + strings.Repeat(" ", newLineWidth%8)
                replaced := strings.Replace(rawLine.textnl, comment+oldSpace, comment+newSpace, 1)
                if replaced != rawLine.textnl {
-                       if G.Opts.ShowAutofix || G.Opts.Autofix {
+                       if G.Logger.IsAutofix() {
                                rawLine.textnl = replaced
                        }
                        fix.Describef(rawLine.Lineno, "Replacing indentation %q with %q.", oldSpace, newSpace)
@@ -380,7 +381,7 @@ func (fix *Autofix) skip() bool {
                fix.diagFormat != "",
                "Autofix: The diagnostic must be given before the action.")
        // This check is necessary for the --only command line option.
-       return !shallBeLogged(fix.diagFormat)
+       return !G.shallBeLogged(fix.diagFormat)
 }
 
 func (fix *Autofix) assertRealLine() {
@@ -397,13 +398,13 @@ func SaveAutofixChanges(lines Lines) (au
        }
 
        // Fast lane for the case that nothing is written back to disk.
-       if !G.Opts.Autofix {
+       if !G.Logger.Opts.Autofix {
                for _, line := range lines.Lines {
                        if line.autofix != nil && line.autofix.modified {
                                G.autofixAvailable = true
-                               if G.Opts.ShowAutofix {
+                               if G.Logger.Opts.ShowAutofix {
                                        // Only in this case can the loaded lines be modified.
-                                       G.fileCache.Evict(line.FileName)
+                                       G.fileCache.Evict(line.Filename)
                                }
                        }
                }
@@ -413,10 +414,10 @@ func SaveAutofixChanges(lines Lines) (au
        changes := make(map[string][]string)
        changed := make(map[string]bool)
        for _, line := range lines.Lines {
-               chlines := changes[line.FileName]
+               chlines := changes[line.Filename]
                if fix := line.autofix; fix != nil {
                        if fix.modified {
-                               changed[line.FileName] = true
+                               changed[line.Filename] = true
                        }
                        chlines = append(chlines, fix.linesBefore...)
                        for _, raw := range line.raw {
@@ -428,25 +429,25 @@ func SaveAutofixChanges(lines Lines) (au
                                chlines = append(chlines, raw.textnl)
                        }
                }
-               changes[line.FileName] = chlines
+               changes[line.Filename] = chlines
        }
 
-       for fileName := range changed {
-               G.fileCache.Evict(fileName)
-               changedLines := changes[fileName]
-               tmpName := fileName + ".pkglint.tmp"
+       for filename := range changed {
+               G.fileCache.Evict(filename)
+               changedLines := changes[filename]
+               tmpName := filename + ".pkglint.tmp"
                text := ""
                for _, changedLine := range changedLines {
                        text += changedLine
                }
                err := ioutil.WriteFile(tmpName, []byte(text), 0666)
                if err != nil {
-                       logf(Error, tmpName, "", "Cannot write: %s", "Cannot write: "+err.Error())
+                       G.Logf(Error, tmpName, "", "Cannot write: %s", "Cannot write: "+err.Error())
                        continue
                }
-               err = os.Rename(tmpName, fileName)
+               err = os.Rename(tmpName, filename)
                if err != nil {
-                       logf(Error, tmpName, "",
+                       G.Logf(Error, tmpName, "",
                                "Cannot overwrite with autofixed content: %s",
                                "Cannot overwrite with autofixed content: "+err.Error())
                        continue
Index: pkgsrc/pkgtools/pkglint/files/autofix_test.go
diff -u pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.12 pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.13
--- pkgsrc/pkgtools/pkglint/files/autofix_test.go:1.12  Wed Nov  7 20:58:22 2018
+++ pkgsrc/pkgtools/pkglint/files/autofix_test.go       Sun Dec  2 01:57:48 2018
@@ -101,8 +101,8 @@ func (s *Suite) Test_Autofix_ReplaceAfte
                "AUTOFIX: ~/Makefile:1: Replacing \"n\" with \"v\".",
                "-\t# line 1 \\",
                "+\t# live 1 \\",
-               ">\tcontinuation 1 \\",
-               ">\tcontinuation 2")
+               "\tcontinuation 1 \\",
+               "\tcontinuation 2")
 }
 
 func (s *Suite) Test_Autofix_ReplaceRegex__show_autofix(c *check.C) {
@@ -261,7 +261,7 @@ func (s *Suite) Test_Autofix__multiple_f
 
        t.SetupCommandLine("--show-autofix", "--explain")
 
-       line := t.NewLine("fileName", 1, "original")
+       line := t.NewLine("filename", 1, "original")
 
        c.Check(line.autofix, check.IsNil)
        c.Check(line.raw, check.DeepEquals, t.NewRawLines(1, "original\n"))
@@ -276,7 +276,7 @@ func (s *Suite) Test_Autofix__multiple_f
        c.Check(line.autofix, check.NotNil)
        c.Check(line.raw, check.DeepEquals, t.NewRawLines(1, "original\n", "lriginao\n"))
        t.CheckOutputLines(
-               "AUTOFIX: fileName:1: Replacing \"original\" with \"lriginao\".")
+               "AUTOFIX: filename:1: Replacing \"original\" with \"lriginao\".")
 
        {
                fix := line.Autofix()
@@ -289,7 +289,7 @@ func (s *Suite) Test_Autofix__multiple_f
        c.Check(line.raw, check.DeepEquals, t.NewRawLines(1, "original\n", "lruginao\n"))
        c.Check(line.raw[0].textnl, equals, "lruginao\n")
        t.CheckOutputLines(
-               "AUTOFIX: fileName:1: Replacing \"i\" with \"u\".")
+               "AUTOFIX: filename:1: Replacing \"i\" with \"u\".")
 
        {
                fix := line.Autofix()
@@ -302,7 +302,7 @@ func (s *Suite) Test_Autofix__multiple_f
        c.Check(line.raw, check.DeepEquals, t.NewRawLines(1, "original\n", "middle\n"))
        c.Check(line.raw[0].textnl, equals, "middle\n")
        t.CheckOutputLines(
-               "AUTOFIX: fileName:1: Replacing \"lruginao\" with \"middle\".")
+               "AUTOFIX: filename:1: Replacing \"lruginao\" with \"middle\".")
 
        c.Check(line.raw[0].textnl, equals, "middle\n")
        t.CheckOutputEmpty()
@@ -316,7 +316,7 @@ func (s *Suite) Test_Autofix__multiple_f
 
        c.Check(line.Autofix().RawText(), equals, "")
        t.CheckOutputLines(
-               "AUTOFIX: fileName:1: Deleting this line.")
+               "AUTOFIX: filename:1: Deleting this line.")
 }
 
 func (s *Suite) Test_Autofix_Explain__without_explain_option(c *check.C) {
@@ -446,20 +446,23 @@ func (s *Suite) Test_Autofix__show_autof
                "after")
        line := mklines.lines.Lines[1]
 
-       {
-               fix := line.Autofix()
-               fix.Warnf("Using \"old\" is deprecated.")
-               fix.Replace("old", "new")
-               fix.Apply()
-       }
+       fix := line.Autofix()
+       fix.Warnf("Using \"old\" is deprecated.")
+       fix.Replace("old", "new")
+       fix.Apply()
 
+       // Using a tab for indentation preserves the exact layout in the output
+       // since in pkgsrc Makefiles, tabs are also used in the middle of the line
+       // to align the variable values. Using a single space for indentation would
+       // make some of the lines appear misaligned in the pkglint output although
+       // they are correct in the Makefiles.
        t.CheckOutputLines(
                "WARN: ~/Makefile:2--4: Using \"old\" is deprecated.",
                "AUTOFIX: ~/Makefile:3: Replacing \"old\" with \"new\".",
-               ">\t# before \\",
+               "\t# before \\",
                "-\tThe old song \\",
                "+\tThe new song \\",
-               ">\tafter")
+               "\tafter")
 }
 
 func (s *Suite) Test_Autofix_InsertBefore(c *check.C) {
@@ -650,7 +653,7 @@ func (s *Suite) Test_Autofix_skip(c *che
 
        t.SetupCommandLine("--only", "few", "--autofix")
 
-       mklines := t.SetupFileMkLines("fileName",
+       mklines := t.SetupFileMkLines("filename",
                "VAR=\t111 222 333 444 555 \\",
                "666")
        lines := mklines.lines
@@ -675,7 +678,7 @@ func (s *Suite) Test_Autofix_skip(c *che
        SaveAutofixChanges(lines)
 
        t.CheckOutputEmpty()
-       t.CheckFileLines("fileName",
+       t.CheckFileLines("filename",
                "VAR=\t111 222 333 444 555 \\",
                "666")
        c.Check(fix.RawText(), equals, ""+
@@ -710,7 +713,7 @@ func (s *Suite) Test_Autofix_Apply__only
 func (s *Suite) Test_Autofix_Apply__panic(c *check.C) {
        t := s.Init(c)
 
-       line := t.NewLine("fileName", 123, "text")
+       line := t.NewLine("filename", 123, "text")
 
        t.ExpectPanic(
                func() {
@@ -837,6 +840,83 @@ func (s *Suite) Test_SaveAutofixChanges_
                "ERROR: ~/file.txt.pkglint.tmp: Cannot overwrite with autofixed content: .*\n")
 }
 
+// Up to 2018-11-25, pkglint in some cases logged only the source without
+// a corresponding warning.
+func (s *Suite) Test_Autofix__lonely_source(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall", "--source")
+       G.Logger.Opts.LogVerbose = false // For realistic conditions; otherwise all diagnostics are logged.
+
+       t.SetupPackage("x11/xorg-cf-files",
+               ".include \"../../x11/xorgproto/buildlink3.mk\"")
+       t.SetupPackage("x11/xorgproto",
+               "DISTNAME=\txorgproto-1.0")
+       t.CreateFileDummyBuildlink3("x11/xorgproto/buildlink3.mk")
+       t.CreateFileLines("x11/xorgproto/builtin.mk",
+               MkRcsID,
+               "",
+               "BUILTIN_PKG:=\txorgproto",
+               "",
+               "PRE_XORGPROTO_LIST_MISSING =\tapplewmproto",
+               "",
+               ".for id in ${PRE_XORGPROTO_LIST_MISSING}",
+               ".endfor")
+       G.Pkgsrc.LoadInfrastructure()
+       t.Chdir(".")
+
+       G.CheckDirent("x11/xorg-cf-files")
+       G.CheckDirent("x11/xorgproto")
+
+       t.CheckOutputLines(
+               ">\tPRE_XORGPROTO_LIST_MISSING =\tapplewmproto",
+               "NOTE: x11/xorgproto/builtin.mk:5: Unnecessary space after variable name \"PRE_XORGPROTO_LIST_MISSING\".")
+}
+
+// Up to 2018-11-26, pkglint in some cases logged only the source without
+// a corresponding warning.
+func (s *Suite) Test_Autofix__lonely_source_2(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall", "--source", "--explain")
+       G.Logger.Opts.LogVerbose = false // For realistic conditions; otherwise all diagnostics are logged.
+
+       t.SetupPackage("print/tex-bibtex8",
+               "MAKE_FLAGS+=\tCFLAGS=${CFLAGS.${PKGSRC_COMPILER}}")
+       G.Pkgsrc.LoadInfrastructure()
+       t.Chdir(".")
+
+       G.CheckDirent("print/tex-bibtex8")
+
+       t.CheckOutputLines(
+               ">\tMAKE_FLAGS+=\tCFLAGS=${CFLAGS.${PKGSRC_COMPILER}}",
+               "WARN: print/tex-bibtex8/Makefile:20: Please use ${CFLAGS.${PKGSRC_COMPILER}:Q} instead of ${CFLAGS.${PKGSRC_COMPILER}}.",
+               "",
+               "\tSee the pkgsrc guide, section \"Echoing a string exactly as-is\":",
+               "\thttps://www.NetBSD.org/docs/pkgsrc/pkgsrc.html#echo-literal";,
+               "",
+               ">\tMAKE_FLAGS+=\tCFLAGS=${CFLAGS.${PKGSRC_COMPILER}}",
+               "WARN: print/tex-bibtex8/Makefile:20: The list variable PKGSRC_COMPILER should not be embedded in a word.",
+               "",
+               "\tWhen a list variable has multiple elements, this expression expands",
+               "\tto something unexpected:",
+               "",
+               "\tExample: ${MASTER_SITE_SOURCEFORGE}directory/ expands to",
+               "",
+               "\t\thttps://mirror1.sf.net/ https://mirror2.sf.net/directory/";,
+               "",
+               "\tThe first URL is missing the directory.  To fix this, write",
+               "\t\t${MASTER_SITE_SOURCEFORGE:=directory/}.",
+               "",
+               "\tExample: -l${LIBS} expands to",
+               "",
+               "\t\t-llib1 lib2",
+               "",
+               "\tThe second library is missing the -l.  To fix this, write",
+               "\t${LIBS:@lib@-l${lib}@}.",
+               "")
+}
+
 // RawText returns the raw text of the fixed line, including line ends.
 // This may differ from the original text when the --show-autofix
 // or --autofix options are enabled.
Index: pkgsrc/pkgtools/pkglint/files/pkgsrc.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.12 pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.13
--- pkgsrc/pkgtools/pkglint/files/pkgsrc.go:1.12        Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc.go     Sun Dec  2 01:57:48 2018
@@ -34,16 +34,18 @@ type Pkgsrc struct {
        LastChange          map[string]*Change  //
        listVersions        map[string][]string // See ListVersions
 
-       UserDefinedVars Scope               // Used for checking BUILD_DEFS
-       Deprecated      map[string]string   //
-       vartypes        map[string]*Vartype // varcanon => type
+       // Variables that may be overridden by the pkgsrc user. Used for checking BUILD_DEFS.
+       UserDefinedVars Scope
 
-       Hashes       map[string]*Hash // Maps "alg:fileName" => hash (inter-package check).
+       Deprecated map[string]string   //
+       vartypes   map[string]*Vartype // varcanon => type
+
+       Hashes       map[string]*Hash // Maps "alg:filename" => hash (inter-package check).
        UsedLicenses map[string]bool  // Maps "license name" => true (inter-package check).
 }
 
 func NewPkgsrc(dir string) *Pkgsrc {
-       src := &Pkgsrc{
+       src := Pkgsrc{
                dir,
                make(map[string]bool),
                NewTools("Pkgsrc"),
@@ -60,6 +62,13 @@ func NewPkgsrc(dir string) *Pkgsrc {
                nil, // Only initialized when pkglint is run for a whole pkgsrc installation
                nil}
 
+       addDefaultBuildDefs(&src)
+
+       return &src
+}
+
+func addDefaultBuildDefs(src *Pkgsrc) {
+
        // Some user-defined variables do not influence the binary
        // package at all and therefore do not have to be added to
        // BUILD_DEFS; therefore they are marked as "already added".
@@ -127,8 +136,6 @@ func NewPkgsrc(dir string) *Pkgsrc {
                "PKGPATH",
                "RESTRICTED",
                "USE_ABI_DEPENDS")
-
-       return src
 }
 
 // LoadInfrastructure reads the pkgsrc infrastructure files to
@@ -243,6 +250,24 @@ func (src *Pkgsrc) ListVersions(category
        return repls
 }
 
+func (src *Pkgsrc) checkToplevelUnusedLicenses() {
+       usedLicenses := src.UsedLicenses
+       if usedLicenses == nil {
+               return
+       }
+
+       licensesDir := src.File("licenses")
+       for _, licenseFile := range src.ReadDir("licenses") {
+               licenseName := licenseFile.Name()
+               if !usedLicenses[licenseName] {
+                       licensePath := licensesDir + "/" + licenseName
+                       if fileExists(licensePath) {
+                               NewLineWhole(licensePath).Warnf("This license seems to be unused.")
+                       }
+               }
+       }
+}
+
 // loadTools loads the tool definitions from `mk/tools/*`.
 func (src *Pkgsrc) loadTools() {
        tools := src.Tools
@@ -253,14 +278,14 @@ func (src *Pkgsrc) loadTools() {
                mklines := LoadMk(toc, MustSucceed|NotEmpty)
                for _, mkline := range mklines.mklines {
                        if mkline.IsInclude() {
-                               includefile := mkline.IncludeFile()
-                               if !contains(includefile, "/") {
-                                       toolFiles = append(toolFiles, includefile)
+                               includedFile := mkline.IncludedFile()
+                               if !contains(includedFile, "/") {
+                                       toolFiles = append(toolFiles, includedFile)
                                }
                        }
                }
                if len(toolFiles) <= 1 {
-                       NewLine(toc, 0, "", nil).Fatalf("Too few tool files.")
+                       NewLineWhole(toc).Fatalf("Too few tool files.")
                }
        }
 
@@ -298,7 +323,7 @@ func (src *Pkgsrc) loadTools() {
                                        tools.ParseToolLine(mkline, true, !mklines.indentation.IsConditional())
 
                                case "_BUILD_DEFS":
-                                       for _, bdvar := range mkline.ValueSplit(mkline.Value(), "") {
+                                       for _, bdvar := range mkline.ValueFields(mkline.Value()) {
                                                src.AddBuildDefs(bdvar)
                                        }
                                }
@@ -320,7 +345,7 @@ func (src *Pkgsrc) loadTools() {
 func (src *Pkgsrc) loadUntypedVars() {
 
        // Setting guessed to false prevents the vartype.guessed case in MkLineChecker.CheckVaruse.
-       unknownType := &Vartype{lkNone, BtUnknown, []ACLEntry{{"*", aclpAll}}, false}
+       unknownType := Vartype{lkNone, BtUnknown, []ACLEntry{{"*", aclpAll}}, false}
 
        handleLine := func(mkline MkLine) {
                if mkline.IsVarassign() {
@@ -338,7 +363,7 @@ func (src *Pkgsrc) loadUntypedVars() {
                                if trace.Tracing {
                                        trace.Stepf("Untyped variable %q in %s", varcanon, mkline)
                                }
-                               src.vartypes[varcanon] = unknownType
+                               src.vartypes[varcanon] = &unknownType
                        }
                }
        }
@@ -401,7 +426,7 @@ func (src *Pkgsrc) loadSuggestedUpdates(
        src.suggestedWipUpdates = src.parseSuggestedUpdates(Load(G.Pkgsrc.File("wip/TODO"), NotEmpty))
 }
 
-func (src *Pkgsrc) loadDocChangesFromFile(fileName string) []*Change {
+func (src *Pkgsrc) loadDocChangesFromFile(filename string) []*Change {
 
        parseChange := func(line Line) *Change {
                text := line.Text
@@ -435,22 +460,22 @@ func (src *Pkgsrc) loadDocChangesFromFil
        }
 
        year := ""
-       if m, yyyy := match1(fileName, `-(\d+)$`); m && yyyy >= "2018" {
+       if m, yyyy := match1(filename, `-(\d+)$`); m && yyyy >= "2018" {
                year = yyyy
        }
 
-       lines := Load(fileName, MustSucceed|NotEmpty)
+       lines := Load(filename, MustSucceed|NotEmpty)
        var changes []*Change
        for _, line := range lines.Lines {
                if change := parseChange(line); change != nil {
                        changes = append(changes, change)
                        if year != "" && change.Date[0:4] != year {
-                               line.Warnf("Year %s for %s does not match the file name %s.", change.Date[0:4], change.Pkgpath, fileName)
+                               line.Warnf("Year %s for %s does not match the filename %s.", change.Date[0:4], change.Pkgpath, filename)
                        }
                        if len(changes) >= 2 && year != "" {
                                if prev := changes[len(changes)-2]; change.Date < prev.Date {
                                        line.Warnf("Date %s for %s is earlier than %s for %s.", change.Date, change.Pkgpath, prev.Date, prev.Pkgpath)
-                                       Explain(
+                                       G.Explain(
                                                "The entries in doc/CHANGES should be in chronological order, and",
                                                "all dates are assumed to be in the UTC timezone, to prevent time",
                                                "warps.",
@@ -465,7 +490,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: %s", text)
-                       Explain("See mk/misc/developer.mk for the rules.")
+                       G.Explain("See mk/misc/developer.mk for the rules.")
                }
        }
        return changes
@@ -486,18 +511,18 @@ func (src *Pkgsrc) loadDocChanges() {
                NewLineWhole(docDir).Fatalf("Cannot be read for loading the package changes.")
        }
 
-       var fileNames []string
+       var filenames []string
        for _, file := range files {
-               fileName := file.Name()
-               if matches(fileName, `^CHANGES-20\d\d$`) && fileName >= "CHANGES-2011" {
-                       fileNames = append(fileNames, fileName)
+               filename := file.Name()
+               if matches(filename, `^CHANGES-20\d\d$`) && filename >= "CHANGES-2011" {
+                       filenames = append(filenames, filename)
                }
        }
 
-       sort.Strings(fileNames)
+       sort.Strings(filenames)
        src.LastChange = make(map[string]*Change)
-       for _, fileName := range fileNames {
-               changes := src.loadDocChangesFromFile(docDir + "/" + fileName)
+       for _, filename := range filenames {
+               changes := src.loadDocChangesFromFile(docDir + "/" + filename)
                for _, change := range changes {
                        src.LastChange[change.Pkgpath] = change
                }
@@ -679,13 +704,13 @@ func (src *Pkgsrc) initDeprecatedVars() 
 }
 
 // Load loads the file relative to the pkgsrc top directory.
-func (src *Pkgsrc) Load(fileName string, options LoadOptions) Lines {
-       return Load(src.File(fileName), options)
+func (src *Pkgsrc) Load(filename string, options LoadOptions) Lines {
+       return Load(src.File(filename), options)
 }
 
 // LoadMk loads the Makefile relative to the pkgsrc top directory.
-func (src *Pkgsrc) LoadMk(fileName string, options LoadOptions) MkLines {
-       return LoadMk(src.File(fileName), options)
+func (src *Pkgsrc) LoadMk(filename string, options LoadOptions) MkLines {
+       return LoadMk(src.File(filename), options)
 }
 
 // ReadDir reads the file listing from the given directory (relative to the pkgsrc root),
@@ -708,7 +733,7 @@ func (src *Pkgsrc) ReadDir(dirName strin
        return relevantFiles
 }
 
-// File resolves a file name relative to the pkgsrc top directory.
+// File resolves a filename relative to the pkgsrc top directory.
 //
 // Example:
 //  NewPkgsrc("/usr/pkgsrc").File("distfiles") => "/usr/pkgsrc/distfiles"
@@ -716,12 +741,12 @@ func (src *Pkgsrc) File(relativeName str
        return cleanpath(src.topdir + "/" + relativeName)
 }
 
-// ToRel returns the path of `fileName`, relative to the pkgsrc top directory.
+// ToRel returns the path of `filename`, relative to the pkgsrc top directory.
 //
 // Example:
 //  NewPkgsrc("/usr/pkgsrc").ToRel("/usr/pkgsrc/distfiles") => "distfiles"
-func (src *Pkgsrc) ToRel(fileName string) string {
-       return relpath(src.topdir, fileName)
+func (src *Pkgsrc) ToRel(filename string) string {
+       return relpath(src.topdir, filename)
 }
 
 func (src *Pkgsrc) AddBuildDefs(varnames ...string) {
@@ -775,8 +800,8 @@ func (src *Pkgsrc) loadPkgOptions() {
        lines := src.Load("mk/defaults/options.description", MustSucceed)
 
        for _, line := range lines.Lines {
-               if m, optname, optdescr := match2(line.Text, `^([-0-9a-z_+]+)(?:[\t ]+(.*))?$`); m {
-                       src.PkgOptions[optname] = optdescr
+               if m, name, description := match2(line.Text, `^([-0-9a-z_+]+)(?:[\t ]+(.*))?$`); m {
+                       src.PkgOptions[name] = description
                } else {
                        line.Fatalf("Unknown line format: %s", line.Text)
                }
@@ -801,7 +826,7 @@ func (src *Pkgsrc) VariableType(varname 
                return vartype
        }
 
-       if tool := G.ToolByVarname(varname, RunTime); tool != nil {
+       if tool := G.ToolByVarname(varname); tool != nil {
                if trace.Tracing {
                        trace.Stepf("Use of tool %+v", tool)
                }
@@ -813,7 +838,7 @@ func (src *Pkgsrc) VariableType(varname 
        }
 
        if m, toolVarname := match1(varname, `^TOOLS_(.*)`); m {
-               if tool := G.ToolByVarname(toolVarname, RunTime); tool != nil {
+               if tool := G.ToolByVarname(toolVarname); tool != nil {
                        return &Vartype{lkNone, BtPathname, []ACLEntry{{"*", aclpUse}}, false}
                }
        }
Index: pkgsrc/pkgtools/pkglint/files/shtokenizer.go
diff -u pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.12 pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.13
--- pkgsrc/pkgtools/pkglint/files/shtokenizer.go:1.12   Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/shtokenizer.go        Sun Dec  2 01:57:48 2018
@@ -7,8 +7,8 @@ type ShTokenizer struct {
 
 func NewShTokenizer(line Line, text string, emitWarnings bool) *ShTokenizer {
        p := NewParser(line, text, emitWarnings)
-       mkp := &MkParser{p}
-       return &ShTokenizer{p, mkp}
+       mkp := MkParser{p}
+       return &ShTokenizer{p, &mkp}
 }
 
 // ShAtom parses a basic building block of a shell program.
@@ -21,11 +21,11 @@ func (p *ShTokenizer) ShAtom(quoting ShQ
                return nil
        }
 
-       repl := p.parser.repl
-       mark := repl.Mark()
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
 
        if varuse := p.mkp.VarUse(); varuse != nil {
-               return &ShAtom{shtVaruse, repl.Since(mark), quoting, varuse}
+               return &ShAtom{shtVaruse, lexer.Since(mark), quoting, varuse}
        }
 
        var atom *ShAtom
@@ -57,11 +57,11 @@ func (p *ShTokenizer) ShAtom(quoting ShQ
        }
 
        if atom == nil {
-               repl.Reset(mark)
-               if hasPrefix(repl.Rest(), "${") {
-                       p.parser.Line.Warnf("Unclosed Make variable starting at %q.", shorten(repl.Rest(), 20))
+               lexer.Reset(mark)
+               if hasPrefix(lexer.Rest(), "${") {
+                       p.parser.Line.Warnf("Unclosed Make variable starting at %q.", shorten(lexer.Rest(), 20))
                } else {
-                       p.parser.Line.Warnf("Pkglint parse error in ShTokenizer.ShAtom at %q (quoting=%s).", repl.Rest(), quoting)
+                       p.parser.Line.Warnf("Pkglint parse error in ShTokenizer.ShAtom at %q (quoting=%s).", lexer.Rest(), quoting)
                }
        }
        return atom
@@ -72,41 +72,46 @@ func (p *ShTokenizer) shAtomPlain() *ShA
        if op := p.shOperator(q); op != nil {
                return op
        }
-       repl := p.parser.repl
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
        switch {
-       case repl.AdvanceHspace():
-               return &ShAtom{shtSpace, repl.Str(), q, nil}
-       case repl.AdvanceStr("\""):
-               return &ShAtom{shtWord, repl.Str(), shqDquot, nil}
-       case repl.AdvanceStr("'"):
-               return &ShAtom{shtWord, repl.Str(), shqSquot, nil}
-       case repl.AdvanceStr("`"):
-               return &ShAtom{shtWord, repl.Str(), shqBackt, nil}
-       case repl.PeekByte() == '#':
-               return &ShAtom{shtComment, repl.AdvanceRest(), q, nil}
-       case repl.AdvanceStr("$$("):
-               return &ShAtom{shtSubshell, repl.Str(), shqSubsh, nil}
+       case lexer.NextHspace() != "":
+               return &ShAtom{shtSpace, lexer.Since(mark), q, nil}
+       case lexer.SkipByte('"'):
+               return &ShAtom{shtText, lexer.Since(mark), shqDquot, nil}
+       case lexer.SkipByte('\''):
+               return &ShAtom{shtText, lexer.Since(mark), shqSquot, nil}
+       case lexer.SkipByte('`'):
+               return &ShAtom{shtText, lexer.Since(mark), shqBackt, nil}
+       case lexer.PeekByte() == '#':
+               rest := lexer.Rest()
+               lexer.Skip(len(rest))
+               return &ShAtom{shtComment, rest, q, nil}
+       case lexer.SkipString("$$("):
+               return &ShAtom{shtSubshell, lexer.Since(mark), shqSubsh, nil}
        }
 
        return p.shAtomInternal(q, false, false)
 }
 
 func (p *ShTokenizer) shAtomDquot() *ShAtom {
-       repl := p.parser.repl
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
        switch {
-       case repl.AdvanceStr("\""):
-               return &ShAtom{shtWord, repl.Str(), shqPlain, nil}
-       case repl.AdvanceStr("`"):
-               return &ShAtom{shtWord, repl.Str(), shqDquotBackt, nil}
+       case lexer.SkipByte('"'):
+               return &ShAtom{shtText, lexer.Since(mark), shqPlain, nil}
+       case lexer.SkipByte('`'):
+               return &ShAtom{shtText, lexer.Since(mark), shqDquotBackt, nil}
        }
        return p.shAtomInternal(shqDquot, true, false)
 }
 
 func (p *ShTokenizer) shAtomSquot() *ShAtom {
-       repl := p.parser.repl
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
        switch {
-       case repl.AdvanceStr("'"):
-               return &ShAtom{shtWord, repl.Str(), shqPlain, nil}
+       case lexer.SkipByte('\''):
+               return &ShAtom{shtText, lexer.Since(mark), shqPlain, nil}
        }
        return p.shAtomInternal(shqSquot, false, true)
 }
@@ -116,18 +121,19 @@ func (p *ShTokenizer) shAtomBackt() *ShA
        if op := p.shOperator(q); op != nil {
                return op
        }
-       repl := p.parser.repl
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
        switch {
-       case repl.AdvanceStr("\""):
-               return &ShAtom{shtWord, repl.Str(), shqBacktDquot, nil}
-       case repl.AdvanceStr("`"):
-               return &ShAtom{shtWord, repl.Str(), shqPlain, nil}
-       case repl.AdvanceStr("'"):
-               return &ShAtom{shtWord, repl.Str(), shqBacktSquot, nil}
-       case repl.AdvanceHspace():
-               return &ShAtom{shtSpace, repl.Str(), q, nil}
-       case repl.AdvanceRegexp("^#[^`]*"):
-               return &ShAtom{shtComment, repl.Str(), q, nil}
+       case lexer.SkipByte('"'):
+               return &ShAtom{shtText, lexer.Since(mark), shqBacktDquot, nil}
+       case lexer.SkipByte('`'):
+               return &ShAtom{shtText, lexer.Since(mark), shqPlain, nil}
+       case lexer.SkipByte('\''):
+               return &ShAtom{shtText, lexer.Since(mark), shqBacktSquot, nil}
+       case lexer.NextHspace() != "":
+               return &ShAtom{shtSpace, lexer.Since(mark), q, nil}
+       case lexer.SkipRegexp(G.res.Compile("^#[^`]*")):
+               return &ShAtom{shtComment, lexer.Since(mark), q, nil}
        }
        return p.shAtomInternal(q, false, false)
 }
@@ -136,24 +142,27 @@ func (p *ShTokenizer) shAtomBackt() *ShA
 // compatibility with /bin/sh from Solaris 7.
 func (p *ShTokenizer) shAtomSubsh() *ShAtom {
        const q = shqSubsh
-       repl := p.parser.repl
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
        switch {
-       case repl.AdvanceHspace():
-               return &ShAtom{shtSpace, repl.Str(), q, nil}
-       case repl.AdvanceStr("\""):
-               return &ShAtom{shtWord, repl.Str(), shqSubshDquot, nil}
-       case repl.AdvanceStr("'"):
-               return &ShAtom{shtWord, repl.Str(), shqSubshSquot, nil}
-       case repl.AdvanceStr("`"):
-               // FIXME: return &ShAtom{shtWord, repl.Str(), shqBackt, nil}
-       case repl.AdvanceRegexp(`^#[^)]*`):
-               return &ShAtom{shtComment, repl.Str(), 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.Str(), q, nil}
+       case lexer.NextHspace() != "":
+               return &ShAtom{shtSpace, lexer.Since(mark), q, nil}
+       case lexer.SkipByte('"'):
+               return &ShAtom{shtText, lexer.Since(mark), shqSubshDquot, nil}
+       case lexer.SkipByte('\''):
+               return &ShAtom{shtText, lexer.Since(mark), shqSubshSquot, nil}
+       case lexer.SkipByte('`'):
+               // FIXME: return &ShAtom{shtText, lexer.Since(mark), shqBackt, nil}
+       case lexer.SkipRegexp(G.res.Compile(`^#[^)]*`)):
+               return &ShAtom{shtComment, lexer.Since(mark), q, nil}
+       case lexer.SkipByte(')'):
+               // shtText instead of shtOperator because this atom belongs to a shtText token.
+               return &ShAtom{shtText, lexer.Since(mark), shqPlain, nil}
        }
-       return p.shOperator(q)
+       if op := p.shOperator(q); op != nil {
+               return op
+       }
+       return p.shAtomInternal(q, false, false)
 }
 
 func (p *ShTokenizer) shAtomDquotBackt() *ShAtom {
@@ -161,149 +170,197 @@ func (p *ShTokenizer) shAtomDquotBackt()
        if op := p.shOperator(q); op != nil {
                return op
        }
-       repl := p.parser.repl
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
        switch {
-       case repl.AdvanceStr("`"):
-               return &ShAtom{shtWord, repl.Str(), shqDquot, nil}
-       case repl.AdvanceStr("\""):
-               return &ShAtom{shtWord, repl.Str(), shqDquotBacktDquot, nil}
-       case repl.AdvanceStr("'"):
-               return &ShAtom{shtWord, repl.Str(), shqDquotBacktSquot, nil}
-       case repl.AdvanceRegexp("^#[^`]*"):
-               return &ShAtom{shtComment, repl.Str(), q, nil}
-       case repl.AdvanceRegexp(`^(?:[!#%*+,\-./0-9:=?@A-Z\[\]_a-z~]+|\\[^$]|` + reShDollar + `)+`):
-               return &ShAtom{shtWord, repl.Str(), q, nil}
-       case repl.AdvanceHspace():
-               return &ShAtom{shtSpace, repl.Str(), q, nil}
+       case lexer.SkipByte('`'):
+               return &ShAtom{shtText, lexer.Since(mark), shqDquot, nil}
+       case lexer.SkipByte('"'):
+               return &ShAtom{shtText, lexer.Since(mark), shqDquotBacktDquot, nil}
+       case lexer.SkipByte('\''):
+               return &ShAtom{shtText, lexer.Since(mark), shqDquotBacktSquot, nil}
+       case lexer.SkipRegexp(G.res.Compile("^#[^`]*")):
+               return &ShAtom{shtComment, lexer.Since(mark), q, nil}
+       case lexer.NextHspace() != "":
+               return &ShAtom{shtSpace, lexer.Since(mark), q, nil}
        }
-       return nil
+       return p.shAtomInternal(q, false, false)
 }
 
 func (p *ShTokenizer) shAtomBacktDquot() *ShAtom {
-       repl := p.parser.repl
+       const q = shqBacktDquot
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
        switch {
-       case repl.AdvanceStr("\""):
-               return &ShAtom{shtWord, repl.Str(), shqBackt, nil}
-       case repl.AdvanceRegexp(`^(?:[\t !%&()*+,\-./0-9:;<=>?@A-Z\[\]^_a-z{|}~]+|\\[^$]|` + reShDollar + `)+`):
-               return &ShAtom{shtWord, repl.Str(), shqBacktDquot, nil}
+       case lexer.SkipByte('"'):
+               return &ShAtom{shtText, lexer.Since(mark), shqBackt, nil}
        }
-       return nil
+       return p.shAtomInternal(q, true, false)
 }
 
 func (p *ShTokenizer) shAtomBacktSquot() *ShAtom {
        const q = shqBacktSquot
-       repl := p.parser.repl
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
        switch {
-       case repl.AdvanceStr("'"):
-               return &ShAtom{shtWord, repl.Str(), shqBackt, nil}
-       case repl.AdvanceRegexp(`^([\t !"#%&()*+,\-./0-9:;<=>?@A-Z\[\\\]^_` + "`" + `a-z{|}~]+|\$\$)+`):
-               return &ShAtom{shtWord, repl.Str(), q, nil}
+       case lexer.SkipByte('\''):
+               return &ShAtom{shtText, lexer.Since(mark), shqBackt, nil}
        }
-       return nil
+       return p.shAtomInternal(q, false, true)
 }
 
 func (p *ShTokenizer) shAtomSubshDquot() *ShAtom {
-       repl := p.parser.repl
+       const q = shqSubshDquot
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
        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.Str(), shqSubshDquot, nil}
+       case lexer.SkipByte('"'):
+               return &ShAtom{shtText, lexer.Since(mark), shqSubsh, nil}
        }
-       return nil
+       return p.shAtomInternal(q, true, false)
 }
 
 func (p *ShTokenizer) shAtomSubshSquot() *ShAtom {
        const q = shqSubshSquot
-       repl := p.parser.repl
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
        switch {
-       case repl.AdvanceStr("'"):
-               return &ShAtom{shtWord, repl.Str(), shqSubsh, nil}
-       case repl.AdvanceRegexp(`^([\t !"#%&()*+,\-./0-9:;<=>?@A-Z\[\\\]^_` + "`" + `a-z{|}~]+|\$\$)+`):
-               return &ShAtom{shtWord, repl.Str(), q, nil}
+       case lexer.SkipByte('\''):
+               return &ShAtom{shtText, lexer.Since(mark), shqSubsh, nil}
        }
-       return nil
+       return p.shAtomInternal(q, false, true)
 }
 
 func (p *ShTokenizer) shAtomDquotBacktDquot() *ShAtom {
        const q = shqDquotBacktDquot
-       repl := p.parser.repl
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
        switch {
-       case repl.AdvanceStr("\""):
-               return &ShAtom{shtWord, repl.Str(), shqDquotBackt, nil}
-       case repl.AdvanceRegexp(`^(?:[\t !%&()*+,\-./0-9:;<=>?@A-Z\[\]^_a-z{|}~]+|\\[^$]|` + reShDollar + `)+`):
-               return &ShAtom{shtWord, repl.Str(), q, nil}
+       case lexer.SkipByte('"'):
+               return &ShAtom{shtText, lexer.Since(mark), shqDquotBackt, nil}
        }
-       return nil
+       return p.shAtomInternal(q, true, false)
 }
 
 func (p *ShTokenizer) shAtomDquotBacktSquot() *ShAtom {
-       repl := p.parser.repl
+       const q = shqDquotBacktSquot
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
        switch {
-       case repl.AdvanceStr("'"):
-               return &ShAtom{shtWord, repl.Str(), shqDquotBackt, nil}
-       case repl.AdvanceRegexp(`^(?:[\t !"#%()*+,\-./0-9:;<=>?@A-Z\[\]^_a-z{|}~]+|\\[^$]|\\\$\$|\$\$)+`):
-               return &ShAtom{shtWord, repl.Str(), shqDquotBacktSquot, nil}
+       case lexer.SkipByte('\''):
+               return &ShAtom{shtText, lexer.Since(mark), shqDquotBackt, nil}
        }
-       return nil
+       return p.shAtomInternal(q, false, true)
 }
 
-// shAtomInternal advances the parser over the next "word",
-// which is everything that does not change the quoting and is not a Make(1) variable.
-// Shell variables may appear as part of a word.
+// shAtomInternal reads the next shtText or shtShVarUse.
 //
 // Examples:
-//  while$var
-//  $$,
-//  $$!$$$$
-//  echo
-//  text${var:=default}text
+//  while
+//  text$$,text
+//  $$!
+//  $$$$
+//  text
+//  ${var:=default}
 func (p *ShTokenizer) shAtomInternal(q ShQuoting, dquot, squot bool) *ShAtom {
-       repl := p.parser.repl
+       if shVarUse := p.shVarUse(q); shVarUse != nil {
+               return shVarUse
+       }
+
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
 
-       mark := repl.Mark()
 loop:
        for {
                _ = `^[\t "$&'();<>\\|]+` // These are not allowed in shqPlain.
 
                switch {
-               case repl.AdvanceRegexp(`^[!#%*+,\-./0-9:=?@A-Z\[\]^_a-z{}~]+`):
-               case dquot && repl.AdvanceRegexp(`^[\t &'();<>|]+`):
-               case squot && repl.AdvanceStr("`"):
-               case squot && repl.AdvanceRegexp(`^[\t "&();<>\\|]+`):
-               case squot && repl.AdvanceStr("$$"):
+               case lexer.SkipRegexp(G.res.Compile(`^[!#%*+,\-./0-9:=?@A-Z\[\]^_a-z{}~]+`)):
+                       break
+               case dquot && lexer.SkipRegexp(G.res.Compile(`^[\t &'();<>|]+`)):
+                       break
+               case squot && lexer.SkipByte('`'):
+                       break
+               case squot && lexer.SkipRegexp(G.res.Compile(`^[\t "&();<>\\|]+`)):
+                       break
+               case squot && lexer.SkipString("$$"):
+                       break
                case squot:
                        break loop
-               case repl.AdvanceRegexp(`^\\[^$]`):
-               case repl.HasPrefixRegexp(`^\$\$[^!#(*\-0-9?@A-Z_a-z{]`):
-                       repl.AdvanceStr("$$")
-               case repl.AdvanceRegexp(`^(?:` + reShDollar + `)`):
+               case lexer.SkipString("\\$$"):
+                       break
+               case lexer.SkipRegexp(G.res.Compile(`^\\[^$]`)):
+                       break
+               case matches(lexer.Rest(), `^\$\$[^!#(*\-0-9?@A-Z_a-z{]`):
+                       lexer.NextString("$$")
                default:
                        break loop
                }
        }
 
-       if token := repl.Since(mark); token != "" {
-               return &ShAtom{shtWord, token, q, nil}
+       if token := lexer.Since(mark); token != "" {
+               return &ShAtom{shtText, token, q, nil}
        }
        return nil
 }
 
+// shVarUse parses a use of a shell variable, like $$var or $${var:=value}.
+//
+// See http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_02
+func (p *ShTokenizer) shVarUse(q ShQuoting) *ShAtom {
+       lexer := p.parser.lexer
+       beforeDollar := lexer.Mark()
+
+       if !lexer.SkipString("$$") {
+               return nil
+       }
+
+       if lexer.PeekByte() >= '0' && lexer.PeekByte() <= '9' {
+               lexer.Skip(1)
+               text := lexer.Since(beforeDollar)
+               return &ShAtom{shtShVarUse, text, q, text[2:]}
+       }
+
+       brace := lexer.SkipByte('{')
+
+       varnameStart := lexer.Mark()
+       if !lexer.SkipRegexp(G.res.Compile(`^(?:[!#*\-?@]|\$\$|[A-Za-z_]\w*|\d+)`)) {
+               lexer.Reset(beforeDollar)
+               return nil
+       }
+
+       shVarname := lexer.Since(varnameStart)
+       if shVarname == "$$" {
+               shVarname = "$"
+       }
+
+       if brace {
+               lexer.SkipRegexp(G.res.Compile(`^(?:##?|%%?|:?[+\-=?])[^$\\{}]*`))
+               if !lexer.SkipByte('}') {
+                       lexer.Reset(beforeDollar)
+                       return nil
+               }
+       }
+
+       return &ShAtom{shtShVarUse, lexer.Since(beforeDollar), q, shVarname}
+}
+
 func (p *ShTokenizer) shOperator(q ShQuoting) *ShAtom {
-       repl := p.parser.repl
+       lexer := p.parser.lexer
+       mark := lexer.Mark()
        switch {
-       case repl.AdvanceStr("||"),
-               repl.AdvanceStr("&&"),
-               repl.AdvanceStr(";;"),
-               repl.AdvanceStr("\n"),
-               repl.AdvanceStr(";"),
-               repl.AdvanceStr("("),
-               repl.AdvanceStr(")"),
-               repl.AdvanceStr("|"),
-               repl.AdvanceStr("&"):
-               return &ShAtom{shtOperator, repl.Str(), q, nil}
-       case repl.AdvanceRegexp(`^\d*(?:<<-|<<|<&|<>|>>|>&|>\||<|>)`):
-               return &ShAtom{shtOperator, repl.Str(), q, nil}
+       case lexer.SkipString("||"),
+               lexer.SkipString("&&"),
+               lexer.SkipString(";;"),
+               lexer.SkipByte('\n'),
+               lexer.SkipByte(';'),
+               lexer.SkipByte('('),
+               lexer.SkipByte(')'),
+               lexer.SkipByte('|'),
+               lexer.SkipByte('&'):
+               return &ShAtom{shtOperator, lexer.Since(mark), q, nil}
+       case lexer.SkipRegexp(G.res.Compile(`^\d*(?:<<-|<<|<&|<>|>>|>&|>\||<|>)`)):
+               return &ShAtom{shtOperator, lexer.Since(mark), q, nil}
        }
        return nil
 }
@@ -338,13 +395,13 @@ func (p *ShTokenizer) ShToken() *ShToken
                curr = nil
        }
 
-       repl := p.parser.repl
-       initialMark := repl.Mark()
+       lexer := p.parser.lexer
+       initialMark := lexer.Mark()
        var atoms []*ShAtom
 
        for peek() != nil && peek().Type == shtSpace {
                skip()
-               initialMark = repl.Mark()
+               initialMark = lexer.Mark()
        }
 
        if peek() == nil {
@@ -355,17 +412,17 @@ func (p *ShTokenizer) ShToken() *ShToken
        }
 
 nextAtom:
-       mark := repl.Mark()
+       mark := lexer.Mark()
        atom := peek()
        if atom != nil && (atom.Type.IsWord() || atom.Quoting != shqPlain) {
                skip()
                atoms = append(atoms, atom)
                goto nextAtom
        }
-       repl.Reset(mark)
+       lexer.Reset(mark)
 
        G.Assertf(len(atoms) > 0, "ShTokenizer.ShToken")
-       return NewShToken(repl.Since(initialMark), atoms...)
+       return NewShToken(lexer.Since(initialMark), atoms...)
 }
 
 func (p *ShTokenizer) Rest() string {

Index: pkgsrc/pkgtools/pkglint/files/buildlink3.go
diff -u pkgsrc/pkgtools/pkglint/files/buildlink3.go:1.14 pkgsrc/pkgtools/pkglint/files/buildlink3.go:1.15
--- pkgsrc/pkgtools/pkglint/files/buildlink3.go:1.14    Wed Nov  7 20:58:22 2018
+++ pkgsrc/pkgtools/pkglint/files/buildlink3.go Sun Dec  2 01:57:48 2018
@@ -225,7 +225,7 @@ func (ck *Buildlink3Checker) checkVaruse
        }
 
        if warned {
-               Explain(
+               G.Explain(
                        "The identifiers in the BUILDLINK_TREE variable should be plain",
                        "strings that do not refer to any variable.",
                        "",
Index: pkgsrc/pkgtools/pkglint/files/line_test.go
diff -u pkgsrc/pkgtools/pkglint/files/line_test.go:1.14 pkgsrc/pkgtools/pkglint/files/line_test.go:1.15
--- pkgsrc/pkgtools/pkglint/files/line_test.go:1.14     Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/line_test.go  Sun Dec  2 01:57:48 2018
@@ -4,81 +4,37 @@ import (
        "gopkg.in/check.v1"
 )
 
-func (s *Suite) Test_Line_log__gcc_format(c *check.C) {
-       t := s.Init(c)
-
-       t.SetupCommandLine("--gcc-output-format")
-       line := t.NewLine("fileName", 123, "text")
-
-       line.Notef("Diagnostics can be logged in GCC-style.")
-
-       t.CheckOutputLines(
-               "fileName:123: note: Diagnostics can be logged in GCC-style.")
-}
-
-func (s *Suite) Test_Line_log__print_source(c *check.C) {
+func (s *Suite) Test_RawLine_String(c *check.C) {
        t := s.Init(c)
 
-       t.SetupCommandLine("--show-autofix", "--source")
-       line := t.NewLine("fileName", 123, "text")
+       line := t.NewLine("filename", 123, "text")
 
-       fix := line.Autofix()
-       fix.Notef("Diagnostics can show the differences in autofix mode.")
-       fix.InsertBefore("new line before")
-       fix.InsertAfter("new line after")
-       fix.Apply()
-
-       t.CheckOutputLines(
-               "NOTE: fileName:123: Diagnostics can show the differences in autofix mode.",
-               "AUTOFIX: fileName:123: Inserting a line \"new line before\" before this line.",
-               "AUTOFIX: fileName:123: Inserting a line \"new line after\" after this line.",
-               "+\tnew line before",
-               ">\ttext",
-               "+\tnew line after")
-}
-
-// Ensures that when two packages produce a warning in the same file, both the
-// warning and the corresponding source code are logged only once.
-func (s *Suite) Test_Line_showSource__duplicates(c *check.C) {
-       t := s.Init(c)
-
-       t.SetupPkgsrc()
-       t.CreateFileLines("category/dependency/patches/patch-aa",
-               RcsID,
-               "",
-               "--- old file",
-               "+++ new file",
-               "@@ -1,1 +1,1 @@",
-               "-old line",
-               "+new line")
-       t.SetupPackage("category/package1",
-               "PATCHDIR=\t../../category/dependency/patches")
-       t.SetupPackage("category/package2",
-               "PATCHDIR=\t../../category/dependency/patches")
-
-       G.Main("pkglint", "--source", "-Wall", t.File("category/package1"), t.File("category/package2"))
-
-       t.CheckOutputLines(
-               "ERROR: ~/category/package1/distinfo: patch \"../dependency/patches/patch-aa\" "+
-                       "is not recorded. Run \""+confMake+" makepatchsum\".",
-               "",
-               ">\t--- old file",
-               "ERROR: ~/category/dependency/patches/patch-aa:3: Each patch must be documented.",
-               "",
-               "ERROR: ~/category/package2/distinfo: patch \"../dependency/patches/patch-aa\" "+
-                       "is not recorded. Run \""+confMake+" makepatchsum\".",
-               "",
-               ">\t--- old file",
-               // FIXME: The above source line is missing a diagnostic.
-               "",
-               "3 errors and 0 warnings found.",
-               "(Run \"pkglint -e\" to show explanations.)")
+       c.Check(line.raw[0].String(), equals, "123:text\n")
 }
 
-func (s *Suite) Test_RawLine_String(c *check.C) {
+// In case of a fatal error, pkglint quits in a controlled manner,
+// and the trace log shows where the fatal error happened.
+func (s *Suite) Test_Line_Fatalf__trace(c *check.C) {
        t := s.Init(c)
 
-       line := t.NewLine("fileName", 123, "text")
+       line := t.NewLine("filename", 123, "")
+       t.EnableTracingToLog()
 
-       c.Check(line.raw[0].String(), equals, "123:text\n")
+       inner := func() {
+               defer trace.Call0()()
+               line.Fatalf("Cannot continue because of %q and %q.", "reason 1", "reason 2")
+       }
+       outer := func() {
+               defer trace.Call0()()
+               inner()
+       }
+
+       t.ExpectFatal(
+               outer,
+               "TRACE: + (*Suite).Test_Line_Fatalf__trace.func2()",
+               "TRACE: 1 + (*Suite).Test_Line_Fatalf__trace.func1()",
+               "TRACE: 1 2   Fatalf: \"Cannot continue because of %q and %q.\", [reason 1 reason 2]",
+               "TRACE: 1 - (*Suite).Test_Line_Fatalf__trace.func1()",
+               "TRACE: - (*Suite).Test_Line_Fatalf__trace.func2()",
+               "FATAL: filename:123: Cannot continue because of \"reason 1\" and \"reason 2\".")
 }
Index: pkgsrc/pkgtools/pkglint/files/toplevel.go
diff -u pkgsrc/pkgtools/pkglint/files/toplevel.go:1.14 pkgsrc/pkgtools/pkglint/files/toplevel.go:1.15
--- pkgsrc/pkgtools/pkglint/files/toplevel.go:1.14      Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/toplevel.go   Sun Dec  2 01:57:48 2018
@@ -11,10 +11,10 @@ func CheckdirToplevel(dir string) {
                defer trace.Call1(dir)()
        }
 
-       ctx := &Toplevel{dir, "", nil}
-       fileName := dir + "/Makefile"
+       ctx := Toplevel{dir, "", nil}
+       filename := dir + "/Makefile"
 
-       mklines := LoadMk(fileName, NotEmpty|LogErrors)
+       mklines := LoadMk(filename, NotEmpty|LogErrors)
        if mklines == nil {
                return
        }

Index: pkgsrc/pkgtools/pkglint/files/buildlink3_test.go
diff -u pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.20 pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.21
--- pkgsrc/pkgtools/pkglint/files/buildlink3_test.go:1.20       Wed Nov  7 20:58:22 2018
+++ pkgsrc/pkgtools/pkglint/files/buildlink3_test.go    Sun Dec  2 01:57:48 2018
@@ -123,7 +123,8 @@ func (s *Suite) Test_ChecklinesBuildlink
        ChecklinesBuildlink3Mk(mklines)
 
        t.CheckOutputLines(
-               "ERROR: buildlink3.mk:5: Package name mismatch between multiple-inclusion guard \"PKGBASE2\" (expected \"PKGBASE1\") and package name \"pkgbase1\" (from line 3).",
+               "ERROR: buildlink3.mk:5: Package name mismatch between multiple-inclusion guard \"PKGBASE2\" "+
+                       "(expected \"PKGBASE1\") and package name \"pkgbase1\" (from line 3).",
                "WARN: buildlink3.mk:9: Definition of BUILDLINK_API_DEPENDS is missing.")
 }
 
@@ -400,17 +401,29 @@ func (s *Suite) Test_ChecklinesBuildlink
                "WARN: buildlink3.mk:3: LICENSE may not be used in any file; it is a write-only variable.",
                // FIXME: License is not a list type, although it can be appended to.
                "WARN: buildlink3.mk:3: The list variable LICENSE should not be embedded in a word.",
+
+               "WARN: buildlink3.mk:8: LICENSE should not be evaluated at load time.",
+               "WARN: buildlink3.mk:8: LICENSE may not be used in any file; it is a write-only variable.",
+               // FIXME: License is not a list type, although it can be appended to.
+               "WARN: buildlink3.mk:8: The list variable LICENSE should not be embedded in a word.",
                "WARN: buildlink3.mk:8: LICENSE should not be evaluated indirectly at load time.",
                "WARN: buildlink3.mk:8: LICENSE may not be used in any file; it is a write-only variable.",
                // FIXME: License is not a list type, although it can be appended to.
                "WARN: buildlink3.mk:8: The list variable LICENSE should not be embedded in a word.",
+
+               "WARN: buildlink3.mk:9: LICENSE should not be evaluated at load time.",
+               "WARN: buildlink3.mk:9: LICENSE may not be used in any file; it is a write-only variable.",
+               // FIXME: License is not a list type, although it can be appended to.
+               "WARN: buildlink3.mk:9: The list variable LICENSE should not be embedded in a word.",
                "WARN: buildlink3.mk:9: LICENSE should not be evaluated indirectly at load time.",
                "WARN: buildlink3.mk:9: LICENSE may not be used in any file; it is a write-only variable.",
                // FIXME: License is not a list type, although it can be appended to.
                "WARN: buildlink3.mk:9: The list variable LICENSE should not be embedded in a word.",
+
                "WARN: buildlink3.mk:13: LICENSE may not be used in any file; it is a write-only variable.",
                // FIXME: License is not a list type, although it can be appended to.
                "WARN: buildlink3.mk:13: The list variable LICENSE should not be embedded in a word.",
+
                "WARN: buildlink3.mk:3: Please replace \"${LICENSE}\" with a simple string (also in other variables in this file).")
 }
 
Index: pkgsrc/pkgtools/pkglint/files/vartype.go
diff -u pkgsrc/pkgtools/pkglint/files/vartype.go:1.20 pkgsrc/pkgtools/pkglint/files/vartype.go:1.21
--- pkgsrc/pkgtools/pkglint/files/vartype.go:1.20       Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/vartype.go    Sun Dec  2 01:57:48 2018
@@ -140,10 +140,13 @@ func (vt *Vartype) IsShell() bool {
        return false
 }
 
-// IsBasicSafe returns whether the basic vartype consists only of
-// characters that don't need escaping in most contexts, like A-Za-z0-9-_.
-func (vt *Vartype) IsBasicSafe() bool {
-       switch vt.basicType {
+// NeedsQ returns whether variables of this type need the :Q
+// modifier to be safely embedded in other variables or shell programs.
+//
+// Variables that can consists only of characters like A-Za-z0-9-._
+// don't need the :Q modifier. All others do, for safety reasons.
+func (bt *BasicType) NeedsQ() bool {
+       switch bt {
        case BtBuildlinkDepmethod,
                BtCategory,
                BtDistSuffix,
@@ -172,9 +175,9 @@ func (vt *Vartype) IsBasicSafe() bool {
                BtWrkdirSubdirectory,
                BtYesNo,
                BtYesNoIndirectly:
-               return true
+               return false
        }
-       return false
+       return !bt.IsEnum()
 }
 
 func (vt *Vartype) IsPlainString() bool {
@@ -213,7 +216,7 @@ var (
        BtDistSuffix             = &BasicType{"DistSuffix", (*VartypeCheck).DistSuffix}
        BtEmulPlatform           = &BasicType{"EmulPlatform", (*VartypeCheck).EmulPlatform}
        BtFetchURL               = &BasicType{"FetchURL", (*VartypeCheck).FetchURL}
-       BtFileName               = &BasicType{"FileName", (*VartypeCheck).FileName}
+       BtFileName               = &BasicType{"Filename", (*VartypeCheck).Filename}
        BtFileMask               = &BasicType{"FileMask", (*VartypeCheck).FileMask}
        BtFileMode               = &BasicType{"FileMode", (*VartypeCheck).FileMode}
        BtGccReqd                = &BasicType{"GccReqd", (*VartypeCheck).GccReqd}
@@ -230,7 +233,7 @@ var (
        BtOption                 = &BasicType{"Option", (*VartypeCheck).Option}
        BtPathlist               = &BasicType{"Pathlist", (*VartypeCheck).Pathlist}
        BtPathmask               = &BasicType{"PathMask", (*VartypeCheck).PathMask}
-       BtPathname               = &BasicType{"PathName", (*VartypeCheck).PathName}
+       BtPathname               = &BasicType{"Pathname", (*VartypeCheck).Pathname}
        BtPerl5Packlist          = &BasicType{"Perl5Packlist", (*VartypeCheck).Perl5Packlist}
        BtPerms                  = &BasicType{"Perms", (*VartypeCheck).Perms}
        BtPkgName                = &BasicType{"Pkgname", (*VartypeCheck).Pkgname}
@@ -242,7 +245,6 @@ var (
        BtRelativePkgDir         = &BasicType{"RelativePkgDir", (*VartypeCheck).RelativePkgDir}
        BtRelativePkgPath        = &BasicType{"RelativePkgPath", (*VartypeCheck).RelativePkgPath}
        BtRestricted             = &BasicType{"Restricted", (*VartypeCheck).Restricted}
-       BtSedCommand             = &BasicType{"SedCommand", (*VartypeCheck).SedCommand}
        BtSedCommands            = &BasicType{"SedCommands", (*VartypeCheck).SedCommands}
        BtShellCommand           = &BasicType{"ShellCommand", nil}
        BtShellCommands          = &BasicType{"ShellCommands", nil}

Index: pkgsrc/pkgtools/pkglint/files/check_test.go
diff -u pkgsrc/pkgtools/pkglint/files/check_test.go:1.28 pkgsrc/pkgtools/pkglint/files/check_test.go:1.29
--- pkgsrc/pkgtools/pkglint/files/check_test.go:1.28    Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/check_test.go Sun Dec  2 01:57:48 2018
@@ -13,7 +13,6 @@ import (
        "testing"
 
        "gopkg.in/check.v1"
-       "netbsd.org/pkglint/textproc"
 )
 
 var equals = check.Equals
@@ -57,14 +56,13 @@ func (s *Suite) Init(c *check.C) *Tester
 }
 
 func (s *Suite) SetUpTest(c *check.C) {
-       t := &Tester{c: c}
-       s.Tester = t
+       t := Tester{c: c}
+       s.Tester = &t
 
        G = NewPkglint()
        G.Testing = true
-       textproc.Testing = true
-       G.logOut = NewSeparatorWriter(&t.stdout)
-       G.logErr = NewSeparatorWriter(&t.stderr)
+       G.out = NewSeparatorWriter(&t.stdout)
+       G.err = NewSeparatorWriter(&t.stderr)
        trace.Out = &t.stdout
 
        // XXX: Maybe the tests can run a bit faster when they don't
@@ -94,8 +92,7 @@ func (s *Suite) TearDownTest(c *check.C)
                _, _ = fmt.Fprintf(os.Stderr, "Cannot chdir back to previous dir: %s", err)
        }
 
-       G = Pkglint{} // unusable because of missing logOut and logErr
-       textproc.Testing = false
+       G = Pkglint{} // unusable because of missing Logger.out and Logger.err
        if out := t.Output(); out != "" {
                var msg strings.Builder
                msg.WriteString("\n")
@@ -152,7 +149,7 @@ func (t *Tester) SetupCommandLine(args .
        //
        // It also reveals diagnostics that are logged multiple times per
        // line and thus can easily get annoying to the pkgsrc developers.
-       G.Opts.LogVerbose = true
+       G.Logger.Opts.LogVerbose = true
 }
 
 // SetupVartypes registers a few hundred variables like MASTER_SITES,
@@ -186,8 +183,8 @@ func (t *Tester) SetupTool(name, varname
 //
 // See SetupFileMkLines for loading a Makefile fragment.
 func (t *Tester) SetupFileLines(relativeFileName string, lines ...string) Lines {
-       fileName := t.CreateFileLines(relativeFileName, lines...)
-       return Load(fileName, MustSucceed)
+       filename := t.CreateFileLines(relativeFileName, lines...)
+       return Load(filename, MustSucceed)
 }
 
 // SetupFileLines creates a temporary file and writes the given lines to it.
@@ -195,8 +192,8 @@ func (t *Tester) SetupFileLines(relative
 //
 // See SetupFileLines for loading an ordinary file.
 func (t *Tester) SetupFileMkLines(relativeFileName string, lines ...string) MkLines {
-       fileName := t.CreateFileLines(relativeFileName, lines...)
-       return LoadMk(fileName, MustSucceed)
+       filename := t.CreateFileLines(relativeFileName, lines...)
+       return LoadMk(filename, MustSucceed)
 }
 
 // SetupPkgsrc sets up a minimal but complete pkgsrc installation in the
@@ -223,7 +220,7 @@ func (t *Tester) SetupPkgsrc() {
        t.CreateFileLines("licenses/2-clause-bsd",
                "Redistribution and use in source and binary forms ...")
        t.CreateFileLines("licenses/gnu-gpl-v2",
-               "The licenses for most software ...")
+               "The licenses for most software are designed to take away ...")
 
        // The various MASTER_SITE_* variables for use in the
        // MASTER_SITES are defined in this file.
@@ -345,23 +342,23 @@ line:
 // given lines to it.
 //
 // It returns the full path to the created file.
-func (t *Tester) CreateFileLines(relativeFileName string, lines ...string) (fileName string) {
+func (t *Tester) CreateFileLines(relativeFileName string, lines ...string) (filename string) {
        var content bytes.Buffer
        for _, line := range lines {
                content.WriteString(line)
                content.WriteString("\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.Bytes()), 0666)
+       err = ioutil.WriteFile(filename, []byte(content.Bytes()), 0666)
        t.c.Assert(err, check.IsNil)
 
-       G.fileCache.Evict(fileName)
+       G.fileCache.Evict(filename)
 
-       return fileName
+       return filename
 }
 
 // CreateFileDummyPatch creates a patch file with the given name in the
@@ -379,9 +376,40 @@ func (t *Tester) CreateFileDummyPatch(re
                "+new")
 }
 
+func (t *Tester) CreateFileDummyBuildlink3(relativeFileName string) {
+       dir := path.Dir(relativeFileName)
+       lower := path.Base(dir)
+       upper := strings.ToUpper(lower)
+
+       width := tabWidth(sprintf("BUILDLINK_API_DEPENDS.%s+=\t", lower))
+
+       aligned := func(format string, args ...interface{}) string {
+               msg := sprintf(format, args...)
+               for tabWidth(msg) < width {
+                       msg += "\t"
+               }
+               return msg
+       }
+
+       t.CreateFileLines(relativeFileName,
+               MkRcsID,
+               sprintf(""),
+               sprintf("BUILDLINK_TREE+=\t%s", lower),
+               sprintf(""),
+               sprintf(".if !defined(%s_BUILDLINK3_MK)", upper),
+               sprintf("%s_BUILDLINK3_MK:=", upper),
+               sprintf(""),
+               aligned("BUILDLINK_API_DEPENDS.%s+=", lower)+sprintf("%s>=0", lower),
+               aligned("BUILDLINK_PKGSRCDIR.%s?=", lower)+sprintf("../../%s", dir),
+               aligned("BUILDLINK_DEPMETHOD.%s?=", lower)+"build",
+               sprintf(".endif # %s_BUILDLINK3_MK", upper),
+               sprintf(""),
+               sprintf("BUILDLINK_TREE+=\t-%s", lower))
+}
+
 // File returns the absolute path to the given file in the
 // temporary directory. It doesn't check whether that file exists.
-// Calls to Tester.Chdir change the base directory for the relative file name.
+// Calls to Tester.Chdir change the base directory for the relative filename.
 func (t *Tester) File(relativeFileName string) string {
        if t.tmpdir == "" {
                t.tmpdir = filepath.ToSlash(t.c.MkDir())
@@ -407,7 +435,7 @@ func (t *Tester) File(relativeFileName s
 func (t *Tester) Chdir(relativeDirName string) {
        if t.relCwd != "" {
                // When multiple calls of Chdir are mixed with calls to CreateFileLines,
-               // the resulting Lines and MkLines variables will use relative file names,
+               // the resulting Lines and MkLines variables will use relative filenames,
                // and these will point to different areas in the file system. This is
                // usually not indented and therefore prevented.
                t.c.Fatalf("Chdir must only be called once per test; already in %q.", t.relCwd)
@@ -422,10 +450,10 @@ func (t *Tester) Chdir(relativeDirName s
 
 // 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)
+       filename := t.File(relativeFileName)
+       err := os.Remove(filename)
        t.c.Assert(err, check.IsNil)
-       G.fileCache.Evict(fileName)
+       G.fileCache.Evict(filename)
 }
 
 // Check delegates a check to the check.Check function.
@@ -518,37 +546,37 @@ func (t *Tester) NewRawLines(args ...int
 
 // NewLine creates an in-memory line with the given text.
 // This line does not correspond to any line in a file.
-func (t *Tester) NewLine(fileName string, lineno int, text string) Line {
+func (t *Tester) NewLine(filename string, lineno int, text string) Line {
        textnl := text + "\n"
        rawLine := RawLine{lineno, textnl, textnl}
-       return NewLine(fileName, lineno, text, []*RawLine{&rawLine})
+       return NewLine(filename, lineno, text, &rawLine)
 }
 
 // NewMkLine creates an in-memory line in the Makefile format with the given text.
-func (t *Tester) NewMkLine(fileName string, lineno int, text string) MkLine {
-       return NewMkLine(t.NewLine(fileName, lineno, text))
+func (t *Tester) NewMkLine(filename string, lineno int, text string) MkLine {
+       return NewMkLine(t.NewLine(filename, lineno, text))
 }
 
-func (t *Tester) NewShellLine(fileName string, lineno int, text string) *ShellLine {
-       return NewShellLine(t.NewMkLine(fileName, lineno, text))
+func (t *Tester) NewShellLine(filename string, lineno int, text string) *ShellLine {
+       return NewShellLine(t.NewMkLine(filename, lineno, text))
 }
 
 // NewLines returns a list of simple lines that belong together.
 //
 // To work with line continuations like in Makefiles, use SetupFileMkLines.
-func (t *Tester) NewLines(fileName string, lines ...string) Lines {
-       return t.NewLinesAt(fileName, 1, lines...)
+func (t *Tester) NewLines(filename string, lines ...string) Lines {
+       return t.NewLinesAt(filename, 1, lines...)
 }
 
 // NewLinesAt returns a list of simple lines that belong together.
 //
 // To work with line continuations like in Makefiles, use SetupFileMkLines.
-func (t *Tester) NewLinesAt(fileName string, firstLine int, texts ...string) Lines {
+func (t *Tester) NewLinesAt(filename string, firstLine int, texts ...string) Lines {
        lines := make([]Line, len(texts))
        for i, text := range texts {
-               lines[i] = t.NewLine(fileName, i+firstLine, text)
+               lines[i] = t.NewLine(filename, i+firstLine, text)
        }
-       return NewLines(fileName, lines)
+       return NewLines(filename, lines)
 }
 
 // NewMkLines returns a list of lines in Makefile format,
@@ -557,13 +585,13 @@ func (t *Tester) NewLinesAt(fileName str
 //
 // No actual file is created for the lines;
 // see SetupFileMkLines for loading Makefile fragments with line continuations.
-func (t *Tester) NewMkLines(fileName string, lines ...string) MkLines {
+func (t *Tester) NewMkLines(filename string, lines ...string) MkLines {
        var rawText strings.Builder
        for _, line := range lines {
                rawText.WriteString(line)
                rawText.WriteString("\n")
        }
-       return NewMkLines(convertToLogicalLines(fileName, rawText.String(), true))
+       return NewMkLines(convertToLogicalLines(filename, rawText.String(), true))
 }
 
 // Returns and consumes the output from both stdout and stderr.
@@ -619,7 +647,7 @@ func (t *Tester) CheckOutputLines(expect
 // In JetBrains GoLand, the tracing output is suppressed after the first
 // failed check, see https://youtrack.jetbrains.com/issue/GO-6154.
 func (t *Tester) EnableTracing() {
-       G.logOut = NewSeparatorWriter(io.MultiWriter(os.Stdout, &t.stdout))
+       G.out = NewSeparatorWriter(io.MultiWriter(os.Stdout, &t.stdout))
        trace.Out = os.Stdout
        trace.Tracing = true
 }
@@ -638,7 +666,7 @@ func (t *Tester) EnableTracingToLog() {
 // It is used to check all calls to trace.Result, since the compiler
 // cannot check them.
 func (t *Tester) EnableSilentTracing() {
-       G.logOut = NewSeparatorWriter(&t.stdout)
+       G.out = NewSeparatorWriter(&t.stdout)
        trace.Out = ioutil.Discard
        trace.Tracing = true
 }
@@ -647,7 +675,7 @@ func (t *Tester) EnableSilentTracing() {
 // The diagnostics go to the in-memory buffer again,
 // ready to be checked with CheckOutputLines.
 func (t *Tester) DisableTracing() {
-       G.logOut = NewSeparatorWriter(&t.stdout)
+       G.out = NewSeparatorWriter(&t.stdout)
        trace.Tracing = false
        trace.Out = nil
 }
Index: pkgsrc/pkgtools/pkglint/files/shell.go
diff -u pkgsrc/pkgtools/pkglint/files/shell.go:1.28 pkgsrc/pkgtools/pkglint/files/shell.go:1.29
--- pkgsrc/pkgtools/pkglint/files/shell.go:1.28 Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/shell.go      Sun Dec  2 01:57:48 2018
@@ -5,13 +5,7 @@ package main
 import (
        "netbsd.org/pkglint/textproc"
        "path"
-)
-
-const (
-       reShVarname      = `(?:[!#*\-\d?@]|\$\$|[A-Za-z_]\w*)`
-       reShVarexpansion = `(?:(?:#|##|%|%%|:-|:=|:\?|:\+|\+)[^$\\{}]*)`
-       reShVaruse       = `\$\$` + `(?:` + reShVarname + `|` + `\{` + reShVarname + `(?:` + reShVarexpansion + `)?` + `\})`
-       reShDollar       = `\\\$\$|` + reShVaruse + `|\$\$[,\-/]`
+       "strings"
 )
 
 // TODO: Can ShellLine and ShellProgramChecker be merged into one type?
@@ -40,7 +34,7 @@ func (shline *ShellLine) CheckWord(token
 
        // Delegate check for shell words consisting of a single variable use
        // to the MkLineChecker. Examples for these are ${VAR:Mpattern} or $@.
-       p := NewMkParser(line, token, false)
+       p := NewMkParser(nil, token, false)
        if varuse := p.VarUse(); varuse != nil && p.EOF() {
                MkLineChecker{shline.mkline}.CheckVaruse(varuse, shellwordVuc)
                return
@@ -53,137 +47,108 @@ func (shline *ShellLine) CheckWord(token
                line.Warnf("Please use the RCD_SCRIPTS mechanism to install rc.d scripts automatically to ${RCD_SCRIPTS_EXAMPLEDIR}.")
        }
 
-       parser := NewMkParser(line, token, false)
-       repl := parser.repl
+       shline.checkWordQuoting(token, checkQuoting, time)
+}
+
+func (shline *ShellLine) checkWordQuoting(token string, checkQuoting bool, time ToolTime) {
+       line := shline.mkline.Line
+       tok := NewShTokenizer(line, token, false)
+
+       atoms := tok.ShAtoms()
        quoting := shqPlain
 outer:
-       for !parser.EOF() {
+       for len(atoms) > 0 {
+               atom := atoms[0]
+               // Cutting off the first atom is done at the end of the loop since in
+               // some cases the called methods need to see the current atom.
+
                if trace.Tracing {
-                       trace.Stepf("shell state %s: %q", quoting, parser.Rest())
+                       trace.Stepf("shell state %s: %q", quoting, atom)
                }
 
                switch {
-               // When parsing inside backticks, it is more
-               // reasonable to check the whole shell command
-               // recursively, instead of splitting off the first
-               // make(1) variable.
-               case quoting == shqBackt || quoting == shqDquotBackt:
-                       var backtCommand string
-                       backtCommand, quoting = shline.unescapeBackticks(token, repl, quoting)
-                       setE := true
-                       shline.CheckShellCommand(backtCommand, &setE, time)
-
-                       // Make(1) variables have the same syntax, no matter in which state we are currently.
-               case shline.checkVaruseToken(parser, quoting):
-                       break
+               case atom.Quoting == shqBackt || atom.Quoting == shqDquotBackt:
+                       backtCommand := shline.unescapeBackticks(&atoms, quoting)
+                       if backtCommand != "" {
+                               setE := true
+                               shline.CheckShellCommand(backtCommand, &setE, time)
+                       }
+                       continue
+
+                       // Make(1) variables have the same syntax, no matter in which state the shell parser is currently.
+               case shline.checkVaruseToken(&atoms, quoting):
+                       continue
 
                case quoting == shqPlain:
                        switch {
-                       // 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("\""):
-                               quoting = shqDquot
-                       case repl.AdvanceStr("`"):
-                               quoting = shqBackt
-                       case repl.AdvanceRegexp(`^\$\$([0-9A-Z_a-z]+|#)`),
-                               repl.AdvanceRegexp(`^\$\$\{([0-9A-Z_a-z]+|#)\}`),
-                               repl.AdvanceRegexp(`^\$\$(\$)\$`):
-                               shvarname := repl.Group(1)
-                               if G.Opts.WarnQuoting && checkQuoting && shline.variableNeedsQuoting(shvarname) {
-                                       line.Warnf("Unquoted shell variable %q.", shvarname)
-                                       Explain(
-                                               "When a shell variable contains white-space, it is expanded (split",
-                                               "into multiple words) when it is written as $variable in a shell",
-                                               "script.  If that is not intended, you should add quotation marks",
-                                               "around it, like \"$variable\".  Then, the variable will always expand",
-                                               "to a single word, preserving all white-space and other special",
-                                               "characters.",
-                                               "",
-                                               "Example:",
-                                               "\tfname=\"Curriculum vitae.doc\"",
-                                               "\tcp $fileName /tmp",
-                                               "\t# tries to copy the two files \"Curriculum\" and \"Vitae.doc\"",
-                                               "\tcp \"$fileName\" /tmp",
-                                               "\t# copies one file, as intended")
-                               }
-
-                       case repl.AdvanceStr("$$@"):
-                               line.Warnf("The $@ shell variable should only be used in double quotes.")
+                       case atom.Type == shtShVarUse:
+                               shline.checkShVarUse(atom, checkQuoting)
 
-                       case repl.AdvanceStr("$$?"):
-                               line.Warnf("The $? shell variable is often not available in \"set -e\" mode.")
-
-                       case repl.AdvanceStr("$$("):
+                       case atom.Type == shtSubshell:
                                line.Warnf("Invoking subshells via $(...) is not portable enough.")
-                               Explain(
+                               G.Explain(
                                        "The Solaris /bin/sh does not know this way to execute a command in a",
                                        "subshell.  Please use backticks (`...`) as a replacement.")
-                               return // To avoid internal parse errors
+                               return // To avoid internal pkglint parse errors
 
-                       case repl.AdvanceStr("$$"): // Not part of a variable.
+                       case atom.Type == shtText:
                                break
 
                        default:
                                break outer
                        }
-
-               case quoting == shqSquot:
-                       switch {
-                       case repl.AdvanceStr("'"):
-                               quoting = shqPlain
-                       case repl.NextBytesFunc(func(b byte) bool { return b != '$' && b != '\'' }) != "":
-                               // just skip
-                       case repl.AdvanceStr("$$"):
-                               // just skip
-                       default:
-                               break outer
-                       }
-
-               case quoting == shqDquot:
-                       switch {
-                       case repl.AdvanceStr("\""):
-                               quoting = shqPlain
-                       case repl.AdvanceStr("`"):
-                               quoting = shqDquotBackt
-                       case repl.NextBytesFunc(func(b byte) bool { return b != '$' && b != '"' && b != '\\' && b != '`' }) != "":
-                               break
-                       case repl.AdvanceStr("\\$$"):
-                               break
-                       case repl.AdvanceRegexp(`^\\.`): // See http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_02_01
-                               break
-                       case repl.AdvanceRegexp(`^\$\$\{\w+[#%+\-:]*[^{}]*\}`),
-                               repl.AdvanceRegexp(`^\$\$(?:\w+|[!#?@]|\$\$)`):
-                               break
-                       case repl.AdvanceStr("$$"):
-                               line.Warnf("Unescaped $ or strange shell variable found.")
-                       default:
-                               break outer
-                       }
                }
+
+               quoting = atom.Quoting
+               atoms = atoms[1:]
        }
 
-       if trimHspace(parser.Rest()) != "" {
-               line.Warnf("Pkglint parse error in ShellLine.CheckWord at %q (quoting=%s), rest: %s", token, quoting, parser.Rest())
+       if trimHspace(tok.Rest()) != "" {
+               line.Warnf("Pkglint parse error in ShellLine.CheckWord at %q (quoting=%s), rest: %s", token, quoting, tok.Rest())
        }
 }
 
-func (shline *ShellLine) checkVaruseToken(parser *MkParser, quoting ShQuoting) bool {
-       if trace.Tracing {
-               defer trace.Call(parser.Rest(), quoting)()
+func (shline *ShellLine) checkShVarUse(atom *ShAtom, checkQuoting bool) {
+       line := shline.mkline.Line
+       shVarname := atom.ShVarname()
+
+       if shVarname == "@" {
+               line.Warnf("The $@ shell variable should only be used in double quotes.")
+
+       } else if G.Opts.WarnQuoting && checkQuoting && shline.variableNeedsQuoting(shVarname) {
+               line.Warnf("Unquoted shell variable %q.", shVarname)
+               G.Explain(
+                       "When a shell variable contains whitespace, it is expanded (split",
+                       "into multiple words) when it is written as $variable in a shell",
+                       "script.  If that is not intended, you should add quotation marks",
+                       "around it, like \"$variable\".  Then, the variable will always expand",
+                       "to a single word, preserving all whitespace and other special",
+                       "characters.",
+                       "",
+                       "Example:",
+                       "\tfname=\"Curriculum vitae.doc\"",
+                       "\tcp $filename /tmp",
+                       "\t# tries to copy the two files \"Curriculum\" and \"Vitae.doc\"",
+                       "\tcp \"$filename\" /tmp",
+                       "\t# copies one file, as intended")
        }
 
-       varuse := parser.VarUse()
+       if shVarname == "?" {
+               line.Warnf("The $? shell variable is often not available in \"set -e\" mode.")
+       }
+}
+
+func (shline *ShellLine) checkVaruseToken(atoms *[]*ShAtom, quoting ShQuoting) bool {
+       varuse := (*atoms)[0].VarUse()
        if varuse == nil {
                return false
        }
+       *atoms = (*atoms)[1:]
        varname := varuse.varname
 
        if varname == "@" {
                shline.mkline.Warnf("Please use \"${.TARGET}\" instead of \"$@\".")
-               Explain(
+               G.Explain(
                        "The variable $@ can easily be confused with the shell variable of",
                        "the same name, which has a completely different meaning.")
                varname = ".TARGET"
@@ -195,75 +160,92 @@ func (shline *ShellLine) checkVaruseToke
                // Fine.
 
        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 [\$\\\"\'\`].
+               // This is ok as long as these variables don't have embedded [$\\"'`].
 
        case quoting == shqDquot && varuse.IsQ():
                shline.mkline.Warnf("Please don't use the :Q operator in double quotes.")
-               Explain(
+               G.Explain(
                        "Either remove the :Q or the double quotes.  In most cases, it is",
                        "more appropriate to remove the double quotes.")
        }
 
        if varname != "@" {
                vucstate := quoting.ToVarUseContext()
-               vuc := &VarUseContext{shellcommandsContextType, vucTimeUnknown, vucstate, true}
-               MkLineChecker{shline.mkline}.CheckVaruse(varuse, vuc)
+               vuc := VarUseContext{shellcommandsContextType, vucTimeUnknown, vucstate, true}
+               MkLineChecker{shline.mkline}.CheckVaruse(varuse, &vuc)
        }
        return true
 }
 
-// Scan for the end of the backticks, checking for single backslashes
-// and removing one level of backslashes. Backslashes are only removed
-// before a dollar, a backslash or a backtick.
+// unescapeBackticks takes a backticks expression like `echo \\"hello\\"` and
+// returns the part inside the backticks, removing one level of backslashes.
+//
+// Backslashes are only removed before a dollar, a backslash or a backtick.
+// Other backslashes generate a warning since it is easier to remember that
+// all backslashes are unescaped.
 //
 // See http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_03
-func (shline *ShellLine) unescapeBackticks(shellword string, repl *textproc.PrefixReplacer, quoting ShQuoting) (unescaped string, newQuoting ShQuoting) {
-       if trace.Tracing {
-               defer trace.Call(shellword, quoting, trace.Result(&unescaped))()
-       }
-
+func (shline *ShellLine) unescapeBackticks(atoms *[]*ShAtom, quoting ShQuoting) string {
        line := shline.mkline.Line
-       for repl.Rest() != "" {
-               switch {
-               case repl.AdvanceStr("`"):
-                       if quoting == shqBackt {
-                               quoting = shqPlain
-                       } else {
-                               quoting = shqDquot
-                       }
-                       return unescaped, quoting
 
-               case repl.AdvanceStr("\\\""), repl.AdvanceStr("\\\\"), repl.AdvanceStr("\\`"), repl.AdvanceStr("\\$"):
-                       unescaped += repl.Str()[1:]
+       // Skip the initial backtick.
+       *atoms = (*atoms)[1:]
 
-               case repl.AdvanceStr("\\"):
-                       line.Warnf("Backslashes should be doubled inside backticks.")
-                       unescaped += "\\"
+       var unescaped strings.Builder
+       for len(*atoms) > 0 {
+               atom := (*atoms)[0]
+               *atoms = (*atoms)[1:]
+
+               if atom.Quoting == quoting {
+                       return unescaped.String()
+               }
+
+               if atom.Type != shtText {
+                       unescaped.WriteString(atom.MkText)
+                       continue
+               }
+
+               lex := textproc.NewLexer(atom.MkText)
+               for !lex.EOF() {
+                       unescaped.WriteString(lex.NextBytesFunc(func(b byte) bool { return b != '\\' }))
+                       if lex.SkipByte('\\') {
+                               switch lex.PeekByte() {
+                               case '"', '\\', '`', '$':
+                                       unescaped.WriteByte(byte(lex.PeekByte()))
+                                       lex.Skip(1)
+                               default:
+                                       line.Warnf("Backslashes should be doubled inside backticks.")
+                                       unescaped.WriteByte('\\')
+                               }
+                       }
+               }
 
-               case quoting == shqDquotBackt && repl.AdvanceStr("\""):
+               // XXX: The regular expression is a bit cheated but is good enough until
+               // pkglint has a real parser for all shell constructs.
+               if atom.Quoting == shqDquotBackt && matches(atom.MkText, `(^|[^\\])"`) {
                        line.Warnf("Double quotes inside backticks inside double quotes are error prone.")
-                       Explain(
+                       G.Explain(
                                "According to the SUSv3, they produce undefined results.",
                                "",
                                "See the paragraph starting \"Within the backquoted ...\" in",
                                "http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html.";,
                                "",
                                "To avoid this uncertainty, escape the double quotes using \\\".")
-
-               default:
-                       G.Assertf(repl.AdvanceRegexp("^([^\\\\`]+)"), "incomplete switch")
-                       unescaped += repl.Group(1)
                }
        }
-       line.Errorf("Unfinished backquotes: %s", repl.Rest())
-       return unescaped, quoting
+
+       line.Errorf("Unfinished backticks after %q.", unescaped.String())
+       return unescaped.String()
 }
 
-func (shline *ShellLine) variableNeedsQuoting(shvarname string) bool {
-       switch shvarname {
-       case "#", "?":
+func (shline *ShellLine) variableNeedsQuoting(shVarname string) bool {
+       switch shVarname {
+       case "#", "?", "$":
                return false // Definitely ok
-       case "d", "f", "i", "dir", "file", "src", "dst":
+       case "d", "f", "i", "id", "file", "src", "dst", "prefix":
+               return false // Probably ok
+       }
+       if hasSuffix(shVarname, "dir") {
                return false // Probably ok
        }
        return true
@@ -278,7 +260,7 @@ func (shline *ShellLine) CheckShellComma
 
        if contains(shelltext, "${SED}") && contains(shelltext, "${MV}") {
                line.Notef("Please use the SUBST framework instead of ${SED} and ${MV}.")
-               Explain(
+               G.Explain(
                        "Using the SUBST framework instead of explicit commands is easier",
                        "to understand, since all the complexity of using sed and mv is",
                        "hidden behind the scenes.",
@@ -286,7 +268,7 @@ func (shline *ShellLine) CheckShellComma
                        // TODO: Provide a copy-and-paste example.
                        sprintf("Run %q for more information.", makeHelp("subst")))
                if contains(shelltext, "#") {
-                       Explain(
+                       G.Explain(
                                "When migrating to the SUBST framework, pay attention to \"#\"",
                                "characters.  In shell commands, make(1) does not interpret them as",
                                "comment character, but in variable assignments it does.  Therefore,",
@@ -310,7 +292,7 @@ func (shline *ShellLine) CheckShellComma
        if hiddenAndSuppress != "" {
                shline.checkHiddenAndSuppress(hiddenAndSuppress, lexer.Rest())
        }
-       setE := lexer.NextString("${RUN}") != ""
+       setE := lexer.SkipString("${RUN}")
        if !setE {
                lexer.NextString("${_PKG_SILENT}${_PKG_DEBUG}")
        }
@@ -334,7 +316,7 @@ func (shline *ShellLine) CheckShellComma
                return
        }
 
-       spc := &ShellProgramChecker{shline}
+       spc := ShellProgramChecker{shline}
        spc.checkConditionalCd(program)
 
        walker := NewMkShWalker()
@@ -400,7 +382,7 @@ func (shline *ShellLine) checkHiddenAndS
                                break
                        default:
                                shline.mkline.Warnf("The shell command %q should not be hidden.", cmd)
-                               Explain(
+                               G.Explain(
                                        "Hidden shell commands do not appear on the terminal or in the log",
                                        "file when they are executed.  When they fail, the error message",
                                        "cannot be assigned to the command, which is very difficult to debug.",
@@ -414,7 +396,7 @@ func (shline *ShellLine) checkHiddenAndS
 
        if contains(hiddenAndSuppress, "-") {
                shline.mkline.Warnf("Using a leading \"-\" to suppress errors is deprecated.")
-               Explain(
+               G.Explain(
                        "If you really want to ignore any errors from this command, append",
                        "\"|| ${TRUE}\" to the command.")
        }
@@ -456,17 +438,25 @@ func (scc *SimpleCommandChecker) checkCo
 
        switch {
        case shellword == "${RUN}" || shellword == "":
+               break
        case scc.handleForbiddenCommand():
+               break
        case scc.handleTool():
+               break
        case scc.handleCommandVariable():
+               break
        case matches(shellword, `^(?::|break|cd|continue|eval|exec|exit|export|read|set|shift|umask|unset)$`):
+               break
        case hasPrefix(shellword, "./"): // All commands from the current directory are fine.
+               break
        case matches(shellword, `\$\{(PKGSRCDIR|PREFIX)(:Q)?\}`):
+               break
        case scc.handleComment():
+               break
        default:
                if G.Opts.WarnExtra && !(G.Mk != nil && G.Mk.indentation.DependsOn("OPSYS")) {
                        scc.shline.mkline.Warnf("Unknown shell command %q.", shellword)
-                       Explain(
+                       G.Explain(
                                "If you want your package to be portable to all platforms that pkgsrc",
                                "supports, you should only use shell commands that are covered by the",
                                "tools framework.")
@@ -505,7 +495,7 @@ func (scc *SimpleCommandChecker) handleF
        switch path.Base(shellword) {
        case "ktrace", "mktexlsr", "strace", "texconfig", "truss":
                scc.shline.mkline.Errorf("%q must not be used in Makefiles.", shellword)
-               Explain(
+               G.Explain(
                        "This command must appear in INSTALL scripts, not in the package",
                        "Makefile, so that the package also works if it is installed as a",
                        "binary package via pkg_add.")
@@ -520,11 +510,11 @@ func (scc *SimpleCommandChecker) handleC
        }
 
        shellword := scc.strcmd.Name
-       parser := NewMkParser(scc.shline.mkline.Line, shellword, false)
+       parser := NewMkParser(nil, shellword, false)
        if varuse := parser.VarUse(); varuse != nil && parser.EOF() {
                varname := varuse.varname
 
-               if tool := G.ToolByVarname(varname, RunTime /* LoadTime would also work */); tool != nil {
+               if tool := G.ToolByVarname(varname); tool != nil {
                        if tool.Validity == Nowhere {
                                scc.shline.mkline.Warnf("The %q tool is used but not added to USE_TOOLS.", tool.Name)
                        }
@@ -574,7 +564,7 @@ func (scc *SimpleCommandChecker) handleC
        }
 
        if semicolon || multiline {
-               Explain(
+               G.Explain(
                        "When you split a shell command into multiple lines that are",
                        "continued with a backslash, they will nevertheless be converted to",
                        "a single line before the shell sees them.  That means that even if",
@@ -600,11 +590,11 @@ func (scc *SimpleCommandChecker) checkRe
        isSubst := false
        for _, arg := range scc.strcmd.Args {
                if !isSubst {
-                       CheckLineAbsolutePathname(scc.shline.mkline.Line, arg)
+                       LineChecker{scc.shline.mkline.Line}.CheckAbsolutePathname(arg)
                }
                if G.Testing && isSubst && !matches(arg, `"^[\"\'].*[\"\']$`) {
                        scc.shline.mkline.Warnf("Substitution commands like %q should always be quoted.", arg)
-                       Explain(
+                       G.Explain(
                                "Usually these substitution commands contain characters like '*' or",
                                "other shell metacharacters that might lead to lookup of matching",
                                "filenames and then expand to more than one word.")
@@ -635,7 +625,7 @@ func (scc *SimpleCommandChecker) checkAu
                        if m, dirname := match1(arg, `^(?:\$\{DESTDIR\})?\$\{PREFIX(?:|:Q)\}/(.*)`); m {
                                if G.Pkg != nil && G.Pkg.Plist.Dirs[dirname] {
                                        scc.shline.mkline.Notef("You can use AUTO_MKDIRS=yes or \"INSTALLATION_DIRS+= %s\" instead of %q.", dirname, cmdname)
-                                       Explain(
+                                       G.Explain(
                                                "Many packages include a list of all needed directories in their",
                                                "PLIST file.  In such a case, you can just set AUTO_MKDIRS=yes and",
                                                "be done.  The pkgsrc infrastructure will then create all directories",
@@ -648,7 +638,7 @@ func (scc *SimpleCommandChecker) checkAu
                                                "INSTALLATION_DIRS takes care of that.")
                                } else {
                                        scc.shline.mkline.Notef("You can use \"INSTALLATION_DIRS+= %s\" instead of %q.", dirname, cmdname)
-                                       Explain(
+                                       G.Explain(
                                                "To create directories during installation, it is easier to just",
                                                "list them in INSTALLATION_DIRS than to execute the commands",
                                                "explicitly.  That way, you don't have to think about which",
@@ -678,7 +668,7 @@ func (scc *SimpleCommandChecker) checkIn
                        default:
                                if prevdir != "" {
                                        scc.shline.mkline.Warnf("The INSTALL_*_DIR commands can only handle one directory at a time.")
-                                       Explain(
+                                       G.Explain(
                                                "Many implementations of install(1) can handle more, but pkgsrc aims",
                                                "at maximum portability.")
                                        return
@@ -696,7 +686,7 @@ func (scc *SimpleCommandChecker) checkPa
 
        if (scc.strcmd.Name == "${PAX}" || scc.strcmd.Name == "pax") && scc.strcmd.HasOption("-pe") {
                scc.shline.mkline.Warnf("Please use the -pp option to pax(1) instead of -pe.")
-               Explain(
+               G.Explain(
                        "The -pe option tells pax to preserve the ownership of the files,",
                        "which means that the installed files will belong to the user that",
                        "has built the package.")
@@ -736,7 +726,7 @@ func (spc *ShellProgramChecker) checkCon
        checkConditionalCd := func(cmd *MkShSimpleCommand) {
                if NewStrCommand(cmd).Name == "cd" {
                        spc.shline.mkline.Errorf("The Solaris /bin/sh cannot handle \"cd\" inside conditionals.")
-                       Explain(
+                       G.Explain(
                                "When the Solaris shell is in \"set -e\" mode and \"cd\" fails, the",
                                "shell will exit, no matter if it is protected by an \"if\" or the",
                                "\"||\" operator.")
@@ -759,7 +749,7 @@ func (spc *ShellProgramChecker) checkCon
        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(
+                       G.Explain(
                                "The GNU Autoconf manual has many more details of what shell",
                                "features to avoid for portable programs.  It can be read at:",
                                "https://www.gnu.org/software/autoconf/manual/autoconf.html#Limitations-of-Builtins";)
@@ -800,7 +790,7 @@ func (spc *ShellProgramChecker) checkPip
                        } else {
                                line.Warnf("The exitcode of the command at the left of the | operator is ignored.")
                        }
-                       Explain(
+                       G.Explain(
                                "In a shell command like \"cat *.txt | grep keyword\", if the command",
                                "on the left side of the \"|\" fails, this failure is ignored.",
                                "",
@@ -869,7 +859,7 @@ func (spc *ShellProgramChecker) canFail(
        args := simple.Args
        argc := len(args)
        switch toolName {
-       case "echo", "printf", "tr":
+       case "echo", "env", "printf", "tr":
                return false
        case "sed", "gsed":
                if argc == 2 && args[0].MkText == "-e" {
@@ -900,7 +890,7 @@ func (spc *ShellProgramChecker) checkSet
 
        line.Warnf("Please switch to \"set -e\" mode before using a semicolon (after %q) to separate commands.",
                NewStrCommand(command.Simple).String())
-       Explain(
+       G.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:",
@@ -943,14 +933,14 @@ func (shline *ShellLine) checkInstallCom
        case "sed", "${SED}",
                "tr", "${TR}":
                line.Warnf("The shell command %q should not be used in the install phase.", shellcmd)
-               Explain(
+               G.Explain(
                        "In the install phase, the only thing that should be done is to",
                        "install the prepared files to their final location.  The file's",
                        "contents should not be changed anymore.")
 
        case "cp", "${CP}":
                line.Warnf("${CP} should not be used to install files.")
-               Explain(
+               G.Explain(
                        "The ${CP} command is highly platform dependent and cannot overwrite",
                        "read-only files.  Please use ${PAX} instead.",
                        "",
@@ -993,7 +983,7 @@ func splitIntoShellTokens(line Line, tex
                prevAtom = atom
                if atom.Type == shtSpace && q == shqPlain {
                        emit()
-               } else if atom.Type == shtWord || atom.Type == shtVaruse || atom.Quoting != shqPlain {
+               } else if atom.Type.IsWord() || atom.Quoting != shqPlain {
                        word += atom.MkText
                } else {
                        emit()

Index: pkgsrc/pkgtools/pkglint/files/distinfo.go
diff -u pkgsrc/pkgtools/pkglint/files/distinfo.go:1.23 pkgsrc/pkgtools/pkglint/files/distinfo.go:1.24
--- pkgsrc/pkgtools/pkglint/files/distinfo.go:1.23      Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/distinfo.go   Sun Dec  2 01:57:48 2018
@@ -13,7 +13,7 @@ func ChecklinesDistinfo(lines Lines) {
                defer trace.Call1(lines.FileName)()
        }
 
-       fileName := lines.FileName
+       filename := lines.FileName
        patchdir := "patches"
        if G.Pkg != nil && dirExists(G.Pkg.File(G.Pkg.Patchdir)) {
                patchdir = G.Pkg.Patchdir
@@ -22,8 +22,8 @@ func ChecklinesDistinfo(lines Lines) {
                trace.Step1("patchdir=%q", patchdir)
        }
 
-       distinfoIsCommitted := isCommitted(fileName)
-       ck := &distinfoLinesChecker{
+       distinfoIsCommitted := isCommitted(filename)
+       ck := distinfoLinesChecker{
                lines, patchdir, distinfoIsCommitted,
                make(map[string]bool), "", nil, unknown, nil}
        ck.checkLines(lines)
@@ -32,7 +32,7 @@ func ChecklinesDistinfo(lines Lines) {
        SaveAutofixChanges(lines)
 }
 
-// XXX: Maybe an approach that first groups the lines by file name
+// XXX: Maybe an approach that first groups the lines by filename
 // is easier to understand.
 
 type distinfoLinesChecker struct {
@@ -50,7 +50,7 @@ type distinfoLinesChecker struct {
 }
 
 func (ck *distinfoLinesChecker) checkLines(lines Lines) {
-       CheckLineRcsid(lines.Lines[0], ``, "")
+       lines.CheckRcsID(0, ``, "")
        if 1 < len(lines.Lines) && lines.Lines[1].Text != "" {
                lines.Lines[1].Notef("Empty line expected.")
        }
@@ -59,19 +59,19 @@ func (ck *distinfoLinesChecker) checkLin
                if i < 2 {
                        continue
                }
-               m, alg, fileName, hash := match3(line.Text, `^(\w+) \((\w[^)]*)\) = (.*)(?: bytes)?$`)
+               m, alg, filename, hash := match3(line.Text, `^(\w+) \((\w[^)]*)\) = (.*)(?: bytes)?$`)
                if !m {
                        line.Errorf("Invalid line: %s", line.Text)
                        continue
                }
 
-               if fileName != ck.currentFileName {
-                       ck.onFilenameChange(line, fileName)
+               if filename != ck.currentFileName {
+                       ck.onFilenameChange(line, filename)
                }
                ck.algorithms = append(ck.algorithms, alg)
 
-               ck.checkGlobalDistfileMismatch(line, fileName, alg, hash)
-               ck.checkUncommittedPatch(line, fileName, alg, hash)
+               ck.checkGlobalDistfileMismatch(line, filename, alg, hash)
+               ck.checkUncommittedPatch(line, filename, alg, hash)
        }
        ck.onFilenameChange(ck.distinfoLines.EOFLine(), "")
 }
@@ -97,14 +97,14 @@ func (ck *distinfoLinesChecker) onFilena
 }
 
 func (ck *distinfoLinesChecker) checkAlgorithms(line Line) {
-       fileName := ck.currentFileName
+       filename := ck.currentFileName
        algorithms := strings.Join(ck.algorithms, ", ")
 
        switch {
 
        case ck.isPatch == yes:
                if algorithms != "SHA1" {
-                       line.Errorf("Expected SHA1 hash for %s, got %s.", fileName, algorithms)
+                       line.Errorf("Expected SHA1 hash for %s, got %s.", filename, algorithms)
                }
 
        case ck.isPatch == unknown:
@@ -113,10 +113,10 @@ func (ck *distinfoLinesChecker) checkAlg
        case G.Pkg != nil && G.Pkg.IgnoreMissingPatches:
                break
 
-       case hasPrefix(fileName, "patch-") && algorithms == "SHA1":
-               pathToPatchdir := relpath(path.Dir(ck.currentFirstLine.FileName), G.Pkg.File(ck.patchdir))
-               ck.currentFirstLine.Warnf("Patch file %q does not exist in directory %q.", fileName, pathToPatchdir)
-               Explain(
+       case hasPrefix(filename, "patch-") && algorithms == "SHA1":
+               pathToPatchdir := relpath(path.Dir(ck.currentFirstLine.Filename), G.Pkg.File(ck.patchdir))
+               ck.currentFirstLine.Warnf("Patch file %q does not exist in directory %q.", filename, pathToPatchdir)
+               G.Explain(
                        "If the patches directory looks correct, the patch may have been",
                        "removed without updating the distinfo file.  In such a case please",
                        "update the distinfo file.",
@@ -124,7 +124,7 @@ func (ck *distinfoLinesChecker) checkAlg
                        "If the patches directory looks wrong, pkglint needs to be improved.")
 
        case algorithms != "SHA1, RMD160, Size" && algorithms != "SHA1, RMD160, SHA512, Size":
-               line.Errorf("Expected SHA1, RMD160, SHA512, Size checksums for %q, got %s.", fileName, algorithms)
+               line.Errorf("Expected SHA1, RMD160, SHA512, Size checksums for %q, got %s.", filename, algorithms)
        }
 }
 
@@ -152,19 +152,19 @@ func (ck *distinfoLinesChecker) checkUnr
 }
 
 // Inter-package check for differing distfile checksums.
-func (ck *distinfoLinesChecker) checkGlobalDistfileMismatch(line Line, fileName, alg, hash string) {
+func (ck *distinfoLinesChecker) checkGlobalDistfileMismatch(line Line, filename, alg, hash string) {
        hashes := G.Pkgsrc.Hashes
 
-       // Intentionally checking the file name instead of ck.isPatch.
+       // Intentionally checking the filename instead of ck.isPatch.
        // Missing the few distfiles that actually start with patch-*
        // is more convenient than having lots of false positive mismatches.
-       if hashes != nil && !hasPrefix(fileName, "patch-") {
-               key := alg + ":" + fileName
+       if hashes != nil && !hasPrefix(filename, "patch-") {
+               key := alg + ":" + filename
                otherHash := hashes[key]
                if otherHash != nil {
                        if otherHash.hash != hash {
                                line.Errorf("The %s hash for %s is %s, which conflicts with %s in %s.",
-                                       alg, fileName, hash, otherHash.hash, line.RefTo(otherHash.line))
+                                       alg, filename, hash, otherHash.hash, line.RefTo(otherHash.line))
                        }
                } else {
                        hashes[key] = &Hash{hash, line}
Index: pkgsrc/pkgtools/pkglint/files/patches_test.go
diff -u pkgsrc/pkgtools/pkglint/files/patches_test.go:1.23 pkgsrc/pkgtools/pkglint/files/patches_test.go:1.24
--- pkgsrc/pkgtools/pkglint/files/patches_test.go:1.23  Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/patches_test.go       Sun Dec  2 01:57:48 2018
@@ -351,7 +351,7 @@ func (s *Suite) Test_ChecklinesPatch__on
 func (s *Suite) Test_ChecklinesPatch__Makefile_with_absolute_pathnames(c *check.C) {
        t := s.Init(c)
 
-       t.SetupCommandLine( /*none*/ )
+       t.SetupCommandLine("-Wabsname", "-Wno-extra")
        lines := t.NewLines("patch-unified",
                RcsID,
                "",
@@ -566,9 +566,9 @@ func (s *Suite) Test_ChecklinesPatch__em
        ChecklinesPatch(lines)
 
        // The first context line should start with a single space character,
-       // but that would mean trailing white-space, so it may be left out.
+       // but that would mean trailing whitespace, so it may be left out.
        // The last context line is omitted completely because it would also
-       // have trailing white-space, and if that were removed, would be a
+       // have trailing whitespace, and if that were removed, would be a
        // trailing empty line.
        t.CheckOutputEmpty()
 }
@@ -592,9 +592,9 @@ func (s *Suite) Test_ChecklinesPatch__in
        ChecklinesPatch(lines)
 
        // The first context line should start with a single space character,
-       // but that would mean trailing white-space, so it may be left out.
+       // but that would mean trailing whitespace, so it may be left out.
        // The last context line is omitted completely because it would also
-       // have trailing white-space, and if that were removed, would be a
+       // have trailing whitespace, and if that were removed, would be a
        // trailing empty line.
        t.CheckOutputLines(
                "ERROR: ~/patch-aa:10: Invalid line in unified patch hunk: <<<<<<<<")

Index: pkgsrc/pkgtools/pkglint/files/distinfo_test.go
diff -u pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.19 pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.20
--- pkgsrc/pkgtools/pkglint/files/distinfo_test.go:1.19 Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/distinfo_test.go      Sun Dec  2 01:57:48 2018
@@ -274,7 +274,7 @@ func (s *Suite) Test_distinfoLinesChecke
        G.Pkg = NewPackage(t.File("category/package"))
        distinfoLine := t.NewLine(t.File("category/package/distinfo"), 5, "")
 
-       checker := &distinfoLinesChecker{}
+       checker := distinfoLinesChecker{}
        checker.checkPatchSha1(distinfoLine, "patch-nonexistent", "distinfo-sha1")
 
        t.CheckOutputLines(
Index: pkgsrc/pkgtools/pkglint/files/files_test.go
diff -u pkgsrc/pkgtools/pkglint/files/files_test.go:1.19 pkgsrc/pkgtools/pkglint/files/files_test.go:1.20
--- pkgsrc/pkgtools/pkglint/files/files_test.go:1.19    Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/files_test.go Sun Dec  2 01:57:48 2018
@@ -9,11 +9,11 @@ func (s *Suite) Test_convertToLogicalLin
                "first line\n" +
                "second line\n"
 
-       lines := convertToLogicalLines("fileName", rawText, false)
+       lines := convertToLogicalLines("filename", rawText, false)
 
        c.Check(lines.Len(), equals, 2)
-       c.Check(lines.Lines[0].String(), equals, "fileName:1: first line")
-       c.Check(lines.Lines[1].String(), equals, "fileName:2: second line")
+       c.Check(lines.Lines[0].String(), equals, "filename:1: first line")
+       c.Check(lines.Lines[1].String(), equals, "filename:2: second line")
 }
 
 func (s *Suite) Test_convertToLogicalLines__continuation(c *check.C) {
@@ -22,11 +22,11 @@ func (s *Suite) Test_convertToLogicalLin
                "still first line\n" +
                "second line\n"
 
-       lines := convertToLogicalLines("fileName", rawText, true)
+       lines := convertToLogicalLines("filename", rawText, true)
 
        c.Check(lines.Len(), equals, 2)
-       c.Check(lines.Lines[0].String(), equals, "fileName:1--2: first line, still first line")
-       c.Check(lines.Lines[1].String(), equals, "fileName:3: second line")
+       c.Check(lines.Lines[0].String(), equals, "filename:1--2: first line, still first line")
+       c.Check(lines.Lines[1].String(), equals, "filename:3: second line")
 }
 
 func (s *Suite) Test_convertToLogicalLines__continuation_in_last_line(c *check.C) {
@@ -35,10 +35,10 @@ func (s *Suite) Test_convertToLogicalLin
        rawText := "" +
                "last line\\\n"
 
-       lines := convertToLogicalLines("fileName", rawText, true)
+       lines := convertToLogicalLines("filename", rawText, true)
 
        c.Check(lines.Len(), equals, 1)
-       c.Check(lines.Lines[0].String(), equals, "fileName:1: last line\\")
+       c.Check(lines.Lines[0].String(), equals, "filename:1: last line\\")
        t.CheckOutputEmpty()
 }
 
@@ -119,12 +119,28 @@ func (s *Suite) Test_convertToLogicalLin
        rawText := "" +
                "last line\\"
 
-       lines := convertToLogicalLines("fileName", rawText, true)
+       lines := convertToLogicalLines("filename", rawText, true)
 
        c.Check(lines.Len(), equals, 1)
-       c.Check(lines.Lines[0].String(), equals, "fileName:1: last line\\")
+       c.Check(lines.Lines[0].String(), equals, "filename:1: last line\\")
        t.CheckOutputLines(
-               "ERROR: fileName:1: File must end with a newline.")
+               "ERROR: filename:1: File must end with a newline.")
+}
+
+func (s *Suite) Test_convertToLogicalLines__missing_newline_at_eof_with_source(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall", "--source")
+       rawText := "" +
+               "last line\\"
+
+       lines := convertToLogicalLines("filename", rawText, true)
+
+       c.Check(lines.Len(), equals, 1)
+       c.Check(lines.Lines[0].String(), equals, "filename:1: last line\\")
+       t.CheckOutputLines(
+               // FIXME: linebreak is missing before ERROR.
+               ">\tlast line\\ERROR: filename:1: File must end with a newline.")
 }
 
 func (s *Suite) Test_matchContinuationLine(c *check.C) {
Index: pkgsrc/pkgtools/pkglint/files/mkparser.go
diff -u pkgsrc/pkgtools/pkglint/files/mkparser.go:1.19 pkgsrc/pkgtools/pkglint/files/mkparser.go:1.20
--- pkgsrc/pkgtools/pkglint/files/mkparser.go:1.19      Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/mkparser.go   Sun Dec  2 01:57:48 2018
@@ -2,6 +2,7 @@ package main
 
 import (
        "netbsd.org/pkglint/regex"
+       "netbsd.org/pkglint/textproc"
        "strings"
 )
 
@@ -14,34 +15,36 @@ type MkParser struct {
 // NewMkParser creates a new parser for the given text.
 // If emitWarnings is false, line may be nil.
 func NewMkParser(line Line, text string, emitWarnings bool) *MkParser {
+       G.Assertf((line != nil) == emitWarnings, "line must be given iff emitWarnings is set")
        return &MkParser{NewParser(line, text, emitWarnings)}
 }
 
 func (p *MkParser) MkTokens() []*MkToken {
-       repl := p.repl
+       lexer := p.lexer
 
        var tokens []*MkToken
        for !p.EOF() {
-               if repl.AdvanceStr("#") {
-                       repl.AdvanceRest()
+               // FIXME: Aren't the comments already gone at this stage?
+               if lexer.SkipByte('#') {
+                       lexer.Skip(len(lexer.Rest()))
                }
 
-               mark := repl.Mark()
+               mark := lexer.Mark()
                if varuse := p.VarUse(); varuse != nil {
-                       tokens = append(tokens, &MkToken{Text: repl.Since(mark), Varuse: varuse})
+                       tokens = append(tokens, &MkToken{Text: lexer.Since(mark), Varuse: varuse})
                        continue
                }
 
        again:
-               dollar := strings.IndexByte(repl.Rest(), '$')
+               dollar := strings.IndexByte(lexer.Rest(), '$')
                if dollar == -1 {
-                       dollar = len(repl.Rest())
+                       dollar = len(lexer.Rest())
                }
-               repl.Skip(dollar)
-               if repl.AdvanceStr("$$") {
+               lexer.Skip(dollar)
+               if lexer.SkipString("$$") {
                        goto again
                }
-               text := repl.Since(mark)
+               text := lexer.Since(mark)
                if text != "" {
                        tokens = append(tokens, &MkToken{Text: text})
                        continue
@@ -53,20 +56,29 @@ func (p *MkParser) MkTokens() []*MkToken
 }
 
 func (p *MkParser) VarUse() *MkVarUse {
-       repl := p.repl
+       lexer := p.lexer
 
-       mark := repl.Mark()
-       if repl.AdvanceStr("${") || repl.AdvanceStr("$(") {
-               usingRoundParen := repl.Since(mark) == "$("
-               closing := ifelseStr(usingRoundParen, ")", "}")
+       if lexer.PeekByte() != '$' {
+               return nil
+       }
+
+       mark := lexer.Mark()
+       lexer.Skip(1)
 
-               varnameMark := repl.Mark()
+       if lexer.SkipByte('{') || lexer.SkipByte('(') {
+               usingRoundParen := lexer.Since(mark)[1] == '('
+               closing := byte('}')
+               if usingRoundParen {
+                       closing = ')'
+               }
+
+               varnameMark := lexer.Mark()
                varname := p.Varname()
                if varname != "" {
                        modifiers := p.VarUseModifiers(varname, closing)
-                       if repl.AdvanceStr(closing) {
+                       if lexer.SkipByte(closing) {
                                if usingRoundParen && p.EmitWarnings {
-                                       parenVaruse := repl.Since(mark)
+                                       parenVaruse := lexer.Since(mark)
                                        bracesVaruse := "${" + parenVaruse[2:len(parenVaruse)-1] + "}"
                                        fix := p.Line.Autofix()
                                        fix.Warnf("Please use curly braces {} instead of round parentheses () for %s.", varname)
@@ -77,88 +89,96 @@ func (p *MkParser) VarUse() *MkVarUse {
                        }
                }
 
-               for p.VarUse() != nil || repl.AdvanceRegexp(regex.Pattern(`^([^$:`+closing+`]|\$\$)+`)) {
+               for p.VarUse() != nil || lexer.SkipRegexp(G.res.Compile(regex.Pattern(`^([^$:`+string(closing)+`]|\$\$)+`))) {
                }
                rest := p.Rest()
                if hasPrefix(rest, ":L") || hasPrefix(rest, ":?") {
-                       varexpr := repl.Since(varnameMark)
+                       varexpr := lexer.Since(varnameMark)
                        modifiers := p.VarUseModifiers(varexpr, closing)
-                       if repl.AdvanceStr(closing) {
+                       if lexer.SkipByte(closing) {
                                return &MkVarUse{varexpr, modifiers}
                        }
                }
-               repl.Reset(mark)
+               lexer.Reset(mark)
        }
 
-       if repl.AdvanceStr("$@") {
+       if lexer.SkipByte('@') {
                return &MkVarUse{"@", nil}
        }
-       if repl.AdvanceStr("$<") {
+       if lexer.SkipByte('<') {
                return &MkVarUse{"<", nil}
        }
-       if repl.PeekByte() == '$' && repl.AdvanceRegexp(`^\$(\w)`) {
-               varname := repl.Group(1)
+       if varname := lexer.NextBytesSet(textproc.AlnumU); varname != "" {
                if p.EmitWarnings {
                        p.Line.Warnf("$%[1]s is ambiguous. Use ${%[1]s} if you mean a Makefile variable or $$%[1]s if you mean a shell variable.", varname)
                }
                return &MkVarUse{varname, nil}
        }
+
+       lexer.Reset(mark)
        return nil
 }
 
-func (p *MkParser) VarUseModifiers(varname, closing string) []MkVarUseModifier {
-       repl := p.repl
+func (p *MkParser) VarUseModifiers(varname string, closing byte) []MkVarUseModifier {
+       lexer := p.lexer
 
        var modifiers []MkVarUseModifier
        appendModifier := func(s string) { modifiers = append(modifiers, MkVarUseModifier{s}) }
        mayOmitColon := false
 loop:
-       for repl.AdvanceStr(":") || mayOmitColon {
+       for lexer.SkipByte(':') || mayOmitColon {
                mayOmitColon = false
-               modifierMark := repl.Mark()
+               modifierMark := lexer.Mark()
 
-               switch repl.PeekByte() {
+               switch lexer.PeekByte() {
                case 'E', 'H', 'L', 'O', 'Q', 'R', 'T', 's', 't', 'u':
-                       if repl.AdvanceRegexp(`^(E|H|L|Ox?|Q|R|T|sh|tA|tW|tl|tu|tw|u)`) {
-                               appendModifier(repl.Since(modifierMark))
+                       if lexer.SkipRegexp(G.res.Compile(`^(E|H|L|Ox?|Q|R|T|sh|tA|tW|tl|tu|tw|u)`)) {
+                               appendModifier(lexer.Since(modifierMark))
                                continue
                        }
-                       if repl.AdvanceStr("ts") {
-                               rest := repl.Rest()
-                               if len(rest) >= 2 && (rest[1] == closing[0] || rest[1] == ':') {
-                                       repl.Skip(1)
-                               } else if len(rest) >= 1 && (rest[0] == closing[0] || rest[0] == ':') {
-                               } else if repl.AdvanceRegexp(`^\\\d+`) {
+                       if lexer.SkipString("ts") {
+                               rest := lexer.Rest()
+                               if len(rest) >= 2 && (rest[1] == closing || rest[1] == ':') {
+                                       lexer.Skip(1)
+                               } else if len(rest) >= 1 && (rest[0] == closing || rest[0] == ':') {
+                               } else if lexer.SkipRegexp(G.res.Compile(`^\\\d+`)) {
                                } else {
                                        break loop
                                }
-                               appendModifier(repl.Since(modifierMark))
+                               appendModifier(lexer.Since(modifierMark))
                                continue
                        }
 
                case '=', 'D', 'M', 'N', 'U':
-                       if repl.AdvanceRegexp(`^[=DMNU]`) {
-                               for p.VarUse() != nil || repl.AdvanceRegexp(regex.Pattern(`^([^$:\\`+closing+`]|\$\$|\\.)+`)) {
-                               }
-                               arg := repl.Since(modifierMark)
-                               appendModifier(strings.Replace(arg, "\\:", ":", -1))
-                               continue
+                       lexer.Skip(1)
+                       re := G.res.Compile(regex.Pattern(ifelseStr(closing == '}', `^([^$:\\}]|\$\$|\\.)+`, `^([^$:\\)]|\$\$|\\.)+`)))
+                       for p.VarUse() != nil || lexer.SkipRegexp(re) {
                        }
+                       arg := lexer.Since(modifierMark)
+                       appendModifier(strings.Replace(arg, "\\:", ":", -1))
+                       continue
 
                case 'C', 'S':
-                       if repl.AdvanceRegexp(`^[CS]([%,/:;@^|])`) {
-                               separator := repl.Group(1)
-                               repl.AdvanceStr("^")
-                               re := regex.Pattern(`^([^\` + separator + `$` + closing + `\\]|\$\$|\\.)+`)
-                               for p.VarUse() != nil || repl.AdvanceRegexp(re) {
-                               }
-                               repl.AdvanceStr("$")
-                               if repl.AdvanceStr(separator) {
-                                       for p.VarUse() != nil || repl.AdvanceRegexp(re) {
+                       // bmake allows _any_ separator, even letters.
+                       lexer.Skip(1)
+                       if m := lexer.NextRegexp(G.res.Compile(`^[%,/:;@^|]`)); m != nil {
+                               separator := m[0][0]
+                               lexer.SkipByte('^')
+                               skipOther := func() {
+                                       for p.VarUse() != nil ||
+                                               lexer.SkipString("$$") ||
+                                               (len(lexer.Rest()) >= 2 && lexer.PeekByte() == '\\' && lexer.Skip(2)) ||
+                                               lexer.NextBytesFunc(func(b byte) bool { return b != separator && b != '$' && b != closing && b != '\\' }) != "" {
+
                                        }
-                                       if repl.AdvanceStr(separator) {
-                                               repl.AdvanceRegexp(`^[1gW]`)
-                                               appendModifier(repl.Since(modifierMark))
+                               }
+                               skipOther()
+                               lexer.SkipByte('$')
+                               if lexer.SkipByte(separator) {
+                                       skipOther()
+                                       if lexer.SkipByte(separator) {
+                                               lexer.SkipRegexp(G.res.Compile(`^[1gW]`)) // FIXME: Multiple modifiers may be mentioned
+                                               appendModifier(lexer.Since(modifierMark))
                                                mayOmitColon = true
                                                continue
                                        }
@@ -166,41 +186,43 @@ loop:
                        }
 
                case '@':
-                       if repl.AdvanceRegexp(`^@([\w.]+)@`) {
-                               loopvar := repl.Group(1)
-                               for p.VarUse() != nil || repl.AdvanceRegexp(regex.Pattern(`^([^$:@`+closing+`\\]|\$\$|\\.)+`)) {
+                       if m := lexer.NextRegexp(G.res.Compile(`^@([\w.]+)@`)); m != nil {
+                               loopvar := m[1]
+                               re := G.res.Compile(regex.Pattern(ifelseStr(closing == '}', `^([^$:@}\\]|\\.)+`, `^([^$:@)\\]|\\.)+`)))
+                               for p.VarUse() != nil || lexer.SkipString("$$") || lexer.SkipRegexp(re) {
                                }
-                               if !repl.AdvanceStr("@") && p.EmitWarnings {
+                               if !lexer.SkipByte('@') && p.EmitWarnings {
                                        p.Line.Warnf("Modifier ${%s:@%s@...@} is missing the final \"@\".", varname, loopvar)
                                }
-                               appendModifier(repl.Since(modifierMark))
+                               appendModifier(lexer.Since(modifierMark))
                                continue
                        }
 
                case '[':
-                       if repl.AdvanceRegexp(`^\[(?:[-.\d]+|#)\]`) {
-                               appendModifier(repl.Since(modifierMark))
+                       if lexer.SkipRegexp(G.res.Compile(`^\[(?:[-.\d]+|#)\]`)) {
+                               appendModifier(lexer.Since(modifierMark))
                                continue
                        }
 
                case '?':
-                       repl.AdvanceStr("?")
-                       re := regex.Pattern(`^([^$:` + closing + `]|\$\$)+`)
-                       for p.VarUse() != nil || repl.AdvanceRegexp(re) {
+                       lexer.Skip(1)
+                       re := G.res.Compile(regex.Pattern(`^([^$:` + string(closing) + `]|\$\$)+`))
+                       for p.VarUse() != nil || lexer.SkipRegexp(re) {
                        }
-                       if repl.AdvanceStr(":") {
-                               for p.VarUse() != nil || repl.AdvanceRegexp(re) {
+                       if lexer.SkipByte(':') {
+                               for p.VarUse() != nil || lexer.SkipRegexp(re) {
                                }
-                               appendModifier(repl.Since(modifierMark))
+                               appendModifier(lexer.Since(modifierMark))
                                continue
                        }
                }
 
-               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+`]|\$\$)+`)) {
+               lexer.Reset(modifierMark)
+               // FIXME: Why skip over unknown modifiers here? This accepts :S,a,b,c,d,e,f but shouldn't.
+               re := G.res.Compile(regex.Pattern(`^([^:$` + string(closing) + `]|\$\$)+`))
+               for p.VarUse() != nil || lexer.SkipRegexp(re) {
                }
-               if suffixSubst := repl.Since(modifierMark); contains(suffixSubst, "=") {
+               if suffixSubst := lexer.Since(modifierMark); contains(suffixSubst, "=") {
                        appendModifier(suffixSubst)
                        continue
                }
@@ -218,14 +240,14 @@ func (p *MkParser) MkCond() MkCond {
 
        ands := []MkCond{and}
        for {
-               mark := p.repl.Mark()
-               p.repl.SkipHspace()
-               if !p.repl.AdvanceStr("||") {
+               mark := p.lexer.Mark()
+               p.lexer.SkipHspace()
+               if !(p.lexer.SkipString("||")) {
                        break
                }
                next := p.mkCondAnd()
                if next == nil {
-                       p.repl.Reset(mark)
+                       p.lexer.Reset(mark)
                        break
                }
                ands = append(ands, next)
@@ -244,13 +266,14 @@ func (p *MkParser) mkCondAnd() MkCond {
 
        atoms := []MkCond{atom}
        for {
-               mark := p.repl.Mark()
-               if !p.repl.AdvanceRegexp(`^[\t ]*&&[\t ]*`) {
+               mark := p.lexer.Mark()
+               p.lexer.SkipHspace()
+               if p.lexer.NextString("&&") == "" {
                        break
                }
                next := p.mkCondAtom()
                if next == nil {
-                       p.repl.Reset(mark)
+                       p.lexer.Reset(mark)
                        break
                }
                atoms = append(atoms, next)
@@ -266,101 +289,123 @@ func (p *MkParser) mkCondAtom() MkCond {
                defer trace.Call1(p.Rest())()
        }
 
-       repl := p.repl
-       mark := repl.Mark()
-       repl.SkipHspace()
+       lexer := p.lexer
+       mark := lexer.Mark()
+       lexer.SkipHspace()
        switch {
-       case repl.AdvanceStr("!"):
+       case lexer.SkipByte('!'):
                cond := p.mkCondAtom()
                if cond != nil {
                        return &mkCond{Not: cond}
                }
-       case repl.AdvanceStr("("):
+
+       case lexer.SkipByte('('):
                cond := p.MkCond()
                if cond != nil {
-                       repl.SkipHspace()
-                       if repl.AdvanceStr(")") {
+                       lexer.SkipHspace()
+                       if lexer.SkipByte(')') {
                                return cond
                        }
                }
-       case repl.HasPrefix("defined") && repl.AdvanceRegexp(`^defined[\t ]*\(`):
-               if varname := p.Varname(); varname != "" {
-                       if repl.AdvanceStr(")") {
-                               return &mkCond{Defined: varname}
-                       }
-               }
-       case repl.HasPrefix("empty") && repl.AdvanceRegexp(`^empty[\t ]*\(`):
-               if varname := p.Varname(); varname != "" {
-                       modifiers := p.VarUseModifiers(varname, ")")
-                       if repl.AdvanceStr(")") {
-                               return &mkCond{Empty: &MkVarUse{varname, modifiers}}
-                       }
-               }
-       case uint(repl.PeekByte()-'a') <= 'z'-'a' && repl.AdvanceRegexp(`^(commands|exists|make|target)[\t ]*\(`):
-               funcname := repl.Group(1)
-               argMark := repl.Mark()
-               for p.VarUse() != nil || repl.NextBytesFunc(func(b byte) bool { return b != '$' && b != ')' }) != "" {
-               }
-               arg := repl.Since(argMark)
-               if repl.AdvanceStr(")") {
-                       return &mkCond{Call: &MkCondCall{funcname, arg}}
-               }
+
+       case 'a' <= lexer.PeekByte() && lexer.PeekByte() <= 'z':
+               return p.mkCondFunc()
+
        default:
                lhs := p.VarUse()
-               mark := repl.Mark()
-               if lhs == nil && repl.AdvanceStr("\"") {
-                       if quotedLHS := p.VarUse(); quotedLHS != nil && repl.AdvanceStr("\"") {
+               mark := lexer.Mark()
+               if lhs == nil && lexer.SkipByte('"') {
+                       if quotedLHS := p.VarUse(); quotedLHS != nil && lexer.SkipByte('"') {
                                lhs = quotedLHS
                        } else {
-                               repl.Reset(mark)
+                               lexer.Reset(mark)
                        }
                }
                if lhs != nil {
-                       if repl.AdvanceRegexp(`^[\t ]*(<|<=|==|!=|>=|>)[\t ]*(\d+(?:\.\d+)?)`) {
-                               return &mkCond{CompareVarNum: &MkCondCompareVarNum{lhs, repl.Group(1), repl.Group(2)}}
+                       if m := lexer.NextRegexp(G.res.Compile(`^[\t ]*(<|<=|==|!=|>=|>)[\t ]*(\d+(?:\.\d+)?)`)); m != nil {
+                               return &mkCond{CompareVarNum: &MkCondCompareVarNum{lhs, m[1], m[2]}}
                        }
-                       if repl.AdvanceRegexp(`^[\t ]*(<|<=|==|!=|>=|>)[\t ]*`) {
-                               op := repl.Group(1)
-                               if (op == "!=" || op == "==") && repl.AdvanceRegexp(`^"([^"\$\\]*)"`) {
-                                       return &mkCond{CompareVarStr: &MkCondCompareVarStr{lhs, op, repl.Group(1)}}
-                               } else if repl.AdvanceRegexp(`^\w+`) {
-                                       return &mkCond{CompareVarStr: &MkCondCompareVarStr{lhs, op, repl.Str()}}
+                       if m := lexer.NextRegexp(G.res.Compile(`^[\t ]*(<|<=|==|!=|>=|>)[\t ]*`)); m != nil {
+                               op := m[1]
+                               if op == "==" || op == "!=" {
+                                       if mrhs := lexer.NextRegexp(G.res.Compile(`^"([^"\$\\]*)"`)); mrhs != nil {
+                                               return &mkCond{CompareVarStr: &MkCondCompareVarStr{lhs, op, mrhs[1]}}
+                                       }
+                               }
+                               if str := lexer.NextBytesSet(textproc.AlnumU); str != "" {
+                                       return &mkCond{CompareVarStr: &MkCondCompareVarStr{lhs, op, str}}
                                } else if rhs := p.VarUse(); rhs != nil {
                                        return &mkCond{CompareVarVar: &MkCondCompareVarVar{lhs, op, rhs}}
-                               } else if repl.PeekByte() == '"' {
-                                       mark := repl.Mark()
-                                       if repl.AdvanceStr("\"") {
+                               } else if lexer.PeekByte() == '"' {
+                                       mark := lexer.Mark()
+                                       if lexer.SkipByte('"') {
                                                if quotedRHS := p.VarUse(); quotedRHS != nil {
-                                                       if repl.AdvanceStr("\"") {
+                                                       if lexer.SkipByte('"') {
                                                                return &mkCond{CompareVarVar: &MkCondCompareVarVar{lhs, op, quotedRHS}}
                                                        }
                                                }
                                        }
-                                       repl.Reset(mark)
+                                       lexer.Reset(mark)
                                }
                        } else {
                                return &mkCond{Not: &mkCond{Empty: lhs}} // See devel/bmake/files/cond.c:/\* For \.if \$/
                        }
                }
-               if repl.AdvanceRegexp(`^\d+(?:\.\d+)?`) {
-                       return &mkCond{Num: repl.Str()}
+               if m := lexer.NextRegexp(G.res.Compile(`^\d+(?:\.\d+)?`)); m != nil {
+                       return &mkCond{Num: m[0]}
                }
        }
-       repl.Reset(mark)
+       lexer.Reset(mark)
        return nil
 }
 
-func (p *MkParser) Varname() string {
-       repl := p.repl
+func (p *MkParser) mkCondFunc() *mkCond {
+       lexer := p.lexer
+       mark := lexer.Mark()
+
+       funcName := lexer.NextBytesFunc(func(b byte) bool { return 'a' <= b && b <= 'z' })
+       lexer.SkipHspace()
+       if !lexer.SkipByte('(') {
+               return nil
+       }
+
+       switch funcName {
+       case "defined":
+               varname := p.Varname()
+               if varname != "" && lexer.SkipByte(')') {
+                       return &mkCond{Defined: varname}
+               }
+
+       case "empty":
+               if varname := p.Varname(); varname != "" {
+                       modifiers := p.VarUseModifiers(varname, ')')
+                       if lexer.SkipByte(')') {
+                               return &mkCond{Empty: &MkVarUse{varname, modifiers}}
+                       }
+               }
 
-       mark := repl.Mark()
-       repl.AdvanceStr(".")
-       isVarnameChar := func(c byte) bool {
-               return 'A' <= c && c <= 'Z' || c == '_' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '+' || c == '-' || c == '.' || c == '*'
+       case "commands", "exists", "make", "target":
+               argMark := lexer.Mark()
+               for p.VarUse() != nil || lexer.NextBytesFunc(func(b byte) bool { return b != '$' && b != ')' }) != "" {
+               }
+               arg := lexer.Since(argMark)
+               if lexer.SkipByte(')') {
+                       return &mkCond{Call: &MkCondCall{funcName, arg}}
+               }
        }
-       for p.VarUse() != nil || repl.NextBytesFunc(isVarnameChar) != "" {
+
+       lexer.Reset(mark)
+       return nil
+}
+
+func (p *MkParser) Varname() string {
+       lexer := p.lexer
+
+       mark := lexer.Mark()
+       lexer.SkipByte('.')
+       for p.VarUse() != nil || lexer.NextBytesSet(VarnameBytes) != "" {
        }
-       return repl.Since(mark)
+       return lexer.Since(mark)
 }
 
 type MkCond = *mkCond
@@ -408,9 +453,11 @@ type MkCondCallback struct {
        VarUse        func(varuse *MkVarUse)
 }
 
-type MkCondWalker struct{}
+func (cond *mkCond) Walk(callback *MkCondCallback) {
+       (&MkCondWalker{}).Walk(cond, callback)
+}
 
-func NewMkCondWalker() *MkCondWalker { return &MkCondWalker{} }
+type MkCondWalker struct{}
 
 func (w *MkCondWalker) Walk(cond MkCond, callback *MkCondCallback) {
        switch {

Index: pkgsrc/pkgtools/pkglint/files/files.go
diff -u pkgsrc/pkgtools/pkglint/files/files.go:1.21 pkgsrc/pkgtools/pkglint/files/files.go:1.22
--- pkgsrc/pkgtools/pkglint/files/files.go:1.21 Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/files.go      Sun Dec  2 01:57:48 2018
@@ -15,18 +15,18 @@ const (
        LogErrors                           //
 )
 
-func Load(fileName string, options LoadOptions) Lines {
-       if fromCache := G.fileCache.Get(fileName, options); fromCache != nil {
+func Load(filename string, options LoadOptions) Lines {
+       if fromCache := G.fileCache.Get(filename, options); fromCache != nil {
                return fromCache
        }
 
-       rawBytes, err := ioutil.ReadFile(fileName)
+       rawBytes, err := ioutil.ReadFile(filename)
        if err != nil {
                switch {
                case options&MustSucceed != 0:
-                       NewLineWhole(fileName).Fatalf("Cannot be read.")
+                       NewLineWhole(filename).Fatalf("Cannot be read.")
                case options&LogErrors != 0:
-                       NewLineWhole(fileName).Errorf("Cannot be read.")
+                       NewLineWhole(filename).Errorf("Cannot be read.")
                }
                return nil
        }
@@ -35,38 +35,38 @@ func Load(fileName string, options LoadO
        if rawText == "" && options&NotEmpty != 0 {
                switch {
                case options&MustSucceed != 0:
-                       NewLineWhole(fileName).Fatalf("Must not be empty.")
+                       NewLineWhole(filename).Fatalf("Must not be empty.")
                case options&LogErrors != 0:
-                       NewLineWhole(fileName).Errorf("Must not be empty.")
+                       NewLineWhole(filename).Errorf("Must not be empty.")
                }
                return nil
        }
 
        if G.Opts.Profiling {
-               G.loaded.Add(path.Clean(fileName), 1)
+               G.loaded.Add(path.Clean(filename), 1)
        }
 
-       result := convertToLogicalLines(fileName, rawText, options&Makefile != 0)
-       if hasSuffix(fileName, ".mk") {
-               G.fileCache.Put(fileName, options, result)
+       result := convertToLogicalLines(filename, rawText, options&Makefile != 0)
+       if hasSuffix(filename, ".mk") {
+               G.fileCache.Put(filename, options, result)
        }
        return result
 }
 
-func LoadMk(fileName string, options LoadOptions) MkLines {
-       lines := Load(fileName, options|Makefile)
+func LoadMk(filename string, options LoadOptions) MkLines {
+       lines := Load(filename, options|Makefile)
        if lines == nil {
                return nil
        }
        return NewMkLines(lines)
 }
 
-func nextLogicalLine(fileName string, rawLines []*RawLine, index int) (Line, int) {
+func nextLogicalLine(filename string, rawLines []*RawLine, index int) (Line, int) {
        { // Handle the common case efficiently
                rawLine := rawLines[index]
                textnl := rawLine.textnl
                if hasSuffix(textnl, "\n") && !hasSuffix(textnl, "\\\n") {
-                       return NewLine(fileName, rawLine.Lineno, textnl[:len(textnl)-1], rawLines[index:index+1]), index + 1
+                       return NewLine(filename, rawLine.Lineno, textnl[:len(textnl)-1], rawLines[index]), index + 1
                }
        }
 
@@ -96,7 +96,7 @@ func nextLogicalLine(fileName string, ra
 
        lastlineno := rawLines[index].Lineno
 
-       return NewLineMulti(fileName, firstlineno, lastlineno, text.String(), lineRawLines), index + 1
+       return NewLineMulti(filename, firstlineno, lastlineno, text.String(), lineRawLines), index + 1
 }
 
 func matchContinuationLine(textnl string) (leadingWhitespace, text, trailingWhitespace, cont string) {
@@ -133,7 +133,7 @@ func matchContinuationLine(textnl string
        return
 }
 
-func convertToLogicalLines(fileName string, rawText string, joinBackslashLines bool) Lines {
+func convertToLogicalLines(filename string, rawText string, joinBackslashLines bool) Lines {
        var rawLines []*RawLine
        for lineno, rawLine := range strings.SplitAfter(rawText, "\n") {
                if rawLine != "" {
@@ -144,14 +144,14 @@ func convertToLogicalLines(fileName stri
        var loglines []Line
        if joinBackslashLines {
                for lineno := 0; lineno < len(rawLines); {
-                       line, nextLineno := nextLogicalLine(fileName, rawLines, lineno)
+                       line, nextLineno := nextLogicalLine(filename, rawLines, lineno)
                        loglines = append(loglines, line)
                        lineno = nextLineno
                }
        } else {
                for _, rawLine := range rawLines {
                        text := strings.TrimSuffix(rawLine.textnl, "\n")
-                       logline := NewLine(fileName, rawLine.Lineno, text, []*RawLine{rawLine})
+                       logline := NewLine(filename, rawLine.Lineno, text, rawLine)
                        loglines = append(loglines, logline)
                }
        }
@@ -160,5 +160,5 @@ func convertToLogicalLines(fileName stri
                loglines[len(loglines)-1].Errorf("File must end with a newline.")
        }
 
-       return NewLines(fileName, loglines)
+       return NewLines(filename, loglines)
 }

Index: pkgsrc/pkgtools/pkglint/files/licenses.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses.go:1.16 pkgsrc/pkgtools/pkglint/files/licenses.go:1.17
--- pkgsrc/pkgtools/pkglint/files/licenses.go:1.16      Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/licenses.go   Sun Dec  2 01:57:48 2018
@@ -2,24 +2,6 @@ package main
 
 import "netbsd.org/pkglint/licenses"
 
-func (src *Pkgsrc) checkToplevelUnusedLicenses() {
-       usedLicenses := src.UsedLicenses
-       if usedLicenses == nil {
-               return
-       }
-
-       licensesDir := src.File("licenses")
-       for _, licenseFile := range src.ReadDir("licenses") {
-               licenseName := licenseFile.Name()
-               if !usedLicenses[licenseName] {
-                       licensePath := licensesDir + "/" + licenseName
-                       if fileExists(licensePath) {
-                               NewLineWhole(licensePath).Warnf("This license seems to be unused.")
-                       }
-               }
-       }
-}
-
 type LicenseChecker struct {
        MkLine MkLine
 }
@@ -40,7 +22,7 @@ func (lc *LicenseChecker) Check(value st
        cond.Walk(lc.checkNode)
 }
 
-func (lc *LicenseChecker) checkLicenseName(license string) {
+func (lc *LicenseChecker) checkName(license string) {
        licenseFile := ""
        if G.Pkg != nil {
                if mkline := G.Pkg.vars.FirstDefinition("LICENSE_FILE"); mkline != nil {
@@ -65,23 +47,24 @@ func (lc *LicenseChecker) checkLicenseNa
                "no-redistribution",
                "shareware":
                lc.MkLine.Errorf("License %q must not be used.", license)
-               Explain(
+               G.Explain(
                        "Instead of using these deprecated licenses, extract the actual",
                        "license from the package into the pkgsrc/licenses/ directory",
-                       "and define LICENSE to that file name.  See the pkgsrc guide,",
-                       "keyword LICENSE, for more information.")
+                       "and define LICENSE to that filename.",
+                       "",
+                       seeGuide("Handling licenses", "handling-licenses"))
        }
 }
 
 func (lc *LicenseChecker) checkNode(cond *licenses.Condition) {
-       if license := cond.Name; license != "" && license != "append-placeholder" {
-               lc.checkLicenseName(license)
+       if name := cond.Name; name != "" && name != "append-placeholder" {
+               lc.checkName(name)
                return
        }
 
        if cond.And && cond.Or {
                lc.MkLine.Errorf("AND and OR operators in license conditions can only be combined using parentheses.")
-               Explain(
+               G.Explain(
                        "Examples for valid license conditions are:",
                        "",
                        "\tlicense1 AND license2 AND (license3 OR license4)",
Index: pkgsrc/pkgtools/pkglint/files/logging.go
diff -u pkgsrc/pkgtools/pkglint/files/logging.go:1.16 pkgsrc/pkgtools/pkglint/files/logging.go:1.17
--- pkgsrc/pkgtools/pkglint/files/logging.go:1.16       Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/logging.go    Sun Dec  2 01:57:48 2018
@@ -1,12 +1,42 @@
 package main
 
 import (
+       "bytes"
        "fmt"
        "io"
+       "netbsd.org/pkglint/histogram"
        "path"
-       "strings"
 )
 
+type Logger struct {
+       Opts LoggerOpts
+
+       out *SeparatorWriter
+       err *SeparatorWriter
+
+       suppressDiag bool
+       suppressExpl bool
+
+       logged    Once
+       explained Once
+       histo     *histogram.Histogram
+
+       errors                int
+       warnings              int
+       explanationsAvailable bool
+       autofixAvailable      bool
+}
+
+type LoggerOpts struct {
+       ShowAutofix,
+       Autofix,
+       Explain,
+       ShowSource,
+       LogVerbose,
+       GccOutput,
+       Quiet bool
+}
+
 type LogLevel struct {
        TraditionalName string
        GccName         string
@@ -20,179 +50,237 @@ var (
        AutofixLogLevel = &LogLevel{"AUTOFIX", "autofix"}
 )
 
-var dummyLine = NewLine("", 0, "", nil)
-var dummyMkLine = NewMkLine(dummyLine)
+var dummyLine = NewLineMulti("", 0, 0, "", nil)
 
-// shallBeLogged tests whether a diagnostic with the given format should
-// be logged. It only inspects the --only arguments.
-//
-// Duplicates are handled in main.logf.
-func shallBeLogged(format string) bool {
-       if len(G.Opts.LogOnly) > 0 {
-               found := false
-               for _, substr := range G.Opts.LogOnly {
-                       if contains(format, substr) {
-                               found = true
-                               break
-                       }
-               }
-               if !found {
-                       return false
-               }
-       }
+func (l *Logger) IsAutofix() bool { return l.Opts.Autofix || l.Opts.ShowAutofix }
 
-       return true
+// Relevant decides and remembers whether the given diagnostic is relevant and should be logged.
+//
+// The result of the decision affects all log items until Relevant is called for the next time.
+func (l *Logger) Relevant(format string) bool {
+       relevant := l.shallBeLogged(format)
+       l.suppressDiag = !relevant
+       l.suppressExpl = !relevant
+       return relevant
 }
 
-func loggedAlready(fileName, lineno, msg string) bool {
-       uniq := path.Clean(fileName) + ":" + lineno + ":" + msg
-       if G.logged[uniq] {
+func (l *Logger) FirstTime(filename, linenos, msg string) bool {
+       if l.Opts.LogVerbose {
                return true
        }
 
-       if G.logged == nil {
-               G.logged = make(map[string]bool)
+       if !l.logged.FirstTimeSlice(path.Clean(filename), linenos, msg) {
+               l.suppressDiag = true
+               l.suppressExpl = true
+               return false
        }
-       G.logged[uniq] = true
-       return false
-}
 
-func logf(level *LogLevel, fileName, lineno, format, msg string) bool {
-       // TODO: Only ever output ASCII, no matter what's in the message.
+       return true
+}
 
-       if fileName != "" {
-               fileName = cleanpath(fileName)
+// Explain outputs an explanation for the preceding diagnostic
+// if the --explain option is given. Otherwise it just records
+// that an explanation is available.
+func (l *Logger) Explain(explanation ...string) {
+       if l.suppressExpl {
+               l.suppressExpl = false
+               return
        }
-       if G.Testing && format != AutofixFormat && !hasSuffix(format, ": %s") && !hasSuffix(format, ". %s") {
-               G.Assertf(hasSuffix(format, "."), "Diagnostic format %q must end in a period.", format)
+
+       l.explanationsAvailable = true
+       if !l.Opts.Explain {
+               return
        }
 
-       if !G.Opts.LogVerbose && format != AutofixFormat && loggedAlready(fileName, lineno, msg) {
-               G.explainNext = false
-               return false
+       if !l.explained.FirstTimeSlice(explanation...) {
+               return
        }
 
-       var text, sep string
-       if !G.Opts.GccOutput {
-               text += sep + level.TraditionalName + ":"
-               sep = " "
-       }
-       if fileName != "" {
-               text += sep + fileName
-               sep = ": "
-               if lineno != "" {
-                       text += ":" + lineno
+       l.out.Separate()
+       wrapped := wrap(68, explanation...)
+       for _, explanationLine := range wrapped {
+               if explanationLine != "" {
+                       l.out.Write("\t")
                }
+               l.out.WriteLine(explanationLine)
        }
-       if G.Opts.GccOutput {
-               text += sep + level.GccName + ":"
-               sep = " "
+       l.out.WriteLine("")
+}
+
+func (l *Logger) ShowSummary() {
+       if l.Opts.Quiet || l.Opts.Autofix {
+               return
        }
-       if G.Opts.Profiling && format != AutofixFormat && level != Fatal {
-               G.loghisto.Add(format, 1)
+
+       if l.errors != 0 || l.warnings != 0 {
+               l.out.Write(sprintf("%d %s and %d %s found.\n",
+                       l.errors, ifelseStr(l.errors == 1, "error", "errors"),
+                       l.warnings, ifelseStr(l.warnings == 1, "warning", "warnings")))
+       } else {
+               l.out.WriteLine("Looks fine.")
        }
-       text += sep + msg + "\n"
 
-       out := G.logOut
-       if level == Fatal {
-               out = G.logErr
+       if l.explanationsAvailable && !l.Opts.Explain {
+               l.out.WriteLine("(Run \"pkglint -e\" to show explanations.)")
+       }
+       if l.autofixAvailable {
+               if !l.Opts.ShowAutofix {
+                       l.out.WriteLine("(Run \"pkglint -fs\" to show what can be fixed automatically.)")
+               }
+               l.out.WriteLine("(Run \"pkglint -F\" to automatically fix some issues.)")
        }
+}
 
-       out.Write(text)
+// shallBeLogged tests whether a diagnostic with the given format should
+// be logged.
+//
+// It only inspects the --only arguments; duplicates are handled in Logger.Logf.
+func (l *Logger) shallBeLogged(format string) bool {
+       if len(G.Opts.LogOnly) == 0 {
+               return true
+       }
 
-       switch level {
-       case Fatal:
-               panic(pkglintFatal{})
-       case Error:
-               G.errors++
-       case Warn:
-               G.warnings++
+       for _, substr := range G.Opts.LogOnly {
+               if contains(format, substr) {
+                       return true
+               }
        }
-       return true
+       return false
 }
 
-// Explain outputs an explanation for the preceding diagnostic
-// if the --explain option is given. Otherwise it just records
-// that an explanation is available.
-func Explain(explanation ...string) {
-       if G.Testing {
-               for _, s := range explanation {
-                       if l := tabWidth(s); l > 68 && contains(s, " ") {
-                               lastSpace := strings.LastIndexByte(s[:68], ' ')
-                               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.Printf("Short space after period: %s\n", s)
-                               }
-                       }
-                       if hasSuffix(s, " ") || hasSuffix(s, "\t") {
-                               G.logErr.Printf("Trailing whitespace: %q\n", s)
-                       }
-               }
+// Diag logs a diagnostic. These are filtered by the --only command line option,
+// and duplicates are suppressed unless the --log-verbose command line option is given.
+//
+// See Logf for logging arbitrary messages.
+func (l *Logger) Diag(line Line, level *LogLevel, format string, args ...interface{}) {
+       if l.Opts.ShowAutofix || l.Opts.Autofix {
+               // In these two cases, the only interesting diagnostics are those that can
+               // be fixed automatically. These are logged by Autofix.Apply.
+               l.suppressExpl = true
+               return
        }
 
-       if !G.explainNext {
+       if !l.Relevant(format) {
                return
        }
-       G.explanationsAvailable = true
-       if !G.Opts.Explain {
+
+       filename := line.Filename
+       linenos := line.Linenos()
+       msg := fmt.Sprintf(format, args...)
+       if !l.FirstTime(filename, linenos, msg) {
+               l.suppressDiag = false
                return
        }
 
-       complete := strings.Join(explanation, "\n")
-       if G.explanationsGiven[complete] {
+       if l.Opts.ShowSource {
+               line.showSource(l.out)
+               l.Logf(level, filename, linenos, format, msg)
+               l.out.Separate()
+       } else {
+               l.Logf(level, filename, linenos, format, msg)
+       }
+}
+
+func (l *Logger) Logf(level *LogLevel, filename, lineno, format, msg string) {
+       if l.suppressDiag {
+               l.suppressDiag = false
                return
        }
-       if G.explanationsGiven == nil {
-               G.explanationsGiven = make(map[string]bool)
+
+       if G.Testing && format != AutofixFormat && !hasSuffix(format, ": %s") && !hasSuffix(format, ". %s") {
+               G.Assertf(hasSuffix(format, "."), "Diagnostic format %q must end in a period.", format)
        }
-       G.explanationsGiven[complete] = true
 
-       G.logOut.WriteLine("")
-       for _, explanationLine := range explanation {
-               G.logOut.WriteLine("\t" + explanationLine)
+       if filename != "" {
+               filename = cleanpath(filename)
+       }
+       if G.Opts.Profiling && format != AutofixFormat && level != Fatal {
+               l.histo.Add(format, 1)
        }
-       G.logOut.WriteLine("")
 
-}
+       out := l.out
+       if level == Fatal {
+               out = l.err
+       }
+
+       filenameSep := ifelseStr(filename != "", ": ", "")
+       effLineno := ifelseStr(filename != "", lineno, "")
+       linenoSep := ifelseStr(effLineno != "", ":", "")
+       var diag string
+       if l.Opts.GccOutput {
+               diag = fmt.Sprintf("%s%s%s%s%s: %s\n", filename, linenoSep, effLineno, filenameSep, level.GccName, msg)
+       } else {
+               diag = fmt.Sprintf("%s%s%s%s%s: %s\n", level.TraditionalName, filenameSep, filename, linenoSep, effLineno, msg)
+       }
+       out.Write(escapePrintable(diag))
 
-type pkglintFatal struct{}
+       switch level {
+       case Fatal:
+               panic(pkglintFatal{})
+       case Error:
+               l.errors++
+       case Warn:
+               l.warnings++
+       }
+}
 
 // SeparatorWriter writes output, occasionally separated by an
 // empty line. This is used for separating the diagnostics when
 // --source is combined with --show-autofix, where each
 // log message consists of multiple lines.
 type SeparatorWriter struct {
-       out            io.Writer
-       needSeparator  bool
-       wroteSomething bool
+       out   io.Writer
+       state uint8 // 0 = beginning of line, 1 = in line, 2 = separator wanted, 3 = paragraph
+       line  bytes.Buffer
 }
 
 func NewSeparatorWriter(out io.Writer) *SeparatorWriter {
-       return &SeparatorWriter{out, false, false}
+       return &SeparatorWriter{out: out}
 }
 
 func (wr *SeparatorWriter) WriteLine(text string) {
        wr.Write(text)
-       _, _ = io.WriteString(wr.out, "\n")
+       wr.write('\n')
 }
 
 func (wr *SeparatorWriter) Write(text string) {
-       if wr.needSeparator && wr.wroteSomething {
-               _, _ = io.WriteString(wr.out, "\n")
-               wr.needSeparator = false
+       for _, b := range []byte(text) {
+               wr.write(b)
        }
-       n, err := io.WriteString(wr.out, text)
-       if err == nil && n > 0 {
-               wr.wroteSomething = true
+}
+
+// Separate remembers to output an empty line before the next character.
+// If the writer is currently in the middle of a line, that line is terminated immediately.
+func (wr *SeparatorWriter) Separate() {
+       if wr.state == 1 {
+               wr.write('\n')
+       }
+       if wr.state < 2 {
+               wr.state = 2
        }
 }
 
-func (wr *SeparatorWriter) Printf(format string, args ...interface{}) {
-       wr.Write(fmt.Sprintf(format, args...))
+func (wr *SeparatorWriter) Flush() {
+       _, _ = io.Copy(wr.out, &wr.line)
+       wr.line.Reset()
 }
 
-func (wr *SeparatorWriter) Separate() {
-       wr.needSeparator = true
+func (wr *SeparatorWriter) write(b byte) {
+       if b == '\n' {
+               if wr.state == 1 {
+                       wr.state = 0
+               } else {
+                       wr.state = 3
+               }
+               wr.line.WriteByte('\n')
+               wr.Flush()
+               return
+       }
+
+       if wr.state == 2 {
+               wr.line.WriteByte('\n')
+               wr.Flush()
+       }
+       wr.state = 1
+       wr.line.WriteByte(b)
 }
Index: pkgsrc/pkgtools/pkglint/files/substcontext_test.go
diff -u pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.16 pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.17
--- pkgsrc/pkgtools/pkglint/files/substcontext_test.go:1.16     Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/substcontext_test.go  Sun Dec  2 01:57:48 2018
@@ -30,6 +30,8 @@ func (s *Suite) Test_SubstContext__incom
        ctx.Finish(newSubstLine(t, 14, ""))
 
        t.CheckOutputLines(
+               "NOTE: Makefile:13: The substitution command \"s,@PREFIX@,${PREFIX},g\" "+
+                       "can be replaced with \"SUBST_VARS.interp+= PREFIX\".",
                "WARN: Makefile:14: Incomplete SUBST block: SUBST_STAGE.interp missing.")
 }
 
@@ -52,7 +54,9 @@ func (s *Suite) Test_SubstContext__compl
 
        ctx.Finish(newSubstLine(t, 15, ""))
 
-       t.CheckOutputEmpty()
+       t.CheckOutputLines(
+               "NOTE: Makefile:13: The substitution command \"s,@PREFIX@,${PREFIX},g\" " +
+                       "can be replaced with \"SUBST_VARS.p+= PREFIX\".")
 }
 
 func (s *Suite) Test_SubstContext__OPSYSVARS(c *check.C) {
@@ -71,7 +75,9 @@ func (s *Suite) Test_SubstContext__OPSYS
 
        ctx.Finish(newSubstLine(t, 15, ""))
 
-       t.CheckOutputEmpty()
+       t.CheckOutputLines(
+               "NOTE: Makefile:14: The substitution command \"s,@PREFIX@,${PREFIX},g\" " +
+                       "can be replaced with \"SUBST_VARS.prefix+= PREFIX\".")
 }
 
 func (s *Suite) Test_SubstContext__no_class(c *check.C) {
@@ -351,13 +357,53 @@ func (s *Suite) Test_SubstContext__SUBST
                "WARN: os.mk:9: TODAY2 is defined but not used.")
 }
 
+func (s *Suite) Test_SubstContext_suggestSubstVars(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupVartypes()
+       t.SetupTool("sh", "SH", AtRunTime)
+
+       mklines := t.NewMkLines("subst.mk",
+               MkRcsID,
+               "",
+               "SUBST_CLASSES+=\t\ttest",
+               "SUBST_STAGE.test=\tpre-configure",
+               "SUBST_FILES.test=\tfilename",
+               "SUBST_SED.test+=\t-e s,@SH@,${SH},g",           // Can be replaced.
+               "SUBST_SED.test+=\t-e s,@SH@,${SH:Q},g",         // Can be replaced, with or without the :Q modifier.
+               "SUBST_SED.test+=\t-e s,@SH@,${SH:T},g",         // Cannot be replaced because of the :T modifier.
+               "SUBST_SED.test+=\t-e s,@SH@,${SH},",            // Can be replaced, even without the g option.
+               "SUBST_SED.test+=\t-e 's,@SH@,${SH},'",          // Can be replaced, whether in single quotes or not.
+               "SUBST_SED.test+=\t-e \"s,@SH@,${SH},\"",        // Can be replaced, whether in double quotes or not.
+               "SUBST_SED.test+=\t-e s,'@SH@','${SH}',",        // Can be replaced, even when the quoting changes midways.
+               "SUBST_SED.test+=\ts,'@SH@','${SH}',",           // Can be replaced, even when the -e is missing.
+               "SUBST_SED.test+=\t-e s,@SH@,${PKGNAME},",       // Cannot be replaced since the variable name differs.
+               "SUBST_SED.test+=\t-e s,@SH@,'\"'${SH:Q}'\"',g", // Cannot be replaced since the double quotes are added.
+               "# end")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: subst.mk:6: Please use ${SH:Q} instead of ${SH}.",
+               "NOTE: subst.mk:6: The substitution command \"s,@SH@,${SH},g\" can be replaced with \"SUBST_VARS.test+= SH\".",
+               "NOTE: subst.mk:7: The substitution command \"s,@SH@,${SH:Q},g\" can be replaced with \"SUBST_VARS.test+= SH\".",
+               "WARN: subst.mk:8: Please use ${SH:T:Q} instead of ${SH:T}.",
+               "WARN: subst.mk:9: Please use ${SH:Q} instead of ${SH}.",
+               "NOTE: subst.mk:9: The substitution command \"s,@SH@,${SH},\" can be replaced with \"SUBST_VARS.test+= SH\".",
+               // TODO: Handle the quotes in line 10
+               // TODO: Handle the quotes in line 11
+               // TODO: Handle the quotes in line 12
+               "NOTE: subst.mk:13: Please always use \"-e\" in sed commands, even if there is only one substitution.")
+}
+
 // simulateSubstLines only tests some of the inner workings of SubstContext.
 // It is not realistic for all cases. If in doubt, use MkLines.Check.
 func simulateSubstLines(t *Tester, texts ...string) {
        ctx := NewSubstContext()
        for _, lineText := range texts {
                var lineno int
-               fmt.Sscanf(lineText[0:4], "%d: ", &lineno)
+               _, err := fmt.Sscanf(lineText[0:4], "%d: ", &lineno)
+               G.Assertf(err == nil, "%s", err)
                text := lineText[4:]
                line := newSubstLine(t, lineno, text)
 

Index: pkgsrc/pkgtools/pkglint/files/licenses_test.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.17 pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.18
--- pkgsrc/pkgtools/pkglint/files/licenses_test.go:1.17 Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/licenses_test.go      Sun Dec  2 01:57:48 2018
@@ -8,10 +8,10 @@ func (s *Suite) Test_LicenseChecker_Chec
        t := s.Init(c)
 
        t.CreateFileLines("licenses/gnu-gpl-v2",
-               "Most software \u2026")
+               "The licenses for most software are designed to take away ...")
        mkline := t.NewMkLine("Makefile", 7, "LICENSE=dummy")
 
-       licenseChecker := &LicenseChecker{mkline}
+       licenseChecker := LicenseChecker{mkline}
        licenseChecker.Check("gpl-v2", opAssign)
 
        t.CheckOutputLines(
@@ -44,67 +44,14 @@ func (s *Suite) Test_LicenseChecker_Chec
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_Pkgsrc_checkToplevelUnusedLicenses(c *check.C) {
+func (s *Suite) Test_LicenseChecker_checkName__LICENSE_FILE(c *check.C) {
        t := s.Init(c)
 
        t.SetupPkgsrc()
-       t.CreateFileLines("mk/misc/category.mk")
-       t.CreateFileLines("licenses/2-clause-bsd")
-       t.CreateFileLines("licenses/gnu-gpl-v3")
-
-       t.CreateFileLines("Makefile",
-               MkRcsID,
-               "SUBDIR+=\tcategory")
-
-       t.CreateFileLines("category/Makefile",
-               MkRcsID,
-               "COMMENT=\tExample category",
+       t.SetupPackage("category/package",
+               "LICENSE=\tmy-license",
                "",
-               "SUBDIR+=\tpackage",
-               "",
-               ".include \"../mk/misc/category.mk\"")
-
-       t.CreateFileLines("category/package/Makefile",
-               MkRcsID,
-               "CATEGORIES=\tcategory",
-               "",
-               "COMMENT=Example package",
-               "LICENSE=\t2-clause-bsd",
-               "NO_CHECKSUM=\tyes")
-       t.CreateFileLines("category/package/PLIST",
-               PlistRcsID,
-               "bin/program")
-
-       G.Main("pkglint", "-r", "-Cglobal", t.File("."))
-
-       t.CheckOutputLines(
-               "WARN: ~/licenses/gnu-gpl-v2: This license seems to be unused.", // Added by Tester.SetupPkgsrc
-               "WARN: ~/licenses/gnu-gpl-v3: This license seems to be unused.",
-               "0 errors and 2 warnings found.")
-}
-
-func (s *Suite) Test_LicenseChecker_checkLicenseName__LICENSE_FILE(c *check.C) {
-       t := s.Init(c)
-
-       t.SetupPkgsrc()
-       t.SetupCommandLine("-Wno-space")
-       t.CreateFileLines("category/package/DESCR",
-               "Package description")
-       t.CreateFileLines("category/package/Makefile",
-               MkRcsID,
-               "",
-               "CATEGORIES=     chinese",
-               "",
-               "COMMENT=        Useful tools",
-               "LICENSE=        my-license",
-               "",
-               "LICENSE_FILE=   my-license",
-               "NO_CHECKSUM=    yes",
-               "",
-               ".include \"../../mk/bsd.pkg.mk\"")
-       t.CreateFileLines("category/package/PLIST",
-               PlistRcsID,
-               "bin/program")
+               "LICENSE_FILE=\tmy-license")
        t.CreateFileLines("category/package/my-license",
                "An individual license file.")
 
Index: pkgsrc/pkgtools/pkglint/files/mkparser_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.17 pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.18
--- pkgsrc/pkgtools/pkglint/files/mkparser_test.go:1.17 Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/mkparser_test.go      Sun Dec  2 01:57:48 2018
@@ -9,7 +9,7 @@ import (
 func (s *Suite) Test_MkParser_MkTokens(c *check.C) {
        t := s.Init(c)
 
-       checkRest := func(input string, expectedTokens []*MkToken, expectedRest string) {
+       testRest := func(input string, expectedTokens []*MkToken, expectedRest string) {
                line := t.NewLines("Test_MkParser_MkTokens.mk", input).Lines[0]
                p := NewMkParser(line, input, true)
                actualTokens := p.MkTokens()
@@ -22,8 +22,8 @@ func (s *Suite) Test_MkParser_MkTokens(c
                }
                c.Check(p.Rest(), equals, expectedRest)
        }
-       check := func(input string, expectedToken *MkToken) {
-               checkRest(input, []*MkToken{expectedToken}, "")
+       test := func(input string, expectedToken *MkToken) {
+               testRest(input, []*MkToken{expectedToken}, "")
        }
        literal := func(text string) *MkToken {
                return &MkToken{Text: text}
@@ -40,103 +40,104 @@ func (s *Suite) Test_MkParser_MkTokens(c
                return &MkToken{Text: text, Varuse: NewMkVarUse(varname, modifiers...)}
        }
 
-       check("literal", literal("literal"))
-       check("\\/share\\/ { print \"share directory\" }", literal("\\/share\\/ { print \"share directory\" }"))
-       check("find . -name \\*.orig -o -name \\*.pre", literal("find . -name \\*.orig -o -name \\*.pre"))
-       check("-e 's|\\$${EC2_HOME.*}|EC2_HOME}|g'", literal("-e 's|\\$${EC2_HOME.*}|EC2_HOME}|g'"))
-
-       check("${VARIABLE}", varuse("VARIABLE"))
-       check("${VARIABLE.param}", varuse("VARIABLE.param"))
-       check("${VARIABLE.${param}}", varuse("VARIABLE.${param}"))
-       check("${VARIABLE.hicolor-icon-theme}", varuse("VARIABLE.hicolor-icon-theme"))
-       check("${VARIABLE.gtk+extra}", varuse("VARIABLE.gtk+extra"))
-       check("${VARIABLE:S/old/new/}", varuse("VARIABLE", "S/old/new/"))
-       check("${GNUSTEP_LFLAGS:S/-L//g}", varuse("GNUSTEP_LFLAGS", "S/-L//g"))
-       check("${SUSE_VERSION:S/.//}", varuse("SUSE_VERSION", "S/.//"))
-       check("${MASTER_SITE_GNOME:=sources/alacarte/0.13/}", varuse("MASTER_SITE_GNOME", "=sources/alacarte/0.13/"))
-       check("${INCLUDE_DIRS:H:T}", varuse("INCLUDE_DIRS", "H", "T"))
-       check("${A.${B.${C.${D}}}}", varuse("A.${B.${C.${D}}}"))
-       check("${RUBY_VERSION:C/([0-9]+)\\.([0-9]+)\\.([0-9]+)/\\1/}", varuse("RUBY_VERSION", "C/([0-9]+)\\.([0-9]+)\\.([0-9]+)/\\1/"))
-       check("${PERL5_${_var_}:Q}", varuse("PERL5_${_var_}", "Q"))
-       check("${PKGNAME_REQD:C/(^.*-|^)py([0-9][0-9])-.*/\\2/}", varuse("PKGNAME_REQD", "C/(^.*-|^)py([0-9][0-9])-.*/\\2/"))
-       check("${PYLIB:S|/|\\\\/|g}", varuse("PYLIB", "S|/|\\\\/|g"))
-       check("${PKGNAME_REQD:C/ruby([0-9][0-9]+)-.*/\\1/}", varuse("PKGNAME_REQD", "C/ruby([0-9][0-9]+)-.*/\\1/"))
-       check("${RUBY_SHLIBALIAS:S/\\//\\\\\\//}", varuse("RUBY_SHLIBALIAS", "S/\\//\\\\\\//"))
-       check("${RUBY_VER_MAP.${RUBY_VER}:U${RUBY_VER}}", varuse("RUBY_VER_MAP.${RUBY_VER}", "U${RUBY_VER}"))
-       check("${RUBY_VER_MAP.${RUBY_VER}:U18}", varuse("RUBY_VER_MAP.${RUBY_VER}", "U18"))
-       check("${CONFIGURE_ARGS:S/ENABLE_OSS=no/ENABLE_OSS=yes/g}", varuse("CONFIGURE_ARGS", "S/ENABLE_OSS=no/ENABLE_OSS=yes/g"))
-       check("${PLIST_RUBY_DIRS:S,DIR=\"PREFIX/,DIR=\",}", varuse("PLIST_RUBY_DIRS", "S,DIR=\"PREFIX/,DIR=\","))
-       check("${LDFLAGS:S/-Wl,//g:Q}", varuse("LDFLAGS", "S/-Wl,//g", "Q"))
-       check("${_PERL5_REAL_PACKLIST:S/^/${DESTDIR}/}", varuse("_PERL5_REAL_PACKLIST", "S/^/${DESTDIR}/"))
-       check("${_PYTHON_VERSION:C/^([0-9])/\\1./1}", varuse("_PYTHON_VERSION", "C/^([0-9])/\\1./1"))
-       check("${PKGNAME:S/py${_PYTHON_VERSION}/py${i}/}", varuse("PKGNAME", "S/py${_PYTHON_VERSION}/py${i}/"))
-       check("${PKGNAME:C/-[0-9].*$/-[0-9]*/}", varuse("PKGNAME", "C/-[0-9].*$/-[0-9]*/"))
-       check("${PKGNAME:S/py${_PYTHON_VERSION}/py${i}/:C/-[0-9].*$/-[0-9]*/}", varuse("PKGNAME", "S/py${_PYTHON_VERSION}/py${i}/", "C/-[0-9].*$/-[0-9]*/"))
-       check("${_PERL5_VARS:tl:S/^/-V:/}", varuse("_PERL5_VARS", "tl", "S/^/-V:/"))
-       check("${_PERL5_VARS_OUT:M${_var_:tl}=*:S/^${_var_:tl}=${_PERL5_PREFIX:=/}//}", varuse("_PERL5_VARS_OUT", "M${_var_:tl}=*", "S/^${_var_:tl}=${_PERL5_PREFIX:=/}//"))
-       check("${RUBY${RUBY_VER}_PATCHLEVEL}", varuse("RUBY${RUBY_VER}_PATCHLEVEL"))
-       check("${DISTFILES:M*.gem}", varuse("DISTFILES", "M*.gem"))
-       check("${LOCALBASE:S^/^_^}", varuse("LOCALBASE", "S^/^_^"))
-       check("${SOURCES:%.c=%.o}", varuse("SOURCES", "%.c=%.o"))
-       check("${GIT_TEMPLATES:@.t.@ ${EGDIR}/${GIT_TEMPLATEDIR}/${.t.} ${PREFIX}/${GIT_CORE_TEMPLATEDIR}/${.t.} @:M*}",
+       test("literal", literal("literal"))
+       test("\\/share\\/ { print \"share directory\" }", literal("\\/share\\/ { print \"share directory\" }"))
+       test("find . -name \\*.orig -o -name \\*.pre", literal("find . -name \\*.orig -o -name \\*.pre"))
+       test("-e 's|\\$${EC2_HOME.*}|EC2_HOME}|g'", literal("-e 's|\\$${EC2_HOME.*}|EC2_HOME}|g'"))
+
+       test("${VARIABLE}", varuse("VARIABLE"))
+       test("${VARIABLE.param}", varuse("VARIABLE.param"))
+       test("${VARIABLE.${param}}", varuse("VARIABLE.${param}"))
+       test("${VARIABLE.hicolor-icon-theme}", varuse("VARIABLE.hicolor-icon-theme"))
+       test("${VARIABLE.gtk+extra}", varuse("VARIABLE.gtk+extra"))
+       test("${VARIABLE:S/old/new/}", varuse("VARIABLE", "S/old/new/"))
+       test("${GNUSTEP_LFLAGS:S/-L//g}", varuse("GNUSTEP_LFLAGS", "S/-L//g"))
+       test("${SUSE_VERSION:S/.//}", varuse("SUSE_VERSION", "S/.//"))
+       test("${MASTER_SITE_GNOME:=sources/alacarte/0.13/}", varuse("MASTER_SITE_GNOME", "=sources/alacarte/0.13/"))
+       test("${INCLUDE_DIRS:H:T}", varuse("INCLUDE_DIRS", "H", "T"))
+       test("${A.${B.${C.${D}}}}", varuse("A.${B.${C.${D}}}"))
+       test("${RUBY_VERSION:C/([0-9]+)\\.([0-9]+)\\.([0-9]+)/\\1/}", varuse("RUBY_VERSION", "C/([0-9]+)\\.([0-9]+)\\.([0-9]+)/\\1/"))
+       test("${PERL5_${_var_}:Q}", varuse("PERL5_${_var_}", "Q"))
+       test("${PKGNAME_REQD:C/(^.*-|^)py([0-9][0-9])-.*/\\2/}", varuse("PKGNAME_REQD", "C/(^.*-|^)py([0-9][0-9])-.*/\\2/"))
+       test("${PYLIB:S|/|\\\\/|g}", varuse("PYLIB", "S|/|\\\\/|g"))
+       test("${PKGNAME_REQD:C/ruby([0-9][0-9]+)-.*/\\1/}", varuse("PKGNAME_REQD", "C/ruby([0-9][0-9]+)-.*/\\1/"))
+       test("${RUBY_SHLIBALIAS:S/\\//\\\\\\//}", varuse("RUBY_SHLIBALIAS", "S/\\//\\\\\\//"))
+       test("${RUBY_VER_MAP.${RUBY_VER}:U${RUBY_VER}}", varuse("RUBY_VER_MAP.${RUBY_VER}", "U${RUBY_VER}"))
+       test("${RUBY_VER_MAP.${RUBY_VER}:U18}", varuse("RUBY_VER_MAP.${RUBY_VER}", "U18"))
+       test("${CONFIGURE_ARGS:S/ENABLE_OSS=no/ENABLE_OSS=yes/g}", varuse("CONFIGURE_ARGS", "S/ENABLE_OSS=no/ENABLE_OSS=yes/g"))
+       test("${PLIST_RUBY_DIRS:S,DIR=\"PREFIX/,DIR=\",}", varuse("PLIST_RUBY_DIRS", "S,DIR=\"PREFIX/,DIR=\","))
+       test("${LDFLAGS:S/-Wl,//g:Q}", varuse("LDFLAGS", "S/-Wl,//g", "Q"))
+       test("${_PERL5_REAL_PACKLIST:S/^/${DESTDIR}/}", varuse("_PERL5_REAL_PACKLIST", "S/^/${DESTDIR}/"))
+       test("${_PYTHON_VERSION:C/^([0-9])/\\1./1}", varuse("_PYTHON_VERSION", "C/^([0-9])/\\1./1"))
+       test("${PKGNAME:S/py${_PYTHON_VERSION}/py${i}/}", varuse("PKGNAME", "S/py${_PYTHON_VERSION}/py${i}/"))
+       test("${PKGNAME:C/-[0-9].*$/-[0-9]*/}", varuse("PKGNAME", "C/-[0-9].*$/-[0-9]*/"))
+       test("${PKGNAME:S/py${_PYTHON_VERSION}/py${i}/:C/-[0-9].*$/-[0-9]*/}", varuse("PKGNAME", "S/py${_PYTHON_VERSION}/py${i}/", "C/-[0-9].*$/-[0-9]*/"))
+       test("${_PERL5_VARS:tl:S/^/-V:/}", varuse("_PERL5_VARS", "tl", "S/^/-V:/"))
+       test("${_PERL5_VARS_OUT:M${_var_:tl}=*:S/^${_var_:tl}=${_PERL5_PREFIX:=/}//}",
+               varuse("_PERL5_VARS_OUT", "M${_var_:tl}=*", "S/^${_var_:tl}=${_PERL5_PREFIX:=/}//"))
+       test("${RUBY${RUBY_VER}_PATCHLEVEL}", varuse("RUBY${RUBY_VER}_PATCHLEVEL"))
+       test("${DISTFILES:M*.gem}", varuse("DISTFILES", "M*.gem"))
+       test("${LOCALBASE:S^/^_^}", varuse("LOCALBASE", "S^/^_^"))
+       test("${SOURCES:%.c=%.o}", varuse("SOURCES", "%.c=%.o"))
+       test("${GIT_TEMPLATES:@.t.@ ${EGDIR}/${GIT_TEMPLATEDIR}/${.t.} ${PREFIX}/${GIT_CORE_TEMPLATEDIR}/${.t.} @:M*}",
                varuse("GIT_TEMPLATES", "@.t.@ ${EGDIR}/${GIT_TEMPLATEDIR}/${.t.} ${PREFIX}/${GIT_CORE_TEMPLATEDIR}/${.t.} @", "M*"))
-       check("${DISTNAME:C:_:-:}", varuse("DISTNAME", "C:_:-:"))
-       check("${CF_FILES:H:O:u:S@^@${PKG_SYSCONFDIR}/@}", varuse("CF_FILES", "H", "O", "u", "S@^@${PKG_SYSCONFDIR}/@"))
-       check("${ALT_GCC_RTS:S%${LOCALBASE}%%:S%/%%}", varuse("ALT_GCC_RTS", "S%${LOCALBASE}%%", "S%/%%"))
-       check("${PREFIX:C;///*;/;g:C;/$;;}", varuse("PREFIX", "C;///*;/;g", "C;/$;;"))
-       check("${GZIP_CMD:[1]:Q}", varuse("GZIP_CMD", "[1]", "Q"))
-       check("${RUBY_RAILS_SUPPORTED:[#]}", varuse("RUBY_RAILS_SUPPORTED", "[#]"))
-       check("${DISTNAME:C/-[0-9]+$$//:C/_/-/}", varuse("DISTNAME", "C/-[0-9]+$$//", "C/_/-/"))
-       check("${DISTNAME:slang%=slang2%}", varuse("DISTNAME", "slang%=slang2%"))
-       check("${OSMAP_SUBSTVARS:@v@-e 's,\\@${v}\\@,${${v}},g' @}", varuse("OSMAP_SUBSTVARS", "@v@-e 's,\\@${v}\\@,${${v}},g' @"))
-       check("${BRANDELF:D${BRANDELF} -t Linux ${LINUX_LDCONFIG}:U${TRUE}}", varuse("BRANDELF", "D${BRANDELF} -t Linux ${LINUX_LDCONFIG}", "U${TRUE}"))
-       check("${${_var_}.*}", varuse("${_var_}.*"))
+       test("${DISTNAME:C:_:-:}", varuse("DISTNAME", "C:_:-:"))
+       test("${CF_FILES:H:O:u:S@^@${PKG_SYSCONFDIR}/@}", varuse("CF_FILES", "H", "O", "u", "S@^@${PKG_SYSCONFDIR}/@"))
+       test("${ALT_GCC_RTS:S%${LOCALBASE}%%:S%/%%}", varuse("ALT_GCC_RTS", "S%${LOCALBASE}%%", "S%/%%"))
+       test("${PREFIX:C;///*;/;g:C;/$;;}", varuse("PREFIX", "C;///*;/;g", "C;/$;;"))
+       test("${GZIP_CMD:[1]:Q}", varuse("GZIP_CMD", "[1]", "Q"))
+       test("${RUBY_RAILS_SUPPORTED:[#]}", varuse("RUBY_RAILS_SUPPORTED", "[#]"))
+       test("${DISTNAME:C/-[0-9]+$$//:C/_/-/}", varuse("DISTNAME", "C/-[0-9]+$$//", "C/_/-/"))
+       test("${DISTNAME:slang%=slang2%}", varuse("DISTNAME", "slang%=slang2%"))
+       test("${OSMAP_SUBSTVARS:@v@-e 's,\\@${v}\\@,${${v}},g' @}", varuse("OSMAP_SUBSTVARS", "@v@-e 's,\\@${v}\\@,${${v}},g' @"))
+       test("${BRANDELF:D${BRANDELF} -t Linux ${LINUX_LDCONFIG}:U${TRUE}}", varuse("BRANDELF", "D${BRANDELF} -t Linux ${LINUX_LDCONFIG}", "U${TRUE}"))
+       test("${${_var_}.*}", varuse("${_var_}.*"))
 
-       check("${GCONF_SCHEMAS:@.s.@${INSTALL_DATA} ${WRKSRC}/src/common/dbus/${.s.} ${DESTDIR}${GCONF_SCHEMAS_DIR}/@}",
+       test("${GCONF_SCHEMAS:@.s.@${INSTALL_DATA} ${WRKSRC}/src/common/dbus/${.s.} ${DESTDIR}${GCONF_SCHEMAS_DIR}/@}",
                varuse("GCONF_SCHEMAS", "@.s.@${INSTALL_DATA} ${WRKSRC}/src/common/dbus/${.s.} ${DESTDIR}${GCONF_SCHEMAS_DIR}/@"))
 
        /* weird features */
-       check("${${EMACS_VERSION_MAJOR}>22:?@comment :}", varuse("${EMACS_VERSION_MAJOR}>22", "?@comment :"))
-       check("${empty(CFLAGS):?:-cflags ${CFLAGS:Q}}", varuse("empty(CFLAGS)", "?:-cflags ${CFLAGS:Q}"))
-       check("${${PKGSRC_COMPILER}==gcc:?gcc:cc}", varuse("${PKGSRC_COMPILER}==gcc", "?gcc:cc"))
+       test("${${EMACS_VERSION_MAJOR}>22:?@comment :}", varuse("${EMACS_VERSION_MAJOR}>22", "?@comment :"))
+       test("${empty(CFLAGS):?:-cflags ${CFLAGS:Q}}", varuse("empty(CFLAGS)", "?:-cflags ${CFLAGS:Q}"))
+       test("${${PKGSRC_COMPILER}==gcc:?gcc:cc}", varuse("${PKGSRC_COMPILER}==gcc", "?gcc:cc"))
 
-       check("${${XKBBASE}/xkbcomp:L:Q}", varuse("${XKBBASE}/xkbcomp", "L", "Q"))
-       check("${${PKGBASE} ${PKGVERSION}:L}", varuse("${PKGBASE} ${PKGVERSION}", "L"))
+       test("${${XKBBASE}/xkbcomp:L:Q}", varuse("${XKBBASE}/xkbcomp", "L", "Q"))
+       test("${${PKGBASE} ${PKGVERSION}:L}", varuse("${PKGBASE} ${PKGVERSION}", "L"))
 
-       check("${${${PKG_INFO} -E ${d} || echo:L:sh}:L:C/[^[0-9]]*/ /g:[1..3]:ts.}",
+       test("${${${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."))
 
        // For :S and :C, the colon can be left out.
-       check("${VAR:S/-//S/.//}",
+       test("${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.
-       check("${VAR:ts\\124}", varuse("VAR", "ts\\124"))       // Or even decimal.
+       test("${VAR:ts}", varuse("VAR", "ts"))                 // The separator character can be left out.
+       test("${VAR:ts\\000012}", varuse("VAR", "ts\\000012")) // The separator character can be a long octal number.
+       test("${VAR:ts\\124}", varuse("VAR", "ts\\124"))       // Or even decimal.
 
-       checkRest("${VAR:ts---}", nil, "${VAR:ts---}") // The :ts modifier only takes single-character separators.
+       testRest("${VAR:ts---}", nil, "${VAR:ts---}") // The :ts modifier only takes single-character separators.
 
-       check("$<", varuseText("$<", "<")) // Same as ${.IMPSRC}
+       test("$<", varuseText("$<", "<")) // Same as ${.IMPSRC}
 
-       check("$(GNUSTEP_USER_ROOT)", varuseText("$(GNUSTEP_USER_ROOT)", "GNUSTEP_USER_ROOT"))
+       test("$(GNUSTEP_USER_ROOT)", varuseText("$(GNUSTEP_USER_ROOT)", "GNUSTEP_USER_ROOT"))
        t.CheckOutputLines(
                "WARN: Test_MkParser_MkTokens.mk:1: Please use curly braces {} instead of round parentheses () for GNUSTEP_USER_ROOT.")
 
-       checkRest("${VAR)", nil, "${VAR)") // Opening brace, closing parenthesis
-       checkRest("$(VAR}", nil, "$(VAR}") // Opening parenthesis, closing brace
-       t.CheckOutputEmpty()               // Warnings are only printed for balanced expressions.
+       testRest("${VAR)", nil, "${VAR)") // Opening brace, closing parenthesis
+       testRest("$(VAR}", nil, "$(VAR}") // Opening parenthesis, closing brace
+       t.CheckOutputEmpty()              // Warnings are only printed for balanced expressions.
 
-       check("${PLIST_SUBST_VARS:@var@${var}=${${var}:Q}@}", varuse("PLIST_SUBST_VARS", "@var@${var}=${${var}:Q}@"))
-       check("${PLIST_SUBST_VARS:@var@${var}=${${var}:Q}}", varuse("PLIST_SUBST_VARS", "@var@${var}=${${var}:Q}")) // Missing @ at the end
+       test("${PLIST_SUBST_VARS:@var@${var}=${${var}:Q}@}", varuse("PLIST_SUBST_VARS", "@var@${var}=${${var}:Q}@"))
+       test("${PLIST_SUBST_VARS:@var@${var}=${${var}:Q}}", varuse("PLIST_SUBST_VARS", "@var@${var}=${${var}:Q}")) // Missing @ at the end
        t.CheckOutputLines(
                "WARN: Test_MkParser_MkTokens.mk:1: Modifier ${PLIST_SUBST_VARS:@var@...@} is missing the final \"@\".")
 
-       checkRest("hello, ${W:L:tl}orld", []*MkToken{
+       testRest("hello, ${W:L:tl}orld", []*MkToken{
                literal("hello, "),
                varuse("W", "L", "tl"),
                literal("orld")},
                "")
-       checkRest("ftp://${PKGNAME}/ ${MASTER_SITES:=subdir/}", []*MkToken{
+       testRest("ftp://${PKGNAME}/ ${MASTER_SITES:=subdir/}", []*MkToken{
                literal("ftp://";),
                varuse("PKGNAME"),
                literal("/ "),
@@ -144,7 +145,7 @@ func (s *Suite) Test_MkParser_MkTokens(c
                "")
 
        // FIXME: Text must match modifiers.
-       checkRest("${VAR:S,a,b,c,d,e,f}",
+       testRest("${VAR:S,a,b,c,d,e,f}",
                []*MkToken{{
                        Text:   "${VAR:S,a,b,c,d,e,f}",
                        Varuse: NewMkVarUse("VAR", "S,a,b,")}},
@@ -152,56 +153,56 @@ func (s *Suite) Test_MkParser_MkTokens(c
 }
 
 func (s *Suite) Test_MkParser_MkCond(c *check.C) {
-       checkRest := func(input string, expectedTree MkCond, expectedRest string) {
-               p := NewMkParser(dummyLine, input, false)
+       testRest := func(input string, expectedTree MkCond, expectedRest string) {
+               p := NewMkParser(nil, input, false)
                actualTree := p.MkCond()
                c.Check(actualTree, deepEquals, expectedTree)
                c.Check(p.Rest(), equals, expectedRest)
        }
-       check := func(input string, expectedTree MkCond) {
-               checkRest(input, expectedTree, "")
+       test := func(input string, expectedTree MkCond) {
+               testRest(input, expectedTree, "")
        }
        varuse := NewMkVarUse
 
-       check("${OPSYS:MNetBSD}",
+       test("${OPSYS:MNetBSD}",
                &mkCond{Not: &mkCond{Empty: varuse("OPSYS", "MNetBSD")}})
-       check("defined(VARNAME)",
+       test("defined(VARNAME)",
                &mkCond{Defined: "VARNAME"})
-       check("empty(VARNAME)",
+       test("empty(VARNAME)",
                &mkCond{Empty: varuse("VARNAME")})
-       check("!empty(VARNAME)",
+       test("!empty(VARNAME)",
                &mkCond{Not: &mkCond{Empty: varuse("VARNAME")}})
-       check("!empty(VARNAME:M[yY][eE][sS])",
+       test("!empty(VARNAME:M[yY][eE][sS])",
                &mkCond{Not: &mkCond{Empty: varuse("VARNAME", "M[yY][eE][sS]")}})
-       check("!empty(USE_TOOLS:Mautoconf\\:run)",
+       test("!empty(USE_TOOLS:Mautoconf\\:run)",
                &mkCond{Not: &mkCond{Empty: varuse("USE_TOOLS", "Mautoconf:run")}})
-       check("${VARNAME} != \"Value\"",
+       test("${VARNAME} != \"Value\"",
                &mkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VARNAME"), "!=", "Value"}})
-       check("${VARNAME:Mi386} != \"Value\"",
+       test("${VARNAME:Mi386} != \"Value\"",
                &mkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VARNAME", "Mi386"), "!=", "Value"}})
-       check("${VARNAME} != Value",
+       test("${VARNAME} != Value",
                &mkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VARNAME"), "!=", "Value"}})
-       check("\"${VARNAME}\" != Value",
+       test("\"${VARNAME}\" != Value",
                &mkCond{CompareVarStr: &MkCondCompareVarStr{varuse("VARNAME"), "!=", "Value"}})
-       check("${pkg} == \"${name}\"",
+       test("${pkg} == \"${name}\"",
                &mkCond{CompareVarVar: &MkCondCompareVarVar{varuse("pkg"), "==", varuse("name")}})
-       check("\"${pkg}\" == \"${name}\"",
+       test("\"${pkg}\" == \"${name}\"",
                &mkCond{CompareVarVar: &MkCondCompareVarVar{varuse("pkg"), "==", varuse("name")}})
-       check("(defined(VARNAME))",
+       test("(defined(VARNAME))",
                &mkCond{Defined: "VARNAME"})
-       check("exists(/etc/hosts)",
+       test("exists(/etc/hosts)",
                &mkCond{Call: &MkCondCall{"exists", "/etc/hosts"}})
-       check("exists(${PREFIX}/var)",
+       test("exists(${PREFIX}/var)",
                &mkCond{Call: &MkCondCall{"exists", "${PREFIX}/var"}})
-       check("${OPSYS} == \"NetBSD\" || ${OPSYS} == \"OpenBSD\"",
+       test("${OPSYS} == \"NetBSD\" || ${OPSYS} == \"OpenBSD\"",
                &mkCond{Or: []*mkCond{
                        {CompareVarStr: &MkCondCompareVarStr{varuse("OPSYS"), "==", "NetBSD"}},
                        {CompareVarStr: &MkCondCompareVarStr{varuse("OPSYS"), "==", "OpenBSD"}}}})
-       check("${OPSYS} == \"NetBSD\" && ${MACHINE_ARCH} == \"i386\"",
+       test("${OPSYS} == \"NetBSD\" && ${MACHINE_ARCH} == \"i386\"",
                &mkCond{And: []*mkCond{
                        {CompareVarStr: &MkCondCompareVarStr{varuse("OPSYS"), "==", "NetBSD"}},
                        {CompareVarStr: &MkCondCompareVarStr{varuse("MACHINE_ARCH"), "==", "i386"}}}})
-       check("defined(A) && defined(B) || defined(C) && defined(D)",
+       test("defined(A) && defined(B) || defined(C) && defined(D)",
                &mkCond{Or: []*mkCond{
                        {And: []*mkCond{
                                {Defined: "A"},
@@ -209,43 +210,43 @@ func (s *Suite) Test_MkParser_MkCond(c *
                        {And: []*mkCond{
                                {Defined: "C"},
                                {Defined: "D"}}}}})
-       check("${MACHINE_ARCH:Mi386} || ${MACHINE_OPSYS:MNetBSD}",
+       test("${MACHINE_ARCH:Mi386} || ${MACHINE_OPSYS:MNetBSD}",
                &mkCond{Or: []*mkCond{
                        {Not: &mkCond{Empty: varuse("MACHINE_ARCH", "Mi386")}},
                        {Not: &mkCond{Empty: varuse("MACHINE_OPSYS", "MNetBSD")}}}})
 
        // Exotic cases
-       check("0",
+       test("0",
                &mkCond{Num: "0"})
-       check("! ( defined(A)  && empty(VARNAME) )",
+       test("! ( defined(A)  && empty(VARNAME) )",
                &mkCond{Not: &mkCond{
                        And: []*mkCond{
                                {Defined: "A"},
                                {Empty: varuse("VARNAME")}}}})
-       check("${REQD_MAJOR} > ${MAJOR}",
+       test("${REQD_MAJOR} > ${MAJOR}",
                &mkCond{CompareVarVar: &MkCondCompareVarVar{varuse("REQD_MAJOR"), ">", varuse("MAJOR")}})
-       check("${OS_VERSION} >= 6.5",
+       test("${OS_VERSION} >= 6.5",
                &mkCond{CompareVarNum: &MkCondCompareVarNum{varuse("OS_VERSION"), ">=", "6.5"}})
-       check("${OS_VERSION} == 5.3",
+       test("${OS_VERSION} == 5.3",
                &mkCond{CompareVarNum: &MkCondCompareVarNum{varuse("OS_VERSION"), "==", "5.3"}})
-       check("!empty(${OS_VARIANT:MIllumos})", // Probably not intended
+       test("!empty(${OS_VARIANT:MIllumos})", // Probably not intended
                &mkCond{Not: &mkCond{Empty: varuse("${OS_VARIANT:MIllumos}")}})
-       check("defined (VARNAME)", // There may be whitespace before the parenthesis; see devel/bmake/files/cond.c:^compare_function.
+       test("defined (VARNAME)", // There may be whitespace before the parenthesis; see devel/bmake/files/cond.c:^compare_function.
                &mkCond{Defined: "VARNAME"})
-       check("${\"${PKG_OPTIONS:Moption}\":?--enable-option:--disable-option}",
+       test("${\"${PKG_OPTIONS:Moption}\":?--enable-option:--disable-option}",
                &mkCond{Not: &mkCond{Empty: varuse("\"${PKG_OPTIONS:Moption}\"", "?--enable-option:--disable-option")}})
 
        // Errors
-       checkRest("!empty(PKG_OPTIONS:Msndfile) || defined(PKG_OPTIONS:Msamplerate)",
+       testRest("!empty(PKG_OPTIONS:Msndfile) || defined(PKG_OPTIONS:Msamplerate)",
                &mkCond{Not: &mkCond{Empty: varuse("PKG_OPTIONS", "Msndfile")}},
-               " || defined(PKG_OPTIONS:Msamplerate)")
-       checkRest("${LEFT} &&",
+               "|| defined(PKG_OPTIONS:Msamplerate)")
+       testRest("${LEFT} &&",
                &mkCond{Not: &mkCond{Empty: varuse("LEFT")}},
                "&&")
-       checkRest("\"unfinished string literal",
+       testRest("\"unfinished string literal",
                nil,
                "\"unfinished string literal")
-       checkRest("${VAR} == \"unfinished string literal",
+       testRest("${VAR} == \"unfinished string literal",
                nil, // Not even the ${VAR} gets through here, although that can be expected.
                "${VAR} == \"unfinished string literal")
 }
@@ -296,7 +297,7 @@ func (s *Suite) Test_MkCondWalker_Walk(c
                events = append(events, fmt.Sprintf("%14s  %s", name, strings.Join(args, ", ")))
        }
 
-       NewMkCondWalker().Walk(mkline.Cond(), &MkCondCallback{
+       mkline.Cond().Walk(&MkCondCallback{
                Defined: func(varname string) {
                        addEvent("defined", varname)
                },
Index: pkgsrc/pkgtools/pkglint/files/util_test.go
diff -u pkgsrc/pkgtools/pkglint/files/util_test.go:1.17 pkgsrc/pkgtools/pkglint/files/util_test.go:1.18
--- pkgsrc/pkgtools/pkglint/files/util_test.go:1.17     Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/util_test.go  Sun Dec  2 01:57:48 2018
@@ -152,13 +152,6 @@ func (s *Suite) Test_isEmptyDir__empty_s
        c.Check(isEmptyDir(t.File(".")), equals, true)
 }
 
-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")
-}
-
 func (s *Suite) Test_detab(c *check.C) {
        c.Check(detab(""), equals, "")
        c.Check(detab("\t"), equals, "        ")
@@ -235,7 +228,7 @@ func (s *Suite) Test_isLocallyModified(c
        t := s.Init(c)
 
        unmodified := t.CreateFileLines("unmodified")
-       modTime := time.Unix(1136239445, 0)
+       modTime := time.Unix(1136239445, 0).UTC()
 
        err := os.Chtimes(unmodified, modTime, modTime)
        c.Check(err, check.IsNil)
@@ -244,7 +237,7 @@ func (s *Suite) Test_isLocallyModified(c
        c.Check(err, check.IsNil)
 
        // Make sure that the file system has second precision and accuracy.
-       c.Check(st.ModTime(), check.DeepEquals, modTime)
+       c.Check(st.ModTime().UTC(), check.DeepEquals, modTime)
 
        modified := t.CreateFileLines("modified")
 
@@ -357,6 +350,8 @@ func (s *Suite) Test_isalnum(c *check.C)
 func (s *Suite) Test_FileCache(c *check.C) {
        t := s.Init(c)
 
+       t.EnableTracingToLog()
+
        cache := NewFileCache(3)
 
        lines := t.NewLines("Makefile",
@@ -373,13 +368,13 @@ func (s *Suite) Test_FileCache(c *check.
        linesFromCache := cache.Get("Makefile", 0)
        c.Check(linesFromCache.FileName, equals, "Makefile")
        c.Check(linesFromCache.Lines, check.HasLen, 2)
-       c.Check(linesFromCache.Lines[0].FileName, equals, "Makefile")
+       c.Check(linesFromCache.Lines[0].Filename, equals, "Makefile")
 
        // Cache keys are normalized using path.Clean.
        linesFromCache2 := cache.Get("./Makefile", 0)
        c.Check(linesFromCache2.FileName, equals, "./Makefile")
        c.Check(linesFromCache2.Lines, check.HasLen, 2)
-       c.Check(linesFromCache2.Lines[0].FileName, equals, "./Makefile")
+       c.Check(linesFromCache2.Lines[0].Filename, equals, "./Makefile")
 
        cache.Put("file1.mk", 0, lines)
        cache.Put("file2.mk", 0, lines)
@@ -408,14 +403,83 @@ func (s *Suite) Test_FileCache(c *check.
        c.Check(cache.misses, equals, 5)
 
        t.CheckOutputLines(
-               "FileCache \"Makefile\" with count 4.",
-               "FileCache \"file1.mk\" with count 2.",
-               "FileCache \"file2.mk\" with count 2.",
-               "FileCache.Evict \"file2.mk\" with count 2.",
-               "FileCache.Evict \"file1.mk\" with count 2.",
-               "FileCache.Halve \"Makefile\" with count 4.")
+               "TRACE:   FileCache \"Makefile\" with count 4.",
+               "TRACE:   FileCache \"file1.mk\" with count 2.",
+               "TRACE:   FileCache \"file2.mk\" with count 2.",
+               "TRACE:   FileCache.Evict \"file2.mk\" with count 2.",
+               "TRACE:   FileCache.Evict \"file1.mk\" with count 2.",
+               "TRACE:   FileCache.Halve \"Makefile\" with count 4.")
 }
 
 func (s *Suite) Test_makeHelp(c *check.C) {
        c.Check(makeHelp("subst"), equals, confMake+" help topic=subst")
 }
+
+func (s *Suite) Test_Once(c *check.C) {
+       var once Once
+
+       c.Check(once.FirstTime("str"), equals, true)
+       c.Check(once.FirstTime("str"), equals, false)
+       c.Check(once.FirstTimeSlice("str"), equals, false)
+       c.Check(once.FirstTimeSlice("str", "str2"), equals, true)
+       c.Check(once.FirstTimeSlice("str", "str2"), equals, false)
+}
+
+func (s *Suite) Test_wrap(c *check.C) {
+
+       wrapped := wrap(20,
+               "See the pkgsrc guide, section \"Package components, Makefile\":",
+               "https://www.NetBSD.org/doc/pkgsrc/pkgsrc.html#components.Makefile.";,
+               "",
+               "For more information, ask on the tech-pkg%NetBSD.org@localhost mailing list.",
+               "",
+               "\tpreformatted line 1",
+               "\tpreformatted line 2",
+               "",
+               "    intentionally indented",
+               "*   itemization",
+               "",
+               "Normal",
+               "text",
+               "continues",
+               "here",
+               "with",
+               "linebreaks.",
+               "",
+               "Sentence one.  Sentence two.")
+
+       expected := []string{
+               "See the pkgsrc",
+               "guide, section",
+               "\"Package components,",
+               "Makefile\":",
+               "https://www.NetBSD.org/doc/pkgsrc/pkgsrc.html#components.Makefile.";,
+               "",
+               "For more",
+               "information, ask on",
+               "the",
+               "tech-pkg%NetBSD.org@localhost",
+               "mailing list.",
+               "",
+               "\tpreformatted line 1",
+               "\tpreformatted line 2",
+               "",
+               "    intentionally indented",
+               "*   itemization",
+               "",
+               "Normal text",
+               "continues here with",
+               "linebreaks.",
+               "",
+               "Sentence one.",
+               "Sentence two."}
+
+       c.Check(wrapped, deepEquals, expected)
+}
+
+func (s *Suite) Test_escapePrintable(c *check.C) {
+       c.Check(escapePrintable(""), equals, "")
+       c.Check(escapePrintable("ASCII only~\n\t"), equals, "ASCII only~\n\t")
+       c.Check(escapePrintable("Bad \xFF character"), equals, "Bad \\xFF character")
+       c.Check(escapePrintable("Unicode \uFFFD replacement"), equals, "Unicode U+FFFD replacement")
+}

Index: pkgsrc/pkgtools/pkglint/files/line.go
diff -u pkgsrc/pkgtools/pkglint/files/line.go:1.27 pkgsrc/pkgtools/pkglint/files/line.go:1.28
--- pkgsrc/pkgtools/pkglint/files/line.go:1.27  Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/line.go       Sun Dec  2 01:57:48 2018
@@ -14,52 +14,59 @@ package main
 // used in the --autofix mode.
 
 import (
-       "fmt"
        "path"
        "strconv"
 )
 
 type RawLine struct {
-       Lineno int
-       // XXX: This is only needed for Autofix; probably should be moved there.
-       orignl string
-       textnl string
-}
+       Lineno int    // Counting starts at 1; 0 means inserted by Autofix
+       orignl string // The line as read in from the file, including newline
+       textnl string // The line as modified by Autofix, including newline
 
-func (rline *RawLine) String() string {
-       return strconv.Itoa(rline.Lineno) + ":" + rline.textnl
+       // XXX: Since only Autofix needs to distinguish between orignl and textnl,
+       // one of these fields should probably be moved there.
 }
 
+func (rline *RawLine) String() string { return sprintf("%d:%s", rline.Lineno, rline.textnl) }
+
+// Line represents a line of text from a file.
+// It aliases a pointer type to reduces the number of *Line occurrences in the code.
+// Using a type alias is more efficient than an interface type, I guess.
 type Line = *LineImpl
 
 type LineImpl struct {
-       FileName  string
-       Basename  string
-       firstLine int32 // Zero means not applicable, -1 means EOF
-       lastLine  int32 // Usually the same as firstLine, may differ in Makefiles
-       Text      string
-       raw       []*RawLine
-       autofix   *Autofix
+       Filename  string // uses / as directory separator on all platforms
+       Basename  string // the last component of the Filename
+       firstLine int32  // zero means the whole file, -1 means EOF
+       lastLine  int32  // usually the same as firstLine, may differ in Makefiles
+       // without the trailing newline character;
+       // in Makefiles, also contains the text from the continuation lines
+       Text    string
+       raw     []*RawLine // contains the original text including trailing newline
+       autofix *Autofix   // any changes that pkglint would like to apply to the line
        Once
+
+       // XXX: Filename and Basename could be replaced with a pointer to a Lines object.
 }
 
-func NewLine(fileName string, lineno int, text string, rawLines []*RawLine) Line {
-       return NewLineMulti(fileName, lineno, lineno, text, rawLines)
+func NewLine(filename string, lineno int, text string, rawLine *RawLine) Line {
+       G.Assertf(rawLine != nil, "use NewLineMulti for creating a Line with no RawLine attached to it")
+       return NewLineMulti(filename, lineno, lineno, text, []*RawLine{rawLine})
 }
 
 // NewLineMulti is for logical Makefile lines that end with backslash.
-func NewLineMulti(fileName string, firstLine, lastLine int, text string, rawLines []*RawLine) Line {
-       return &LineImpl{fileName, path.Base(fileName), int32(firstLine), int32(lastLine), text, rawLines, nil, Once{}}
+func NewLineMulti(filename string, firstLine, lastLine int, text string, rawLines []*RawLine) Line {
+       return &LineImpl{filename, path.Base(filename), int32(firstLine), int32(lastLine), text, rawLines, nil, Once{}}
 }
 
 // NewLineEOF creates a dummy line for logging, with the "line number" EOF.
-func NewLineEOF(fileName string) Line {
-       return NewLineMulti(fileName, -1, 0, "", nil)
+func NewLineEOF(filename string) Line {
+       return NewLineMulti(filename, -1, 0, "", nil)
 }
 
 // NewLineWhole creates a dummy line for logging messages that affect a file as a whole.
-func NewLineWhole(fileName string) Line {
-       return NewLine(fileName, 0, "", nil)
+func NewLineWhole(filename string) Line {
+       return NewLineMulti(filename, 0, 0, "", nil)
 }
 
 func (line *LineImpl) Linenos() string {
@@ -71,15 +78,15 @@ func (line *LineImpl) Linenos() string {
        case line.firstLine == line.lastLine:
                return strconv.Itoa(int(line.firstLine))
        default:
-               return strconv.Itoa(int(line.firstLine)) + "--" + strconv.Itoa(int(line.lastLine))
+               return sprintf("%d--%d", line.firstLine, line.lastLine)
        }
 }
 
 // RefTo returns a reference to another line,
 // which can be in the same file or in a different file.
 func (line *LineImpl) RefTo(other Line) string {
-       if line.FileName != other.FileName {
-               return cleanpath(relpath(path.Dir(line.FileName), other.FileName)) + ":" + other.Linenos()
+       if line.Filename != other.Filename {
+               return cleanpath(relpath(path.Dir(line.Filename), other.Filename)) + ":" + other.Linenos()
        }
        return "line " + other.Linenos()
 }
@@ -88,7 +95,7 @@ func (line *LineImpl) RefTo(other Line) 
 // This is typically used for arguments in diagnostics, which should always be
 // relative to the line with which the diagnostic is associated.
 func (line *LineImpl) PathToFile(filePath string) string {
-       return relpath(path.Dir(line.FileName), filePath)
+       return relpath(path.Dir(line.Filename), filePath)
 }
 
 func (line *LineImpl) IsMultiline() bool {
@@ -96,11 +103,18 @@ func (line *LineImpl) IsMultiline() bool
 }
 
 func (line *LineImpl) showSource(out *SeparatorWriter) {
-       if !G.Opts.ShowSource {
+       if !G.Logger.Opts.ShowSource {
                return
        }
 
        printDiff := func(rawLines []*RawLine) {
+               prefix := ">\t"
+               for _, rawLine := range rawLines {
+                       if rawLine.textnl != rawLine.orignl {
+                               prefix = "\t" // Make it look like an actual diff
+                       }
+               }
+
                for _, rawLine := range rawLines {
                        if rawLine.textnl != rawLine.orignl {
                                if rawLine.orignl != "" {
@@ -110,7 +124,7 @@ func (line *LineImpl) showSource(out *Se
                                        out.Write("+\t" + rawLine.textnl)
                                }
                        } else {
-                               out.Write(">\t" + rawLine.orignl)
+                               out.Write(prefix + rawLine.orignl)
                        }
                }
        }
@@ -128,45 +142,29 @@ func (line *LineImpl) showSource(out *Se
        }
 }
 
-func (line *LineImpl) log(level *LogLevel, format string, args []interface{}) {
-       if G.Opts.ShowAutofix || G.Opts.Autofix {
-               // In these two cases, the only interesting diagnostics are
-               // those that can be fixed automatically.
-               // These are logged by Autofix.Apply.
-               return
-       }
-       G.explainNext = shallBeLogged(format)
-       if !G.explainNext {
-               return
-       }
-
-       if G.Opts.ShowSource {
-               line.showSource(G.logOut)
-       }
-       logf(level, line.FileName, line.Linenos(), format, fmt.Sprintf(format, args...))
-       if G.Opts.ShowSource {
-               G.logOut.Separate()
-       }
-}
-
 func (line *LineImpl) Fatalf(format string, args ...interface{}) {
-       line.log(Fatal, format, args)
+       if trace.Tracing {
+               trace.Stepf("Fatalf: %q, %v", format, args)
+       }
+       G.Diag(line, Fatal, format, args...)
 }
 
 func (line *LineImpl) Errorf(format string, args ...interface{}) {
-       line.log(Error, format, args)
+       G.Diag(line, Error, format, args...)
 }
 
 func (line *LineImpl) Warnf(format string, args ...interface{}) {
-       line.log(Warn, format, args)
+       G.Diag(line, Warn, format, args...)
 }
 
 func (line *LineImpl) Notef(format string, args ...interface{}) {
-       line.log(Note, format, args)
+       G.Diag(line, Note, format, args...)
 }
 
+func (line *LineImpl) Explain(explanation ...string) { G.Explain(explanation...) }
+
 func (line *LineImpl) String() string {
-       return line.FileName + ":" + line.Linenos() + ": " + line.Text
+       return sprintf("%s:%s: %s", line.Filename, line.Linenos(), line.Text)
 }
 
 // Autofix returns the autofix instance belonging to the line.
Index: pkgsrc/pkgtools/pkglint/files/pkglint_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.27 pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.28
--- pkgsrc/pkgtools/pkglint/files/pkglint_test.go:1.27  Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/pkglint_test.go       Sun Dec  2 01:57:48 2018
@@ -72,8 +72,8 @@ func (s *Suite) Test_Pkglint_Main__unkno
                "  -h, --help                  show a detailed usage message",
                "  -I, --dumpmakefile          dump the Makefile after parsing",
                "  -i, --import                prepare the import of a wip package",
-               "  -m, --log-verbose           allow the same log message more than once",
-               "  -o, --only                  only log messages containing the given text",
+               "  -m, --log-verbose           allow the same diagnostic more than once",
+               "  -o, --only                  only log diagnostics containing the given text",
                "  -p, --profiling             profile the executing program",
                "  -q, --quiet                 don't show a summary line when finishing",
                "  -r, --recursive             check subdirectories, too",
@@ -101,7 +101,7 @@ func (s *Suite) Test_Pkglint_Main__unkno
                "  Flags for -W, --warning:",
                "    all          all of the following",
                "    none         none of the following",
-               "    absname      warn about use of absolute file names (enabled)",
+               "    absname      warn about use of absolute filenames (disabled)",
                "    directcmd    warn about use of direct command names instead of Make variables (enabled)",
                "    extra        enable some extra warnings (disabled)",
                "    order        warn if Makefile entries are unordered (enabled)",
@@ -109,7 +109,7 @@ func (s *Suite) Test_Pkglint_Main__unkno
                "    plist-depr   warn about deprecated paths in PLISTs (disabled)",
                "    plist-sort   warn about unsorted entries in PLISTs (disabled)",
                "    quoting      warn about quoting issues (disabled)",
-               "    space        warn about inconsistent use of white-space (disabled)",
+               "    space        warn about inconsistent use of whitespace (disabled)",
                "    style        warn about stylistic issues (disabled)",
                "    types        do some simple type checking in Makefiles (enabled)",
                "",
@@ -121,7 +121,7 @@ func (s *Suite) Test_Pkglint_Main__panic
 
        pkg := t.SetupPackage("category/package")
 
-       G.logOut = nil // Force an error that cannot happen in practice.
+       G.out = nil // Force an error that cannot happen in practice.
 
        c.Check(
                func() { G.Main("pkglint", pkg) },
@@ -229,7 +229,8 @@ func (s *Suite) Test_Pkglint_Main__compl
                "",
                "SHA1 (checkperms-1.12.tar.gz) = 34c084b4d06bcd7a8bba922ff57677e651eeced5",
                "RMD160 (checkperms-1.12.tar.gz) = cd95029aa930b6201e9580b3ab7e36dd30b8f925",
-               "SHA512 (checkperms-1.12.tar.gz) = 43e37b5963c63fdf716acdb470928d7e21a7bdfddd6c85cf626a11acc7f45fa52a53d4bcd83d543150328fe8cec5587987d2d9a7c5f0aaeb02ac1127ab41f8ae",
+               "SHA512 (checkperms-1.12.tar.gz) = 43e37b5963c63fdf716acdb470928d7e21a7bdfd"+
+                       "dd6c85cf626a11acc7f45fa52a53d4bcd83d543150328fe8cec5587987d2d9a7c5f0aaeb02ac1127ab41f8ae",
                "Size (checkperms-1.12.tar.gz) = 6621 bytes",
                "SHA1 (patch-checkperms.c) = asdfasdf") // Invalid SHA-1 checksum
 
@@ -250,14 +251,38 @@ func (s *Suite) Test_Pkglint_Main__compl
                "(Run \"pkglint -F\" to automatically fix some issues.)")
 }
 
-// go test -c -covermode count
-// pkgsrcdir=...
-// env PKGLINT_TESTCMDLINE="$pkgsrcdir -r" ./pkglint.test -test.coverprofile pkglint.cov
-// go tool cover -html=pkglint.cov -o coverage.html
+// Run pkglint in a realistic environment.
+//
+//  env \
+//  PKGLINT_TESTDIR="..." \
+//  PKGLINT_TESTCMDLINE="-r" \
+//  go test -covermode=count -test.coverprofile pkglint.cov
+//
+//  go tool cover -html=pkglint.cov -o coverage.html
+//
+// To measure the branch coverage of the pkglint tests:
+//
+//  env \
+//  PKGLINT_TESTDIR=C:/Users/rillig/git/pkgsrc \
+//  PKGLINT_TESTCMDLINE="-r -Wall -Call -e" \
+//  gobco -vet=off -test.covermode=count \
+//      -test.coverprofile=pkglint-pkgsrc.pprof \
+//      -timeout=3600s -check.f '^' \
+//      > pkglint-pkgsrc.out
+//
+// See https://github.com/rillig/gobco for the tool to measure the branch coverage.
 func (s *Suite) Test_Pkglint__coverage(c *check.C) {
+
+       if cwd := os.Getenv("PKGLINT_TESTDIR"); cwd != "" {
+               err := os.Chdir(cwd)
+               c.Assert(err, check.IsNil)
+       }
+
        cmdline := os.Getenv("PKGLINT_TESTCMDLINE")
        if cmdline != "" {
-               G.logOut, G.logErr, trace.Out = NewSeparatorWriter(os.Stdout), NewSeparatorWriter(os.Stderr), os.Stdout
+               G.out = NewSeparatorWriter(os.Stdout)
+               G.err = NewSeparatorWriter(os.Stderr)
+               trace.Out = os.Stdout
                G.Main(append([]string{"pkglint"}, fields(cmdline)...)...)
        }
 }
@@ -347,7 +372,7 @@ func (s *Suite) Test_Pkglint_CheckDirent
 func (s *Suite) Test_resolveVariableRefs__circular_reference(c *check.C) {
        t := s.Init(c)
 
-       mkline := t.NewMkLine("fileName", 1, "GCC_VERSION=${GCC_VERSION}")
+       mkline := t.NewMkLine("filename", 1, "GCC_VERSION=${GCC_VERSION}")
        G.Pkg = NewPackage(t.File("category/pkgbase"))
        G.Pkg.vars.Define("GCC_VERSION", mkline)
 
@@ -359,9 +384,9 @@ func (s *Suite) Test_resolveVariableRefs
 func (s *Suite) Test_resolveVariableRefs__multilevel(c *check.C) {
        t := s.Init(c)
 
-       mkline1 := t.NewMkLine("fileName", 10, "_=${SECOND}")
-       mkline2 := t.NewMkLine("fileName", 11, "_=${THIRD}")
-       mkline3 := t.NewMkLine("fileName", 12, "_=got it")
+       mkline1 := t.NewMkLine("filename", 10, "_=${SECOND}")
+       mkline2 := t.NewMkLine("filename", 11, "_=${THIRD}")
+       mkline3 := t.NewMkLine("filename", 12, "_=got it")
        G.Pkg = NewPackage(t.File("category/pkgbase"))
        defineVar(mkline1, "FIRST")
        defineVar(mkline2, "SECOND")
@@ -378,7 +403,7 @@ func (s *Suite) Test_resolveVariableRefs
 func (s *Suite) Test_resolveVariableRefs__special_chars(c *check.C) {
        t := s.Init(c)
 
-       mkline := t.NewMkLine("fileName", 10, "_=x11")
+       mkline := t.NewMkLine("filename", 10, "_=x11")
        G.Pkg = NewPackage(t.File("category/pkg"))
        G.Pkg.vars.Define("GST_PLUGINS0.10_TYPE", mkline)
 
@@ -479,7 +504,7 @@ func (s *Suite) Test_Pkglint_Checkfile__
        G.Main("pkglint", lines.FileName)
 
        t.CheckOutputLines(
-               "NOTE: ~/category/package/ALTERNATIVES:1: @PREFIX@/ can be omitted from the file name.",
+               "NOTE: ~/category/package/ALTERNATIVES:1: @PREFIX@/ can be omitted from the filename.",
                "Looks fine.",
                "(Run \"pkglint -e\" to show explanations.)")
 }
@@ -550,9 +575,10 @@ func (s *Suite) Test_Pkglint_Checkfile__
 func (s *Suite) Test_Pkglint_Tool__prefer_mk_over_pkgsrc(c *check.C) {
        t := s.Init(c)
 
+       mkline := t.NewMkLine("dummy.mk", 123, "DUMMY=\tvalue")
        G.Mk = t.NewMkLines("Makefile", MkRcsID)
-       global := G.Pkgsrc.Tools.Define("tool", "TOOL", dummyMkLine)
-       local := G.Mk.Tools.Define("tool", "TOOL", dummyMkLine)
+       global := G.Pkgsrc.Tools.Define("tool", "TOOL", mkline)
+       local := G.Mk.Tools.Define("tool", "TOOL", mkline)
 
        global.Validity = Nowhere
        local.Validity = AtRunTime
@@ -569,8 +595,9 @@ func (s *Suite) Test_Pkglint_Tool__prefe
 func (s *Suite) Test_Pkglint_Tool__lookup_by_name_fallback(c *check.C) {
        t := s.Init(c)
 
+       mkline := t.NewMkLine("dummy.mk", 123, "DUMMY=\tvalue")
        G.Mk = t.NewMkLines("Makefile", MkRcsID)
-       global := G.Pkgsrc.Tools.Define("tool", "TOOL", dummyMkLine)
+       global := G.Pkgsrc.Tools.Define("tool", "TOOL", mkline)
 
        global.Validity = Nowhere
 
@@ -586,9 +613,10 @@ func (s *Suite) Test_Pkglint_Tool__looku
 func (s *Suite) Test_Pkglint_Tool__lookup_by_varname(c *check.C) {
        t := s.Init(c)
 
+       mkline := t.NewMkLine("dummy.mk", 123, "DUMMY=\tvalue")
        G.Mk = t.NewMkLines("Makefile", MkRcsID)
-       global := G.Pkgsrc.Tools.Define("tool", "TOOL", dummyMkLine)
-       local := G.Mk.Tools.Define("tool", "TOOL", dummyMkLine)
+       global := G.Pkgsrc.Tools.Define("tool", "TOOL", mkline)
+       local := G.Mk.Tools.Define("tool", "TOOL", mkline)
 
        global.Validity = Nowhere
        local.Validity = AtRunTime
@@ -635,15 +663,15 @@ func (s *Suite) Test_Pkglint_Tool__looku
 func (s *Suite) Test_Pkglint_ToolByVarname__prefer_mk_over_pkgsrc(c *check.C) {
        t := s.Init(c)
 
+       mkline := t.NewMkLine("dummy.mk", 123, "DUMMY=\tvalue")
        G.Mk = t.NewMkLines("Makefile", MkRcsID)
-       global := G.Pkgsrc.Tools.Define("tool", "TOOL", dummyMkLine)
-       local := G.Mk.Tools.Define("tool", "TOOL", dummyMkLine)
+       global := G.Pkgsrc.Tools.Define("tool", "TOOL", mkline)
+       local := G.Mk.Tools.Define("tool", "TOOL", mkline)
 
        global.Validity = Nowhere
        local.Validity = AtRunTime
 
-       c.Check(G.ToolByVarname("TOOL", LoadTime), equals, local)
-       c.Check(G.ToolByVarname("TOOL", RunTime), equals, local)
+       c.Check(G.ToolByVarname("TOOL"), equals, local)
 }
 
 func (s *Suite) Test_Pkglint_ToolByVarname(c *check.C) {
@@ -652,8 +680,7 @@ func (s *Suite) Test_Pkglint_ToolByVarna
        G.Mk = t.NewMkLines("Makefile", MkRcsID)
        G.Pkgsrc.Tools.def("tool", "TOOL", false, AtRunTime)
 
-       c.Check(G.ToolByVarname("TOOL", LoadTime).String(), equals, "tool:TOOL::AtRunTime")
-       c.Check(G.ToolByVarname("TOOL", RunTime).String(), equals, "tool:TOOL::AtRunTime")
+       c.Check(G.ToolByVarname("TOOL").String(), equals, "tool:TOOL::AtRunTime")
 }
 
 func (s *Suite) Test_CheckfileExtra(c *check.C) {
@@ -791,7 +818,7 @@ func (s *Suite) Test_Pkglint_Checkfile__
        // FIXME: Do this resetting properly
        G.errors = 0
        G.warnings = 0
-       G.logged = make(map[string]bool)
+       G.logged = Once{}
        G.Main("pkglint", "--import", "category/package", "wip/package")
 
        t.CheckOutputLines(
@@ -974,54 +1001,29 @@ func (s *Suite) Test_Pkglint_checkdirPac
                        "Alternative implementation \"bin/wrapper-impl\" must appear in the PLIST.")
 }
 
-func (s *Suite) Test_Pkglint_ShowSummary__explanations_with_only(c *check.C) {
-       t := s.Init(c)
-
-       t.SetupCommandLine("--only", "interesting")
-       line := t.NewLine("Makefile", 27, "The old song")
-
-       line.Warnf("Filtered warning.")               // Is not logged.
-       Explain("Explanation for the above warning.") // Neither would this explanation be logged.
-       G.ShowSummary()
-
-       c.Check(G.explanationsAvailable, equals, false)
-       t.CheckOutputLines(
-               "Looks fine.") // "pkglint -e" is not advertised since the above explanation is not relevant.
-
-       line.Warnf("What an interesting line.")
-       Explain("This explanation is available.")
-       G.ShowSummary()
-
-       c.Check(G.explanationsAvailable, equals, true)
-       t.CheckOutputLines(
-               "WARN: Makefile:27: What an interesting line.",
-               "0 errors and 1 warning found.",
-               "(Run \"pkglint -e\" to show explanations.)")
-}
-
 func (s *Suite) Test_CheckfileMk__enoent(c *check.C) {
        t := s.Init(c)
 
-       CheckfileMk(t.File("fileName.mk"))
+       CheckfileMk(t.File("filename.mk"))
 
        t.CheckOutputLines(
-               "ERROR: ~/fileName.mk: Cannot be read.")
+               "ERROR: ~/filename.mk: Cannot be read.")
 }
 
 func (s *Suite) Test_Pkglint_checkExecutable(c *check.C) {
        t := s.Init(c)
 
-       fileName := t.File("file.mk")
-       fileInfo := ExecutableFileInfo{path.Base(fileName)}
+       filename := t.File("file.mk")
+       fileInfo := ExecutableFileInfo{path.Base(filename)}
 
-       G.checkExecutable(fileName, fileInfo)
+       G.checkExecutable(filename, fileInfo)
 
        t.CheckOutputLines(
                "WARN: ~/file.mk: Should not be executable.")
 
        t.SetupCommandLine("--autofix")
 
-       G.checkExecutable(fileName, fileInfo)
+       G.checkExecutable(filename, fileInfo)
 
        // FIXME: The error message "Cannot clear executable bits" is swallowed.
        t.CheckOutputLines(
@@ -1033,24 +1035,26 @@ func (s *Suite) Test_Pkglint_checkExecut
 
        t.CreateFileLines("CVS/Entries",
                "/file.mk/modified////")
-       fileName := t.File("file.mk")
-       fileInfo := ExecutableFileInfo{path.Base(fileName)}
+       filename := t.File("file.mk")
+       fileInfo := ExecutableFileInfo{path.Base(filename)}
 
-       G.checkExecutable(fileName, fileInfo)
+       G.checkExecutable(filename, fileInfo)
 
        // See the "Too late" comment in Pkglint.checkExecutable.
        t.CheckOutputEmpty()
 }
-
 func (s *Suite) Test_main(c *check.C) {
        t := s.Init(c)
 
        out, err := os.Create(t.CreateFileLines("out"))
        c.Check(err, check.IsNil)
+       outProfiling, err := os.Create(t.CreateFileLines("out.profiling"))
+       c.Check(err, check.IsNil)
 
-       pkg := t.SetupPackage("category/package")
+       t.SetupPackage("category/package")
+       t.Chdir("category/package")
 
-       func() {
+       runMain := func(out *os.File, commandLine ...string) {
                args := os.Args
                stdout := os.Stdout
                stderr := os.Stderr
@@ -1061,7 +1065,7 @@ func (s *Suite) Test_main(c *check.C) {
                        os.Args = args
                        exit = prevExit
                }()
-               os.Args = []string{"pkglint", pkg}
+               os.Args = commandLine
                os.Stdout = out
                os.Stderr = out
                exit = func(code int) {
@@ -1069,14 +1073,18 @@ func (s *Suite) Test_main(c *check.C) {
                }
 
                main()
-       }()
+       }
 
-       err = out.Close()
-       c.Check(err, check.IsNil)
+       runMain(out, "pkglint", ".")
+       runMain(outProfiling, "pkglint", "--profiling", ".")
 
-       t.CheckOutputEmpty()
-       t.CheckFileLines("out",
+       c.Check(out.Close(), check.IsNil)
+       c.Check(outProfiling.Close(), check.IsNil)
+
+       t.CheckOutputEmpty()          // Because all output is redirected.
+       t.CheckFileLines("../../out", // See the t.Chdir above.
                "Looks fine.")
+       // outProfiling is not checked because it contains timing information.
 }
 
 // ExecutableFileInfo mocks a FileInfo because on Windows,
Index: pkgsrc/pkgtools/pkglint/files/plist_test.go
diff -u pkgsrc/pkgtools/pkglint/files/plist_test.go:1.27 pkgsrc/pkgtools/pkglint/files/plist_test.go:1.28
--- pkgsrc/pkgtools/pkglint/files/plist_test.go:1.27    Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/plist_test.go Sun Dec  2 01:57:48 2018
@@ -46,7 +46,7 @@ func (s *Suite) Test_ChecklinesPlist(c *
                "WARN: PLIST:14: Packages that install icon theme files should set ICON_THEMES.",
                "ERROR: PLIST:15: Packages that install hicolor icons "+
                        "must include \"../../graphics/hicolor-icon-theme/buildlink3.mk\" in the Makefile.",
-               "ERROR: PLIST:18: Duplicate file name \"share/tzinfo\", already appeared in line 17.")
+               "ERROR: PLIST:18: Duplicate filename \"share/tzinfo\", already appeared in line 17.")
 }
 
 func (s *Suite) Test_ChecklinesPlist__empty(c *check.C) {
@@ -131,7 +131,7 @@ func (s *Suite) Test_plistLineSorter_Sor
                "${PLIST.linux}${PLIST.x86_64}lib/lib-linux-x86_64.so", // Double condition, see graphics/graphviz
                "lib/after.la",
                "@exec echo \"after lib/after.la\"")
-       ck := &PlistChecker{nil, nil, "", Once{}}
+       ck := PlistChecker{nil, nil, "", Once{}}
        plines := ck.NewLines(lines)
 
        sorter1 := NewPlistLineSorter(plines)
@@ -288,11 +288,11 @@ func (s *Suite) Test_PlistChecker__remov
        ChecklinesPlist(lines)
 
        t.CheckOutputLines(
-               "ERROR: ~/PLIST:2: Duplicate file name \"bin/true\", already appeared in line 3.",
-               "ERROR: ~/PLIST:4: Duplicate file name \"bin/true\", already appeared in line 3.",
-               "ERROR: ~/PLIST:5: Duplicate file name \"bin/true\", already appeared in line 3.",
+               "ERROR: ~/PLIST:2: Duplicate filename \"bin/true\", already appeared in line 3.",
+               "ERROR: ~/PLIST:4: Duplicate filename \"bin/true\", already appeared in line 3.",
+               "ERROR: ~/PLIST:5: Duplicate filename \"bin/true\", already appeared in line 3.",
                "WARN: ~/PLIST:6: \"bin/false\" should be sorted before \"bin/true\".",
-               "ERROR: ~/PLIST:8: Duplicate file name \"bin/true\", already appeared in line 3.")
+               "ERROR: ~/PLIST:8: Duplicate filename \"bin/true\", already appeared in line 3.")
 
        t.SetupCommandLine("-Wall", "--autofix")
 
@@ -513,7 +513,7 @@ func (s *Suite) Test_PlistLine_CheckTrai
        ChecklinesPlist(lines)
 
        t.CheckOutputLines(
-               "ERROR: ~/PLIST:2: pkgsrc does not support filenames ending in white-space.")
+               "ERROR: ~/PLIST:2: pkgsrc does not support filenames ending in whitespace.")
 }
 
 func (s *Suite) Test_PlistLine_CheckDirective(c *check.C) {
@@ -555,8 +555,8 @@ func (s *Suite) Test_plistLineSorter__un
 
        t.CheckOutputLines(
                "TRACE: + ChecklinesPlist(\"~/PLIST\")",
-               "TRACE: 1 + CheckLineRcsid(\"@comment \", \"@comment \")",
-               "TRACE: 1 - CheckLineRcsid(\"@comment \", \"@comment \")",
+               "TRACE: 1 + (*LinesImpl).CheckRcsID(\"@comment \", \"@comment \")",
+               "TRACE: 1 - (*LinesImpl).CheckRcsID(\"@comment \", \"@comment \")",
                "TRACE: 1   ~/PLIST:2: bin/program${OPSYS}: This line prevents pkglint from sorting the PLIST automatically.",
                "TRACE: 1 + SaveAutofixChanges()",
                "TRACE: 1 - SaveAutofixChanges()",

Index: pkgsrc/pkgtools/pkglint/files/linechecker.go
diff -u pkgsrc/pkgtools/pkglint/files/linechecker.go:1.9 pkgsrc/pkgtools/pkglint/files/linechecker.go:1.10
--- pkgsrc/pkgtools/pkglint/files/linechecker.go:1.9    Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/linechecker.go        Sun Dec  2 01:57:48 2018
@@ -2,15 +2,27 @@ package main
 
 import (
        "fmt"
-       "netbsd.org/pkglint/regex"
        "strings"
 )
 
-func CheckLineAbsolutePathname(line Line, text string) {
+type LineChecker struct {
+       line Line
+}
+
+// CheckAbsolutePathname checks whether any absolute pathnames occur in the line.
+//
+// XXX: Is this check really useful? It had been added 10 years ago because some
+// style guide said that "absolute pathnames should be avoided", but there was no
+// evidence for that.
+func (ck LineChecker) CheckAbsolutePathname(text string) {
        if trace.Tracing {
                defer trace.Call1(text)()
        }
 
+       // XXX: The following code only checks the first absolute pathname per line.
+       // The remaining pathnames are ignored. This is probably harmless in practice
+       // since it doesn't occur often.
+
        // In the GNU coding standards, DESTDIR is defined as a (usually
        // empty) prefix that can be used to install files to a different
        // location from what they have been built for. Therefore
@@ -20,86 +32,91 @@ func CheckLineAbsolutePathname(line Line
        // assignments like "bindir=/bin".
        if m, path := match1(text, `(?:^|[\t ]|\$[{(]DESTDIR[)}]|[\w_]+[\t ]*=[\t ]*)(/(?:[^"' \t\\]|"[^"*]"|'[^']*')*)`); m {
                if matches(path, `^/\w`) {
-                       CheckwordAbsolutePathname(line, path)
+
+                       // XXX: Why is the "before" text from the above regular expression
+                       // not passed on to this method?
+                       ck.CheckWordAbsolutePathname(path)
                }
        }
 }
 
-func CheckLineLength(line Line, maxlength int) {
-       if len(line.Text) > maxlength {
-               line.Warnf("Line too long (should be no more than %d characters).", maxlength)
-               Explain(
+func (ck LineChecker) CheckLength(maxLength int) {
+       if len(ck.line.Text) > maxLength {
+               ck.line.Warnf("Line too long (should be no more than %d characters).", maxLength)
+               G.Explain(
                        "Back in the old time, terminals with 80x25 characters were common.",
                        "And this is still the default size of many terminal emulators.",
                        "Moderately short lines also make reading easier.")
        }
 }
 
-func CheckLineValidCharacters(line Line) {
-       uni := ""
-       for _, r := range line.Text {
+func (ck LineChecker) CheckValidCharacters() {
+       var uni strings.Builder
+       for _, r := range ck.line.Text {
                if r != '\t' && !(' ' <= r && r <= '~') {
-                       uni += fmt.Sprintf(" %U", r)
+                       _, _ = fmt.Fprintf(&uni, " %U", r)
                }
        }
-       if uni != "" {
-               line.Warnf("Line contains invalid characters (%s).", uni[1:])
+       if uni.Len() > 0 {
+               ck.line.Warnf("Line contains invalid characters (%s).", uni.String()[1:])
        }
 }
 
-func CheckLineTrailingWhitespace(line Line) {
-       if strings.HasSuffix(line.Text, " ") || strings.HasSuffix(line.Text, "\t") {
-               fix := line.Autofix()
-               fix.Notef("Trailing white-space.")
+func (ck LineChecker) CheckTrailingWhitespace() {
+
+       // XXX: Markdown files may need trailing whitespace. If there should ever
+       // be Markdown files in pkgsrc, this code has to be adjusted.
+
+       if strings.HasSuffix(ck.line.Text, " ") || strings.HasSuffix(ck.line.Text, "\t") {
+               fix := ck.line.Autofix()
+               fix.Notef("Trailing whitespace.")
                fix.Explain(
-                       "When a line ends with some white-space, that space is in most cases",
+                       "When a line ends with some whitespace, that space is in most cases",
                        "irrelevant and can be removed.")
                fix.ReplaceRegex(`[ \t\r]+\n$`, "\n", 1)
                fix.Apply()
        }
 }
 
-func CheckLineRcsid(line Line, prefixRe regex.Pattern, suggestedPrefix string) bool {
+// CheckWordAbsolutePathname checks the given word (which is often part of a
+// shell command) for absolute pathnames.
+//
+// XXX: Is this check really useful? It had been added 10 years ago because some
+// style guide said that "absolute pathnames should be avoided", but there was no
+// evidence for that.
+func (ck LineChecker) CheckWordAbsolutePathname(word string) {
        if trace.Tracing {
-               defer trace.Call(prefixRe, suggestedPrefix)()
-       }
-
-       if matches(line.Text, `^`+prefixRe+`\$`+`NetBSD(?::[^\$]+)?\$$`) {
-               return true
+               defer trace.Call1(word)()
        }
 
-       fix := line.Autofix()
-       fix.Errorf("Expected %q.", suggestedPrefix+"$"+"NetBSD$")
-       fix.Explain(
-               "Several files in pkgsrc must contain the CVS Id, so that their",
-               "current version can be traced back later from a binary package.",
-               "This is to ensure reproducible builds, for example for finding bugs.")
-       fix.InsertBefore(suggestedPrefix + "$" + "NetBSD$")
-       fix.Apply()
-
-       return false
-}
-
-func CheckwordAbsolutePathname(line Line, word string) {
-       if trace.Tracing {
-               defer trace.Call1(word)()
+       if !G.Opts.WarnAbsname {
+               return
        }
 
        switch {
        case matches(word, `^/dev/(?:null|tty|zero)$`):
                // These are defined by POSIX.
 
+       case matches(word, `^/dev/(?:stdin|stdout|stderr)$`):
+               ck.line.Warnf("The %q file is not portable.", word)
+               G.Explain(
+                       "The special files /dev/{stdin,stdout,stderr}, although present",
+                       "on Linux systems, are not available on other systems, and POSIX",
+                       "explicitly mentions them as examples of system-specific filenames.",
+                       "",
+                       "See https://unix.stackexchange.com/q/36403.";)
+
        case word == "/bin/sh":
                // 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,
+               // Probably a sed(1) command, such as /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") {
-                       Explain(
+               ck.line.Warnf("Found absolute pathname: %s", word)
+               if contains(ck.line.Text, "DESTDIR") {
+                       G.Explain(
                                "Absolute pathnames are often an indicator for unportable code.  As",
                                "pkgsrc aims to be a portable system, absolute pathnames should be",
                                "avoided whenever possible.",
@@ -110,10 +127,12 @@ func CheckwordAbsolutePathname(line Line
                                "empty, so if anything after that variable starts with a slash, it is",
                                "considered an absolute pathname.")
                } else {
-                       Explain(
+                       G.Explain(
                                "Absolute pathnames are often an indicator for unportable code.  As",
                                "pkgsrc aims to be a portable system, absolute pathnames should be",
                                "avoided whenever possible.")
+
+                       // TODO: Explain how to actually fix this warning properly.
                }
        }
 }
Index: pkgsrc/pkgtools/pkglint/files/linechecker_test.go
diff -u pkgsrc/pkgtools/pkglint/files/linechecker_test.go:1.9 pkgsrc/pkgtools/pkglint/files/linechecker_test.go:1.10
--- pkgsrc/pkgtools/pkglint/files/linechecker_test.go:1.9       Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/linechecker_test.go   Sun Dec  2 01:57:48 2018
@@ -4,59 +4,100 @@ import (
        "gopkg.in/check.v1"
 )
 
-func (s *Suite) Test_CheckLineAbsolutePathname(c *check.C) {
+func (s *Suite) Test_LineChecker_CheckAbsolutePathname(c *check.C) {
        t := s.Init(c)
 
+       t.SetupCommandLine("-Wabsname", "--explain")
+       mklines := t.NewMkLines("Makefile",
+               MkRcsID,
+               "\tbindir=/bin",
+               "\tbindir=/../lib",
+               "\tcat /dev/null",
+               "\tcat /dev/tty",
+               "\tcat /dev/zero",
+               "\tcat /dev/stdin",
+               "\tcat /dev/stdout",
+               "\tcat /dev/stderr",
+               "\tprintf '#! /bin/sh\\nexit 0'",
+               "\tprogram=$$bindir/program",
+               "\tbindir=${PREFIX}/bin",
+               "\tbindir=${DESTDIR}${PREFIX}/bin",
+               "\tbindir=${DESTDIR}/bin",
+
+               // This is not a filename at all, but certainly looks like one.
+               // Nevertheless, pkglint doesn't fall into the trap.
+               "\tsed -e /usr/s/usr/var/g")
+
+       mklines.ForEach(func(mkline MkLine) {
+               if !mkline.IsComment() {
+                       LineChecker{mkline.Line}.CheckAbsolutePathname(mkline.ShellCommand())
+               }
+       })
+
+       t.CheckOutputLines(
+               "WARN: Makefile:2: Found absolute pathname: /bin",
+               "",
+               "\tAbsolute pathnames are often an indicator for unportable code.  As",
+               "\tpkgsrc aims to be a portable system, absolute pathnames should be",
+               "\tavoided whenever possible.",
+               "",
+               "WARN: Makefile:7: The \"/dev/stdin\" file is not portable.",
+               "",
+               "\tThe special files /dev/{stdin,stdout,stderr}, although present on",
+               "\tLinux systems, are not available on other systems, and POSIX",
+               "\texplicitly mentions them as examples of system-specific filenames.",
+               "",
+               "\tSee https://unix.stackexchange.com/q/36403.";,
+               "",
+               "WARN: Makefile:8: The \"/dev/stdout\" file is not portable.",
+               "WARN: Makefile:9: The \"/dev/stderr\" file is not portable.",
+               "WARN: Makefile:14: Found absolute pathname: /bin",
+               "",
+               "\tAbsolute pathnames are often an indicator for unportable code.  As",
+               "\tpkgsrc aims to be a portable system, absolute pathnames should be",
+               "\tavoided whenever possible.",
+               "",
+               "\tA special variable in this context is ${DESTDIR}, which is used in",
+               "\tGNU projects to specify a different directory for installation than",
+               "\twhat the programs see later when they are executed.  Usually it is",
+               "\tempty, so if anything after that variable starts with a slash, it is",
+               "\tconsidered an absolute pathname.",
+               "")
+}
+
+// It is unclear whether pkglint should check for absolute pathnames by default.
+// It might be useful, but all the code surrounding this check was added for
+// theoretical reasons instead of a practical bug. Therefore the code is still
+// there, it is just not enabled by default.
+func (s *Suite) Test_LineChecker_CheckAbsolutePathname__disabled_by_default(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine( /* none, which means -Wall is suppressed */ )
        line := t.NewLine("Makefile", 1, "# dummy")
 
-       CheckLineAbsolutePathname(line, "bindir=/bin")
-       CheckLineAbsolutePathname(line, "bindir=/../lib")
-       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")
+       LineChecker{line}.CheckAbsolutePathname("bindir=/bin")
 
-       t.CheckOutputLines(
-               "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")
+       t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_CheckLineTrailingWhitespace(c *check.C) {
+func (s *Suite) Test_LineChecker_CheckTrailingWhitespace(c *check.C) {
        t := s.Init(c)
 
        line := t.NewLine("Makefile", 32, "The line must go on   ")
 
-       CheckLineTrailingWhitespace(line)
+       LineChecker{line}.CheckTrailingWhitespace()
 
        t.CheckOutputLines(
-               "NOTE: Makefile:32: Trailing white-space.")
+               "NOTE: Makefile:32: Trailing whitespace.")
 }
 
-func (s *Suite) Test_CheckLineRcsid(c *check.C) {
+func (s *Suite) Test_LineChecker_CheckTrailingWhitespace__tab(c *check.C) {
        t := s.Init(c)
 
-       lines := t.NewLines("fileName",
-               "$"+"NetBSD: dummy $",
-               "$"+"NetBSD$",
-               "$"+"Id: dummy $",
-               "$"+"Id$",
-               "$"+"FreeBSD$")
-
-       for _, line := range lines.Lines {
-               CheckLineRcsid(line, ``, "")
-       }
+       line := t.NewLine("Makefile", 32, "The line must go on\t")
+
+       LineChecker{line}.CheckTrailingWhitespace()
 
        t.CheckOutputLines(
-               "ERROR: fileName:3: Expected \"$"+"NetBSD$\".",
-               "ERROR: fileName:4: Expected \"$"+"NetBSD$\".",
-               "ERROR: fileName:5: Expected \"$"+"NetBSD$\".")
+               "NOTE: Makefile:32: Trailing whitespace.")
 }
Index: pkgsrc/pkgtools/pkglint/files/mkshtypes.go
diff -u pkgsrc/pkgtools/pkglint/files/mkshtypes.go:1.9 pkgsrc/pkgtools/pkglint/files/mkshtypes.go:1.10
--- pkgsrc/pkgtools/pkglint/files/mkshtypes.go:1.9      Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/mkshtypes.go  Sun Dec  2 01:57:48 2018
@@ -101,7 +101,7 @@ type MkShForClause struct {
 
 // MkShCaseClause is a "case" statement, including all its branches.
 //
-// Example: case $fileName in *.c) echo "C source" ;; esac
+// Example: case $filename in *.c) echo "C source" ;; esac
 type MkShCaseClause struct {
        Word  *ShToken
        Cases []*MkShCaseItem
@@ -160,7 +160,7 @@ type MkShSimpleCommand struct {
 }
 
 func NewStrCommand(cmd *MkShSimpleCommand) *StrCommand {
-       strcmd := &StrCommand{
+       strcmd := StrCommand{
                make([]string, len(cmd.Assignments)),
                "",
                make([]string, len(cmd.Args))}
@@ -173,7 +173,7 @@ func NewStrCommand(cmd *MkShSimpleComman
        for i, arg := range cmd.Args {
                strcmd.Args[i] = arg.MkText
        }
-       return strcmd
+       return &strcmd
 }
 
 // StrCommand is structurally similar to MkShSimpleCommand, but all
@@ -227,7 +227,7 @@ func (c *StrCommand) String() string {
 type MkShRedirection struct {
        Fd     int      // Or -1
        Op     string   // See io_file in shell.y for possible values
-       Target *ShToken // The file name or &fd
+       Target *ShToken // The filename or &fd
 }
 
 type MkShSeparator uint8
Index: pkgsrc/pkgtools/pkglint/files/parser_test.go
diff -u pkgsrc/pkgtools/pkglint/files/parser_test.go:1.9 pkgsrc/pkgtools/pkglint/files/parser_test.go:1.10
--- pkgsrc/pkgtools/pkglint/files/parser_test.go:1.9    Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/parser_test.go        Sun Dec  2 01:57:48 2018
@@ -6,23 +6,23 @@ import (
 
 func (s *Suite) Test_Parser_PkgbasePattern(c *check.C) {
 
-       checkRest := func(pattern, expected, rest string) {
+       testRest := func(pattern, expected, rest string) {
                parser := NewParser(dummyLine, pattern, false)
                actual := parser.PkgbasePattern()
                c.Check(actual, equals, expected)
                c.Check(parser.Rest(), equals, rest)
        }
 
-       checkRest("fltk", "fltk", "")
-       checkRest("fltk|", "fltk", "|")
-       checkRest("boost-build-1.59.*", "boost-build", "-1.59.*")
-       checkRest("${PHP_PKG_PREFIX}-pdo-5.*", "${PHP_PKG_PREFIX}-pdo", "-5.*")
-       checkRest("${PYPKGPREFIX}-metakit-[0-9]*", "${PYPKGPREFIX}-metakit", "-[0-9]*")
+       testRest("fltk", "fltk", "")
+       testRest("fltk|", "fltk", "|")
+       testRest("boost-build-1.59.*", "boost-build", "-1.59.*")
+       testRest("${PHP_PKG_PREFIX}-pdo-5.*", "${PHP_PKG_PREFIX}-pdo", "-5.*")
+       testRest("${PYPKGPREFIX}-metakit-[0-9]*", "${PYPKGPREFIX}-metakit", "-[0-9]*")
 }
 
 func (s *Suite) Test_Parser_Dependency(c *check.C) {
 
-       checkRest := func(pattern string, expected DependencyPattern, rest string) {
+       testRest := func(pattern string, expected DependencyPattern, rest string) {
                parser := NewParser(dummyLine, pattern, false)
                dp := parser.Dependency()
                if c.Check(dp, check.NotNil) {
@@ -31,7 +31,7 @@ func (s *Suite) Test_Parser_Dependency(c
                }
        }
 
-       checkNil := func(pattern string) {
+       testNil := func(pattern string) {
                parser := NewParser(dummyLine, pattern, false)
                dp := parser.Dependency()
                if c.Check(dp, check.IsNil) {
@@ -39,26 +39,26 @@ func (s *Suite) Test_Parser_Dependency(c
                }
        }
 
-       check := func(pattern string, expected DependencyPattern) {
-               checkRest(pattern, expected, "")
+       test := func(pattern string, expected DependencyPattern) {
+               testRest(pattern, expected, "")
        }
 
-       check("fltk>=1.1.5rc1<1.3", DependencyPattern{"fltk", ">=", "1.1.5rc1", "<", "1.3", ""})
-       check("libwcalc-1.0*", DependencyPattern{"libwcalc", "", "", "", "", "1.0*"})
-       check("${PHP_PKG_PREFIX}-pdo-5.*", DependencyPattern{"${PHP_PKG_PREFIX}-pdo", "", "", "", "", "5.*"})
-       check("${PYPKGPREFIX}-metakit-[0-9]*", DependencyPattern{"${PYPKGPREFIX}-metakit", "", "", "", "", "[0-9]*"})
-       check("boost-build-1.59.*", DependencyPattern{"boost-build", "", "", "", "", "1.59.*"})
-       check("${_EMACS_REQD}", DependencyPattern{"${_EMACS_REQD}", "", "", "", "", ""})
-       check("{gcc46,gcc46-libs}>=4.6.0", DependencyPattern{"{gcc46,gcc46-libs}", ">=", "4.6.0", "", "", ""})
-       check("perl5-*", DependencyPattern{"perl5", "", "", "", "", "*"})
-       check("verilog{,-current}-[0-9]*", DependencyPattern{"verilog{,-current}", "", "", "", "", "[0-9]*"})
-       check("mpg123{,-esound,-nas}>=0.59.18", DependencyPattern{"mpg123{,-esound,-nas}", ">=", "0.59.18", "", "", ""})
-       check("mysql*-{client,server}-[0-9]*", DependencyPattern{"mysql*-{client,server}", "", "", "", "", "[0-9]*"})
-       check("postgresql8[0-35-9]-${module}-[0-9]*", DependencyPattern{"postgresql8[0-35-9]-${module}", "", "", "", "", "[0-9]*"})
-       check("ncurses-${NC_VERS}{,nb*}", DependencyPattern{"ncurses", "", "", "", "", "${NC_VERS}{,nb*}"})
-       check("xulrunner10>=${MOZ_BRANCH}${MOZ_BRANCH_MINOR}", DependencyPattern{"xulrunner10", ">=", "${MOZ_BRANCH}${MOZ_BRANCH_MINOR}", "", "", ""})
-       checkRest("gnome-control-center>=2.20.1{,nb*}", DependencyPattern{"gnome-control-center", ">=", "2.20.1", "", "", ""}, "{,nb*}")
-       checkNil(">=2.20.1{,nb*}")
-       checkNil("pkgbase<=")
+       test("fltk>=1.1.5rc1<1.3", DependencyPattern{"fltk", ">=", "1.1.5rc1", "<", "1.3", ""})
+       test("libwcalc-1.0*", DependencyPattern{"libwcalc", "", "", "", "", "1.0*"})
+       test("${PHP_PKG_PREFIX}-pdo-5.*", DependencyPattern{"${PHP_PKG_PREFIX}-pdo", "", "", "", "", "5.*"})
+       test("${PYPKGPREFIX}-metakit-[0-9]*", DependencyPattern{"${PYPKGPREFIX}-metakit", "", "", "", "", "[0-9]*"})
+       test("boost-build-1.59.*", DependencyPattern{"boost-build", "", "", "", "", "1.59.*"})
+       test("${_EMACS_REQD}", DependencyPattern{"${_EMACS_REQD}", "", "", "", "", ""})
+       test("{gcc46,gcc46-libs}>=4.6.0", DependencyPattern{"{gcc46,gcc46-libs}", ">=", "4.6.0", "", "", ""})
+       test("perl5-*", DependencyPattern{"perl5", "", "", "", "", "*"})
+       test("verilog{,-current}-[0-9]*", DependencyPattern{"verilog{,-current}", "", "", "", "", "[0-9]*"})
+       test("mpg123{,-esound,-nas}>=0.59.18", DependencyPattern{"mpg123{,-esound,-nas}", ">=", "0.59.18", "", "", ""})
+       test("mysql*-{client,server}-[0-9]*", DependencyPattern{"mysql*-{client,server}", "", "", "", "", "[0-9]*"})
+       test("postgresql8[0-35-9]-${module}-[0-9]*", DependencyPattern{"postgresql8[0-35-9]-${module}", "", "", "", "", "[0-9]*"})
+       test("ncurses-${NC_VERS}{,nb*}", DependencyPattern{"ncurses", "", "", "", "", "${NC_VERS}{,nb*}"})
+       test("xulrunner10>=${MOZ_BRANCH}${MOZ_BRANCH_MINOR}", DependencyPattern{"xulrunner10", ">=", "${MOZ_BRANCH}${MOZ_BRANCH_MINOR}", "", "", ""})
+       testRest("gnome-control-center>=2.20.1{,nb*}", DependencyPattern{"gnome-control-center", ">=", "2.20.1", "", "", ""}, "{,nb*}")
+       testNil(">=2.20.1{,nb*}")
+       testNil("pkgbase<=")
        // "{ssh{,6}-[0-9]*,openssh-[0-9]*}" is not representable using the current data structure
 }

Index: pkgsrc/pkgtools/pkglint/files/lines.go
diff -u pkgsrc/pkgtools/pkglint/files/lines.go:1.1 pkgsrc/pkgtools/pkglint/files/lines.go:1.2
--- pkgsrc/pkgtools/pkglint/files/lines.go:1.1  Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/lines.go      Sun Dec  2 01:57:48 2018
@@ -1,6 +1,9 @@
 package main
 
-import "path"
+import (
+       "netbsd.org/pkglint/regex"
+       "path"
+)
 
 type Lines = *LinesImpl
 
@@ -10,15 +13,15 @@ type LinesImpl struct {
        Lines    []Line
 }
 
-func NewLines(fileName string, lines []Line) Lines {
-       return &LinesImpl{fileName, path.Base(fileName), lines}
+func NewLines(filename string, lines []Line) Lines {
+       return &LinesImpl{filename, path.Base(filename), lines}
 }
 
 func (ls *LinesImpl) Len() int { return len(ls.Lines) }
 
 func (ls *LinesImpl) LastLine() Line { return ls.Lines[ls.Len()-1] }
 
-func (ls *LinesImpl) EOFLine() Line { return NewLine(ls.FileName, -1, "", nil) }
+func (ls *LinesImpl) EOFLine() Line { return NewLineMulti(ls.FileName, -1, -1, "", nil) }
 
 func (ls *LinesImpl) Errorf(format string, args ...interface{}) {
        NewLineWhole(ls.FileName).Errorf(format, args...)
@@ -31,3 +34,46 @@ func (ls *LinesImpl) Warnf(format string
 func (ls *LinesImpl) SaveAutofixChanges() {
        SaveAutofixChanges(ls)
 }
+
+func (ls *LinesImpl) CheckRcsID(index int, prefixRe regex.Pattern, suggestedPrefix string) bool {
+       if trace.Tracing {
+               defer trace.Call(prefixRe, suggestedPrefix)()
+       }
+
+       line := ls.Lines[index]
+       if m, expanded := match1(line.Text, `^`+prefixRe+`\$`+`NetBSD(:[^\$]+)?\$$`); m {
+
+               if G.Wip && expanded != "" {
+                       fix := line.Autofix()
+                       fix.Errorf("Expected exactly %q.", suggestedPrefix+"$"+"NetBSD$")
+                       fix.Explain(
+                               "Several files in pkgsrc must contain the CVS Id, so that their",
+                               "current version can be traced back later from a binary package.",
+                               "This is to ensure reproducible builds, for example for finding bugs.",
+                               "",
+                               "These CVS Ids are specific to the CVS version control system, and",
+                               "pkgsrc-wip uses Git instead.  Therefore, having the expanded CVS Ids",
+                               "in those files represents the file from which they were originally",
+                               "copied but not their current state.  Because of that, these markers",
+                               "should be replaced with the plain, unexpanded string $"+"NetBSD$.",
+                               "",
+                               "To preserve the history of the CVS Id, should that ever be needed,",
+                               "remove the leading $.")
+                       fix.InsertBefore(suggestedPrefix + "$" + "NetBSD$")
+                       fix.Apply()
+               }
+
+               return true
+       }
+
+       fix := line.Autofix()
+       fix.Errorf("Expected %q.", suggestedPrefix+"$"+"NetBSD$")
+       fix.Explain(
+               "Most files in pkgsrc contain the CVS Id, so that their current",
+               "version can be traced back later from a binary package.",
+               "This is to ensure reproducible builds, for example for finding bugs.")
+       fix.InsertBefore(suggestedPrefix + "$" + "NetBSD$")
+       fix.Apply()
+
+       return false
+}

Index: pkgsrc/pkgtools/pkglint/files/mkline.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline.go:1.40 pkgsrc/pkgtools/pkglint/files/mkline.go:1.41
--- pkgsrc/pkgtools/pkglint/files/mkline.go:1.40        Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/mkline.go     Sun Dec  2 01:57:48 2018
@@ -4,6 +4,7 @@ package main
 
 import (
        "fmt"
+       "netbsd.org/pkglint/regex"
        "netbsd.org/pkglint/textproc"
        "path"
        "strings"
@@ -12,60 +13,67 @@ import (
 // MkLine is a line from a Makefile fragment.
 // There are several types of lines.
 // The most common types in pkgsrc are variable assignments,
-// shell commands and preprocessor instructions.
+// shell commands and directives like .if and .for.
 type MkLine = *MkLineImpl
 
 type MkLineImpl struct {
        Line
        data interface{} // One of the following mkLine* types
 }
-type mkLineAssign = *mkLineAssignImpl
+type mkLineAssign = *mkLineAssignImpl // See https://github.com/golang/go/issues/28045
 type mkLineAssignImpl struct {
-       commented  bool       // Whether the whole variable assignment is commented out
-       varname    string     // e.g. "HOMEPAGE", "SUBST_SED.perl"
-       varcanon   string     // e.g. "HOMEPAGE", "SUBST_SED.*"
-       varparam   string     // e.g. "", "perl"
-       op         MkOperator //
-       valueAlign string     // The text up to and including the assignment operator, e.g. VARNAME+=\t
-       value      string     // The trimmed value
-       comment    string
+       commented   bool       // Whether the whole variable assignment is commented out
+       varname     string     // e.g. "HOMEPAGE", "SUBST_SED.perl"
+       varcanon    string     // e.g. "HOMEPAGE", "SUBST_SED.*"
+       varparam    string     // e.g. "", "perl"
+       op          MkOperator //
+       valueAlign  string     // The text up to and including the assignment operator, e.g. VARNAME+=\t
+       value       string     // The trimmed value
+       valueMk     []*MkToken // The value, sent through splitIntoMkWords
+       valueMkRest string     // nonempty in case of parse errors
+       comment     string
 }
 type mkLineShell struct {
        command string
 }
-type mkLineComment struct{}
+type mkLineComment struct{} // See mkLineAssignImpl.commented for another type of comment line
 type mkLineEmpty struct{}
-type mkLineDirective = *mkLineDirectiveImpl
+type mkLineDirective = *mkLineDirectiveImpl // See https://github.com/golang/go/issues/28045
 type mkLineDirectiveImpl struct {
-       indent    string
-       directive string
+       indent    string // the space between the leading "." and the directive
+       directive string // "if", "else", "for", etc.
        args      string
-       comment   string
-       elseLine  MkLine // (filled in later)
-       cond      MkCond // (filled in later, as needed)
+       comment   string // mainly interesting for .endif and .endfor
+       elseLine  MkLine // for .if (filled in later)
+       cond      MkCond // for .if and .elif (filled in later as needed)
 }
-type mkLineInclude = *mkLineIncludeImpl
+type mkLineInclude = *mkLineIncludeImpl // See https://github.com/golang/go/issues/28045
 type mkLineIncludeImpl struct {
-       mustExist       bool
-       sys             bool
-       indent          string
-       includeFile     string
-       conditionalVars string // (filled in later)
+       mustExist       bool     // for .sinclude, nonexistent files are ignored
+       sys             bool     // whether the include uses <file.mk> (very rare) instead of "file.mk"
+       indent          string   // the space between the leading "." and the directive
+       includedFile    string   // the text between the <brackets> or "quotes"
+       conditionalVars []string // variables on which this inclusion depends (filled in later, as needed)
 }
 type mkLineDependency struct {
        targets string
        sources string
 }
 
+// NewMkLine parses the text of a Makefile line to see what kind of line
+// it is: variable assignment, include, comment, etc.
+//
+// See devel/bmake/parse.c:/^Parse_File/
 func NewMkLine(line Line) *MkLineImpl {
        text := line.Text
 
+       // XXX: This check should be moved somewhere else. NewMkLine should only be concerned with parsing.
        if hasPrefix(text, " ") && line.Basename != "bsd.buildlink3.mk" {
                line.Warnf("Makefile lines should not start with space characters.")
-               Explain(
+               G.Explain(
                        "If you want this line to contain a shell program, use a tab",
                        "character for indentation.  Otherwise please remove the leading",
-                       "white-space.")
+                       "whitespace.")
        }
 
        if m, commented, varname, spaceAfterVarname, op, valueAlign, value, spaceAfterValue, comment := MatchVarassign(text); m {
@@ -76,31 +84,32 @@ func NewMkLine(line Line) *MkLineImpl {
                        case matches(varname, `^[a-z]`) && op == ":=":
                                break
                        default:
+                               // XXX: This check should be moved somewhere else. NewMkLine should only be concerned with parsing.
                                fix := line.Autofix()
-                               fix.Warnf("Unnecessary space after variable name %q.", varname)
+                               fix.Notef("Unnecessary space after variable name %q.", varname)
                                fix.Replace(varname+spaceAfterVarname+op, varname+op)
                                fix.Apply()
                        }
                }
 
+               // XXX: This check should be moved somewhere else. NewMkLine should only be concerned with parsing.
                if comment != "" && value != "" && spaceAfterValue == "" {
                        line.Warnf("The # character starts a comment.")
-                       Explain(
+                       G.Explain(
                                "In a variable assignment, an unescaped # starts a comment that",
                                "continues until the end of the line.  To escape the #, write \\#.")
                }
 
-               value = strings.Replace(value, "\\#", "#", -1)
-               varparam := varnameParam(varname)
-
                return &MkLineImpl{line, &mkLineAssignImpl{
                        commented,
                        varname,
                        varnameCanon(varname),
-                       varparam,
+                       varnameParam(varname),
                        NewMkOperator(op),
                        valueAlign,
-                       value,
+                       strings.Replace(value, "\\#", "#", -1),
+                       nil,
+                       "",
                        comment}}
        }
 
@@ -122,17 +131,20 @@ func NewMkLine(line Line) *MkLineImpl {
                return &MkLineImpl{line, &mkLineDirectiveImpl{indent, directive, args, comment, nil, nil}}
        }
 
-       if m, indent, directive, includefile := MatchMkInclude(text); m {
-               return &MkLineImpl{line, &mkLineIncludeImpl{directive == "include", false, indent, includefile, ""}}
+       if m, indent, directive, includedFile := MatchMkInclude(text); m {
+               return &MkLineImpl{line, &mkLineIncludeImpl{directive == "include", false, indent, includedFile, nil}}
        }
 
-       if m, indent, directive, includefile := match3(text, `^\.([\t ]*)(s?include)[\t ]+<([^>]+)>[\t ]*(?:#.*)?$`); m {
-               return &MkLineImpl{line, &mkLineIncludeImpl{directive == "include", true, indent, includefile, ""}}
+       if m, indent, directive, includedFile := match3(text, `^\.([\t ]*)(s?include)[\t ]+<([^>]+)>[\t ]*(?:#.*)?$`); m {
+               return &MkLineImpl{line, &mkLineIncludeImpl{directive == "include", true, indent, includedFile, nil}}
        }
 
+       // XXX: Replace this regular expression with proper parsing.
+       // There might be a ${VAR:M*.c} in these variables, which currently confuses the "parser".
        if m, targets, whitespace, sources := match3(text, `^([^\t :]+(?:[\t ]*[^\t :]+)*)([\t ]*):[\t ]*([^#]*?)(?:[\t ]*#.*)?$`); m {
+               // XXX: This check should be moved somewhere else. NewMkLine should only be concerned with parsing.
                if whitespace != "" {
-                       line.Warnf("Space before colon in dependency line.")
+                       line.Notef("Space before colon in dependency line.")
                }
                return &MkLineImpl{line, mkLineDependency{targets, sources}}
        }
@@ -141,14 +153,18 @@ func NewMkLine(line Line) *MkLineImpl {
                return &MkLineImpl{line, nil}
        }
 
+       // The %q is deliberate here since it shows possible strange characters.
        line.Errorf("Unknown Makefile line format: %q.", text)
        return &MkLineImpl{line, nil}
 }
 
 func (mkline *MkLineImpl) String() string {
-       return fmt.Sprintf("%s:%s", mkline.FileName, mkline.Linenos())
+       return fmt.Sprintf("%s:%s", mkline.Filename, mkline.Linenos())
 }
 
+// IsVarassign returns true for variable assignments of the form VAR=value.
+//
+// See IsCommentedVarassign.
 func (mkline *MkLineImpl) IsVarassign() bool {
        data, ok := mkline.data.(mkLineAssign)
        return ok && !data.commented
@@ -172,6 +188,7 @@ func (mkline *MkLineImpl) IsShellCommand
        return ok
 }
 
+// IsComment returns true for lines that consist entirely of a comment.
 func (mkline *MkLineImpl) IsComment() bool {
        _, ok := mkline.data.(mkLineComment)
        return ok || mkline.IsCommentedVarassign()
@@ -182,20 +199,31 @@ func (mkline *MkLineImpl) IsEmpty() bool
        return ok
 }
 
-// IsDirective checks whether the line is a conditional (.if/.elif/.else/.if) or a loop (.for/.endfor).
+// IsDirective returns true for conditionals (.if/.elif/.else/.if) or loops (.for/.endfor).
+//
+// See IsInclude.
 func (mkline *MkLineImpl) IsDirective() bool {
        _, ok := mkline.data.(mkLineDirective)
        return ok
 }
 
+// IsInclude returns true for lines like: .include "other.mk"
+//
+// See IsSysinclude for lines like: .include <sys.mk>
 func (mkline *MkLineImpl) IsInclude() bool {
        incl, ok := mkline.data.(mkLineInclude)
        return ok && !incl.sys
 }
+
+// IsSysinclude returns true for lines like: .include <sys.mk>
+//
+// See IsInclude for lines like: .include "other.mk"
 func (mkline *MkLineImpl) IsSysinclude() bool {
        incl, ok := mkline.data.(mkLineInclude)
        return ok && incl.sys
 }
+
+// IsDependency returns true for dependency lines like "target: source".
 func (mkline *MkLineImpl) IsDependency() bool {
        _, ok := mkline.data.(mkLineDependency)
        return ok
@@ -205,19 +233,21 @@ func (mkline *MkLineImpl) IsDependency()
 // of the variable that is assigned or appended to.
 //
 // Example:
-//  VARNAME?=       value
+//  VARNAME.${param}?=      value   # Varname is "VARNAME.${param}"
 func (mkline *MkLineImpl) Varname() string { return mkline.data.(mkLineAssign).varname }
 
 // Varcanon applies to variable assignments and returns the canonicalized variable name for parameterized variables.
 // Examples:
-//  HOMEPAGE           => HOMEPAGE
-//  SUBST_SED.anything => SUBST_SED.*
+//  HOMEPAGE           => "HOMEPAGE"
+//  SUBST_SED.anything => "SUBST_SED.*"
+//  SUBST_SED.${param} => "SUBST_SED.*"
 func (mkline *MkLineImpl) Varcanon() string { return mkline.data.(mkLineAssign).varcanon }
 
 // Varparam applies to variable assignments and returns the parameter for parameterized variables.
 // Examples:
 //  HOMEPAGE           => ""
-//  SUBST_SED.anything => anything
+//  SUBST_SED.anything => "anything"
+//  SUBST_SED.${param} => "${param}"
 func (mkline *MkLineImpl) Varparam() string { return mkline.data.(mkLineAssign).varparam }
 
 // Op applies to variable assignments and returns the assignment operator.
@@ -226,15 +256,21 @@ func (mkline *MkLineImpl) Op() MkOperato
 // ValueAlign applies to variable assignments and returns all the text
 // before the variable value, e.g. "VARNAME+=\t".
 func (mkline *MkLineImpl) ValueAlign() string { return mkline.data.(mkLineAssign).valueAlign }
-func (mkline *MkLineImpl) Value() string      { return mkline.data.(mkLineAssign).value }
+
+func (mkline *MkLineImpl) Value() string { return mkline.data.(mkLineAssign).value }
 
 // VarassignComment applies to variable assignments and returns the comment.
+//
 // Example:
 //  VAR=value # comment
+//
 // In the above line, the comment is "# comment".
+//
 // The leading "#" is included so that pkglint can distinguish between no comment at all and an empty comment.
 func (mkline *MkLineImpl) VarassignComment() string { return mkline.data.(mkLineAssign).comment }
-func (mkline *MkLineImpl) ShellCommand() string     { return mkline.data.(mkLineShell).command }
+
+func (mkline *MkLineImpl) ShellCommand() string { return mkline.data.(mkLineShell).command }
+
 func (mkline *MkLineImpl) Indent() string {
        if mkline.IsDirective() {
                return mkline.data.(mkLineDirective).indent
@@ -243,7 +279,7 @@ func (mkline *MkLineImpl) Indent() strin
        }
 }
 
-// Directive returns one of "if", "ifdef", "ifndef", "else", "elif", "endif", "for", "endfor", "undef".
+// Directive returns the preprocessing directive, like "if", "for", "endfor", etc.
 //
 // See matchMkDirective.
 func (mkline *MkLineImpl) Directive() string { return mkline.data.(mkLineDirective).directive }
@@ -258,7 +294,7 @@ func (mkline *MkLineImpl) Args() string 
 func (mkline *MkLineImpl) Cond() MkCond {
        cond := mkline.data.(mkLineDirective).cond
        if cond == nil {
-               cond = NewMkParser(mkline.Line, mkline.Args(), false).MkCond()
+               cond = NewMkParser(nil, mkline.Args(), false).MkCond()
                mkline.data.(mkLineDirective).cond = cond
        }
        return cond
@@ -266,24 +302,30 @@ func (mkline *MkLineImpl) Cond() MkCond 
 
 // DirectiveComment is the trailing end-of-line comment, typically at a deeply nested .endif or .endfor.
 func (mkline *MkLineImpl) DirectiveComment() string { return mkline.data.(mkLineDirective).comment }
-func (mkline *MkLineImpl) HasElseBranch() bool      { return mkline.data.(mkLineDirective).elseLine != nil }
+
+func (mkline *MkLineImpl) HasElseBranch() bool { return mkline.data.(mkLineDirective).elseLine != nil }
+
 func (mkline *MkLineImpl) SetHasElseBranch(elseLine MkLine) {
        data := mkline.data.(mkLineDirective)
        data.elseLine = elseLine
        mkline.data = data
 }
 
-func (mkline *MkLineImpl) MustExist() bool     { return mkline.data.(mkLineInclude).mustExist }
-func (mkline *MkLineImpl) IncludeFile() string { return mkline.data.(mkLineInclude).includeFile }
+func (mkline *MkLineImpl) MustExist() bool { return mkline.data.(mkLineInclude).mustExist }
+
+func (mkline *MkLineImpl) IncludedFile() string { return mkline.data.(mkLineInclude).includedFile }
 
 func (mkline *MkLineImpl) Targets() string { return mkline.data.(mkLineDependency).targets }
+
 func (mkline *MkLineImpl) Sources() string { return mkline.data.(mkLineDependency).sources }
 
 // ConditionalVars applies to .include lines and is a space-separated
 // list of those variable names on which the inclusion depends.
 // It is initialized later, step by step, when parsing other lines.
-func (mkline *MkLineImpl) ConditionalVars() string { return mkline.data.(mkLineInclude).conditionalVars }
-func (mkline *MkLineImpl) SetConditionalVars(varnames string) {
+func (mkline *MkLineImpl) ConditionalVars() []string {
+       return mkline.data.(mkLineInclude).conditionalVars
+}
+func (mkline *MkLineImpl) SetConditionalVars(varnames []string) {
        include := mkline.data.(mkLineInclude)
        include.conditionalVars = varnames
        mkline.data = include
@@ -291,9 +333,19 @@ func (mkline *MkLineImpl) SetConditional
 
 // Tokenize extracts variable uses and other text from the string.
 //
+// TODO: Check this paragraph for correctness.
+// Either:
+// The given s must have exactly the format from the file, i.e. an escaped
+// comment is written as \#.
+// Or:
+// The given s must have the format after parsing comments, i.e. the trailing
+// comment is already removed, and a # does not introduce another comment.
+//
 // Example:
 //  input:  ${PREFIX}/bin abc
 //  output: [MkToken("${PREFIX}", MkVarUse("PREFIX")), MkToken("/bin abc")]
+//
+// See ValueTokens, which is the tokenized version of Value.
 func (mkline *MkLineImpl) Tokenize(s string, warn bool) []*MkToken {
        if trace.Tracing {
                defer trace.Call(mkline, s)()
@@ -307,37 +359,117 @@ func (mkline *MkLineImpl) Tokenize(s str
        return tokens
 }
 
-// ValueSplit splits the variable value of an assignment line,
-// taking care of variable references. For example, when the value
-// "/bin:${PATH:S,::,::,}" is split at ":", it results in
-// {"/bin", "${PATH:S,::,::,}"}.
+// ValueSplit splits the given value, taking care of variable references.
+// Example:
+//
+//  ValueSplit("${VAR:Udefault}::${VAR2}two:words", ":")
+//  => "${VAR:Udefault}"
+//     ""
+//     "${VAR2}two"
+//     "words"
 //
-// If the separator is empty, splitting is done on whitespace.
+// Note that even though the first word contains a colon, it is not split
+// at that point since the colon is inside a variable use.
+//
+// When several separators are adjacent, this results in empty words in the output.
 func (mkline *MkLineImpl) ValueSplit(value string, separator string) []string {
+       G.Assertf(separator != "", "Separator must not be empty; use ValueFields to split on whitespace")
+
        tokens := mkline.Tokenize(value, false)
        var split []string
-       for _, token := range tokens {
-               if split == nil {
-                       split = []string{""}
+       cont := false
+
+       out := func(s string) {
+               if cont {
+                       split[len(split)-1] += s
+               } else {
+                       split = append(split, s)
                }
-               if token.Varuse == nil && contains(token.Text, separator) {
-                       var subs []string
-                       if separator == "" {
-                               subs = fields(token.Text)
-                       } else {
-                               subs = strings.Split(token.Text, separator)
+       }
+
+       for _, token := range tokens {
+               if token.Varuse != nil {
+                       out(token.Text)
+                       cont = true
+               } else {
+                       lexer := textproc.NewLexer(token.Text)
+                       for !lexer.EOF() {
+                               if lexer.SkipString(separator) {
+                                       out("")
+                                       cont = false
+                               }
+                               idx := strings.Index(lexer.Rest(), separator)
+                               if idx == -1 {
+                                       idx = len(lexer.Rest())
+                               }
+                               if idx > 0 {
+                                       out(lexer.NextString(lexer.Rest()[:idx]))
+                                       cont = true
+                               }
                        }
-                       split[len(split)-1] += subs[0]
-                       split = append(split, subs[1:]...)
+               }
+       }
+       return split
+}
+
+// ValueFields splits the given value, taking care of variable references.
+// Example:
+//
+//  ValueFields("${VAR:Udefault value} ${VAR2}two words")
+//  => "${VAR:Udefault value}"
+//     "${VAR2}two"
+//     "words"
+//
+// Note that even though the first word contains a space, it is not split
+// at that point since the space is inside a variable use.
+func (mkline *MkLineImpl) ValueFields(value string) []string {
+       tokens := mkline.Tokenize(value, false)
+       var split []string
+       cont := false
+
+       out := func(s string) {
+               if cont {
+                       split[len(split)-1] += s
+               } else {
+                       split = append(split, s)
+               }
+       }
+
+       for _, token := range tokens {
+               if token.Varuse != nil {
+                       out(token.Text)
+                       cont = true
                } else {
-                       split[len(split)-1] += token.Text
+                       lexer := textproc.NewLexer(token.Text)
+                       for !lexer.EOF() {
+                               for lexer.NextBytesSet(textproc.Space) != "" {
+                                       cont = false
+                               }
+                               if word := lexer.NextBytesSet(textproc.Space.Inverse()); word != "" {
+                                       out(word)
+                                       cont = true
+                               }
+                       }
                }
        }
        return split
 }
 
 func (mkline *MkLineImpl) ValueTokens() []*MkToken {
-       return mkline.Tokenize(mkline.Value(), false)
+       value := mkline.Value()
+       if value == "" {
+               return nil
+       }
+
+       assign := mkline.data.(mkLineAssign)
+       if assign.valueMk != nil || assign.valueMkRest != "" {
+               return assign.valueMk
+       }
+
+       p := NewMkParser(mkline.Line, value, true)
+       assign.valueMk = p.MkTokens()
+       assign.valueMkRest = p.Rest()
+       return assign.valueMk
 }
 
 func (mkline *MkLineImpl) WithoutMakeVariables(value string) string {
@@ -356,7 +488,7 @@ func (mkline *MkLineImpl) ResolveVarsInR
        if G.Pkg != nil {
                basedir = G.Pkg.File(".")
        } else {
-               basedir = path.Dir(mkline.FileName)
+               basedir = path.Dir(mkline.Filename)
        }
        pkgsrcdir := relpath(basedir, G.Pkgsrc.File("."))
 
@@ -374,27 +506,27 @@ func (mkline *MkLineImpl) ResolveVarsInR
        tmp = strings.Replace(tmp, "${PKGSRCDIR}", pkgsrcdir, -1)
        tmp = strings.Replace(tmp, "${.CURDIR}", ".", -1)
        tmp = strings.Replace(tmp, "${.PARSEDIR}", ".", -1)
-       if contains(tmp, "${LUA_PKGSRCDIR}") {
-               tmp = strings.Replace(tmp, "${LUA_PKGSRCDIR}", G.Pkgsrc.Latest("lang", `^lua[0-9]+$`, "../../lang/$0"), -1)
-       }
-       if contains(tmp, "${PHPPKGSRCDIR}") {
-               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")
-               tmp = strings.Replace(tmp, "${SUSE_DIR_PREFIX}", suseDirPrefix, -1)
-       }
-       if contains(tmp, "${PYPKGSRCDIR}") {
-               tmp = strings.Replace(tmp, "${PYPKGSRCDIR}", G.Pkgsrc.Latest("lang", `^python[0-9]+$`, "../../lang/$0"), -1)
-       }
-       if contains(tmp, "${PYPACKAGE}") {
-               tmp = strings.Replace(tmp, "${PYPACKAGE}", G.Pkgsrc.Latest("lang", `^python[0-9]+$`, "$0"), -1)
+
+       replaceLatest := func(varuse, category string, pattern regex.Pattern, replacement string) {
+               if contains(tmp, varuse) {
+                       latest := G.Pkgsrc.Latest(category, pattern, replacement)
+                       tmp = strings.Replace(tmp, varuse, latest, -1)
+               }
        }
+       replaceLatest("${LUA_PKGSRCDIR}", "lang", `^lua[0-9]+$`, "../../lang/$0")
+       replaceLatest("${PHPPKGSRCDIR}", "lang", `^php[0-9]+$`, "../../lang/$0")
+       replaceLatest("${SUSE_DIR_PREFIX}", "emulators", `^(suse[0-9]+)_base$`, "$1")
+       replaceLatest("${PYPKGSRCDIR}", "lang", `^python[0-9]+$`, "../../lang/$0")
+       replaceLatest("${PYPACKAGE}", "lang", `^python[0-9]+$`, "$0")
        if G.Pkg != nil {
+               // XXX: Even if these variables are defined indirectly,
+               // pkglint should be able to resolve them properly.
+               // There is already G.Pkg.Value, maybe that can be used here.
                tmp = strings.Replace(tmp, "${FILESDIR}", G.Pkg.Filesdir, -1)
                tmp = strings.Replace(tmp, "${PKGDIR}", G.Pkg.Pkgdir, -1)
        }
 
+       // TODO: What is this good for, and in which cases does it make a difference?
        if adjustDepth {
                if hasPrefix(tmp, "../../") && !hasPrefix(tmp[6:], ".") {
                        tmp = pkgsrcdir + "/" + tmp[6:]
@@ -409,15 +541,8 @@ func (mkline *MkLineImpl) ResolveVarsInR
        return tmp
 }
 
-func (ind *Indentation) RememberUsedVariables(cond MkCond) {
-       NewMkCondWalker().Walk(cond, &MkCondCallback{
-               VarUse: func(varuse *MkVarUse) {
-                       ind.AddVar(varuse.varname)
-               }})
-}
-
 func (mkline *MkLineImpl) ExplainRelativeDirs() {
-       Explain(
+       G.Explain(
                "Directories in the form \"../../category/package\" make it easier to",
                "move a package around in pkgsrc, for example from pkgsrc-wip to the",
                "main pkgsrc repository.")
@@ -432,26 +557,16 @@ func (mkline *MkLineImpl) RefTo(other Mk
        return mkline.Line.RefTo(other.Line)
 }
 
+var AlnumDash = textproc.NewByteSet("a-z---")
+
 func matchMkDirective(text string) (m bool, indent, directive, args, comment string) {
-       i, n := 0, len(text)
-       if i < n && text[i] == '.' {
-               i++
-       } else {
+       lexer := textproc.NewLexer(text)
+       if !lexer.SkipByte('.') {
                return
        }
 
-       indentStart := i
-       for i < n && (text[i] == ' ' || text[i] == '\t') {
-               i++
-       }
-       indentEnd := i
-
-       directiveStart := i
-       for i < n && ('a' <= text[i] && text[i] <= 'z' || text[i] == '-') {
-               i++
-       }
-       directiveEnd := i
-       directive = text[directiveStart:directiveEnd]
+       indent = lexer.NextHspace()
+       directive = lexer.NextBytesSet(AlnumDash)
        switch directive {
        case "if", "else", "elif", "endif",
                "ifdef", "ifndef",
@@ -464,83 +579,82 @@ func matchMkDirective(text string) (m bo
                return
        }
 
-       for i < n && (text[i] == ' ' || text[i] == '\t') {
-               i++
-       }
+       lexer.SkipHspace()
 
-       argsStart := i
-       for i < n && (text[i] != '#' || text[i-1] == '\\') {
-               i++
-       }
-       commentStart := i
-       if commentStart < n {
-               commentStart++
-               for commentStart < n && (text[commentStart] == ' ' || text[commentStart] == '\t') {
-                       commentStart++
+       argsStart := lexer.Mark()
+       for !lexer.EOF() && lexer.PeekByte() != '#' {
+               switch {
+               case lexer.SkipString("[#"):
+                       // See devel/bmake/files/parse.c:/as in modifier/
+
+               case lexer.PeekByte() == '\\' && len(lexer.Rest()) > 1:
+                       lexer.Skip(2)
+
+               default:
+                       lexer.Skip(1)
                }
        }
-       for i > argsStart && (text[i-1] == ' ' || text[i-1] == '\t') {
-               i--
+       args = lexer.Since(argsStart)
+       args = strings.TrimFunc(args, func(r rune) bool { return isHspace(byte(r)) })
+       args = strings.Replace(args, "\\#", "#", -1)
+
+       if !lexer.EOF() {
+               lexer.Skip(1)
+               lexer.SkipHspace()
+               comment = lexer.Rest()
        }
-       argsEnd := i
 
        m = true
-       indent = text[indentStart:indentEnd]
-       args = strings.Replace(text[argsStart:argsEnd], "\\#", "#", -1)
-       comment = text[commentStart:]
        return
 }
 
-type NeedsQuoting uint8
-
-const (
-       nqNo NeedsQuoting = iota
-       nqYes
-       nqDoesntMatter
-       nqDontKnow
-)
-
-func (nq NeedsQuoting) String() string {
-       return [...]string{"no", "yes", "doesn't matter", "don't know"}[nq]
-}
-
-func (mkline *MkLineImpl) VariableNeedsQuoting(varname string, vartype *Vartype, vuc *VarUseContext) (needsQuoting NeedsQuoting) {
+// VariableNeedsQuoting determines whether the given variable needs the :Q operator
+// in the given context.
+//
+// This decision depends on many factors, such as whether the type of the context is
+// a list of things, whether the variable is a list, whether it can contain only
+// safe characters, and so on.
+func (mkline *MkLineImpl) VariableNeedsQuoting(varname string, vartype *Vartype, vuc *VarUseContext) (needsQuoting YesNoUnknown) {
        if trace.Tracing {
                defer trace.Call(varname, vartype, vuc, trace.Result(&needsQuoting))()
        }
 
-       if vartype == nil || vuc.vartype == nil || vartype.basicType == BtUnknown {
-               return nqDontKnow
+       // TODO: Systematically test this function, each and every case, from top to bottom.
+       // TODO: Re-check the order of all these if clauses whether it really makes sense.
+
+       vucVartype := vuc.vartype
+       if vartype == nil || vucVartype == nil || vartype.basicType == BtUnknown {
+               return unknown
        }
 
-       if vartype.basicType.IsEnum() || vartype.IsBasicSafe() {
+       if !vartype.basicType.NeedsQ() {
                if vartype.kindOfList == lkNone {
                        if vartype.guessed {
-                               return nqDontKnow
+                               return unknown
                        }
-                       return nqDoesntMatter
+                       return no
                }
                if vartype.kindOfList == lkShell && !vuc.IsWordPart {
-                       return nqNo
+                       return no
                }
        }
 
        // In .for loops, the :Q operator is always misplaced, since
-       // the items are broken up at white-space, not as shell words
+       // the items are broken up at whitespace, not as shell words
        // like in all other parts of make(1).
        if vuc.quoting == vucQuotFor {
-               return nqNo
+               return no
        }
 
        // A shell word may appear as part of a shell word, for example COMPILER_RPATH_FLAG.
        if vuc.IsWordPart && vuc.quoting == vucQuotPlain {
                if vartype.kindOfList == lkNone && vartype.basicType == BtShellWord {
-                       return nqNo
+                       return no
                }
        }
 
        // Determine whether the context expects a list of shell words or not.
-       wantList := vuc.vartype.IsConsideredList()
+       wantList := vucVartype.IsConsideredList()
        haveList := vartype.IsConsideredList()
        if trace.Tracing {
                trace.Stepf("wantList=%v, haveList=%v", wantList, haveList)
@@ -551,70 +665,72 @@ func (mkline *MkLineImpl) VariableNeedsQ
        // 2. xargs ${PERL5}
        if !vuc.IsWordPart && vuc.quoting == vucQuotPlain {
                if wantList && haveList {
-                       return nqDontKnow
+                       return unknown
                }
        }
 
        // Pkglint assumes that the tool definitions don't include very
        // special characters, so they can safely be used inside any quotes.
-       if tool := G.ToolByVarname(varname, vuc.time.ToToolTime()); tool != nil {
+       if tool := G.ToolByVarname(varname); tool != nil {
                switch vuc.quoting {
                case vucQuotPlain:
                        if !vuc.IsWordPart {
-                               return nqNo
+                               return no
                        }
+                       // XXX: Should there be a return here? It looks as if it could have been forgotten.
                case vucQuotBackt:
-                       return nqNo
+                       return no
                case vucQuotDquot, vucQuotSquot:
-                       return nqDoesntMatter
+                       return unknown
                }
        }
 
-       // Variables that appear as parts of shell words generally need
-       // to be quoted. An exception is in the case of backticks,
-       // because the whole backticks expression is parsed as a single
-       // shell word by pkglint.
-       if vuc.IsWordPart && vuc.vartype != nil && vuc.vartype.IsShell() && vuc.quoting != vucQuotBackt {
-               return nqYes
+       // Variables that appear as parts of shell words generally need to be quoted.
+       //
+       // An exception is in the case of backticks, because the whole backticks expression
+       // is parsed as a single shell word by pkglint. (XXX: This comment may be outdated.)
+       if vuc.IsWordPart && vucVartype.IsShell() && vuc.quoting != vucQuotBackt {
+               return yes
        }
 
        // SUBST_MESSAGE.perl= Replacing in ${REPLACE_PERL}
-       if vuc.vartype != nil && vuc.vartype.IsPlainString() {
-               return nqNo
+       if vucVartype.IsPlainString() {
+               return no
        }
 
        if wantList != haveList {
-               if vuc.vartype != nil && vartype != nil {
-                       if vuc.vartype.basicType == BtFetchURL && vartype.basicType == BtHomepage {
-                               return nqNo
-                       }
-                       if vuc.vartype.basicType == BtHomepage && vartype.basicType == BtFetchURL {
-                               return nqNo // Just for HOMEPAGE=${MASTER_SITE_*:=subdir/}.
-                       }
+               if vucVartype.basicType == BtFetchURL && vartype.basicType == BtHomepage {
+                       return no
+               }
+               if vucVartype.basicType == BtHomepage && vartype.basicType == BtFetchURL {
+                       return no // Just for HOMEPAGE=${MASTER_SITE_*:=subdir/}.
                }
-               return nqYes
+               return yes
        }
 
-       // Bad: LDADD += -l${LIBS}
-       // Good: LDADD += ${LIBS:@lib@-l${lib} @}
+       // Bad: LDADD+= -l${LIBS}
+       // Good: LDADD+= ${LIBS:S,^,-l,}
        if wantList && haveList && vuc.IsWordPart {
-               return nqYes
+               return yes
        }
 
        if trace.Tracing {
                trace.Step1("Don't know whether :Q is needed for %q", varname)
        }
-       return nqDontKnow
+       return unknown
 }
 
 func (mkline *MkLineImpl) DetermineUsedVariables() []string {
+       // TODO: It would be good to have these variables as MkVarUse objects
+       // including the context in which they are used.
+
        var varnames []string
 
        add := func(varname string) {
                varnames = append(varnames, varname)
        }
 
-       var searchIn func(text string)
+       var searchIn func(text string) // mutually recursive with searchInVarUse
 
        searchInVarUse := func(varuse *MkVarUse) {
                varname := varuse.varname
@@ -642,15 +758,14 @@ func (mkline *MkLineImpl) DetermineUsedV
        switch {
 
        case mkline.IsVarassign():
+               searchIn(mkline.Varname())
                searchIn(mkline.Value())
 
        case mkline.IsDirective() && mkline.Directive() == "for":
                searchIn(mkline.Args())
 
        case mkline.IsDirective() && mkline.Cond() != nil:
-               NewMkCondWalker().Walk(
-                       mkline.Cond(),
-                       &MkCondCallback{VarUse: searchInVarUse})
+               mkline.Cond().Walk(&MkCondCallback{VarUse: searchInVarUse})
 
        case mkline.IsShellCommand():
                searchIn(mkline.ShellCommand())
@@ -660,7 +775,7 @@ func (mkline *MkLineImpl) DetermineUsedV
                searchIn(mkline.Sources())
 
        case mkline.IsInclude():
-               searchIn(mkline.IncludeFile())
+               searchIn(mkline.IncludedFile())
        }
 
        return varnames
@@ -716,9 +831,12 @@ type VarUseContext struct {
        vartype    *Vartype
        time       vucTime
        quoting    vucQuoting
-       IsWordPart bool // Example: echo ${LOCALBASE} LOCALBASE=${LOCALBASE}
+       IsWordPart bool // Example: LOCALBASE=${LOCALBASE}
 }
 
+// vucTime is the time at which a variable is used.
+//
+// See ToolTime, which is the same except that there is no unknown.
 type vucTime uint8
 
 const (
@@ -732,20 +850,19 @@ const (
 
        // All files have been read, all variables can be referenced.
        // Variable values don't change anymore.
+       //
+       // Well, except for the ::= modifier.
+       // But that modifier is usually not used in pkgsrc.
        vucTimeRun
 )
 
 func (t vucTime) String() string { return [...]string{"unknown", "parse", "run"}[t] }
 
-func (t vucTime) ToToolTime() ToolTime {
-       if t == vucTimeParse {
-               return LoadTime
-       }
-       return RunTime
-}
-
 // The quoting context in which the variable is used.
 // Depending on this context, the modifiers :Q or :M can be allowed or not.
+//
+// The shell tokenizer knows multi-level quoting modes (see ShQuoting),
+// but for deciding whether :Q is necessary or not, a single level is enough.
 type vucQuoting uint8
 
 const (
@@ -753,7 +870,7 @@ const (
        vucQuotPlain              // Example: echo LOCALBASE=${LOCALBASE}
        vucQuotDquot              // Example: echo "The version is ${PKGVERSION}."
        vucQuotSquot              // Example: echo 'The version is ${PKGVERSION}.'
-       vucQuotBackt              // Example: echo \`sed 1q ${WRKSRC}/README\`
+       vucQuotBackt              // Example: echo `sed 1q ${WRKSRC}/README`
 
        // The .for loop in Makefiles. This is the only place where
        // variables are split on whitespace. Everywhere else (:Q, :M)
@@ -784,31 +901,38 @@ type Indentation struct {
 }
 
 func NewIndentation() *Indentation {
-       ind := &Indentation{}
+       ind := Indentation{}
        ind.Push(nil, 0, "") // Dummy
-       return ind
+       return &ind
 }
 
 func (ind *Indentation) String() string {
-       s := ""
+       var s strings.Builder
        for _, level := range ind.levels[1:] {
-               s += fmt.Sprintf(" %d", level.depth)
+               _, _ = fmt.Fprintf(&s, " %d", level.depth)
                if len(level.conditionalVars) > 0 {
-                       s += " (" + strings.Join(level.conditionalVars, " ") + ")"
+                       _, _ = fmt.Fprintf(&s, " (%s)", strings.Join(level.conditionalVars, " "))
                }
        }
-       return "[" + trimHspace(s) + "]"
+       return "[" + trimHspace(s.String()) + "]"
+}
+
+func (ind *Indentation) RememberUsedVariables(cond MkCond) {
+       cond.Walk(&MkCondCallback{
+               VarUse: func(varuse *MkVarUse) { ind.AddVar(varuse.varname) }})
 }
 
 type indentationLevel struct {
        mkline          MkLine   // The line in which the indentation started; the .if/.for
        depth           int      // Number of space characters; always a multiple of 2
-       condition       string   // The corresponding condition from the .if or latest .elif
+       args            string   // The arguments from the .if or .for, or the latest .elif
        conditionalVars []string // Variables on which the current path depends
 
-       // Files whose existence has been checked in a related path.
-       // The check counts for both the "if" and the "else" branch,
-       // but that sloppiness will be discovered by functional tests.
+       // Files whose existence has been checked in an if branch that is
+       // related to the current indentation. After a .if exists(fname),
+       // pkglint will happily accept .include "fname" in both the then and
+       // the else branch. This is ok since the primary job of this file list
+       // is to prevent wrong pkglint warnings about missing files.
        checkedFiles []string
 }
 
@@ -822,6 +946,9 @@ func (ind *Indentation) top() *indentati
 
 // Depth returns the number of space characters by which the directive
 // should be indented.
+//
+// This is typically two more than the surrounding level, except for
+// multiple-inclusion guards.
 func (ind *Indentation) Depth(directive string) int {
        switch directive {
        case "if", "elif", "else", "endfor", "endif":
@@ -838,7 +965,15 @@ func (ind *Indentation) Push(mkline MkLi
        ind.levels = append(ind.levels, indentationLevel{mkline, indent, condition, nil, nil})
 }
 
+// AddVar remembers that the current indentation depends on the given variable,
+// most probably because that variable is used in a .if directive.
+//
+// Variables named *_MK are ignored since they are usually not interesting.
 func (ind *Indentation) AddVar(varname string) {
+       if hasSuffix(varname, "_MK") {
+               return
+       }
+
        vars := &ind.top().conditionalVars
        for _, existingVarname := range *vars {
                if varname == existingVarname {
@@ -862,12 +997,12 @@ func (ind *Indentation) DependsOn(varnam
 
 // IsConditional returns whether the current line depends on evaluating
 // any variable in an .if or .elif expression or from a .for loop.
+//
+// Variables named *_MK are excluded since they are usually not interesting.
 func (ind *Indentation) IsConditional() bool {
        for _, level := range ind.levels {
-               for _, varname := range level.conditionalVars {
-                       if !hasSuffix(varname, "_MK") {
-                               return true
-                       }
+               if len(level.conditionalVars) > 0 {
+                       return true
                }
        }
        return false
@@ -875,35 +1010,35 @@ func (ind *Indentation) IsConditional() 
 
 // Varnames returns the list of all variables that are mentioned in any
 // condition or loop surrounding the current line.
+//
 // Variables named *_MK are excluded since they are usually not interesting.
-func (ind *Indentation) Varnames() string {
-       sep := ""
-       varnames := ""
+func (ind *Indentation) Varnames() []string {
+       var varnames []string
        for _, level := range ind.levels {
                for _, levelVarname := range level.conditionalVars {
-                       if !hasSuffix(levelVarname, "_MK") {
-                               varnames += sep + levelVarname
-                               sep = ", "
-                       }
+                       G.Assertf(
+                               !hasSuffix(levelVarname, "_MK"),
+                               "multiple-inclusion guard must be filtered out earlier.")
+                       varnames = append(varnames, levelVarname)
                }
        }
        return varnames
 }
 
-// Condition returns the condition of the innermost .if, .elif or .for.
-func (ind *Indentation) Condition() string {
-       return ind.top().condition
+// Args returns the arguments of the innermost .if, .elif or .for.
+func (ind *Indentation) Args() string {
+       return ind.top().args
 }
 
-func (ind *Indentation) AddCheckedFile(fileName string) {
+func (ind *Indentation) AddCheckedFile(filename string) {
        top := ind.top()
-       top.checkedFiles = append(top.checkedFiles, fileName)
+       top.checkedFiles = append(top.checkedFiles, filename)
 }
 
-func (ind *Indentation) IsCheckedFile(fileName string) bool {
+func (ind *Indentation) IsCheckedFile(filename string) bool {
        for _, level := range ind.levels {
                for _, levelFilename := range level.checkedFiles {
-                       if fileName == levelFilename {
+                       if filename == levelFilename {
                                return true
                        }
                }
@@ -935,23 +1070,18 @@ func (ind *Indentation) TrackAfter(mklin
 
        switch directive {
        case "if":
+               cond := mkline.Cond()
+
                // For multiple-inclusion guards, the indentation stays at the same level.
-               guard := false
-               if hasPrefix(args, "!defined(") && hasSuffix(args, "_MK)") {
-                       varname := args[9 : len(args)-1]
-                       if varname != "" && isalnum(varname) {
-                               ind.AddVar(varname)
-                               guard = true
-                       }
-               }
+               guard := cond != nil && cond.Not != nil && hasSuffix(cond.Not.Defined, "_MK")
                if !guard {
                        ind.top().depth += 2
                }
 
-               if cond := mkline.Cond(); cond != nil {
+               if cond != nil {
                        ind.RememberUsedVariables(cond)
 
-                       NewMkCondWalker().Walk(cond, &MkCondCallback{
+                       cond.Walk(&MkCondCallback{
                                Call: func(name string, arg string) {
                                        if name == "exists" {
                                                ind.AddCheckedFile(arg)
@@ -964,7 +1094,7 @@ func (ind *Indentation) TrackAfter(mklin
 
        case "elif":
                // Handled here instead of TrackBefore to allow the action to access the previous condition.
-               ind.top().condition = args
+               ind.top().args = args
 
        case "else":
                top := ind.top()
@@ -983,11 +1113,11 @@ func (ind *Indentation) TrackAfter(mklin
        }
 }
 
-func (ind *Indentation) CheckFinish(fileName string) {
+func (ind *Indentation) CheckFinish(filename string) {
        if ind.Len() <= 1 {
                return
        }
-       eofLine := NewLineEOF(fileName)
+       eofLine := NewLineEOF(filename)
        for ind.Len() > 1 {
                openingMkline := ind.levels[ind.Len()-1].mkline
                eofLine.Errorf(".%s from %s must be closed.", openingMkline.Directive(), eofLine.RefTo(openingMkline.Line))
@@ -995,113 +1125,88 @@ func (ind *Indentation) CheckFinish(file
        }
 }
 
+// VarnameBytes contains characters that may be used in variable names.
+// The bracket is included here for the tool of the same name, e.g. "TOOLS_PATH.[".
+var VarnameBytes = textproc.NewByteSet("A-Za-z_0-9*+---.[")
+
 func MatchVarassign(text string) (m, commented bool, varname, spaceAfterVarname, op, valueAlign, value, spaceAfterValue, comment string) {
-       i, n := 0, len(text)
+       lexer := textproc.NewLexer(text)
 
-       if i < n && text[i] == '#' {
-               commented = true
-               i++
-       } else {
-               for i < n && text[i] == ' ' {
-                       i++
-               }
+       commented = lexer.SkipByte('#')
+       for !commented && lexer.SkipByte(' ') {
        }
 
-       varnameStart := i
-       for ; i < n; i++ {
-               b := text[i]
+       varnameStart := lexer.Mark()
+       for !lexer.EOF() {
                switch {
 
-               // As of go1.11.1 (October 2018), the Go compiler doesn't emit good
-               // code for these kinds of comparisons.
-               // See https://github.com/golang/go/issues/17372.
-               case 'A' <= b && b <= 'Z',
-                       'a' <= b && b <= 'z',
-                       b == '_',
-                       '0' <= b && b <= '9',
-                       '*' <= b && b <= '.' && (b == '*' || b == '+' || b == '-' || b == '.'),
-                       b == '[': // For the tool of the same name, e.g. "TOOLS_PATH.[".
+               case lexer.NextBytesSet(VarnameBytes) != "":
                        continue
 
-               case b == '$':
-                       parser := NewMkParser(nil, text[i:], false)
+               case lexer.PeekByte() == '$':
+                       parser := NewMkParser(nil, lexer.Rest(), false)
                        varuse := parser.VarUse()
                        if varuse == nil {
                                return
                        }
-                       varuseLen := len(text[i:]) - len(parser.Rest())
-                       i += varuseLen - 1
+                       varuseLen := len(lexer.Rest()) - len(parser.Rest())
+                       lexer.Skip(varuseLen)
                        continue
                }
                break
        }
-       varnameEnd := i
+       varname = lexer.Since(varnameStart)
 
-       if varnameEnd == varnameStart {
+       if varname == "" {
                return
        }
 
-       for i < n && (text[i] == ' ' || text[i] == '\t') {
-               i++
-       }
+       spaceAfterVarname = lexer.NextHspace()
 
-       opStart := i
-       if i < n {
-               if b := text[i]; b == '!' || b == '+' || b == ':' || b == '?' {
-                       i++
-               }
+       opStart := lexer.Mark()
+       switch lexer.PeekByte() {
+       case '!', '+', ':', '?':
+               lexer.Skip(1)
        }
-       if i < n && text[i] == '=' {
-               i++
-       } else {
+       if !lexer.SkipByte('=') {
                return
        }
-       opEnd := i
+       op = lexer.Since(opStart)
 
-       if text[varnameEnd-1] == '+' && varnameEnd == opStart && text[opStart] == '=' {
-               varnameEnd--
-               opStart--
+       if hasSuffix(varname, "+") && op == "=" && spaceAfterVarname == "" {
+               varname = varname[:len(varname)-1]
+               op = "+="
        }
 
-       for i < n && (text[i] == ' ' || text[i] == '\t') {
-               i++
-       }
+       lexer.SkipHspace()
 
-       valueStart := i
-       valuebuf := make([]byte, n-valueStart)
-       j := 0
-       for ; i < n; i++ {
-               b := text[i]
-               if b == '#' && (i == valueStart || text[i-1] != '\\') {
+       valueAlign = text[:len(text)-len(lexer.Rest())]
+       valueStart := lexer.Mark()
+       // FIXME: This is the same code as in matchMkDirective.
+       for !lexer.EOF() && lexer.PeekByte() != '#' {
+               switch {
+               case lexer.SkipString("[#"):
                        break
-               } else if b != '\\' || i+1 >= n || text[i+1] != '#' {
-                       valuebuf[j] = b
-                       j++
-               }
-       }
 
-       commentStart := i
-       for text[i-1] == ' ' || text[i-1] == '\t' {
-               i--
-       }
-       valueEnd := i
+               case lexer.PeekByte() == '\\' && len(lexer.Rest()) > 1:
+                       lexer.Skip(2)
 
-       commentEnd := n
+               default:
+                       lexer.Skip(1)
+               }
+       }
+       rawValueWithSpace := lexer.Since(valueStart)
+       spaceAfterValue = rawValueWithSpace[len(strings.TrimRight(rawValueWithSpace, " \t")):]
+       value = trimHspace(strings.Replace(lexer.Since(valueStart), "\\#", "#", -1))
+       comment = lexer.Rest()
 
        m = true
-       varname = text[varnameStart:varnameEnd]
-       spaceAfterVarname = text[varnameEnd:opStart]
-       op = text[opStart:opEnd]
-       valueAlign = text[0:valueStart]
-       value = trimHspace(string(valuebuf[:j]))
-       spaceAfterValue = text[valueEnd:commentStart]
-       comment = text[commentStart:commentEnd]
        return
 }
 
-func MatchMkInclude(text string) (m bool, indentation, directive, fileName string) {
+func MatchMkInclude(text string) (m bool, indentation, directive, filename string) {
        lexer := textproc.NewLexer(text)
-       if lexer.NextString(".") != "" {
+       if lexer.SkipByte('.') {
                indentation = lexer.NextHspace()
                directive = lexer.NextString("include")
                if directive == "" {
@@ -1109,14 +1214,14 @@ func MatchMkInclude(text string) (m bool
                }
                if directive != "" {
                        lexer.NextHspace()
-                       if lexer.NextByte('"') {
+                       if lexer.SkipByte('"') {
                                // Note: strictly speaking, the full MkVarUse would have to be parsed
                                // here. But since these usually don't contain double quotes, it has
                                // worked fine up to now.
-                               fileName = lexer.NextBytesFunc(func(c byte) bool { return c != '"' })
-                               if fileName != "" && lexer.NextByte('"') {
+                               filename = lexer.NextBytesFunc(func(c byte) bool { return c != '"' })
+                               if filename != "" && lexer.SkipByte('"') {
                                        lexer.NextHspace()
-                                       if lexer.EOF() || lexer.NextByte('#') {
+                                       if lexer.EOF() || lexer.SkipByte('#') {
                                                m = true
                                                return
                                        }
Index: pkgsrc/pkgtools/pkglint/files/pkglint.go
diff -u pkgsrc/pkgtools/pkglint/files/pkglint.go:1.40 pkgsrc/pkgtools/pkglint/files/pkglint.go:1.41
--- pkgsrc/pkgtools/pkglint/files/pkglint.go:1.40       Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/pkglint.go    Sun Dec  2 01:57:48 2018
@@ -5,12 +5,12 @@ import (
        "netbsd.org/pkglint/getopt"
        "netbsd.org/pkglint/histogram"
        "netbsd.org/pkglint/regex"
-       "netbsd.org/pkglint/textproc"
        tracePkg "netbsd.org/pkglint/trace"
        "os"
        "os/user"
        "path"
        "path/filepath"
+       "runtime"
        "runtime/pprof"
        "strings"
 )
@@ -18,19 +18,12 @@ import (
 const confMake = "@BMAKE@"
 const confVersion = "@VERSION@"
 
-// Pkglint contains all global variables of this Go package.
-// The rest of the global state is in the other packages:
-//  regex.Profiling    (not thread-local)
-//  regex.res          (and related variables; not thread-safe)
-//  textproc.Testing   (not thread-local; harmless)
-//  tracing.Tracing    (not thread-safe)
-//  tracing.Out        (not thread-safe)
-//  tracing.traceDepth (not thread-safe)
+// Pkglint is a container for all global variables of this Go package.
 type Pkglint struct {
        Opts   CmdOpts  // Command line options.
-       Pkgsrc *Pkgsrc  // Global data, mostly extracted from mk/*.
-       Pkg    *Package // The package that is currently checked.
-       Mk     MkLines  // The Makefile (or fragment) that is currently checked.
+       Pkgsrc *Pkgsrc  // Global data, mostly extracted from mk/*, never nil.
+       Pkg    *Package // The package that is currently checked, or nil.
+       Mk     MkLines  // The Makefile (or fragment) that is currently checked, or nil.
 
        Todo            []string // The files or directories that still need to be checked.
        Wip             bool     // Is the currently checked item from pkgsrc-wip?
@@ -40,17 +33,8 @@ type Pkglint struct {
        CvsEntriesDir   string   // Cached to avoid I/O
        CvsEntriesLines Lines
 
-       errors                int
-       warnings              int
-       explainNext           bool
-       logged                map[string]bool
-       explanationsAvailable bool
-       explanationsGiven     map[string]bool
-       autofixAvailable      bool
-       logOut                *SeparatorWriter
-       logErr                *SeparatorWriter
+       Logger
 
-       loghisto  *histogram.Histogram
        loaded    *histogram.Histogram
        res       regex.Registry
        fileCache *FileCache
@@ -89,18 +73,11 @@ type CmdOpts struct {
        WarnStyle,
        WarnTypes bool
 
-       Explain,
-       Autofix,
-       GccOutput,
+       Profiling,
        ShowHelp,
        DumpMakefile,
        Import,
-       LogVerbose,
-       Profiling,
-       Quiet,
        Recursive,
-       ShowAutofix,
-       ShowSource,
        ShowVersion bool
 
        LogOnly []string
@@ -113,6 +90,8 @@ type Hash struct {
        line Line
 }
 
+type pkglintFatal struct{}
+
 // G is the abbreviation for "global state";
 // these are the only global variable in this Go package
 var (
@@ -122,10 +101,15 @@ var (
 )
 
 func main() {
-       G.logOut = NewSeparatorWriter(os.Stdout)
-       G.logErr = NewSeparatorWriter(os.Stderr)
+       G.out = NewSeparatorWriter(os.Stdout)
+       G.err = NewSeparatorWriter(os.Stderr)
        trace.Out = os.Stdout
-       exit(G.Main(os.Args...))
+       exitcode := G.Main(os.Args...)
+       if G.Opts.Profiling {
+               G = Pkglint{} // Free all memory.
+               runtime.GC()  // Detect possible memory leaks.
+       }
+       exit(exitcode)
 }
 
 // Main runs the main program with the given arguments.
@@ -158,18 +142,19 @@ func (pkglint *Pkglint) Main(argv ...str
                }
                defer f.Close()
 
-               pprof.StartCPUProfile(f)
+               err = pprof.StartCPUProfile(f)
+               G.Assertf(err == nil, "Cannot start profiling: %s", err)
                defer pprof.StopCPUProfile()
 
                pkglint.res.Profiling()
-               pkglint.loghisto = histogram.New()
+               pkglint.histo = histogram.New()
                pkglint.loaded = histogram.New()
                defer func() {
-                       pkglint.logOut.Write("")
-                       pkglint.loghisto.PrintStats(pkglint.logOut.out, "loghisto", -1)
-                       G.res.PrintStats(pkglint.logOut.out)
-                       pkglint.loaded.PrintStats(pkglint.logOut.out, "loaded", 10)
-                       pkglint.logOut.WriteLine(fmt.Sprintf("fileCache: %d hits, %d misses", pkglint.fileCache.hits, pkglint.fileCache.misses))
+                       pkglint.out.Write("")
+                       pkglint.histo.PrintStats(pkglint.out.out, "loghisto", -1)
+                       pkglint.res.PrintStats(pkglint.out.out)
+                       pkglint.loaded.PrintStats(pkglint.out.out, "loaded", 10)
+                       pkglint.out.WriteLine(fmt.Sprintf("fileCache: %d hits, %d misses", pkglint.fileCache.hits, pkglint.fileCache.misses))
                }()
        }
 
@@ -180,7 +165,7 @@ func (pkglint *Pkglint) Main(argv ...str
                pkglint.Todo = []string{"."}
        }
 
-       firstArg := G.Todo[0]
+       firstArg := pkglint.Todo[0]
        if fileExists(firstArg) {
                firstArg = path.Dir(firstArg)
        }
@@ -219,23 +204,24 @@ func (pkglint *Pkglint) Main(argv ...str
 
 func (pkglint *Pkglint) ParseCommandLine(args []string) *int {
        gopts := &pkglint.Opts
+       lopts := &pkglint.Logger.Opts
        opts := getopt.NewOptions()
 
        check := opts.AddFlagGroup('C', "check", "check,...", "enable or disable specific checks")
        opts.AddFlagVar('d', "debug", &trace.Tracing, false, "log verbose call traces for debugging")
-       opts.AddFlagVar('e', "explain", &gopts.Explain, false, "explain the diagnostics or give further help")
-       opts.AddFlagVar('f', "show-autofix", &gopts.ShowAutofix, false, "show what pkglint can fix automatically")
-       opts.AddFlagVar('F', "autofix", &gopts.Autofix, false, "try to automatically fix some errors")
-       opts.AddFlagVar('g', "gcc-output-format", &gopts.GccOutput, false, "mimic the gcc output format")
+       opts.AddFlagVar('e', "explain", &lopts.Explain, false, "explain the diagnostics or give further help")
+       opts.AddFlagVar('f', "show-autofix", &lopts.ShowAutofix, false, "show what pkglint can fix automatically")
+       opts.AddFlagVar('F', "autofix", &lopts.Autofix, false, "try to automatically fix some errors")
+       opts.AddFlagVar('g', "gcc-output-format", &lopts.GccOutput, false, "mimic the gcc output format")
        opts.AddFlagVar('h', "help", &gopts.ShowHelp, false, "show a detailed usage message")
        opts.AddFlagVar('I', "dumpmakefile", &gopts.DumpMakefile, false, "dump the Makefile after parsing")
        opts.AddFlagVar('i', "import", &gopts.Import, false, "prepare the import of a wip package")
-       opts.AddFlagVar('m', "log-verbose", &gopts.LogVerbose, false, "allow the same log message more than once")
-       opts.AddStrList('o', "only", &gopts.LogOnly, "only log messages containing the given text")
+       opts.AddFlagVar('m', "log-verbose", &lopts.LogVerbose, false, "allow the same diagnostic more than once")
+       opts.AddStrList('o', "only", &gopts.LogOnly, "only log diagnostics containing the given text")
        opts.AddFlagVar('p', "profiling", &gopts.Profiling, false, "profile the executing program")
-       opts.AddFlagVar('q', "quiet", &gopts.Quiet, false, "don't show a summary line when finishing")
+       opts.AddFlagVar('q', "quiet", &lopts.Quiet, false, "don't show a summary line when finishing")
        opts.AddFlagVar('r', "recursive", &gopts.Recursive, false, "check subdirectories, too")
-       opts.AddFlagVar('s', "source", &gopts.ShowSource, false, "show the source lines together with diagnostics")
+       opts.AddFlagVar('s', "source", &lopts.ShowSource, false, "show the source lines together with diagnostics")
        opts.AddFlagVar('V', "version", &gopts.ShowVersion, false, "show the version number of pkglint")
        warn := opts.AddFlagGroup('W', "warning", "warning,...", "enable or disable groups of warnings")
 
@@ -253,7 +239,7 @@ func (pkglint *Pkglint) ParseCommandLine
        check.AddFlagVar("patches", &gopts.CheckPatches, true, "check patches")
        check.AddFlagVar("PLIST", &gopts.CheckPlist, true, "check PLIST files")
 
-       warn.AddFlagVar("absname", &gopts.WarnAbsname, true, "warn about use of absolute file names")
+       warn.AddFlagVar("absname", &gopts.WarnAbsname, false, "warn about use of absolute filenames")
        warn.AddFlagVar("directcmd", &gopts.WarnDirectcmd, true, "warn about use of direct command names instead of Make variables")
        warn.AddFlagVar("extra", &gopts.WarnExtra, false, "enable some extra warnings")
        warn.AddFlagVar("order", &gopts.WarnOrder, true, "warn if Makefile entries are unordered")
@@ -261,27 +247,27 @@ func (pkglint *Pkglint) ParseCommandLine
        warn.AddFlagVar("plist-depr", &gopts.WarnPlistDepr, false, "warn about deprecated paths in PLISTs")
        warn.AddFlagVar("plist-sort", &gopts.WarnPlistSort, false, "warn about unsorted entries in PLISTs")
        warn.AddFlagVar("quoting", &gopts.WarnQuoting, false, "warn about quoting issues")
-       warn.AddFlagVar("space", &gopts.WarnSpace, false, "warn about inconsistent use of white-space")
+       warn.AddFlagVar("space", &gopts.WarnSpace, false, "warn about inconsistent use of whitespace")
        warn.AddFlagVar("style", &gopts.WarnStyle, false, "warn about stylistic issues")
        warn.AddFlagVar("types", &gopts.WarnTypes, true, "do some simple type checking in Makefiles")
 
        remainingArgs, err := opts.Parse(args)
        if err != nil {
-               fmt.Fprintf(pkglint.logErr.out, "%s\n\n", err)
-               opts.Help(pkglint.logErr.out, "pkglint [options] dir...")
+               _, _ = fmt.Fprintf(pkglint.err.out, "%s\n\n", err)
+               opts.Help(pkglint.err.out, "pkglint [options] dir...")
                exitcode := 1
                return &exitcode
        }
        gopts.args = remainingArgs
 
        if gopts.ShowHelp {
-               opts.Help(pkglint.logOut.out, "pkglint [options] dir...")
+               opts.Help(pkglint.out.out, "pkglint [options] dir...")
                exitcode := 0
                return &exitcode
        }
 
        if pkglint.Opts.ShowVersion {
-               _, _ = fmt.Fprintf(pkglint.logOut.out, "%s\n", confVersion)
+               _, _ = fmt.Fprintf(pkglint.out.out, "%s\n", confVersion)
                exitcode := 0
                return &exitcode
        }
@@ -289,55 +275,34 @@ func (pkglint *Pkglint) ParseCommandLine
        return nil
 }
 
-func (pkglint *Pkglint) ShowSummary() {
-       if !pkglint.Opts.Quiet && !pkglint.Opts.Autofix {
-               if pkglint.errors != 0 || pkglint.warnings != 0 {
-                       pkglint.logOut.Printf("%d %s and %d %s found.\n",
-                               pkglint.errors, ifelseStr(pkglint.errors == 1, "error", "errors"),
-                               pkglint.warnings, ifelseStr(pkglint.warnings == 1, "warning", "warnings"))
-               } else {
-                       pkglint.logOut.WriteLine("Looks fine.")
-               }
-               if pkglint.explanationsAvailable && !pkglint.Opts.Explain {
-                       pkglint.logOut.WriteLine("(Run \"pkglint -e\" to show explanations.)")
-               }
-               if pkglint.autofixAvailable && !pkglint.Opts.ShowAutofix {
-                       pkglint.logOut.WriteLine("(Run \"pkglint -fs\" to show what can be fixed automatically.)")
-               }
-               if pkglint.autofixAvailable && !pkglint.Opts.Autofix {
-                       pkglint.logOut.WriteLine("(Run \"pkglint -F\" to automatically fix some issues.)")
-               }
-       }
-}
-
-func (pkglint *Pkglint) CheckDirent(fileName string) {
+func (pkglint *Pkglint) CheckDirent(filename string) {
        if trace.Tracing {
-               defer trace.Call1(fileName)()
+               defer trace.Call1(filename)()
        }
 
-       st, err := os.Lstat(fileName)
+       st, err := os.Lstat(filename)
        if err != nil || !st.Mode().IsDir() && !st.Mode().IsRegular() {
-               NewLineWhole(fileName).Errorf("No such file or directory.")
+               NewLineWhole(filename).Errorf("No such file or directory.")
                return
        }
        isDir := st.Mode().IsDir()
        isReg := st.Mode().IsRegular()
 
-       dir := ifelseStr(isReg, path.Dir(fileName), fileName)
-       pkgsrcRel := G.Pkgsrc.ToRel(dir)
+       dir := ifelseStr(isReg, path.Dir(filename), filename)
+       pkgsrcRel := pkglint.Pkgsrc.ToRel(dir)
        pkglint.Wip = matches(pkgsrcRel, `^wip(/|$)`)
        pkglint.Infrastructure = matches(pkgsrcRel, `^mk(/|$)`)
        pkgsrcdir := findPkgsrcTopdir(dir)
        if pkgsrcdir == "" {
-               NewLineWhole(fileName).Errorf("Cannot determine the pkgsrc root directory for %q.", cleanpath(dir))
+               NewLineWhole(filename).Errorf("Cannot determine the pkgsrc root directory for %q.", cleanpath(dir))
                return
        }
 
        switch {
-       case isDir && isEmptyDir(fileName):
+       case isDir && isEmptyDir(filename):
                return
        case isReg:
-               pkglint.Checkfile(fileName)
+               pkglint.Checkfile(filename)
                return
        }
 
@@ -349,7 +314,7 @@ func (pkglint *Pkglint) CheckDirent(file
        case ".":
                CheckdirToplevel(dir)
        default:
-               NewLineWhole(fileName).Errorf("Cannot check directories outside a pkgsrc tree.")
+               NewLineWhole(filename).Errorf("Cannot check directories outside a pkgsrc tree.")
        }
 }
 
@@ -360,11 +325,12 @@ func (pkglint *Pkglint) checkdirPackage(
                defer trace.Call1(dir)()
        }
 
-       G.Pkg = NewPackage(dir)
-       defer func() { G.Pkg = nil }()
-       pkg := G.Pkg
+       pkglint.Pkg = NewPackage(dir)
+       defer func() { pkglint.Pkg = nil }()
+       pkg := pkglint.Pkg
 
-       // we need to handle the Makefile first to get some variables
+       // Load the package Makefile and all included files,
+       // to collect all used and defined variables and similar data.
        mklines := pkg.loadPackageMakefile()
        if mklines == nil {
                return
@@ -374,7 +340,7 @@ func (pkglint *Pkglint) checkdirPackage(
        if pkg.Pkgdir != "." {
                files = append(files, dirglob(pkg.File(pkg.Pkgdir))...)
        }
-       if G.Opts.CheckExtra {
+       if pkglint.Opts.CheckExtra {
                files = append(files, dirglob(pkg.File(pkg.Filesdir))...)
        }
        files = append(files, dirglob(pkg.File(pkg.Patchdir))...)
@@ -386,48 +352,48 @@ func (pkglint *Pkglint) checkdirPackage(
        havePatches := false
 
        // Determine the used variables and PLIST directories before checking any of the Makefile fragments.
-       for _, fileName := range files {
-               basename := path.Base(fileName)
-               if (hasPrefix(basename, "Makefile.") || hasSuffix(fileName, ".mk")) &&
-                       !matches(fileName, `patch-`) &&
-                       !contains(fileName, pkg.Pkgdir+"/") &&
-                       !contains(fileName, pkg.Filesdir+"/") {
-                       if fragmentMklines := LoadMk(fileName, MustSucceed); fragmentMklines != nil {
+       for _, filename := range files {
+               basename := path.Base(filename)
+               if (hasPrefix(basename, "Makefile.") || hasSuffix(filename, ".mk")) &&
+                       !matches(filename, `patch-`) &&
+                       !contains(filename, pkg.Pkgdir+"/") &&
+                       !contains(filename, pkg.Filesdir+"/") {
+                       if fragmentMklines := LoadMk(filename, MustSucceed); fragmentMklines != nil {
                                fragmentMklines.DetermineUsedVariables()
                        }
                }
                if hasPrefix(basename, "PLIST") {
-                       pkg.loadPlistDirs(fileName)
+                       pkg.loadPlistDirs(filename)
                }
        }
 
-       for _, fileName := range files {
-               if containsVarRef(fileName) {
+       for _, filename := range files {
+               if containsVarRef(filename) {
                        if trace.Tracing {
-                               trace.Stepf("Skipping file %q because the name contains an unresolved variable.", fileName)
+                               trace.Stepf("Skipping file %q because the name contains an unresolved variable.", filename)
                        }
                        continue
                }
 
-               if path.Base(fileName) == "Makefile" {
-                       if st, err := os.Lstat(fileName); err == nil {
-                               pkglint.checkExecutable(fileName, st)
+               if path.Base(filename) == "Makefile" {
+                       if st, err := os.Lstat(filename); err == nil {
+                               pkglint.checkExecutable(filename, st)
                        }
-                       if G.Opts.CheckMakefile {
-                               pkg.checkfilePackageMakefile(fileName, mklines)
+                       if pkglint.Opts.CheckMakefile {
+                               pkg.checkfilePackageMakefile(filename, mklines)
                        }
                } else {
-                       pkglint.Checkfile(fileName)
+                       pkglint.Checkfile(filename)
                }
-               if contains(fileName, "/patches/patch-") {
+               if contains(filename, "/patches/patch-") {
                        havePatches = true
-               } else if hasSuffix(fileName, "/distinfo") {
+               } else if hasSuffix(filename, "/distinfo") {
                        haveDistinfo = true
                }
-               pkg.checkLocallyModified(fileName)
+               pkg.checkLocallyModified(filename)
        }
 
-       if pkg.Pkgdir == "." && G.Opts.CheckDistinfo && G.Opts.CheckPatches {
+       if pkg.Pkgdir == "." && pkglint.Opts.CheckDistinfo && pkglint.Opts.CheckPatches {
                if havePatches && !haveDistinfo {
                        NewLineWhole(pkg.File(pkg.DistinfoFile)).Warnf("File not found. Please run %q.", bmake("makepatchsum"))
                }
@@ -445,14 +411,10 @@ func (pkglint *Pkglint) Assertf(cond boo
        }
 }
 
-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(fileName string) string {
+func findPkgsrcTopdir(filename string) string {
        for _, dir := range [...]string{".", "..", "../..", "../../.."} {
-               if fileExists(fileName + "/" + dir + "/mk/bsd.pkg.mk") {
+               if fileExists(filename + "/" + dir + "/mk/bsd.pkg.mk") {
                        return dir
                }
        }
@@ -486,6 +448,7 @@ func resolveVariableRefs(text string) (r
 
        str := text
        for {
+               // TODO: Replace regular expression with full parser.
                replaced := replaceAllFunc(str, `\$\{([\w.]+)\}`, replacer)
                if replaced == str {
                        if trace.Tracing && str != text {
@@ -497,12 +460,12 @@ func resolveVariableRefs(text string) (r
        }
 }
 
-func CheckfileExtra(fileName string) {
+func CheckfileExtra(filename string) {
        if trace.Tracing {
-               defer trace.Call1(fileName)()
+               defer trace.Call1(filename)()
        }
 
-       if lines := Load(fileName, NotEmpty|LogErrors); lines != nil {
+       if lines := Load(filename, NotEmpty|LogErrors); lines != nil {
                ChecklinesTrailingEmptyLines(lines)
        }
 }
@@ -513,9 +476,10 @@ func ChecklinesDescr(lines Lines) {
        }
 
        for _, line := range lines.Lines {
-               CheckLineLength(line, 80)
-               CheckLineTrailingWhitespace(line)
-               CheckLineValidCharacters(line)
+               ck := LineChecker{line}
+               ck.CheckLength(80)
+               ck.CheckTrailingWhitespace()
+               ck.CheckValidCharacters()
                if contains(line.Text, "${") {
                        line.Notef("Variables are not expanded in the DESCR file.")
                }
@@ -526,7 +490,7 @@ func ChecklinesDescr(lines Lines) {
                line := lines.Lines[maxLines]
 
                line.Warnf("File too long (should be no more than %d lines).", maxLines)
-               Explain(
+               G.Explain(
                        "The DESCR file should fit on a traditional terminal of 80x25",
                        "characters.  It is also intended to give a _brief_ summary about",
                        "the package's contents.")
@@ -548,7 +512,7 @@ func ChecklinesMessage(lines Lines) {
 
        if lines.Len() < 3 {
                lines.LastLine().Warnf("File too short.")
-               Explain(explanation...)
+               G.Explain(explanation...)
                return
        }
 
@@ -559,14 +523,15 @@ func ChecklinesMessage(lines Lines) {
                fix.Explain(explanation...)
                fix.InsertBefore(hline)
                fix.Apply()
-               CheckLineRcsid(lines.Lines[0], ``, "")
+               lines.CheckRcsID(0, ``, "")
        } else if 1 < lines.Len() {
-               CheckLineRcsid(lines.Lines[1], ``, "")
+               lines.CheckRcsID(1, ``, "")
        }
        for _, line := range lines.Lines {
-               CheckLineLength(line, 80)
-               CheckLineTrailingWhitespace(line)
-               CheckLineValidCharacters(line)
+               ck := LineChecker{line}
+               ck.CheckLength(80)
+               ck.CheckTrailingWhitespace()
+               ck.CheckValidCharacters()
        }
        if lastLine := lines.LastLine(); lastLine.Text != hline {
                fix := lastLine.Autofix()
@@ -580,12 +545,12 @@ func ChecklinesMessage(lines Lines) {
        SaveAutofixChanges(lines)
 }
 
-func CheckfileMk(fileName string) {
+func CheckfileMk(filename string) {
        if trace.Tracing {
-               defer trace.Call1(fileName)()
+               defer trace.Call1(filename)()
        }
 
-       mklines := LoadMk(fileName, NotEmpty|LogErrors)
+       mklines := LoadMk(filename, NotEmpty|LogErrors)
        if mklines == nil {
                return
        }
@@ -594,18 +559,18 @@ func CheckfileMk(fileName string) {
        mklines.SaveAutofixChanges()
 }
 
-func (pkglint *Pkglint) Checkfile(fileName string) {
+func (pkglint *Pkglint) Checkfile(filename string) {
        if trace.Tracing {
-               defer trace.Call1(fileName)()
+               defer trace.Call1(filename)()
        }
 
-       basename := path.Base(fileName)
-       pkgsrcRel := G.Pkgsrc.ToRel(fileName)
+       basename := path.Base(filename)
+       pkgsrcRel := pkglint.Pkgsrc.ToRel(filename)
        depth := strings.Count(pkgsrcRel, "/")
 
-       if depth == 2 && !G.Wip {
+       if depth == 2 && !pkglint.Wip {
                if contains(basename, "README") || contains(basename, "TODO") {
-                       NewLineWhole(fileName).Errorf("Packages in main pkgsrc must not have a %s file.", basename)
+                       NewLineWhole(filename).Errorf("Packages in main pkgsrc must not have a %s file.", basename)
                        return
                }
        }
@@ -618,137 +583,137 @@ func (pkglint *Pkglint) Checkfile(fileNa
                contains(basename, "README") && depth == 2,
                contains(basename, "TODO") && depth == 2:
                if pkglint.Opts.Import {
-                       NewLineWhole(fileName).Errorf("Must be cleaned up before committing the package.")
+                       NewLineWhole(filename).Errorf("Must be cleaned up before committing the package.")
                }
                return
        }
 
-       st, err := os.Lstat(fileName)
+       st, err := os.Lstat(filename)
        if err != nil {
-               NewLineWhole(fileName).Errorf("Cannot determine file type: %s", err)
+               NewLineWhole(filename).Errorf("Cannot determine file type: %s", err)
                return
        }
 
-       pkglint.checkExecutable(fileName, st)
-       pkglint.checkMode(fileName, st.Mode())
+       pkglint.checkExecutable(filename, st)
+       pkglint.checkMode(filename, st.Mode())
 }
 
-// checkMode checks a directory entry based on its file name and its mode
+// checkMode checks a directory entry based on its filename and its mode
 // (regular file, directory, symlink).
-func (pkglint *Pkglint) checkMode(fileName string, mode os.FileMode) {
-       basename := path.Base(fileName)
+func (pkglint *Pkglint) checkMode(filename string, mode os.FileMode) {
+       basename := path.Base(filename)
        switch {
        case mode.IsDir():
                switch {
                case basename == "files" || basename == "patches" || isIgnoredFilename(basename):
                        // Ok
-               case matches(fileName, `(?:^|/)files/[^/]*$`):
+               case matches(filename, `(?:^|/)files/[^/]*$`):
                        // Ok
-               case !isEmptyDir(fileName):
-                       NewLineWhole(fileName).Warnf("Unknown directory name.")
+               case !isEmptyDir(filename):
+                       NewLineWhole(filename).Warnf("Unknown directory name.")
                }
 
        case mode&os.ModeSymlink != 0:
                if !hasPrefix(basename, "work") {
-                       NewLineWhole(fileName).Warnf("Unknown symlink name.")
+                       NewLineWhole(filename).Warnf("Unknown symlink name.")
                }
 
        case !mode.IsRegular():
-               NewLineWhole(fileName).Errorf("Only files and directories are allowed in pkgsrc.")
+               NewLineWhole(filename).Errorf("Only files and directories are allowed in pkgsrc.")
 
        case basename == "ALTERNATIVES":
                if pkglint.Opts.CheckAlternatives {
-                       CheckfileAlternatives(fileName)
+                       CheckfileAlternatives(filename)
                }
 
        case basename == "buildlink3.mk":
                if pkglint.Opts.CheckBuildlink3 {
-                       if mklines := LoadMk(fileName, NotEmpty|LogErrors); mklines != nil {
+                       if mklines := LoadMk(filename, NotEmpty|LogErrors); mklines != nil {
                                ChecklinesBuildlink3Mk(mklines)
                        }
                }
 
        case hasPrefix(basename, "DESCR"):
                if pkglint.Opts.CheckDescr {
-                       if lines := Load(fileName, NotEmpty|LogErrors); lines != nil {
+                       if lines := Load(filename, NotEmpty|LogErrors); lines != nil {
                                ChecklinesDescr(lines)
                        }
                }
 
        case basename == "distinfo":
                if pkglint.Opts.CheckDistinfo {
-                       if lines := Load(fileName, NotEmpty|LogErrors); lines != nil {
+                       if lines := Load(filename, NotEmpty|LogErrors); lines != nil {
                                ChecklinesDistinfo(lines)
                        }
                }
 
        case basename == "DEINSTALL" || basename == "INSTALL":
                if pkglint.Opts.CheckInstall {
-                       CheckfileExtra(fileName)
+                       CheckfileExtra(filename)
                }
 
        case hasPrefix(basename, "MESSAGE"):
                if pkglint.Opts.CheckMessage {
-                       if lines := Load(fileName, NotEmpty|LogErrors); lines != nil {
+                       if lines := Load(filename, NotEmpty|LogErrors); lines != nil {
                                ChecklinesMessage(lines)
                        }
                }
 
        case basename == "options.mk":
                if pkglint.Opts.CheckOptions {
-                       if mklines := LoadMk(fileName, NotEmpty|LogErrors); mklines != nil {
+                       if mklines := LoadMk(filename, NotEmpty|LogErrors); mklines != nil {
                                ChecklinesOptionsMk(mklines)
                        }
                }
 
        case matches(basename, `^patch-[-A-Za-z0-9_.~+]*[A-Za-z0-9_]$`):
                if pkglint.Opts.CheckPatches {
-                       if lines := Load(fileName, NotEmpty|LogErrors); lines != nil {
+                       if lines := Load(filename, NotEmpty|LogErrors); lines != nil {
                                ChecklinesPatch(lines)
                        }
                }
 
-       case matches(fileName, `(?:^|/)patches/manual[^/]*$`):
+       case matches(filename, `(?:^|/)patches/manual[^/]*$`):
                if trace.Tracing {
-                       trace.Step1("Unchecked file %q.", fileName)
+                       trace.Step1("Unchecked file %q.", filename)
                }
 
-       case matches(fileName, `(?:^|/)patches/[^/]*$`):
-               NewLineWhole(fileName).Warnf("Patch files should be named \"patch-\", followed by letters, '-', '_', '.', and digits only.")
+       case matches(filename, `(?:^|/)patches/[^/]*$`):
+               NewLineWhole(filename).Warnf("Patch files should be named \"patch-\", followed by letters, '-', '_', '.', and digits only.")
 
-       case matches(basename, `^(?:.*\.mk|Makefile.*)$`) && !matches(fileName, `files/`) && !matches(fileName, `patches/`):
+       case matches(basename, `^(?:.*\.mk|Makefile.*)$`) && !matches(filename, `files/`) && !matches(filename, `patches/`):
                if pkglint.Opts.CheckMk {
-                       CheckfileMk(fileName)
+                       CheckfileMk(filename)
                }
 
        case hasPrefix(basename, "PLIST"):
                if pkglint.Opts.CheckPlist {
-                       if lines := Load(fileName, NotEmpty|LogErrors); lines != nil {
+                       if lines := Load(filename, NotEmpty|LogErrors); lines != nil {
                                ChecklinesPlist(lines)
                        }
                }
 
        case hasPrefix(basename, "CHANGES-"):
                // This only checks the file but doesn't register the changes globally.
-               _ = pkglint.Pkgsrc.loadDocChangesFromFile(fileName)
+               _ = pkglint.Pkgsrc.loadDocChangesFromFile(filename)
 
-       case matches(fileName, `(?:^|/)files/[^/]*$`):
+       case matches(filename, `(?:^|/)files/[^/]*$`):
                // Skip
 
        case basename == "spec":
-               if !hasPrefix(G.Pkgsrc.ToRel(fileName), "regress/") {
-                       NewLineWhole(fileName).Warnf("Only packages in regress/ may have spec files.")
+               if !hasPrefix(pkglint.Pkgsrc.ToRel(filename), "regress/") {
+                       NewLineWhole(filename).Warnf("Only packages in regress/ may have spec files.")
                }
 
        default:
-               NewLineWhole(fileName).Warnf("Unexpected file found.")
+               NewLineWhole(filename).Warnf("Unexpected file found.")
                if pkglint.Opts.CheckExtra {
-                       CheckfileExtra(fileName)
+                       CheckfileExtra(filename)
                }
        }
 }
 
-func (pkglint *Pkglint) checkExecutable(fileName string, st os.FileInfo) {
+func (pkglint *Pkglint) checkExecutable(filename string, st os.FileInfo) {
        switch {
        case !st.Mode().IsRegular():
                // Directories and other entries may be executable.
@@ -756,13 +721,13 @@ func (pkglint *Pkglint) checkExecutable(
        case st.Mode().Perm()&0111 == 0:
                // Good.
 
-       case isCommitted(fileName):
+       case isCommitted(filename):
                // Too late to be fixed by the package developer, since
                // CVS remembers the executable bit in the repo file.
                // At this point, it can only be reset by the CVS admins.
 
        default:
-               line := NewLine(fileName, 0, "", nil)
+               line := NewLineWhole(filename)
                fix := line.Autofix()
                fix.Warnf("Should not be executable.")
                fix.Explain(
@@ -773,7 +738,7 @@ func (pkglint *Pkglint) checkExecutable(
                fix.Custom(func(showAutofix, autofix bool) {
                        fix.Describef(0, "Clearing executable bits")
                        if autofix {
-                               if err := os.Chmod(fileName, st.Mode()&^0111); err != nil {
+                               if err := os.Chmod(filename, st.Mode()&^0111); err != nil {
                                        line.Errorf("Cannot clear executable bits: %s", err)
                                }
                        }
@@ -828,16 +793,17 @@ func (pkglint *Pkglint) Tool(command str
 // 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 {
+// current package. It is not guaranteed to be usable, only defined;
+// that must be checked by the calling code, see Tool.UsableAtLoadTime and
+// Tool.UsableAtRunTime.
+func (pkglint *Pkglint) ToolByVarname(varname string) *Tool {
        return pkglint.tools().ByVarname(varname)
 }
 
 func (pkglint *Pkglint) tools() *Tools {
-       if G.Mk != nil {
-               return G.Mk.Tools
+       if pkglint.Mk != nil {
+               return pkglint.Mk.Tools
        } else {
-               return G.Pkgsrc.Tools
+               return pkglint.Pkgsrc.Tools
        }
 }

Index: pkgsrc/pkgtools/pkglint/files/mkline_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.44 pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.45
--- pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.44   Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/mkline_test.go        Sun Dec  2 01:57:48 2018
@@ -2,101 +2,166 @@ package main
 
 import "gopkg.in/check.v1"
 
-func (s *Suite) Test_NewMkLine(c *check.C) {
+func (s *Suite) Test_NewMkLine__varassign(c *check.C) {
        t := s.Init(c)
 
-       t.SetupCommandLine("-Wspace")
-       mklines := t.NewMkLines("test.mk",
-               "VARNAME.param?=value # varassign comment",
-               "\tshell command # shell comment",
-               "# whole line comment",
-               "",
-               ".  if !empty(PKGNAME:M*-*) && ${RUBY_RAILS_SUPPORTED:[\\#]} == 1 # directive comment",
-               ".    include \"../../mk/bsd.prefs.mk\" # include comment",
-               ".    include <subdir.mk> # sysinclude comment",
-               "target1 target2: source1 source2",
-               "target : source",
-               "VARNAME+=value",
-               "<<<<<<<<<<<<<<<<<")
-       ln := mklines.mklines
+       mkline := t.NewMkLine("test.mk", 101,
+               "VARNAME.param?=value # varassign comment")
+
+       c.Check(mkline.IsVarassign(), equals, true)
+       c.Check(mkline.Varname(), equals, "VARNAME.param")
+       c.Check(mkline.Varcanon(), equals, "VARNAME.*")
+       c.Check(mkline.Varparam(), equals, "param")
+       c.Check(mkline.Op(), equals, opAssignDefault)
+       c.Check(mkline.Value(), equals, "value")
+       c.Check(mkline.VarassignComment(), equals, "# varassign comment")
+}
+
+func (s *Suite) Test_NewMkLine__shellcmd(c *check.C) {
+       t := s.Init(c)
 
-       c.Check(ln[0].IsVarassign(), equals, true)
-       c.Check(ln[0].Varname(), equals, "VARNAME.param")
-       c.Check(ln[0].Varcanon(), equals, "VARNAME.*")
-       c.Check(ln[0].Varparam(), equals, "param")
-       c.Check(ln[0].Op(), equals, opAssignDefault)
-       c.Check(ln[0].Value(), equals, "value")
-       c.Check(ln[0].VarassignComment(), equals, "# varassign comment")
-
-       c.Check(ln[1].IsShellCommand(), equals, true)
-       c.Check(ln[1].ShellCommand(), equals, "shell command # shell comment")
-
-       c.Check(ln[2].IsComment(), equals, true)
-
-       c.Check(ln[3].IsEmpty(), equals, true)
-
-       c.Check(ln[4].IsDirective(), equals, true)
-       c.Check(ln[4].Indent(), equals, "  ")
-       c.Check(ln[4].Directive(), equals, "if")
-       c.Check(ln[4].Args(), equals, "!empty(PKGNAME:M*-*) && ${RUBY_RAILS_SUPPORTED:[#]} == 1")
-       c.Check(ln[4].DirectiveComment(), equals, "directive comment")
-
-       c.Check(ln[5].IsInclude(), equals, true)
-       c.Check(ln[5].Indent(), equals, "    ")
-       c.Check(ln[5].MustExist(), equals, true)
-       c.Check(ln[5].IncludeFile(), equals, "../../mk/bsd.prefs.mk")
-
-       c.Check(ln[6].IsSysinclude(), equals, true)
-       c.Check(ln[6].Indent(), equals, "    ")
-       c.Check(ln[6].MustExist(), equals, true)
-       c.Check(ln[6].IncludeFile(), equals, "subdir.mk")
-
-       c.Check(ln[7].IsDependency(), equals, true)
-       c.Check(ln[7].Targets(), equals, "target1 target2")
-       c.Check(ln[7].Sources(), equals, "source1 source2")
-
-       c.Check(ln[9].IsVarassign(), equals, true)
-       c.Check(ln[9].Varname(), equals, "VARNAME")
-       c.Check(ln[9].Varcanon(), equals, "VARNAME")
-       c.Check(ln[9].Varparam(), equals, "")
+       mkline := t.NewMkLine("test.mk", 101,
+               "\tshell command # shell comment")
 
-       // Merge conflicts are of neither type.
-       c.Check(ln[10].IsVarassign(), equals, false)
-       c.Check(ln[10].IsDirective(), equals, false)
-       c.Check(ln[10].IsInclude(), equals, false)
-       c.Check(ln[10].IsEmpty(), equals, false)
-       c.Check(ln[10].IsComment(), equals, false)
-       c.Check(ln[10].IsDependency(), equals, false)
-       c.Check(ln[10].IsShellCommand(), equals, false)
-       c.Check(ln[10].IsSysinclude(), equals, false)
+       c.Check(mkline.IsShellCommand(), equals, true)
+       c.Check(mkline.ShellCommand(), equals, "shell command # shell comment")
+}
+
+func (s *Suite) Test_NewMkLine__comment(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("test.mk", 101,
+               "# whole line comment")
+
+       c.Check(mkline.IsComment(), equals, true)
+}
+
+func (s *Suite) Test_NewMkLine__empty(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("test.mk", 101, "")
+
+       c.Check(mkline.IsEmpty(), equals, true)
+}
+
+func (s *Suite) Test_NewMkLine__directive(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("test.mk", 101,
+               ".  if !empty(PKGNAME:M*-*) && ${RUBY_RAILS_SUPPORTED:[\\#]} == 1 # directive comment")
+
+       c.Check(mkline.IsDirective(), equals, true)
+       c.Check(mkline.Indent(), equals, "  ")
+       c.Check(mkline.Directive(), equals, "if")
+       c.Check(mkline.Args(), equals, "!empty(PKGNAME:M*-*) && ${RUBY_RAILS_SUPPORTED:[#]} == 1")
+       c.Check(mkline.DirectiveComment(), equals, "directive comment")
+}
+
+func (s *Suite) Test_NewMkLine__include(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("test.mk", 101,
+               ".    include \"../../mk/bsd.prefs.mk\" # include comment")
+
+       c.Check(mkline.IsInclude(), equals, true)
+       c.Check(mkline.Indent(), equals, "    ")
+       c.Check(mkline.MustExist(), equals, true)
+       c.Check(mkline.IncludedFile(), equals, "../../mk/bsd.prefs.mk")
+
+       c.Check(mkline.IsSysinclude(), equals, false)
+}
+
+func (s *Suite) Test_NewMkLine__sysinclude(c *check.C) {
+       t := s.Init(c)
 
+       mkline := t.NewMkLine("test.mk", 101,
+               ".    include <subdir.mk> # sysinclude comment")
+
+       c.Check(mkline.IsSysinclude(), equals, true)
+       c.Check(mkline.Indent(), equals, "    ")
+       c.Check(mkline.MustExist(), equals, true)
+       c.Check(mkline.IncludedFile(), equals, "subdir.mk")
+
+       c.Check(mkline.IsInclude(), equals, false)
+}
+
+func (s *Suite) Test_NewMkLine__dependency(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("test.mk", 101,
+               "target1 target2: source1 source2")
+
+       c.Check(mkline.IsDependency(), equals, true)
+       c.Check(mkline.Targets(), equals, "target1 target2")
+       c.Check(mkline.Sources(), equals, "source1 source2")
+}
+
+func (s *Suite) Test_NewMkLine__dependency_space(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("test.mk", 101,
+               "target : source")
+
+       c.Check(mkline.Targets(), equals, "target")
+       c.Check(mkline.Sources(), equals, "source")
        t.CheckOutputLines(
-               "WARN: test.mk:9: Space before colon in dependency line.")
+               "NOTE: test.mk:101: Space before colon in dependency line.")
+}
+
+func (s *Suite) Test_NewMkLine__varassign_append(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("test.mk", 101,
+               "VARNAME+=value")
+
+       c.Check(mkline.IsVarassign(), equals, true)
+       c.Check(mkline.Varname(), equals, "VARNAME")
+       c.Check(mkline.Varcanon(), equals, "VARNAME")
+       c.Check(mkline.Varparam(), equals, "")
+}
+
+func (s *Suite) Test_NewMkLine__merge_conflict(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("test.mk", 101,
+               "<<<<<<<<<<<<<<<<<")
+
+       // Merge conflicts are of neither type.
+       c.Check(mkline.IsVarassign(), equals, false)
+       c.Check(mkline.IsDirective(), equals, false)
+       c.Check(mkline.IsInclude(), equals, false)
+       c.Check(mkline.IsEmpty(), equals, false)
+       c.Check(mkline.IsComment(), equals, false)
+       c.Check(mkline.IsDependency(), equals, false)
+       c.Check(mkline.IsShellCommand(), equals, false)
+       c.Check(mkline.IsSysinclude(), equals, false)
 }
 
 func (s *Suite) Test_NewMkLine__autofix_space_after_varname(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wspace")
-       fileName := t.CreateFileLines("Makefile",
+       filename := t.CreateFileLines("Makefile",
                MkRcsID,
                "VARNAME +=\t${VARNAME}",
                "VARNAME+ =\t${VARNAME+}",
                "VARNAME+ +=\t${VARNAME+}",
                "pkgbase := pkglint")
 
-       CheckfileMk(fileName)
+       CheckfileMk(filename)
 
        t.CheckOutputLines(
-               "WARN: ~/Makefile:2: Unnecessary space after variable name \"VARNAME\".",
-               "WARN: ~/Makefile:4: Unnecessary space after variable name \"VARNAME+\".")
+               "NOTE: ~/Makefile:2: Unnecessary space after variable name \"VARNAME\".",
+               // FIXME: Don't say anything here because the spaced form is clearer that the compressed form.
+               "NOTE: ~/Makefile:4: Unnecessary space after variable name \"VARNAME+\".")
 
        t.SetupCommandLine("-Wspace", "--autofix")
 
-       CheckfileMk(fileName)
+       CheckfileMk(filename)
 
        t.CheckOutputLines(
                "AUTOFIX: ~/Makefile:2: Replacing \"VARNAME +=\" with \"VARNAME+=\".",
+               // FIXME: Don't fix anything here because the spaced form is clearer that the compressed form.
                "AUTOFIX: ~/Makefile:4: Replacing \"VARNAME+ +=\" with \"VARNAME++=\".")
        t.CheckFileLines("Makefile",
                MkRcsID+"",
@@ -106,6 +171,47 @@ func (s *Suite) Test_NewMkLine__autofix_
                "pkgbase := pkglint")
 }
 
+func (s *Suite) Test_NewMkLine__varname_with_hash(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("Makefile", 123, "VARNAME.#=\tvalue")
+
+       // Parse error because the # starts a comment.
+       c.Check(mkline.IsVarassign(), equals, false)
+
+       mkline2 := t.NewMkLine("Makefile", 123, "VARNAME.\\#=\tvalue")
+
+       // FIXME: Varname() should be "VARNAME.#".
+       c.Check(mkline2.IsVarassign(), equals, false)
+
+       t.CheckOutputLines(
+               "ERROR: Makefile:123: Unknown Makefile line format: \"VARNAME.#=\\tvalue\".",
+               "ERROR: Makefile:123: Unknown Makefile line format: \"VARNAME.\\\\#=\\tvalue\".")
+}
+
+func (s *Suite) Test_MkLine_Varparam(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("Makefile", 2, "SUBST_SED.${param}=\tvalue")
+
+       varparam := mkline.Varparam()
+
+       c.Check(varparam, equals, "${param}")
+}
+
+func (s *Suite) Test_MkLine_ValueAlign__commented(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("Makefile", 2, "#SUBST_SED.${param}=\tvalue")
+
+       valueAlign := mkline.ValueAlign()
+
+       c.Check(mkline.IsCommentedVarassign(), equals, true)
+       c.Check(valueAlign, equals, "#SUBST_SED.${param}=\t")
+}
+
+// Demonstrates how a simple condition is structured internally.
+// For most of the checks, using cond.Walk is the simplest way to go.
 func (s *Suite) Test_MkLine_Cond(c *check.C) {
        t := s.Init(c)
 
@@ -134,29 +240,29 @@ func (s *Suite) Test_VarUseContext_Strin
 func (s *Suite) Test_NewMkLine__number_sign(c *check.C) {
        t := s.Init(c)
 
-       mklineVarassignEscaped := t.NewMkLine("fileName", 1, "SED_CMD=\t's,\\#,hash,g'")
+       mklineVarassignEscaped := t.NewMkLine("filename", 1, "SED_CMD=\t's,\\#,hash,g'")
 
        c.Check(mklineVarassignEscaped.Varname(), equals, "SED_CMD")
        c.Check(mklineVarassignEscaped.Value(), equals, "'s,#,hash,g'")
 
-       mklineCommandEscaped := t.NewMkLine("fileName", 1, "\tsed -e 's,\\#,hash,g'")
+       mklineCommandEscaped := t.NewMkLine("filename", 1, "\tsed -e 's,\\#,hash,g'")
 
        c.Check(mklineCommandEscaped.ShellCommand(), equals, "sed -e 's,\\#,hash,g'")
 
        // From shells/zsh/Makefile.common, rev. 1.78
-       mklineCommandUnescaped := t.NewMkLine("fileName", 1, "\t# $ sha1 patches/patch-ac")
+       mklineCommandUnescaped := t.NewMkLine("filename", 1, "\t# $ sha1 patches/patch-ac")
 
        c.Check(mklineCommandUnescaped.ShellCommand(), equals, "# $ sha1 patches/patch-ac")
        t.CheckOutputEmpty() // No warning about parsing the lonely dollar sign.
 
-       mklineVarassignUnescaped := t.NewMkLine("fileName", 1, "SED_CMD=\t's,#,hash,'")
+       mklineVarassignUnescaped := t.NewMkLine("filename", 1, "SED_CMD=\t's,#,hash,'")
 
        c.Check(mklineVarassignUnescaped.Value(), equals, "'s,")
        t.CheckOutputLines(
-               "WARN: fileName:1: The # character starts a comment.")
+               "WARN: filename:1: The # character starts a comment.")
 }
 
-func (s *Suite) Test_NewMkLine__leading_space(c *check.C) {
+func (s *Suite) Test_NewMkLine__varassign_leading_space(c *check.C) {
        t := s.Init(c)
 
        _ = t.NewMkLine("rubyversion.mk", 427, " _RUBYVER=\t2.15")
@@ -168,8 +274,12 @@ func (s *Suite) Test_NewMkLine__leading_
                "WARN: rubyversion.mk:427: Makefile lines should not start with space characters.")
 }
 
-// Exotic test cases from the pkgsrc infrastructure.
+// Exotic code examples from the pkgsrc infrastructure.
 // Hopefully, pkgsrc packages don't need such complicated code.
+// Still, pkglint needs to parse them correctly, or it would not
+// be able to parse and check the infrastructure files as well.
+//
+// See Pkgsrc.loadUntypedVars.
 func (s *Suite) Test_NewMkLine__infrastructure(c *check.C) {
        t := s.Init(c)
 
@@ -198,6 +308,7 @@ func (s *Suite) Test_NewMkLine__infrastr
 
        t.CheckOutputLines(
                "WARN: infra.mk:2: USE_BUILTIN.${_pkg_:S/^-//} is defined but not used.",
+               "WARN: infra.mk:2: _pkg_ is used but not defined.",
                "ERROR: infra.mk:5: \".export\" requires arguments.",
                "NOTE: infra.mk:2: This variable value should be aligned to column 41.",
                "ERROR: infra.mk:10: Unmatched .endif.")
@@ -206,13 +317,13 @@ func (s *Suite) Test_NewMkLine__infrastr
 func (s *Suite) Test_MkLine_VariableNeedsQuoting__unknown_rhs(c *check.C) {
        t := s.Init(c)
 
-       mkline := t.NewMkLine("fileName", 1, "PKGNAME:= ${UNKNOWN}")
+       mkline := t.NewMkLine("filename", 1, "PKGNAME:= ${UNKNOWN}")
        t.SetupVartypes()
 
-       vuc := &VarUseContext{G.Pkgsrc.vartypes["PKGNAME"], vucTimeParse, vucQuotUnknown, false}
+       vuc := &VarUseContext{G.Pkgsrc.VariableType("PKGNAME"), vucTimeParse, vucQuotUnknown, false}
        nq := mkline.VariableNeedsQuoting("UNKNOWN", nil, vuc)
 
-       c.Check(nq, equals, nqDontKnow)
+       c.Check(nq, equals, unknown)
 }
 
 func (s *Suite) Test_MkLine_VariableNeedsQuoting__append_URL_to_list_of_URLs(c *check.C) {
@@ -225,11 +336,11 @@ func (s *Suite) Test_MkLine_VariableNeed
        vuc := &VarUseContext{G.Pkgsrc.vartypes["MASTER_SITES"], vucTimeRun, vucQuotPlain, false}
        nq := mkline.VariableNeedsQuoting("HOMEPAGE", G.Pkgsrc.vartypes["HOMEPAGE"], vuc)
 
-       c.Check(nq, equals, nqNo)
+       c.Check(nq, equals, no)
 
        MkLineChecker{mkline}.checkVarassign()
 
-       t.CheckOutputEmpty() // Up to pkglint 5.3.6, it warned about a missing :Q here, which was wrong.
+       t.CheckOutputEmpty() // Up to version 5.3.6, pkglint warned about a missing :Q here, which was wrong.
 }
 
 func (s *Suite) Test_MkLine_VariableNeedsQuoting__append_list_to_list(c *check.C) {
@@ -249,7 +360,8 @@ func (s *Suite) Test_MkLine_VariableNeed
        t := s.Init(c)
 
        t.SetupVartypes()
-       mkline := t.NewMkLine("builtin.mk", 3, "USE_BUILTIN.Xfixes!=\t${PKG_ADMIN} pmatch 'pkg-[0-9]*' ${BUILTIN_PKG.Xfixes:Q}")
+       mkline := t.NewMkLine("builtin.mk", 3,
+               "USE_BUILTIN.Xfixes!=\t${PKG_ADMIN} pmatch 'pkg-[0-9]*' ${BUILTIN_PKG.Xfixes:Q}")
 
        MkLineChecker{mkline}.checkVarassign()
 
@@ -262,12 +374,14 @@ func (s *Suite) Test_MkLine_VariableNeed
        t := s.Init(c)
 
        t.SetupVartypes()
-       mkline := t.NewMkLine("Makefile", 3, "SUBST_SED.hpath=\t-e 's|^\\(INSTALL[\t:]*=\\).*|\\1${INSTALL}|'")
+       mkline := t.NewMkLine("Makefile", 3,
+               "SUBST_SED.hpath=\t-e 's|^\\(INSTALL[\t:]*=\\).*|\\1${INSTALL}|'")
 
        MkLineChecker{mkline}.checkVarassign()
 
        t.CheckOutputLines(
-               "WARN: Makefile:3: Please use ${INSTALL:Q} instead of ${INSTALL} and make sure the variable appears outside of any quoting characters.")
+               "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) {
@@ -325,7 +439,7 @@ func (s *Suite) Test_MkLine_VariableNeed
                "WARN: Makefile:3: The exitcode of the command at the left of the | operator is ignored.")
 }
 
-// Based on mail/mailfront/Makefile.
+// As seen in mail/mailfront/Makefile.
 func (s *Suite) Test_MkLine_VariableNeedsQuoting__URL_as_part_of_word_in_list(c *check.C) {
        t := s.Init(c)
 
@@ -339,11 +453,10 @@ func (s *Suite) Test_MkLine_VariableNeed
        t.CheckOutputEmpty() // Don't suggest to use ${HOMEPAGE:Q}.
 }
 
-// Pkglint currently does not parse $$(subshell) commands very well. As
-// a side effect, it sometimes issues wrong warnings about the :Q
-// modifier.
+// Before November 2018, pkglint did not parse $$(subshell) commands very well.
+// As a side effect, it sometimes issued wrong warnings about the :Q modifier.
 //
-// Based on www/firefox31/xpi.mk.
+// As seen in www/firefox31/xpi.mk.
 func (s *Suite) Test_MkLine_VariableNeedsQuoting__command_in_subshell(c *check.C) {
        t := s.Init(c)
 
@@ -365,7 +478,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.
+// correctly is trickier than with other variables.
 func (s *Suite) Test_MkLine_VariableNeedsQuoting__LDFLAGS_in_single_quotes(c *check.C) {
        t := s.Init(c)
 
@@ -382,7 +495,7 @@ func (s *Suite) Test_MkLine_VariableNeed
                "WARN: x11/mlterm/Makefile:2: Please move ${LDFLAGS:M*:Q} outside of any quoting characters.")
 }
 
-// No quoting is necessary here.
+// No quoting is necessary when lists of options are appended to each other.
 // PKG_OPTIONS are declared as "lkShell" although they are processed
 // using make's .for loop, which splits them at whitespace and usually
 // requires the variable to be declared as "lkSpace".
@@ -398,6 +511,7 @@ func (s *Suite) Test_MkLine_VariableNeed
 
        MkLineChecker{G.Mk.mklines[1]}.Check()
 
+       // No warning about a missing :Q modifier.
        t.CheckOutputEmpty()
 }
 
@@ -528,6 +642,7 @@ func (s *Suite) Test_MkLine_VariableNeed
        // using it inside backticks, and luckily there is no need for it.
        t.CheckOutputLines(
                "WARN: Makefile:4: COMMENT may not be used in any file; it is a write-only variable.",
+               // TODO: Better suggest that COMMENT should not be used inside backticks or other quotes.
                "WARN: Makefile:4: The variable COMMENT should be quoted as part of a shell word.")
 }
 
@@ -578,7 +693,9 @@ func (s *Suite) Test_MkLine_VariableNeed
 
        mklines.Check()
 
-       t.CheckOutputEmpty()
+       t.CheckOutputLines(
+               "NOTE: ~/Makefile:6: The substitution command \"s:@LINKER_RPATH_FLAG@:${LINKER_RPATH_FLAG}:g\" " +
+                       "can be replaced with \"SUBST_VARS.class+= LINKER_RPATH_FLAG\".")
 }
 
 // Tools, when used in a shell command, must not be quoted.
@@ -599,7 +716,8 @@ func (s *Suite) Test_MkLine_VariableNeed
        t.CheckOutputEmpty()
 }
 
-// These examples from real pkgsrc end up in the final nqDontKnow case.
+// As of October 2018, these examples from real pkgsrc end up in the
+// final "unknown" case.
 func (s *Suite) Test_MkLine_VariableNeedsQuoting__uncovered_cases(c *check.C) {
        t := s.Init(c)
 
@@ -618,6 +736,7 @@ func (s *Suite) Test_MkLine_VariableNeed
        mklines.Check()
 
        t.CheckOutputLines(
+               // TODO: Explain why the variable may not be set, by listing the current rules.
                "WARN: ~/Makefile:4: The variable LINKER_RPATH_FLAG may not be set by any package.",
                "WARN: ~/Makefile:4: Please use ${LINKER_RPATH_FLAG:S/-rpath/& /:Q} instead of ${LINKER_RPATH_FLAG:S/-rpath/& /}.",
                "WARN: ~/Makefile:4: LINKER_RPATH_FLAG should not be evaluated at load time.",
@@ -657,45 +776,132 @@ func (s *Suite) Test_MkLine__comment_in_
                "WARN: Makefile:2: The # character starts a comment.")
 }
 
+// Ensures that the conditional variables of a line can be set even
+// after initializing the MkLine.
+//
+// If this test should fail, it is probably because mkLineDirective
+// is not a pointer type anymore.
+//
+// See https://github.com/golang/go/issues/28045.
 func (s *Suite) Test_MkLine_ConditionalVars(c *check.C) {
        t := s.Init(c)
 
        mkline := t.NewMkLine("Makefile", 45, ".include \"../../category/package/buildlink3.mk\"")
 
-       c.Check(mkline.ConditionalVars(), equals, "")
+       c.Check(mkline.ConditionalVars(), check.HasLen, 0)
 
-       mkline.SetConditionalVars("OPSYS")
+       mkline.SetConditionalVars([]string{"OPSYS"})
 
-       c.Check(mkline.ConditionalVars(), equals, "OPSYS")
+       c.Check(mkline.ConditionalVars(), deepEquals, []string{"OPSYS"})
 }
 
 func (s *Suite) Test_MkLine_ValueSplit(c *check.C) {
        t := s.Init(c)
 
-       checkSplit := func(value string, expected ...string) {
+       test := func(value string, expected ...string) {
                mkline := t.NewMkLine("Makefile", 1, "PATH=\t"+value)
                split := mkline.ValueSplit(value, ":")
                c.Check(split, deepEquals, expected)
        }
 
-       checkSplit("#empty",
+       test("#empty",
                []string(nil)...)
 
-       checkSplit("/bin",
+       test("/bin",
                "/bin")
 
-       checkSplit("/bin:/sbin",
+       test("/bin:/sbin",
                "/bin",
                "/sbin")
 
-       checkSplit("${DESTDIR}/bin:/bin/${SUBDIR}",
+       test("${DESTDIR}/bin:/bin/${SUBDIR}",
                "${DESTDIR}/bin",
                "/bin/${SUBDIR}")
 
-       checkSplit("/bin:${DESTDIR}${PREFIX}:${DESTDIR:S,/,\\:,:S,:,:,}/sbin",
+       test("/bin:${DESTDIR}${PREFIX}:${DESTDIR:S,/,\\:,:S,:,:,}/sbin",
                "/bin",
                "${DESTDIR}${PREFIX}",
                "${DESTDIR:S,/,\\:,:S,:,:,}/sbin")
+
+       test("${VAR:Udefault}::${VAR2}two:words",
+               "${VAR:Udefault}",
+               "",
+               "${VAR2}two",
+               "words")
+}
+
+func (s *Suite) Test_MkLine_ValueFields(c *check.C) {
+       t := s.Init(c)
+
+       test := func(value string, expected ...string) {
+               mkline := t.NewMkLine("Makefile", 1, "VAR=\t"+value)
+               split := mkline.ValueFields(value)
+               c.Check(split, deepEquals, expected)
+       }
+
+       test("one   two\t\t${THREE:Uthree:Nsome \tspaces}",
+               "one",
+               "two",
+               "${THREE:Uthree:Nsome \tspaces}")
+
+       test("${VAR:Udefault value} ${VAR2}two words",
+               "${VAR:Udefault value}",
+               "${VAR2}two",
+               "words")
+}
+
+// Before 2018-11-26, this test panicked.
+func (s *Suite) Test_MkLine_ValueFields__adjacent_vars(c *check.C) {
+       t := s.Init(c)
+
+       test := func(value string, expected ...string) {
+               mkline := t.NewMkLine("Makefile", 1, "")
+               split := mkline.ValueFields(value)
+               c.Check(split, deepEquals, expected)
+       }
+
+       test("\t; ${RM} ${WRKSRC}",
+               ";",
+               "${RM}",
+               "${WRKSRC}")
+}
+
+func (s *Suite) Test_MkLine_ValueTokens(c *check.C) {
+       t := s.Init(c)
+
+       testTokens := func(value string, expected ...*MkToken) {
+               mkline := t.NewMkLine("Makefile", 1, "PATH=\t"+value)
+               split := mkline.ValueTokens()
+               c.Check(split, deepEquals, expected)
+       }
+
+       testTokens("#empty",
+               []*MkToken(nil)...)
+
+       testTokens("value",
+               &MkToken{"value", nil})
+
+       testTokens("value ${VAR} rest",
+               &MkToken{"value ", nil},
+               &MkToken{"${VAR}", NewMkVarUse("VAR")},
+               &MkToken{" rest", nil})
+
+       testTokens("value ${UNFINISHED",
+               &MkToken{"value ", nil})
+}
+
+func (s *Suite) Test_MkLine_ValueTokens__caching(c *check.C) {
+       t := s.Init(c)
+
+       mkline := t.NewMkLine("Makefile", 1, "PATH=\tvalue ${UNFINISHED")
+       split := mkline.ValueTokens()
+
+       c.Check(split, deepEquals, []*MkToken{{"value ", nil}})
+
+       split2 := mkline.ValueTokens() // This time the slice is taken from the cache.
+
+       // In Go, it's not possible to compare slices for reference equality.
+       c.Check(split2, deepEquals, split)
 }
 
 func (s *Suite) Test_MkLine_ResolveVarsInRelativePath(c *check.C) {
@@ -713,7 +919,7 @@ func (s *Suite) Test_MkLine_ResolveVarsI
                c.Check(mkline.ResolveVarsInRelativePath(before, false), equals, after)
        }
 
-       checkResolve("", "")
+       checkResolve("", ".")
        checkResolve("${LUA_PKGSRCDIR}", "../../lang/lua53")
        checkResolve("${PHPPKGSRCDIR}", "../../lang/php72")
        checkResolve("${SUSE_DIR_PREFIX}", "suse100")
@@ -747,8 +953,11 @@ func (s *Suite) Test_MatchVarassign(c *c
 
        checkVarassign := func(text string, commented bool, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment string) {
                type VarAssign struct {
-                       commented                                                              bool
-                       varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment string
+                       commented                  bool
+                       varname, spaceAfterVarname string
+                       op, align                  string
+                       value, spaceAfterValue     string
+                       comment                    string
                }
                expected := VarAssign{commented, varname, spaceAfterVarname, op, align, value, spaceAfterValue, comment}
                am, acommented, avarname, aspaceAfterVarname, aop, aalign, avalue, aspaceAfterValue, acomment := MatchVarassign(text)
@@ -775,7 +984,6 @@ func (s *Suite) Test_MatchVarassign(c *c
        checkVarassign("VAR += value", false, "VAR", " ", "+=", "VAR += ", "value", "", "")
        checkVarassign(" VAR=value", false, "VAR", "", "=", " VAR=", "value", "", "")
        checkVarassign("VAR=value #comment", false, "VAR", "", "=", "VAR=", "value", " ", "#comment")
-       checkVarassign("#VAR=value", true, "VAR", "", "=", "#VAR=", "value", "", "")
 
        checkNotVarassign("\tVAR=value")
        checkNotVarassign("?=value")
@@ -783,8 +991,12 @@ func (s *Suite) Test_MatchVarassign(c *c
        checkNotVarassign("#")
        checkNotVarassign("VAR.$$=value")
 
-       // A single space is typically used for writing documentation,
-       // not for commenting out code.
+       // A commented variable assignment must start immediately after the comment character.
+       // There must be no additional whitespace before the variable name.
+       checkVarassign("#VAR=value", true, "VAR", "", "=", "#VAR=", "value", "", "")
+
+       // A single space is typically used for writing documentation, not for commenting out code.
+       // Therefore this line doesn't count as commented variable assignment.
        checkNotVarassign("# VAR=value")
 }
 
@@ -812,11 +1024,11 @@ func (s *Suite) Test_Indentation(c *chec
 
        ind.AddVar("LEVEL1.VAR1")
 
-       c.Check(ind.Varnames(), equals, "LEVEL1.VAR1")
+       c.Check(ind.Varnames(), deepEquals, []string{"LEVEL1.VAR1"})
 
        ind.AddVar("LEVEL1.VAR2")
 
-       c.Check(ind.Varnames(), equals, "LEVEL1.VAR1, LEVEL1.VAR2")
+       c.Check(ind.Varnames(), deepEquals, []string{"LEVEL1.VAR1", "LEVEL1.VAR2"})
        c.Check(ind.DependsOn("LEVEL1.VAR1"), equals, true)
        c.Check(ind.DependsOn("OTHER_VAR"), equals, false)
 
@@ -824,17 +1036,17 @@ func (s *Suite) Test_Indentation(c *chec
 
        ind.AddVar("LEVEL2.VAR")
 
-       c.Check(ind.Varnames(), equals, "LEVEL1.VAR1, LEVEL1.VAR2, LEVEL2.VAR")
+       c.Check(ind.Varnames(), deepEquals, []string{"LEVEL1.VAR1", "LEVEL1.VAR2", "LEVEL2.VAR"})
        c.Check(ind.String(), equals, "[2 (LEVEL1.VAR1 LEVEL1.VAR2) 2 (LEVEL2.VAR)]")
 
        ind.Pop()
 
-       c.Check(ind.Varnames(), equals, "LEVEL1.VAR1, LEVEL1.VAR2")
+       c.Check(ind.Varnames(), deepEquals, []string{"LEVEL1.VAR1", "LEVEL1.VAR2"})
        c.Check(ind.IsConditional(), equals, true)
 
        ind.Pop()
 
-       c.Check(ind.Varnames(), equals, "")
+       c.Check(ind.Varnames(), check.HasLen, 0)
        c.Check(ind.IsConditional(), equals, false)
        c.Check(ind.String(), equals, "[]")
 }
@@ -848,7 +1060,7 @@ func (s *Suite) Test_Indentation_Remembe
        ind.RememberUsedVariables(mkline.Cond())
 
        t.CheckOutputEmpty()
-       c.Check(ind.Varnames(), equals, "PKGREVISION")
+       c.Check(ind.Varnames(), deepEquals, []string{"PKGREVISION"})
 }
 
 func (s *Suite) Test_MkLine_DetermineUsedVariables(c *check.C) {
@@ -863,13 +1075,12 @@ func (s *Suite) Test_MkLine_DetermineUse
                "${TARGETS}: ${SOURCES} # ${dependency.comment}",
                ".include \"${OTHER_FILE}\"",
                "",
-               "\t"+
-                       "${VAR.${param}}"+
-                       "${VAR}and${VAR2}"+
-                       "${VAR:M${pattern}}"+
-                       "$(ROUND_PARENTHESES)"+
-                       "$$shellvar"+
-                       "$<$@$x")
+               "\t${VAR.${param}}",
+               "\t${VAR}and${VAR2}",
+               "\t${VAR:M${pattern}}",
+               "\t$(ROUND_PARENTHESES)",
+               "\t$$shellvar",
+               "\t$< $@ $x")
 
        var varnames []string
        for _, mkline := range mklines.mklines {
@@ -898,3 +1109,19 @@ func (s *Suite) Test_MkLine_DetermineUse
                "@",
                "x"})
 }
+
+func (s *Suite) Test_matchMkDirective(c *check.C) {
+
+       test := func(input, expectedIndent, expectedDirective, expectedArgs, expectedComment string) {
+               m, indent, directive, args, comment := matchMkDirective(input)
+               c.Check(
+                       []interface{}{m, indent, directive, args, comment},
+                       deepEquals,
+                       []interface{}{true, expectedIndent, expectedDirective, expectedArgs, expectedComment})
+       }
+
+       test(".if ${VAR} == value", "", "if", "${VAR} == value", "")
+       test(".\tendif # comment", "\t", "endif", "", "comment")
+       test(".if ${VAR} == \"#\"", "", "if", "${VAR} == \"", "\"")
+       test(".if ${VAR:[#]}", "", "if", "${VAR:[#]}", "")
+}

Index: pkgsrc/pkgtools/pkglint/files/mklinechecker.go
diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.22 pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.23
--- pkgsrc/pkgtools/pkglint/files/mklinechecker.go:1.22 Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/mklinechecker.go      Sun Dec  2 01:57:48 2018
@@ -15,41 +15,51 @@ type MkLineChecker struct {
 func (ck MkLineChecker) Check() {
        mkline := ck.MkLine
 
-       CheckLineTrailingWhitespace(mkline.Line)
-       CheckLineValidCharacters(mkline.Line)
+       LineChecker{mkline.Line}.CheckTrailingWhitespace()
+       LineChecker{mkline.Line}.CheckValidCharacters()
 
        switch {
        case mkline.IsVarassign():
                ck.checkVarassign()
 
        case mkline.IsShellCommand():
-               shellCommand := mkline.ShellCommand()
-
-               if G.Opts.WarnSpace && hasPrefix(mkline.Text, "\t\t") {
-                       fix := mkline.Autofix()
-                       fix.Notef("Shell programs should be indented with a single tab.")
-                       fix.Explain(
-                               "The first tab in the line marks the line as a shell command.  Since",
-                               "every line of shell commands starts with a completely new shell",
-                               "environment, there is no need to indent some of the commands, or to",
-                               "use more horizontal space than necessary.")
-                       fix.ReplaceRegex(`^\t\t+`, "\t", 1)
-                       fix.Apply()
-               }
-
-               ck.checkText(shellCommand)
-               NewShellLine(mkline).CheckShellCommandLine(shellCommand)
+               ck.checkShellCommand()
 
        case mkline.IsComment():
-               if hasPrefix(mkline.Text, "# url2pkg-marker") {
-                       mkline.Errorf("This comment indicates unfinished work (url2pkg).")
-               }
+               ck.checkComment()
 
        case mkline.IsInclude():
                ck.checkInclude()
        }
 }
 
+func (ck MkLineChecker) checkComment() {
+       mkline := ck.MkLine
+
+       if hasPrefix(mkline.Text, "# url2pkg-marker") {
+               mkline.Errorf("This comment indicates unfinished work (url2pkg).")
+       }
+}
+
+func (ck MkLineChecker) checkShellCommand() {
+       mkline := ck.MkLine
+
+       shellCommand := mkline.ShellCommand()
+       if G.Opts.WarnSpace && hasPrefix(mkline.Text, "\t\t") {
+               fix := mkline.Autofix()
+               fix.Notef("Shell programs should be indented with a single tab.")
+               fix.Explain(
+                       "The first tab in the line marks the line as a shell command.  Since",
+                       "every line of shell commands starts with a completely new shell",
+                       "environment, there is no need to indent some of the commands, or to",
+                       "use more horizontal space than necessary.")
+               fix.ReplaceRegex(`^\t\t+`, "\t", 1)
+               fix.Apply()
+       }
+       ck.checkText(shellCommand)
+       NewShellLine(mkline).CheckShellCommandLine(shellCommand)
+}
+
 func (ck MkLineChecker) checkInclude() {
        if trace.Tracing {
                defer trace.Call0()()
@@ -60,38 +70,50 @@ func (ck MkLineChecker) checkInclude() {
                ck.checkDirectiveIndentation(G.Mk.indentation.Depth("include"))
        }
 
-       includefile := mkline.IncludeFile()
+       includedFile := mkline.IncludedFile()
        mustExist := mkline.MustExist()
        if trace.Tracing {
-               trace.Step2("includingFile=%s includefile=%s", mkline.FileName, includefile)
+               trace.Step2("includingFile=%s includedFile=%s", mkline.Filename, includedFile)
        }
-       ck.CheckRelativePath(includefile, mustExist)
+       ck.CheckRelativePath(includedFile, mustExist)
 
        switch {
-       case hasSuffix(includefile, "/Makefile"):
+       case hasSuffix(includedFile, "/Makefile"):
                mkline.Errorf("Other Makefiles must not be included directly.")
-               Explain(
-                       "If you want to include portions of another Makefile, extract",
-                       "the common parts and put them into a Makefile.common.  After",
-                       "that, both this one and the other package should include the",
-                       "Makefile.common.")
+               G.Explain(
+                       "To include portions of another Makefile, extract the common parts",
+                       "and put them into a Makefile.common or a Makefile fragment called",
+                       "module.mk or similar.  After that, both this one and the other",
+                       "package should include the newly created file.")
 
-       case IsPrefs(includefile):
-               if mkline.Basename == "buildlink3.mk" && includefile == "../../mk/bsd.prefs.mk" {
-                       mkline.Notef("For efficiency reasons, please include bsd.fast.prefs.mk instead of bsd.prefs.mk.")
+       case IsPrefs(includedFile):
+               if mkline.Basename == "buildlink3.mk" && includedFile == "../../mk/bsd.prefs.mk" {
+                       fix := mkline.Autofix()
+                       fix.Notef("For efficiency reasons, please include bsd.fast.prefs.mk instead of bsd.prefs.mk.")
+                       fix.Replace("bsd.prefs.mk", "bsd.fast.prefs.mk")
+                       fix.Apply()
                }
 
-       case hasSuffix(includefile, "/x11-links/buildlink3.mk"):
-               mkline.Errorf("%s must not be included directly. Include \"../../mk/x11.buildlink3.mk\" instead.", includefile)
+       case hasSuffix(includedFile, "pkgtools/x11-links/buildlink3.mk"):
+               fix := mkline.Autofix()
+               fix.Errorf("%s must not be included directly. Include \"../../mk/x11.buildlink3.mk\" instead.", includedFile)
+               fix.Replace("pkgtools/x11-links/buildlink3.mk", "mk/x11.buildlink3.mk")
+               fix.Apply()
 
-       case hasSuffix(includefile, "/jpeg/buildlink3.mk"):
-               mkline.Errorf("%s must not be included directly. Include \"../../mk/jpeg.buildlink3.mk\" instead.", includefile)
+       case hasSuffix(includedFile, "graphics/jpeg/buildlink3.mk"):
+               fix := mkline.Autofix()
+               fix.Errorf("%s must not be included directly. Include \"../../mk/jpeg.buildlink3.mk\" instead.", includedFile)
+               fix.Replace("graphics/jpeg/buildlink3.mk", "mk/jpeg.buildlink3.mk")
+               fix.Apply()
 
-       case hasSuffix(includefile, "/intltool/buildlink3.mk"):
+       case hasSuffix(includedFile, "/intltool/buildlink3.mk"):
                mkline.Warnf("Please write \"USE_TOOLS+= intltool\" instead of this line.")
 
-       case hasSuffix(includefile, "/builtin.mk"):
-               mkline.Errorf("%s must not be included directly. Include \"%s/buildlink3.mk\" instead.", includefile, path.Dir(includefile))
+       case hasSuffix(includedFile, "/builtin.mk"):
+               fix := mkline.Autofix()
+               fix.Errorf("%s must not be included directly. Include \"%s/buildlink3.mk\" instead.", includedFile, path.Dir(includedFile))
+               fix.Replace("builtin.mk", "buildlink3.mk")
+               fix.Apply()
        }
 }
 
@@ -118,27 +140,28 @@ func (ck MkLineChecker) checkDirective(f
                needsArgument = true
        }
 
-       if needsArgument && args == "" {
+       switch {
+       case needsArgument && args == "":
                mkline.Errorf("\".%s\" requires arguments.", directive)
 
-       } else if !needsArgument && args != "" {
+       case !needsArgument && args != "":
                if directive == "else" {
                        mkline.Errorf("\".%s\" does not take arguments. If you meant \"else if\", use \".elif\".", directive)
                } else {
                        mkline.Errorf("\".%s\" does not take arguments.", directive)
                }
 
-       } else if directive == "if" || directive == "elif" {
+       case directive == "if" || directive == "elif":
                ck.checkDirectiveCond()
 
-       } else if directive == "ifdef" || directive == "ifndef" {
+       case directive == "ifdef" || directive == "ifndef":
                mkline.Warnf("The \".%s\" directive is deprecated. Please use \".if %sdefined(%s)\" instead.",
                        directive, ifelseStr(directive == "ifdef", "", "!"), args)
 
-       } else if directive == "for" {
+       case directive == "for":
                ck.checkDirectiveFor(forVars, ind)
 
-       } else if directive == "undef" {
+       case directive == "undef":
                for _, varname := range fields(args) {
                        if forVars[varname] {
                                mkline.Notef("Using \".undef\" after a \".for\" loop is unnecessary.")
@@ -153,13 +176,13 @@ func (ck MkLineChecker) checkDirectiveEn
        comment := mkline.DirectiveComment()
 
        if directive == "endif" && comment != "" {
-               if condition := ind.Condition(); !contains(condition, comment) {
-                       mkline.Warnf("Comment %q does not match condition %q.", comment, condition)
+               if args := ind.Args(); !contains(args, comment) {
+                       mkline.Warnf("Comment %q does not match condition %q.", comment, args)
                }
        }
        if directive == "endfor" && comment != "" {
-               if condition := ind.Condition(); !contains(condition, comment) {
-                       mkline.Warnf("Comment %q does not match loop %q.", comment, condition)
+               if args := ind.Args(); !contains(args, comment) {
+                       mkline.Warnf("Comment %q does not match loop %q.", comment, args)
                }
        }
        if ind.Len() <= 1 {
@@ -171,7 +194,7 @@ func (ck MkLineChecker) checkDirectiveFo
        mkline := ck.MkLine
        args := mkline.Args()
 
-       if m, vars, values := match2(args, `^([^\t ]+(?:[\t ]*[^\t ]+)*?)[\t ]+in[\t ]+(.*)$`); m {
+       if m, vars, _ := match2(args, `^([^\t ]+(?:[\t ]*[^\t ]+)*?)[\t ]+in[\t ]+(.*)$`); m {
                for _, forvar := range fields(vars) {
                        indentation.AddVar(forvar)
                        if !G.Infrastructure && hasPrefix(forvar, "_") {
@@ -180,7 +203,7 @@ func (ck MkLineChecker) checkDirectiveFo
 
                        if matches(forvar, `^[_a-z][_a-z0-9]*$`) {
                                // Fine.
-                       } else if matches(forvar, `[A-Z]`) {
+                       } else if strings.IndexFunc(forvar, func(r rune) bool { return 'A' <= r && r <= 'Z' }) != -1 {
                                mkline.Warnf(".for variable names should not contain uppercase letters.")
                        } else {
                                mkline.Errorf("Invalid variable name %q.", forvar)
@@ -189,21 +212,17 @@ func (ck MkLineChecker) checkDirectiveFo
                        forVars[forvar] = true
                }
 
-               // Check if any of the value's types is not guessed.
-               guessed := true
-               for _, value := range fields(values) {
-                       if m, vname := match1(value, `^\$\{(.*)\}`); m {
-                               vartype := G.Pkgsrc.VariableType(vname)
-                               if vartype != nil && !vartype.guessed {
-                                       guessed = false
-                               }
-                       }
-               }
-
-               forLoopType := &Vartype{lkSpace, BtUnknown, []ACLEntry{{"*", aclpAllRead}}, guessed}
-               forLoopContext := &VarUseContext{forLoopType, vucTimeParse, vucQuotFor, false}
+               // XXX: The type BtUnknown is very unspecific here. For known variables
+               // or constant values this could probably be improved.
+               //
+               // The guessed flag could also be determined more correctly. As of November 2018,
+               // running pkglint over the whole pkgsrc tree did not produce any different result
+               // whether guessed was true or false, so currently it is not worth investing
+               // any work.
+               forLoopType := Vartype{lkSpace, BtUnknown, []ACLEntry{{"*", aclpAllRead}}, false}
+               forLoopContext := VarUseContext{&forLoopType, vucTimeParse, vucQuotFor, false}
                for _, forLoopVar := range mkline.DetermineUsedVariables() {
-                       ck.CheckVaruse(&MkVarUse{forLoopVar, nil}, forLoopContext)
+                       ck.CheckVaruse(&MkVarUse{forLoopVar, nil}, &forLoopContext)
                }
        }
 }
@@ -224,8 +243,8 @@ func (ck MkLineChecker) checkDirectiveIn
 
 func (ck MkLineChecker) checkDependencyRule(allowedTargets map[string]bool) {
        mkline := ck.MkLine
-       targets := fields(mkline.Targets())
-       sources := fields(mkline.Sources())
+       targets := ck.MkLine.ValueFields(mkline.Targets())
+       sources := ck.MkLine.ValueFields(mkline.Sources())
 
        for _, source := range sources {
                if source == ".PHONY" {
@@ -245,11 +264,11 @@ func (ck MkLineChecker) checkDependencyR
                        // TODO: Check for spelling mistakes.
 
                } else if hasPrefix(target, "${.CURDIR}/") {
-                       // OK, this is intentional
+                       // This is deliberate, see the explanation below.
 
                } else if !allowedTargets[target] {
                        mkline.Warnf("Unusual target %q.", target)
-                       Explain(
+                       G.Explain(
                                "If you want to define your own target, declare it like this:",
                                "",
                                "\t.PHONY: my-target",
@@ -317,6 +336,7 @@ func (ck MkLineChecker) checkVarassignPe
                alternativeFiles := vartype.AllowedFiles(needed)
                switch {
                case alternativeActions != 0 && alternativeFiles != "":
+                       // FIXME: Sometimes the message says "ok in *", which is missing that in buildlink3.mk, none of the actions are allowed.
                        mkline.Warnf("The variable %s may not be %s (only %s) in this file; it would be ok in %s.",
                                varname, needed.HumanString(), alternativeActions.HumanString(), alternativeFiles)
                case alternativeFiles != "":
@@ -329,14 +349,16 @@ func (ck MkLineChecker) checkVarassignPe
                        mkline.Warnf("The variable %s may not be %s by any package.",
                                varname, needed.HumanString())
                }
-               Explain(
+               G.Explain(
                        "The allowed actions for a variable are determined based on the file",
                        "name in which the variable is used or defined.  The exact rules are",
+                       // FIXME: List the rules in this very explanation.
                        "hard-coded into pkglint.  If they seem to be incorrect, please ask",
                        "on the tech-pkg%NetBSD.org@localhost mailing list.")
        }
 }
 
+// CheckVaruse checks a single use of a variable in a specific context.
 func (ck MkLineChecker) CheckVaruse(varuse *MkVarUse, vuc *VarUseContext) {
        mkline := ck.MkLine
        if trace.Tracing {
@@ -349,25 +371,13 @@ func (ck MkLineChecker) CheckVaruse(varu
 
        varname := varuse.varname
        vartype := G.Pkgsrc.VariableType(varname)
-       switch {
-       case !G.Opts.WarnExtra:
-       case vartype != nil && !vartype.guessed:
-               // Well-known variables are probably defined by the infrastructure.
-       case varIsDefinedSimilar(varname):
-       case containsVarRef(varname):
-       case G.Pkgsrc.vartypes[varname] != nil:
-       case G.Pkgsrc.vartypes[varnameCanon(varname)] != nil:
-       case G.Mk != nil && !G.Mk.FirstTime("used but not defined: "+varname):
-
-       default:
-               mkline.Warnf("%s is used but not defined.", varname)
-       }
+       ck.checkVaruseUndefined(vartype, varname)
 
-       ck.checkVaruseMod(varuse, vartype)
+       ck.checkVaruseModifiers(varuse, vartype)
 
        if varuse.varname == "@" {
                ck.MkLine.Warnf("Please use %q instead of %q.", "${.TARGET}", "$@")
-               Explain(
+               G.Explain(
                        "It is more readable and prevents confusion with the shell variable",
                        "of the same name.")
        }
@@ -375,43 +385,91 @@ func (ck MkLineChecker) CheckVaruse(varu
        ck.checkVarusePermissions(varname, vartype, vuc)
 
        if varname == "LOCALBASE" && !G.Infrastructure {
-               ck.MkLine.Warnf("Please use PREFIX instead of LOCALBASE.")
+               fix := ck.MkLine.Autofix()
+               fix.Warnf("Please use PREFIX instead of LOCALBASE.")
+               fix.ReplaceRegex(`\$\{LOCALBASE\b`, "${PREFIX", 1)
+               fix.Apply()
        }
 
        needsQuoting := mkline.VariableNeedsQuoting(varname, vartype, vuc)
 
-       if G.Opts.WarnQuoting && vuc.quoting != vucQuotUnknown && needsQuoting != nqDontKnow {
+       if G.Opts.WarnQuoting && vuc.quoting != vucQuotUnknown && needsQuoting != unknown {
+               // FIXME: Why "Shellword" when there's no indication that this is actually a shell type?
                ck.CheckVaruseShellword(varname, vartype, vuc, varuse.Mod(), needsQuoting)
        }
 
-       if G.Pkgsrc.UserDefinedVars.Defined(varname) && !G.Pkgsrc.IsBuildDef(varname) && !G.Mk.buildDefs[varname] {
-               mkline.Warnf("The user-defined variable %s is used but not added to BUILD_DEFS.", varname)
-               Explain(
-                       "When a pkgsrc package is built, many things can be configured by the",
-                       "pkgsrc user in the mk.conf file.  All these configurations should be",
-                       "recorded in the binary package, so the package can be reliably",
-                       "rebuilt.  The BUILD_DEFS variable contains a list of all these",
-                       "user-settable variables, so please add your variable to it, too.")
+       if G.Pkgsrc.UserDefinedVars.Defined(varname) && !G.Pkgsrc.IsBuildDef(varname) {
+               if !G.Mk.buildDefs[varname] && G.Mk.FirstTimeSlice("BUILD_DEFS", varname) {
+                       mkline.Warnf("The user-defined variable %s is used but not added to BUILD_DEFS.", varname)
+                       G.Explain(
+                               "When a pkgsrc package is built, many things can be configured by the",
+                               "pkgsrc user in the mk.conf file.  All these configurations should be",
+                               "recorded in the binary package so the package can be reliably",
+                               "rebuilt.  The BUILD_DEFS variable contains a list of all these",
+                               "user-settable variables, so please add your variable to it, too.")
+               }
+       }
+
+       ck.checkVaruseDeprecated(varuse)
+
+       ck.checkTextVarUse(varname, vartype, vuc.time)
+}
+
+func (ck MkLineChecker) checkVaruseUndefined(vartype *Vartype, varname string) {
+       switch {
+       case !G.Opts.WarnExtra:
+               break
+       case vartype != nil && !vartype.guessed:
+               // Well-known variables are probably defined by the infrastructure.
+       case varIsDefinedSimilar(varname):
+               break
+       case containsVarRef(varname):
+               break
+       case G.Pkgsrc.vartypes[varname] != nil:
+               break
+       case G.Pkgsrc.vartypes[varnameCanon(varname)] != nil:
+               break
+       case G.Mk != nil && !G.Mk.FirstTimeSlice("used but not defined: ", varname):
+               break
+
+       default:
+               ck.MkLine.Warnf("%s is used but not defined.", varname)
        }
 }
 
-func (ck MkLineChecker) checkVaruseMod(varuse *MkVarUse, vartype *Vartype) {
+func (ck MkLineChecker) checkVaruseModifiers(varuse *MkVarUse, vartype *Vartype) {
        mods := varuse.modifiers
        if len(mods) == 0 {
                return
        }
 
-       if mods[0].IsSuffixSubst() && vartype != nil && !vartype.IsConsideredList() {
+       ck.checkVaruseModifiersSuffix(varuse, vartype)
+       ck.checkVaruseModifiersRange(varuse)
+
+       // TODO: Add checks for a single modifier, among them:
+       // TODO: Suggest to replace ${VAR:@l@-l${l}@} with the simpler ${VAR:S,^,-l,}.
+       // TODO: Suggest to replace ${VAR:@l@${l}suffix@} with the simpler ${VAR:=suffix}.
+       // TODO: Investigate why :Q is not checked at this exact place.
+}
+
+func (ck MkLineChecker) checkVaruseModifiersSuffix(varuse *MkVarUse, vartype *Vartype) {
+       if varuse.modifiers[0].IsSuffixSubst() && vartype != nil && !vartype.IsConsideredList() {
                ck.MkLine.Warnf("The :from=to modifier should only be used with lists, not with %s.", varuse.varname)
-               Explain(
-                       "Instead of:",
+               G.Explain(
+                       "Instead of (for example):",
                        "\tMASTER_SITES=\t${HOMEPAGE:=repository/}",
                        "",
                        "Write:",
                        "\tMASTER_SITES=\t${HOMEPAGE}repository/",
                        "",
-                       "This is a much clearer expression of the same thought.")
+                       "This is a clearer expression of the same thought.")
        }
+}
+
+// checkVaruseModifiersRange suggests to replace
+// ${VAR:S,^,__magic__,1:M__magic__*:S,^__magic__,,} with the simpler ${VAR:[1]}.
+func (ck MkLineChecker) checkVaruseModifiersRange(varuse *MkVarUse) {
+       mods := varuse.modifiers
 
        if len(mods) == 3 {
                if m, _, from, to, options := mods[0].MatchSubst(); m && from == "^" && matches(to, `^\w+$`) && options == "1" {
@@ -456,12 +514,12 @@ func (ck MkLineChecker) checkVarusePermi
        }
 
        mkline := ck.MkLine
-       perms := vartype.EffectivePermissions(mkline.Basename)
+       effPerms := vartype.EffectivePermissions(mkline.Basename)
 
        // Is the variable used at load time although that is not allowed?
        directly := false
        indirectly := false
-       if !perms.Contains(aclpUseLoadtime) { // May not be used at load time.
+       if !effPerms.Contains(aclpUseLoadtime) { // May not be used at load time.
                if vuc.time == vucTimeParse {
                        directly = true
                } else if vuc.vartype != nil && vuc.vartype.Union().Contains(aclpUseLoadtime) {
@@ -470,7 +528,7 @@ func (ck MkLineChecker) checkVarusePermi
        }
 
        if (directly || indirectly) && !vartype.guessed {
-               if tool := G.ToolByVarname(varname, LoadTime); tool != nil {
+               if tool := G.ToolByVarname(varname); tool != nil {
                        if !tool.UsableAtLoadTime(G.Mk.Tools.SeenPrefs) {
                                ck.warnVaruseToolLoadTime(varname, tool)
                        }
@@ -486,7 +544,7 @@ func (ck MkLineChecker) checkVarusePermi
                }
        }
 
-       if !perms.Contains(aclpUseLoadtime) && !perms.Contains(aclpUse) {
+       if !effPerms.Contains(aclpUseLoadtime) && !effPerms.Contains(aclpUse) {
                needed := aclpUse
                if directly || indirectly {
                        needed = aclpUseLoadtime
@@ -498,9 +556,10 @@ func (ck MkLineChecker) checkVarusePermi
                } else {
                        mkline.Warnf("%s may not be used in any file; it is a write-only variable.", varname)
                }
-               Explain(
+               G.Explain(
                        "The allowed actions for a variable are determined based on the file",
                        "name in which the variable is used or defined.  The exact rules are",
+                       // FIXME: List the rules in this very explanation.
                        "hard-coded into pkglint.  If they seem to be incorrect, please ask",
                        "on the tech-pkg%NetBSD.org@localhost mailing list.")
        }
@@ -509,6 +568,15 @@ func (ck MkLineChecker) checkVarusePermi
 // warnVaruseToolLoadTime logs a warning that the tool ${varname}
 // may not be used at load time.
 func (ck MkLineChecker) warnVaruseToolLoadTime(varname string, tool *Tool) {
+       // TODO: While using a tool by its variable name may be ok at load time,
+       // doing the same with the plain name of a tool is never ok.
+       // "VAR!= cat" is never guaranteed to call the correct cat.
+       // Even for shell builtins like echo and printf, bmake may decide
+       // to skip the shell and execute the commands via execve, which
+       // means that even echo is not a shell-builtin anymore.
+
+       // TODO: Replace "parse time" with "load time" everywhere.
+
        if tool.Validity == AfterPrefsMk {
                ck.MkLine.Warnf("To use the tool ${%s} at load time, bsd.prefs.mk has to be included before.", varname)
                return
@@ -525,7 +593,7 @@ func (ck MkLineChecker) warnVaruseToolLo
        }
 
        ck.MkLine.Warnf("The tool ${%s} cannot be used at load time.", varname)
-       Explain(
+       G.Explain(
                "To use a tool at load time, it must be declared in the package",
                "Makefile by adding it to USE_TOOLS.  After that, bsd.prefs.mk must",
                "be included.  Adding the tool to USE_TOOLS at any later time has",
@@ -543,7 +611,7 @@ func (ck MkLineChecker) warnVaruseLoadTi
 
        if !isIndirect {
                mkline.Warnf("%s should not be evaluated at load time.", varname)
-               Explain(
+               G.Explain(
                        "Many variables, especially lists of something, get their values",
                        "incrementally.  Therefore it is generally unsafe to rely on their",
                        "value until it is clear that it will never change again.  This",
@@ -557,7 +625,7 @@ func (ck MkLineChecker) warnVaruseLoadTi
        }
 
        mkline.Warnf("%s should not be evaluated indirectly at load time.", varname)
-       Explain(
+       G.Explain(
                "The variable on the left-hand side may be evaluated at load time,",
                "but the variable on the right-hand side may not.  Because of the",
                "assignment in this line, the variable might be used indirectly",
@@ -565,35 +633,33 @@ func (ck MkLineChecker) warnVaruseLoadTi
 }
 
 // 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) {
+// or ${VAR:modifiers} is allowed in a certain context.
+func (ck MkLineChecker) CheckVaruseShellword(varname string, vartype *Vartype, vuc *VarUseContext, mod string, needsQuoting YesNoUnknown) {
        if trace.Tracing {
                defer trace.Call(varname, vartype, vuc, mod, needsQuoting)()
        }
 
-       // In GNU configure scripts, a few variables need to be
-       // passed through the :M* operator before they reach the
-       // configure scripts.
+       // In GNU configure scripts, a few variables need to be passed through
+       // the :M* operator before they reach the configure scripts. Otherwise
+       // the leading or trailing spaces will lead to strange caching errors
+       // since the GNU configure scripts cannot handle these space characters.
        //
        // When doing checks outside a package, the :M* operator is needed for safety.
-       needMstar := matches(varname, `^(?:.*_)?(?:CFLAGS|CPPFLAGS|CXXFLAGS|FFLAGS|LDFLAGS|LIBS)$`) &&
-               (G.Pkg == nil || G.Pkg.vars.Defined("GNU_CONFIGURE"))
-
-       strippedMod := mod
-       if m, stripped := match1(mod, `(.*?)(?::M\*)?(?::Q)?$`); m {
-               strippedMod = stripped
-       }
+       needMstar := (G.Pkg == nil || G.Pkg.vars.Defined("GNU_CONFIGURE")) &&
+               matches(varname, `^(?:.*_)?(?:CFLAGS|CPPFLAGS|CXXFLAGS|FFLAGS|LDFLAGS|LIBS)$`)
 
        mkline := ck.MkLine
        if mod == ":M*:Q" && !needMstar {
                mkline.Notef("The :M* modifier is not needed here.")
 
-       } else if needsQuoting == nqYes {
-               correctMod := strippedMod + ifelseStr(needMstar, ":M*:Q", ":Q")
+       } else if needsQuoting == yes {
+               modNoQ := strings.TrimSuffix(mod, ":Q")
+               modNoM := strings.TrimSuffix(modNoQ, ":M*")
+               correctMod := modNoM + ifelseStr(needMstar, ":M*:Q", ":Q")
                if correctMod == mod+":Q" && vuc.IsWordPart && !vartype.IsShell() {
                        if vartype.IsConsideredList() {
                                mkline.Warnf("The list variable %s should not be embedded in a word.", varname)
-                               Explain(
+                               mkline.Explain(
                                        "When a list variable has multiple elements, this expression expands",
                                        "to something unexpected:",
                                        "",
@@ -612,51 +678,62 @@ func (ck MkLineChecker) CheckVaruseShell
                                        "${LIBS:@lib@-l${lib}@}.")
                        } else {
                                mkline.Warnf("The variable %s should be quoted as part of a shell word.", varname)
-                               Explain(
+                               mkline.Explain(
                                        "This variable can contain spaces or other special characters.",
                                        "Therefore it should be quoted by replacing ${VAR} with ${VAR:Q}.")
                        }
 
                } else if mod != correctMod {
                        if vuc.quoting == vucQuotPlain {
-                               fix := mkline.Line.Autofix()
+                               fix := mkline.Autofix()
                                fix.Warnf("Please use ${%s%s} instead of ${%s%s}.", varname, correctMod, varname, mod)
+                               fix.Explain(
+                                       seeGuide("Echoing a string exactly as-is", "echo-literal"))
                                fix.Replace("${"+varname+mod+"}", "${"+varname+correctMod+"}")
                                fix.Apply()
                        } else {
                                mkline.Warnf("Please use ${%s%s} instead of ${%s%s} and make sure"+
                                        " the variable appears outside of any quoting characters.", varname, correctMod, varname, mod)
+                               mkline.Explain(
+                                       "The :Q modifier only works reliably when it is used outside of any",
+                                       "quoting characters like 'single' or \"double\" quotes or `backticks`.",
+                                       "",
+                                       "Examples:",
+                                       "Instead of CFLAGS=\"${CFLAGS:Q}\",",
+                                       "     write CFLAGS=${CFLAGS:Q}.",
+                                       "Instead of 's,@CFLAGS@,${CFLAGS:Q},',",
+                                       "     write 's,@CFLAGS@,'${CFLAGS:Q}','.",
+                                       "",
+                                       seeGuide("Echoing a string exactly as-is", "echo-literal"))
                        }
-                       Explain(
-                               "See the pkgsrc guide, section \"quoting guideline\", for details.")
 
                } else if vuc.quoting != vucQuotPlain {
                        mkline.Warnf("Please move ${%s%s} outside of any quoting characters.", varname, mod)
-                       Explain(
+                       mkline.Explain(
                                "The :Q modifier only works reliably when it is used outside of any",
-                               "quoting characters.",
+                               "quoting characters like 'single' or \"double\" quotes or `backticks`.",
                                "",
                                "Examples:",
                                "Instead of CFLAGS=\"${CFLAGS:Q}\",",
                                "     write CFLAGS=${CFLAGS:Q}.",
                                "Instead of 's,@CFLAGS@,${CFLAGS:Q},',",
-                               "     write 's,@CFLAGS@,'${CFLAGS:Q}','.")
+                               "     write 's,@CFLAGS@,'${CFLAGS:Q}','.",
+                               "",
+                               seeGuide("Echoing a string exactly as-is", "echo-literal"))
                }
        }
 
-       if hasSuffix(mod, ":Q") && (needsQuoting == nqNo || needsQuoting == nqDoesntMatter) {
+       if hasSuffix(mod, ":Q") && needsQuoting != yes {
                bad := "${" + varname + mod + "}"
                good := "${" + varname + strings.TrimSuffix(mod, ":Q") + "}"
 
                fix := mkline.Line.Autofix()
-               if needsQuoting == nqNo {
-                       fix.Warnf("The :Q operator should not be used for ${%s} here.", varname)
-               } else {
+               if needsQuoting == no {
                        fix.Notef("The :Q operator isn't necessary for ${%s} here.", varname)
                }
                fix.Explain(
-                       "Many variables in pkgsrc do not need the :Q operator, since they",
-                       "are not expected to contain white-space or other special characters.",
+                       "Many variables in pkgsrc do not need the :Q operator since they",
+                       "are not expected to contain whitespace or other special characters.",
                        "Examples for these \"safe\" variables are:",
                        "",
                        "\t* filenames",
@@ -664,33 +741,50 @@ func (ck MkLineChecker) CheckVaruseShell
                        "\t* user and group names",
                        "\t* tool names and tool paths",
                        "\t* variable names",
-                       "\t* PKGNAME")
+                       "\t* package names (but not dependency patterns like pkg>=1.2)")
                fix.Replace(bad, good)
                fix.Apply()
        }
 }
 
-func (ck MkLineChecker) checkVarassignPythonVersions(varname, value string) {
+func (ck MkLineChecker) checkVaruseDeprecated(varuse *MkVarUse) {
+       // Temporarily disabled since this method is not called for all places,
+       // such as ${_PKG_SILENT} in a shell command.
+       if varuse != nil {
+               return
+       }
+
+       varname := varuse.varname
+       instead := G.Pkgsrc.Deprecated[varname]
+       if instead == "" {
+               instead = G.Pkgsrc.Deprecated[varnameCanon(varname)]
+       }
+       if instead != "" {
+               ck.MkLine.Warnf("Use of %q is deprecated. %s", varname, instead)
+       }
+}
+
+func (ck MkLineChecker) checkVarassignDecreasingVersions(varname, value string) {
        if trace.Tracing {
                defer trace.Call2(varname, value)()
        }
 
        mkline := ck.MkLine
-       strversions := fields(value)
-       intversions := make([]int, len(strversions))
-       for i, strversion := range strversions {
-               iver, err := strconv.Atoi(strversion)
+       strVersions := fields(value)
+       intVersions := make([]int, len(strVersions))
+       for i, strVersion := range strVersions {
+               iver, err := strconv.Atoi(strVersion)
                if err != nil || !(iver > 0) {
                        mkline.Errorf("All values for %s must be positive integers.", varname)
                        return
                }
-               intversions[i] = iver
+               intVersions[i] = iver
        }
 
-       for i, ver := range intversions {
-               if i > 0 && ver >= intversions[i-1] {
+       for i, ver := range intVersions {
+               if i > 0 && ver >= intVersions[i-1] {
                        mkline.Warnf("The values for %s should be in decreasing order.", varname)
-                       Explain(
+                       G.Explain(
                                "If they aren't, it may be possible that needless versions of",
                                "packages are installed.")
                }
@@ -703,7 +797,6 @@ func (ck MkLineChecker) checkVarassign()
        op := mkline.Op()
        value := mkline.Value()
        comment := mkline.VarassignComment()
-       varcanon := varnameCanon(varname)
 
        if trace.Tracing {
                defer trace.Call(varname, op, value)()
@@ -714,10 +807,32 @@ func (ck MkLineChecker) checkVarassign()
        ck.checkVarassignBsdPrefs()
 
        ck.checkText(value)
-       ck.CheckVartype(varname, op, value, comment)
+       ck.checkVartype(varname, op, value, comment)
+
+       ck.checkVarassignUnused()
+
+       ck.checkVarassignSpecific()
+
+       ck.checkVarassignDeprecated()
+
+       ck.checkVarassignVaruse()
+}
+
+func (ck MkLineChecker) checkVarassignDeprecated() {
+       varname := ck.MkLine.Varname()
+       if fix := G.Pkgsrc.Deprecated[varname]; fix != "" {
+               ck.MkLine.Warnf("Definition of %s is deprecated. %s", varname, fix)
+       } else if fix = G.Pkgsrc.Deprecated[varnameCanon(varname)]; fix != "" {
+               ck.MkLine.Warnf("Definition of %s is deprecated. %s", varname, fix)
+       }
+}
+
+func (ck MkLineChecker) checkVarassignUnused() {
+       varname := ck.MkLine.Varname()
+       varcanon := varnameCanon(varname)
 
        // If the variable is not used and is untyped, it may be a spelling mistake.
-       if op == opAssignEval && varname == strings.ToLower(varname) {
+       if ck.MkLine.Op() == opAssignEval && varname == strings.ToLower(varname) {
                if trace.Tracing {
                        trace.Step1("%s might be unused unless it is an argument to a procedure file.", varname)
                }
@@ -727,24 +842,16 @@ func (ck MkLineChecker) checkVarassign()
                        // Ok
                } else if deprecated := G.Pkgsrc.Deprecated; deprecated[varname] != "" || deprecated[varcanon] != "" {
                        // Ok
-               } else if G.Mk != nil && !G.Mk.FirstTime("defined but not used: "+varname) {
+               } else if G.Mk != nil && !G.Mk.FirstTimeSlice("defined but not used: ", varname) {
                        // Skip
                } else {
-                       mkline.Warnf("%s is defined but not used.", varname)
+                       ck.MkLine.Warnf("%s is defined but not used.", varname)
                }
        }
-
-       ck.checkVarassignSpecific()
-
-       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 != "" {
-               mkline.Warnf("Definition of %s is deprecated. %s", varname, fix)
-       }
-
-       ck.checkVarassignVaruse()
 }
 
+// checkVarassignVaruse checks that in a variable assignment, each variables used on either side
+// of the assignment has the correct data type and quoting.
 func (ck MkLineChecker) checkVarassignVaruse() {
        if trace.Tracing {
                defer trace.Call()()
@@ -763,30 +870,41 @@ func (ck MkLineChecker) checkVarassignVa
                vartype = shellcommandsContextType
        }
 
+       ck.checkTextVarUse(
+               ck.MkLine.Varname(),
+               &Vartype{lkNone, BtVariableName, []ACLEntry{{"*", aclpAll}}, false},
+               vucTimeParse)
+
        if vartype != nil && vartype.IsShell() {
                ck.checkVarassignVaruseShell(vartype, time)
-       } else {
-               ck.checkVarassignVaruseMk(vartype, time)
+       } else { // XXX: This else looks as if it should be omitted.
+               ck.checkTextVarUse(ck.MkLine.Value(), vartype, time)
        }
 }
 
-func (ck MkLineChecker) checkVarassignVaruseMk(vartype *Vartype, time vucTime) {
+func (ck MkLineChecker) checkTextVarUse(text string, vartype *Vartype, time vucTime) {
+       if !contains(text, "$") {
+               return
+       }
+
        if trace.Tracing {
                defer trace.Call(vartype, time)()
        }
-       mkline := ck.MkLine
-       tokens := NewMkParser(mkline.Line, mkline.Value(), false).MkTokens()
+
+       tokens := NewMkParser(nil, text, false).MkTokens()
        for i, token := range tokens {
                if token.Varuse != nil {
                        spaceLeft := i-1 < 0 || matches(tokens[i-1].Text, `[\t ]$`)
                        spaceRight := i+1 >= len(tokens) || matches(tokens[i+1].Text, `^[\t ]`)
                        isWordPart := !(spaceLeft && spaceRight)
-                       vuc := &VarUseContext{vartype, time, vucQuotPlain, isWordPart}
-                       ck.CheckVaruse(token.Varuse, vuc)
+                       vuc := VarUseContext{vartype, time, vucQuotPlain, isWordPart}
+                       ck.CheckVaruse(token.Varuse, &vuc)
                }
        }
 }
 
+// checkVarassignVaruseShell is very similar to checkVarassignVaruse, they just differ
+// in the way they determine isWordPart.
 func (ck MkLineChecker) checkVarassignVaruseShell(vartype *Vartype, time vucTime) {
        if trace.Tracing {
                defer trace.Call(vartype, time)()
@@ -807,8 +925,8 @@ func (ck MkLineChecker) checkVarassignVa
        for i, atom := range atoms {
                if varuse := atom.VarUse(); varuse != nil {
                        isWordPart := isWordPart(atoms, i)
-                       vuc := &VarUseContext{vartype, time, atom.Quoting.ToVarUseContext(), isWordPart}
-                       ck.CheckVaruse(varuse, vuc)
+                       vuc := VarUseContext{vartype, time, atom.Quoting.ToVarUseContext(), isWordPart}
+                       ck.CheckVaruse(varuse, &vuc)
                }
        }
 }
@@ -818,7 +936,7 @@ func (ck MkLineChecker) checkVarassignSp
        varname := mkline.Varname()
        value := mkline.Value()
 
-       if contains(value, "/etc/rc.d") {
+       if contains(value, "/etc/rc.d") && mkline.Varname() != "RPMIGNOREPATH" {
                mkline.Warnf("Please use the RCD_SCRIPTS mechanism to install rc.d scripts automatically to ${RCD_SCRIPTS_EXAMPLEDIR}.")
        }
 
@@ -827,12 +945,12 @@ func (ck MkLineChecker) checkVarassignSp
        }
 
        if varname == "PYTHON_VERSIONS_ACCEPTED" {
-               ck.checkVarassignPythonVersions(varname, value)
+               ck.checkVarassignDecreasingVersions(varname, value)
        }
 
        if mkline.VarassignComment() == "# defined" && !hasSuffix(varname, "_MK") && !hasSuffix(varname, "_COMMON") {
-               mkline.Notef("Please use \"# empty\", \"# none\" or \"yes\" instead of \"# defined\".")
-               Explain(
+               mkline.Notef("Please use \"# empty\", \"# none\" or \"# yes\" instead of \"# defined\".")
+               G.Explain(
                        "The value #defined says something about the state of the variable,",
                        "but not what that _means_.  In some cases a variable that is defined",
                        "means \"yes\", in other cases it is an empty list (which is also",
@@ -840,18 +958,21 @@ func (ck MkLineChecker) checkVarassignSp
                        "with \"none\".  It is this meaning that should be described.")
        }
 
-       if m, revvarname := match1(value, `\$\{(PKGNAME|PKGVERSION)[:\}]`); m {
-               if varname == "DIST_SUBDIR" || varname == "WRKSRC" {
-                       mkline.Warnf("%s should not be used in %s, as it includes the PKGREVISION. Please use %s_NOREV instead.", revvarname, varname, revvarname)
+       if varname == "DIST_SUBDIR" || varname == "WRKSRC" {
+               if m, revVarname := match1(value, `\$\{(PKGNAME|PKGVERSION)[:\}]`); m {
+                       mkline.Warnf("%s should not be used in %s as it includes the PKGREVISION. "+
+                               "Please use %[1]s_NOREV instead.", revVarname, varname)
                }
        }
 
        if hasPrefix(varname, "SITES_") {
                mkline.Warnf("SITES_* is deprecated. Please use SITES.* instead.")
+               // No autofix since it doesn't occur anymore.
        }
 
        if varname == "PKG_SKIP_REASON" && G.Mk.indentation.DependsOn("OPSYS") {
-               mkline.Notef("Consider defining NOT_FOR_PLATFORM instead of setting PKG_SKIP_REASON depending on ${OPSYS}.")
+               mkline.Notef("Consider defining NOT_FOR_PLATFORM instead of " +
+                       "setting PKG_SKIP_REASON depending on ${OPSYS}.")
        }
 }
 
@@ -876,7 +997,7 @@ func (ck MkLineChecker) checkVarassignBs
        }
 
        mkline.Warnf("Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".")
-       Explain(
+       G.Explain(
                "The ?= operator is used to provide a default value to a variable.",
                "In pkgsrc, many variables can be set by the pkgsrc user in the",
                "mk.conf file.  This file must be included explicitly.  If a ?=",
@@ -888,7 +1009,7 @@ func (ck MkLineChecker) checkVarassignBs
                "bsd.prefs.mk file, which will take care of everything.")
 }
 
-func (ck MkLineChecker) CheckVartype(varname string, op MkOperator, value, comment string) {
+func (ck MkLineChecker) checkVartype(varname string, op MkOperator, value, comment string) {
        if trace.Tracing {
                defer trace.Call(varname, op, value, comment)()
        }
@@ -901,6 +1022,8 @@ func (ck MkLineChecker) CheckVartype(var
        vartype := G.Pkgsrc.VariableType(varname)
 
        if op == opAssignAppend {
+               // XXX: MayBeAppendedTo also depends on the current file, see checkVarusePermissions.
+               // These checks may be combined.
                if vartype != nil && !vartype.MayBeAppendedTo() {
                        mkline.Warnf("The \"+=\" operator should only be used with lists, not with %s.", varname)
                }
@@ -918,46 +1041,59 @@ func (ck MkLineChecker) CheckVartype(var
                }
 
        case vartype.kindOfList == lkNone:
-               ck.CheckVartypePrimitive(varname, vartype.basicType, op, value, comment, vartype.guessed)
+               ck.CheckVartypeBasic(varname, vartype.basicType, op, value, comment, vartype.guessed)
 
        case value == "":
                break
 
        case vartype.kindOfList == lkSpace:
                for _, word := range fields(value) {
-                       ck.CheckVartypePrimitive(varname, vartype.basicType, op, word, comment, vartype.guessed)
+                       ck.CheckVartypeBasic(varname, vartype.basicType, op, word, comment, vartype.guessed)
                }
 
        case vartype.kindOfList == lkShell:
                words, _ := splitIntoMkWords(mkline.Line, value)
                for _, word := range words {
-                       ck.CheckVartypePrimitive(varname, vartype.basicType, op, word, comment, vartype.guessed)
+                       ck.CheckVartypeBasic(varname, vartype.basicType, op, word, comment, vartype.guessed)
                }
        }
 }
 
+// CheckVartypeBasic checks a single list element of the given type.
+//
 // For some variables (like `BuildlinkDepth`), `op` influences the valid values.
 // The `comment` parameter comes from a variable assignment, when a part of the line is commented out.
-func (ck MkLineChecker) CheckVartypePrimitive(varname string, checker *BasicType, op MkOperator, value, comment string, guessed bool) {
+func (ck MkLineChecker) CheckVartypeBasic(varname string, checker *BasicType, op MkOperator, value, comment string, guessed bool) {
        if trace.Tracing {
                defer trace.Call(varname, checker.name, op, value, comment, guessed)()
        }
 
        mkline := ck.MkLine
        valueNoVar := mkline.WithoutMakeVariables(value)
-       ctx := &VartypeCheck{mkline, mkline.Line, varname, op, value, valueNoVar, comment, guessed}
-       checker.checker(ctx)
+       ctx := VartypeCheck{mkline, mkline.Line, varname, op, value, valueNoVar, comment, guessed}
+       checker.checker(&ctx)
 }
 
+// checkText checks the given text (which is typically the right-hand side of a variable
+// assignment or a shell command).
+//
+// Note: checkTextVarUse cannot be called here since it needs to know the context where it is included.
+// Maybe that context should be added here as parameters.
 func (ck MkLineChecker) checkText(text string) {
        if trace.Tracing {
                defer trace.Call1(text)()
        }
 
-       mkline := ck.MkLine
+       ck.checkTextWrksrcDotDot(text)
+       ck.checkTextRpath(text)
+       ck.checkTextDeprecated(text)
+
+}
+
+func (ck MkLineChecker) checkTextWrksrcDotDot(text string) {
        if contains(text, "${WRKSRC}/..") {
-               mkline.Warnf("Building the package should take place entirely inside ${WRKSRC}, not \"${WRKSRC}/..\".")
-               Explain(
+               ck.MkLine.Warnf("Building the package should take place entirely inside ${WRKSRC}, not \"${WRKSRC}/..\".")
+               ck.MkLine.Explain(
                        "WRKSRC should be defined so that there is no need to do anything",
                        "outside of this directory.",
                        "",
@@ -967,15 +1103,20 @@ func (ck MkLineChecker) checkText(text s
                        "\tCONFIGURE_DIRS=\t${WRKSRC}/lib ${WRKSRC}/src",
                        "\tBUILD_DIRS=\t${WRKSRC}/lib ${WRKSRC}/src ${WRKSRC}/cmd",
                        "",
-                       "See the pkgsrc guide, section \"Directories used during the build",
-                       "process\" for more information.")
+                       seeGuide("Directories used during the build process", "build.builddirs"))
        }
+}
 
-       // Note: A simple -R is not detected, as the rate of false positives is too high.
+// checkTextPath checks for literal -Wl,--rpath options.
+//
+// Note: A simple -R is not detected, as the rate of false positives is too high.
+func (ck MkLineChecker) checkTextRpath(text string) {
        if m, flag := match1(text, `(-Wl,--rpath,|-Wl,-rpath-link,|-Wl,-rpath,|-Wl,-R\b)`); m {
-               mkline.Warnf("Please use ${COMPILER_RPATH_FLAG} instead of %q.", flag)
+               ck.MkLine.Warnf("Please use ${COMPILER_RPATH_FLAG} instead of %q.", flag)
        }
+}
 
+func (ck MkLineChecker) checkTextDeprecated(text string) {
        rest := text
        for {
                m, r := G.res.ReplaceFirst(rest, `(?:^|[^$])\$\{([-A-Z0-9a-z_]+)(\.[\-0-9A-Z_a-z]+)?(?::[^\}]+)?\}`, "")
@@ -992,7 +1133,7 @@ func (ck MkLineChecker) checkText(text s
                        instead = G.Pkgsrc.Deprecated[varcanon]
                }
                if instead != "" {
-                       mkline.Warnf("Use of %q is deprecated. %s", varname, instead)
+                       ck.MkLine.Warnf("Use of %q is deprecated. %s", varname, instead)
                }
        }
 }
@@ -1003,7 +1144,7 @@ func (ck MkLineChecker) checkDirectiveCo
                defer trace.Call1(mkline.Args())()
        }
 
-       p := NewMkParser(mkline.Line, mkline.Args(), false) // XXX: Why false?
+       p := NewMkParser(nil, mkline.Args(), false) // No emitWarnings here, see the code below.
        cond := p.MkCond()
        if !p.EOF() {
                mkline.Warnf("Invalid condition, unrecognized part: %q.", p.Rest())
@@ -1014,7 +1155,7 @@ func (ck MkLineChecker) checkDirectiveCo
                varname := varuse.varname
                if matches(varname, `^\$.*:[MN]`) {
                        mkline.Warnf("The empty() function takes a variable name as parameter, not a variable expression.")
-                       Explain(
+                       G.Explain(
                                "Instead of empty(${VARNAME:Mpattern}), you should write either",
                                "of the following:",
                                "",
@@ -1031,12 +1172,12 @@ func (ck MkLineChecker) checkDirectiveCo
                modifiers := varuse.modifiers
                for _, modifier := range modifiers {
                        if m, positive, pattern := modifier.MatchMatch(); m && (positive || len(modifiers) == 1) {
-                               ck.CheckVartype(varname, opUseMatch, pattern, "")
+                               ck.checkVartype(varname, opUseMatch, pattern, "")
 
                                vartype := G.Pkgsrc.VariableType(varname)
                                if matches(pattern, `^[\w-/]+$`) && vartype != nil && !vartype.IsConsideredList() {
                                        mkline.Notef("%s should be compared using == instead of the :M or :N modifier without wildcards.", varname)
-                                       Explain(
+                                       G.Explain(
                                                "This variable has a single value, not a list of values.  Therefore",
                                                "it feels strange to apply list operators like :M and :N onto it.",
                                                "A more direct approach is to use the == and != operators.",
@@ -1056,29 +1197,29 @@ func (ck MkLineChecker) checkDirectiveCo
                        ck.checkCompareVarStr(varname, op, value)
                } else if len(varmods) == 1 {
                        if m, _, _ := varmods[0].MatchMatch(); m && value != "" {
-                               ck.CheckVartype(varname, opUseMatch, value, "")
+                               ck.checkVartype(varname, opUseMatch, value, "")
                        }
                }
        }
 
        checkVarUse := func(varuse *MkVarUse) {
                var vartype *Vartype // TODO: Insert a better type guess here.
-               vuc := &VarUseContext{vartype, vucTimeParse, vucQuotPlain, false}
-               ck.CheckVaruse(varuse, vuc)
+               vuc := VarUseContext{vartype, vucTimeParse, vucQuotPlain, false}
+               ck.CheckVaruse(varuse, &vuc)
        }
 
-       NewMkCondWalker().Walk(cond, &MkCondCallback{
+       cond.Walk(&MkCondCallback{
                Empty:         checkEmpty,
                CompareVarStr: checkCompareVarStr,
                VarUse:        checkVarUse})
 }
 
 func (ck MkLineChecker) checkCompareVarStr(varname, op, value string) {
-       ck.CheckVartype(varname, opUseCompare, value, "")
+       ck.checkVartype(varname, opUseCompare, value, "")
 
        if varname == "PKGSRC_COMPILER" {
                ck.MkLine.Warnf("Use ${PKGSRC_COMPILER:%s%s} instead of the %s operator.", ifelseStr(op == "==", "M", "N"), value, op)
-               Explain(
+               G.Explain(
                        "The PKGSRC_COMPILER can be a list of chained compilers, e.g. \"ccache",
                        "distcc clang\".  Therefore, comparing it using == or != leads to",
                        "wrong results in these cases.")
@@ -1101,7 +1242,7 @@ func (ck MkLineChecker) CheckRelativePkg
 
        } else if !containsVarRef(pkgdir) {
                mkline.Warnf("%q is not a valid relative package directory.", pkgdir)
-               Explain(
+               G.Explain(
                        "A relative pathname always starts with \"../../\", followed",
                        "by a category, a slash and a the directory name of the package.",
                        "For example, \"../../misc/screen\" is a valid relative pathname.")
@@ -1125,7 +1266,7 @@ func (ck MkLineChecker) CheckRelativePat
 
        abs := resolvedPath
        if !hasPrefix(abs, "/") {
-               abs = path.Dir(mkline.FileName) + "/" + abs
+               abs = path.Dir(mkline.Filename) + "/" + abs
        }
        if _, err := os.Stat(abs); err != nil {
                if mustExist {
@@ -1136,11 +1277,12 @@ func (ck MkLineChecker) CheckRelativePat
 
        switch {
        case !hasPrefix(relativePath, "../"):
+               break
        case hasPrefix(relativePath, "../../mk/"):
                // From a package to the infrastructure.
        case matches(relativePath, `^\.\./\.\./[^/]+/[^/]`):
                // From a package to another package.
-       case hasPrefix(relativePath, "../mk/") && relpath(path.Dir(mkline.FileName), G.Pkgsrc.File(".")) == "..":
+       case hasPrefix(relativePath, "../mk/") && relpath(path.Dir(mkline.Filename), G.Pkgsrc.File(".")) == "..":
                // For category Makefiles.
        default:
                mkline.Warnf("Invalid relative path %q.", relativePath)

Index: pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.18 pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.19
--- pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go:1.18    Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/mklinechecker_test.go Sun Dec  2 01:57:48 2018
@@ -7,12 +7,12 @@ func (s *Suite) Test_MkLineChecker_Check
 
        t.SetupVartypes()
 
-       mkline := t.NewMkLine("fileName.mk", 1, "# url2pkg-marker")
+       mkline := t.NewMkLine("filename.mk", 1, "# url2pkg-marker")
 
        MkLineChecker{mkline}.Check()
 
        t.CheckOutputLines(
-               "ERROR: fileName.mk:1: This comment indicates unfinished work (url2pkg).")
+               "ERROR: filename.mk:1: This comment indicates unfinished work (url2pkg).")
 }
 
 func (s *Suite) Test_MkLineChecker_Check__buildlink3_include_prefs(c *check.C) {
@@ -42,7 +42,7 @@ func (s *Suite) Test_MkLineChecker_check
        t.CreateFileLines("graphics/jpeg/buildlink3.mk")
        t.CreateFileLines("devel/intltool/buildlink3.mk")
        t.CreateFileLines("devel/intltool/builtin.mk")
-       mklines := t.SetupFileMkLines("category/package/fileName.mk",
+       mklines := t.SetupFileMkLines("category/package/filename.mk",
                MkRcsID,
                "",
                ".include \"../../pkgtools/x11-links/buildlink3.mk\"",
@@ -53,12 +53,12 @@ func (s *Suite) Test_MkLineChecker_check
        mklines.Check()
 
        t.CheckOutputLines(
-               "ERROR: ~/category/package/fileName.mk:3: ../../pkgtools/x11-links/buildlink3.mk must not be included directly. "+
+               "ERROR: ~/category/package/filename.mk:3: ../../pkgtools/x11-links/buildlink3.mk must not be included directly. "+
                        "Include \"../../mk/x11.buildlink3.mk\" instead.",
-               "ERROR: ~/category/package/fileName.mk:4: ../../graphics/jpeg/buildlink3.mk must not be included directly. "+
+               "ERROR: ~/category/package/filename.mk:4: ../../graphics/jpeg/buildlink3.mk must not be included directly. "+
                        "Include \"../../mk/jpeg.buildlink3.mk\" instead.",
-               "WARN: ~/category/package/fileName.mk:5: Please write \"USE_TOOLS+= intltool\" instead of this line.",
-               "ERROR: ~/category/package/fileName.mk:6: ../../devel/intltool/builtin.mk must not be included directly. "+
+               "WARN: ~/category/package/filename.mk:5: Please write \"USE_TOOLS+= intltool\" instead of this line.",
+               "ERROR: ~/category/package/filename.mk:6: ../../devel/intltool/builtin.mk must not be included directly. "+
                        "Include \"../../devel/intltool/buildlink3.mk\" instead.")
 }
 
@@ -79,7 +79,7 @@ func (s *Suite) Test_MkLineChecker_check
 
        t.SetupVartypes()
 
-       mklines := t.NewMkLines("category/package/fileName.mk",
+       mklines := t.NewMkLines("category/package/filename.mk",
                MkRcsID,
                "",
                ".for",
@@ -107,15 +107,15 @@ func (s *Suite) Test_MkLineChecker_check
        mklines.Check()
 
        t.CheckOutputLines(
-               "ERROR: category/package/fileName.mk:3: \".for\" requires arguments.",
-               "ERROR: category/package/fileName.mk:6: \".if\" requires arguments.",
-               "ERROR: category/package/fileName.mk:7: \".else\" does not take arguments. If you meant \"else if\", use \".elif\".",
-               "ERROR: category/package/fileName.mk:8: \".endif\" does not take arguments.",
-               "WARN: category/package/fileName.mk:10: The \".ifdef\" directive is deprecated. Please use \".if defined(FNAME_MK)\" instead.",
-               "WARN: category/package/fileName.mk:12: The \".ifndef\" directive is deprecated. Please use \".if !defined(FNAME_MK)\" instead.",
-               "NOTE: category/package/fileName.mk:17: Using \".undef\" after a \".for\" loop is unnecessary.",
-               "WARN: category/package/fileName.mk:19: .for variable names should not contain uppercase letters.",
-               "ERROR: category/package/fileName.mk:22: Invalid variable name \"$\".")
+               "ERROR: category/package/filename.mk:3: \".for\" requires arguments.",
+               "ERROR: category/package/filename.mk:6: \".if\" requires arguments.",
+               "ERROR: category/package/filename.mk:7: \".else\" does not take arguments. If you meant \"else if\", use \".elif\".",
+               "ERROR: category/package/filename.mk:8: \".endif\" does not take arguments.",
+               "WARN: category/package/filename.mk:10: The \".ifdef\" directive is deprecated. Please use \".if defined(FNAME_MK)\" instead.",
+               "WARN: category/package/filename.mk:12: The \".ifndef\" directive is deprecated. Please use \".if !defined(FNAME_MK)\" instead.",
+               "NOTE: category/package/filename.mk:17: Using \".undef\" after a \".for\" loop is unnecessary.",
+               "WARN: category/package/filename.mk:19: .for variable names should not contain uppercase letters.",
+               "ERROR: category/package/filename.mk:22: Invalid variable name \"$\".")
 }
 
 func (s *Suite) Test_MkLineChecker_checkDependencyRule(c *check.C) {
@@ -123,7 +123,7 @@ func (s *Suite) Test_MkLineChecker_check
 
        t.SetupVartypes()
 
-       mklines := t.NewMkLines("category/package/fileName.mk",
+       mklines := t.NewMkLines("category/package/filename.mk",
                MkRcsID,
                "",
                ".PHONY: target-1",
@@ -136,10 +136,10 @@ func (s *Suite) Test_MkLineChecker_check
        mklines.Check()
 
        t.CheckOutputLines(
-               "WARN: category/package/fileName.mk:8: Unusual target \"target-3\".")
+               "WARN: category/package/filename.mk:8: Unusual target \"target-3\".")
 }
 
-func (s *Suite) Test_MkLineChecker_CheckVartype__simple_type(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkVartype__simple_type(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wtypes")
@@ -156,40 +156,41 @@ func (s *Suite) Test_MkLineChecker_Check
        c.Check(vartype.guessed, equals, false)
        c.Check(vartype.kindOfList, equals, lkNone)
 
-       MkLineChecker{dummyMkLine}.CheckVartype("COMMENT", opAssign, "A nice package", "")
+       mkline := t.NewMkLine("Makefile", 123, "COMMENT=\tA nice package")
+       MkLineChecker{mkline}.checkVartype(mkline.Varname(), mkline.Op(), mkline.Value(), mkline.VarassignComment())
 
        t.CheckOutputLines(
-               "WARN: COMMENT should not begin with \"A\".")
+               "WARN: Makefile:123: COMMENT should not begin with \"A\".")
 }
 
-func (s *Suite) Test_MkLineChecker_CheckVartype(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkVartype(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
-       mkline := t.NewMkLine("fileName", 1, "DISTNAME=gcc-${GCC_VERSION}")
+       mkline := t.NewMkLine("filename", 1, "DISTNAME=gcc-${GCC_VERSION}")
 
-       MkLineChecker{mkline}.CheckVartype("DISTNAME", opAssign, "gcc-${GCC_VERSION}", "")
+       MkLineChecker{mkline}.checkVartype("DISTNAME", opAssign, "gcc-${GCC_VERSION}", "")
 
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_MkLineChecker_CheckVartype__skip(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkVartype__skip(c *check.C) {
        t := s.Init(c)
 
        t.SetupCommandLine("-Wno-types")
        t.SetupVartypes()
-       mkline := t.NewMkLine("fileName", 1, "DISTNAME=invalid:::distname")
+       mkline := t.NewMkLine("filename", 1, "DISTNAME=invalid:::distname")
 
        MkLineChecker{mkline}.Check()
 
        t.CheckOutputEmpty()
 }
 
-func (s *Suite) Test_MkLineChecker_CheckVartype__append_to_non_list(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkVartype__append_to_non_list(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
-       mklines := t.NewMkLines("fileName.mk",
+       mklines := t.NewMkLines("filename.mk",
                MkRcsID,
                "DISTNAME+=\tsuffix",
                "COMMENT=\tComment for",
@@ -198,8 +199,8 @@ func (s *Suite) Test_MkLineChecker_Check
        mklines.Check()
 
        t.CheckOutputLines(
-               "WARN: fileName.mk:2: The variable DISTNAME may not be appended to (only set, given a default value) in this file.",
-               "WARN: fileName.mk:2: The \"+=\" operator should only be used with lists, not with DISTNAME.")
+               "WARN: filename.mk:2: The variable DISTNAME may not be appended to (only set, given a default value) in this file.",
+               "WARN: filename.mk:2: The \"+=\" operator should only be used with lists, not with DISTNAME.")
 }
 
 // Pkglint once interpreted all lists as consisting of shell tokens,
@@ -209,7 +210,7 @@ func (s *Suite) Test_MkLineChecker_check
 
        G.Pkg = NewPackage(t.File("graphics/gimp-fix-ca"))
        t.SetupVartypes()
-       mkline := t.NewMkLine("fileName", 10, "MASTER_SITES=http://registry.gimp.org/file/fix-ca.c?action=download&id=9884&file=";)
+       mkline := t.NewMkLine("filename", 10, "MASTER_SITES=http://registry.gimp.org/file/fix-ca.c?action=download&id=9884&file=";)
 
        MkLineChecker{mkline}.checkVarassign()
 
@@ -223,7 +224,7 @@ func (s *Suite) Test_MkLineChecker_Check
        t.SetupVartypes()
 
        testCond := func(cond string, output ...string) {
-               MkLineChecker{t.NewMkLine("fileName", 1, cond)}.checkDirectiveCond()
+               MkLineChecker{t.NewMkLine("filename", 1, cond)}.checkDirectiveCond()
                if len(output) > 0 {
                        t.CheckOutputLines(output...)
                } else {
@@ -232,25 +233,25 @@ func (s *Suite) Test_MkLineChecker_Check
        }
 
        testCond(".if !empty(PKGSRC_COMPILER:Mmycc)",
-               "WARN: fileName:1: The pattern \"mycc\" cannot match any of "+
+               "WARN: filename:1: The pattern \"mycc\" cannot match any of "+
                        "{ ccache ccc clang distcc f2c gcc hp icc ido "+
                        "mipspro mipspro-ucode pcc sunpro xlc } for PKGSRC_COMPILER.")
 
        testCond(".elif ${A} != ${B}")
 
        testCond(".if ${HOMEPAGE} == \"mailto:someone%example.org@localhost\"";,
-               "WARN: fileName:1: \"mailto:someone%example.org@localhost\"; is not a valid URL.")
+               "WARN: filename:1: \"mailto:someone%example.org@localhost\"; is not a valid URL.")
 
        testCond(".if !empty(PKGSRC_RUN_TEST:M[Y][eE][sS])",
-               "WARN: fileName:1: PKGSRC_RUN_TEST should be matched against \"[yY][eE][sS]\" or \"[nN][oO]\", not \"[Y][eE][sS]\".")
+               "WARN: filename:1: PKGSRC_RUN_TEST should be matched against \"[yY][eE][sS]\" or \"[nN][oO]\", not \"[Y][eE][sS]\".")
 
        testCond(".if !empty(IS_BUILTIN.Xfixes:M[yY][eE][sS])")
 
        testCond(".if !empty(${IS_BUILTIN.Xfixes:M[yY][eE][sS]})",
-               "WARN: fileName:1: The empty() function takes a variable name as parameter, not a variable expression.")
+               "WARN: filename:1: The empty() function takes a variable name as parameter, not a variable expression.")
 
        testCond(".if ${EMUL_PLATFORM} == \"linux-x386\"",
-               "WARN: fileName:1: "+
+               "WARN: filename:1: "+
                        "\"x386\" is not valid for the hardware architecture part of EMUL_PLATFORM. "+
                        "Use one of "+
                        "{ aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 cobalt coldfire convex "+
@@ -261,7 +262,7 @@ func (s *Suite) Test_MkLineChecker_Check
                        "} instead.")
 
        testCond(".if ${EMUL_PLATFORM:Mlinux-x386}",
-               "WARN: fileName:1: "+
+               "WARN: filename:1: "+
                        "The pattern \"x386\" cannot match any of { aarch64 aarch64eb alpha amd64 arc arm arm26 "+
                        "arm32 cobalt coldfire convex dreamcast earm earmeb earmhf earmhfeb earmv4 earmv4eb "+
                        "earmv5 earmv5eb earmv6 earmv6eb earmv6hf earmv6hfeb earmv7 earmv7eb earmv7hf "+
@@ -269,15 +270,15 @@ func (s *Suite) Test_MkLineChecker_Check
                        "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 EMUL_PLATFORM.",
-               "NOTE: fileName:1: EMUL_PLATFORM should be compared using == instead of the :M or :N modifier without wildcards.")
+               "NOTE: filename:1: EMUL_PLATFORM should be compared using == instead of the :M or :N modifier without wildcards.")
 
        testCond(".if ${MACHINE_PLATFORM:MUnknownOS-*-*} || ${MACHINE_ARCH:Mx86}",
-               "WARN: fileName:1: "+
+               "WARN: filename:1: "+
                        "The pattern \"UnknownOS\" cannot match any of "+
                        "{ AIX BSDOS Bitrig Cygwin Darwin DragonFly FreeBSD FreeMiNT GNUkFreeBSD HPUX Haiku "+
                        "IRIX Interix Linux Minix MirBSD NetBSD OSF1 OpenBSD QNX SCO_SV SunOS UnixWare "+
                        "} for the operating system part of MACHINE_PLATFORM.",
-               "WARN: fileName:1: "+
+               "WARN: filename:1: "+
                        "The pattern \"x86\" cannot match any of "+
                        "{ aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 cobalt coldfire convex dreamcast earm "+
                        "earmeb earmhf earmhfeb earmv4 earmv4eb earmv5 earmv5eb earmv6 earmv6eb earmv6hf earmv6hfeb "+
@@ -285,7 +286,7 @@ func (s *Suite) Test_MkLineChecker_Check
                        "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 MACHINE_ARCH.",
-               "NOTE: fileName:1: MACHINE_ARCH should be compared using == instead of the :M or :N modifier without wildcards.")
+               "NOTE: filename:1: MACHINE_ARCH should be compared using == instead of the :M or :N modifier without wildcards.")
 
        testCond(".if ${MASTER_SITES:Mftp://*} == \"ftp://netbsd.org/\"";)
 }
@@ -339,6 +340,20 @@ func (s *Suite) Test_MkLineChecker_check
        t.CheckOutputEmpty()
 }
 
+func (s *Suite) Test_MkLineChecker_checkVarassignVaruse(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupVartypes()
+
+       mkline := t.NewMkLine("module.mk", 123, "PLIST_SUBST+=\tLOCALBASE=${LOCALBASE:Q}")
+
+       MkLineChecker{mkline}.checkVarassignVaruse()
+
+       t.CheckOutputLines(
+               "WARN: module.mk:123: Please use PREFIX instead of LOCALBASE.",
+               "NOTE: module.mk:123: The :Q operator isn't necessary for ${LOCALBASE} here.")
+}
+
 func (s *Suite) Test_MkLineChecker_checkVarusePermissions(c *check.C) {
        t := s.Init(c)
 
@@ -509,7 +524,7 @@ func (s *Suite) Test_MkLineChecker_check
                "WARN: audio/pulseaudio/Makefile:2: Use ${PKGSRC_COMPILER:Mclang} instead of the == operator.")
 }
 
-func (s *Suite) Test_MkLineChecker_CheckVartype__CFLAGS_with_backticks(c *check.C) {
+func (s *Suite) Test_MkLineChecker_checkVartype__CFLAGS_with_backticks(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
@@ -523,7 +538,7 @@ func (s *Suite) Test_MkLineChecker_Check
        c.Check(words, deepEquals, []string{"`pkg-config pidgin --cflags`"})
        c.Check(rest, equals, "")
 
-       MkLineChecker{G.Mk.mklines[1]}.CheckVartype("CFLAGS", opAssignAppend, "`pkg-config pidgin --cflags`", "")
+       MkLineChecker{G.Mk.mklines[1]}.checkVartype("CFLAGS", opAssignAppend, "`pkg-config pidgin --cflags`", "")
 
        // No warning about "`pkg-config" being an unknown CFlag.
        t.CheckOutputEmpty()
@@ -532,7 +547,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()
@@ -656,7 +671,7 @@ func (s *Suite) Test_MkLineChecker_Check
        G.CheckDirent(pkg)
 
        t.CheckOutputLines(
-               "WARN: ~/category/package/Makefile:5: The :Q operator should not be used for ${HOMEPAGE} here.")
+               "NOTE: ~/category/package/Makefile:5: The :Q operator isn't necessary for ${HOMEPAGE} here.")
 }
 
 // The ${VARNAME:=suffix} expression should only be used with lists.
@@ -693,6 +708,38 @@ func (s *Suite) Test_MkLineChecker_Check
        t.CheckOutputEmpty()
 }
 
+// When a parameterized variable is defined in the pkgsrc infrastructure,
+// it does not generate a warning about being "used but not defined".
+// Even if the variable parameter differs, like .Linux and .SunOS in this
+// case. This pattern is typical for pkgsrc, therefore pkglint doesn't
+// check that the variable names match exactly.
+func (s *Suite) Test_MkLineChecker_CheckVaruse__varcanon(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupVartypes()
+       t.SetupPkgsrc()
+       t.CreateFileLines("mk/sys-vars.mk",
+               MkRcsID,
+               "CPPPATH.Linux=\t/usr/bin/cpp")
+       G.Pkgsrc.LoadInfrastructure()
+
+       ck := MkLineChecker{t.NewMkLine("module.mk", 101, "COMMENT=\t${CPPPATH.SunOS}")}
+
+       ck.CheckVaruse(NewMkVarUse("CPPPATH.SunOS"), &VarUseContext{
+               vartype: &Vartype{
+                       kindOfList: lkNone,
+                       basicType:  BtPathname,
+                       aclEntries: nil,
+                       guessed:    true,
+               },
+               time:       vucTimeRun,
+               quoting:    vucQuotPlain,
+               IsWordPart: false,
+       })
+
+       t.CheckOutputEmpty()
+}
+
 func (s *Suite) Test_MkLineChecker_CheckVaruse__defined_in_infrastructure(c *check.C) {
        t := s.Init(c)
 
@@ -727,8 +774,9 @@ func (s *Suite) Test_MkLineChecker_Check
        t.SetupVartypes()
        mklines := t.SetupFileMkLines("options.mk",
                MkRcsID,
-               "COMMENT=        ${VARBASE} ${X11_TYPE}",
-               "BUILD_DEFS+=    X11_TYPE")
+               "COMMENT=                ${VARBASE} ${X11_TYPE}",
+               "PKG_FAIL_REASON+=       ${VARBASE} ${X11_TYPE}",
+               "BUILD_DEFS+=            X11_TYPE")
 
        mklines.Check()
 
@@ -754,6 +802,22 @@ func (s *Suite) Test_MkLineChecker_Check
                "+\tCC:=\t${CC:[1]}")
 }
 
+func (s *Suite) Test_MkLineChecker_CheckVaruse__deprecated_PKG_DEBUG(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupVartypes()
+       G.Pkgsrc.initDeprecatedVars()
+
+       mkline := t.NewMkLine("module.mk", 123,
+               "\t${_PKG_SILENT}${_PKG_DEBUG} :")
+
+       MkLineChecker{mkline}.Check()
+
+       t.CheckOutputLines(
+               "WARN: module.mk:123: Use of \"_PKG_SILENT\" is deprecated. Use RUN (with more error checking) instead.",
+               "WARN: module.mk:123: Use of \"_PKG_DEBUG\" is deprecated. Use RUN (with more error checking) instead.")
+}
+
 func (s *Suite) Test_MkLineChecker_checkVarassignSpecific(c *check.C) {
        t := s.Init(c)
 
@@ -780,8 +844,8 @@ func (s *Suite) Test_MkLineChecker_check
                "WARN: ~/module.mk:2: Please use the RCD_SCRIPTS mechanism to install rc.d scripts automatically to ${RCD_SCRIPTS_EXAMPLEDIR}.",
                "WARN: ~/module.mk:3: _TOOLS_VARNAME.sed is defined but not used.",
                "WARN: ~/module.mk:3: Variable names starting with an underscore (_TOOLS_VARNAME.sed) are reserved for internal pkgsrc use.",
-               "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: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:7: The variable PYTHON_VERSIONS_ACCEPTED may not be set "+
@@ -812,6 +876,36 @@ func (s *Suite) Test_MkLineChecker_check
                "WARN: ~/module.mk:3: Use of \"GAMEGRP\" is deprecated. Use GAMES_GROUP instead.")
 }
 
+func (s *Suite) Test_MkLineChecker_checkText__WRKSRC(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupCommandLine("-Wall", "--explain")
+       mklines := t.SetupFileMkLines("module.mk",
+               MkRcsID,
+               "pre-configure:",
+               "\tcd ${WRKSRC}/..")
+
+       mklines.Check()
+
+       t.CheckOutputLines(
+               "WARN: ~/module.mk:3: Building the package should take place entirely inside ${WRKSRC}, not \"${WRKSRC}/..\".",
+               "",
+               "\tWRKSRC should be defined so that there is no need to do anything",
+               "\toutside of this directory.",
+               "",
+               "\tExample:",
+               "",
+               "\t\tWRKSRC=\t${WRKDIR}",
+               "\t\tCONFIGURE_DIRS=\t${WRKSRC}/lib ${WRKSRC}/src",
+               "\t\tBUILD_DIRS=\t${WRKSRC}/lib ${WRKSRC}/src ${WRKSRC}/cmd",
+               "",
+               "\tSee the pkgsrc guide, section \"Directories used during the build",
+               "\tprocess\":",
+               "\thttps://www.NetBSD.org/docs/pkgsrc/pkgsrc.html#build.builddirs";,
+               "",
+               "WARN: ~/module.mk:3: WRKSRC is used but not defined.")
+}
+
 func (s *Suite) Test_MkLineChecker_CheckRelativePath(c *check.C) {
        t := s.Init(c)
 

Index: pkgsrc/pkgtools/pkglint/files/mklines.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines.go:1.34 pkgsrc/pkgtools/pkglint/files/mklines.go:1.35
--- pkgsrc/pkgtools/pkglint/files/mklines.go:1.34       Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/mklines.go    Sun Dec  2 01:57:48 2018
@@ -87,7 +87,7 @@ func (mklines *MkLinesImpl) checkAll() {
                return targets
        }()
 
-       CheckLineRcsid(mklines.lines.Lines[0], `#[\t ]+`, "# ")
+       mklines.lines.CheckRcsID(0, `#[\t ]+`, "# ")
 
        substContext := NewSubstContext()
        var varalign VaralignBlock
@@ -114,7 +114,7 @@ func (mklines *MkLinesImpl) checkAll() {
 
                        switch mkline.Varcanon() {
                        case "PLIST_VARS":
-                               ids := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "")
+                               ids := mkline.ValueFields(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)
@@ -234,7 +234,7 @@ func (mklines *MkLinesImpl) DetermineDef
                        }
 
                case "PLIST_VARS":
-                       ids := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "")
+                       ids := mkline.ValueFields(resolveVariableRefs(mkline.Value()))
                        for _, id := range ids {
                                if trace.Tracing {
                                        trace.Step1("PLIST.%s is added to PLIST_VARS.", id)
@@ -269,7 +269,7 @@ func (mklines *MkLinesImpl) collectPlist
                if mkline.IsVarassign() {
                        switch mkline.Varcanon() {
                        case "PLIST_VARS":
-                               ids := mkline.ValueSplit(resolveVariableRefs(mkline.Value()), "")
+                               ids := mkline.ValueFields(resolveVariableRefs(mkline.Value()))
                                for _, id := range ids {
                                        if containsVarRef(id) {
                                                mklines.plistVarSkip = true
@@ -333,12 +333,12 @@ func (mklines *MkLinesImpl) determineDoc
 
                        commentLines++
 
-                       parser := NewMkParser(mkline.Line, words[1], false)
+                       parser := NewMkParser(nil, words[1], false)
                        varname := parser.Varname()
-                       if hasSuffix(varname, ".") && parser.repl.AdvanceRegexp(`^<\w+>`) {
+                       if hasSuffix(varname, ".") && parser.lexer.SkipRegexp(G.res.Compile(`^<\w+>`)) {
                                varname += "*"
                        }
-                       parser.repl.AdvanceStr(":")
+                       parser.lexer.SkipByte(':')
 
                        varbase := varnameBase(varname)
                        if varbase == strings.ToUpper(varbase) && matches(varbase, `[A-Z]`) && parser.EOF() {
@@ -376,7 +376,7 @@ func (mklines *MkLinesImpl) CheckRedunda
        scope.OnOverwrite = func(old, new MkLine) {
                if isRelevant(old, new) {
                        old.Warnf("Variable %s is overwritten in %s.", new.Varname(), old.RefTo(new))
-                       Explain(
+                       G.Explain(
                                "The variable definition in this line does not have an effect since",
                                "it is overwritten elsewhere.  This typically happens because of a",
                                "typo (writing = instead of +=) or because the line that overwrites",

Index: pkgsrc/pkgtools/pkglint/files/mklines_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.30 pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.31
--- pkgsrc/pkgtools/pkglint/files/mklines_test.go:1.30  Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/mklines_test.go       Sun Dec  2 01:57:48 2018
@@ -10,7 +10,7 @@ func (s *Suite) Test_MkLines_Check__auto
        t := s.Init(c)
 
        t.SetupCommandLine("--autofix", "-Wspace")
-       lines := t.SetupFileLines("fileName.mk",
+       lines := t.SetupFileLines("filename.mk",
                MkRcsID,
                ".if defined(A)",
                ".for a in ${A}",
@@ -23,11 +23,11 @@ func (s *Suite) Test_MkLines_Check__auto
        mklines.Check()
 
        t.CheckOutputLines(
-               "AUTOFIX: ~/fileName.mk:3: Replacing \".\" with \".  \".",
-               "AUTOFIX: ~/fileName.mk:4: Replacing \".\" with \".    \".",
-               "AUTOFIX: ~/fileName.mk:5: Replacing \".\" with \".    \".",
-               "AUTOFIX: ~/fileName.mk:6: Replacing \".\" with \".  \".")
-       t.CheckFileLines("fileName.mk",
+               "AUTOFIX: ~/filename.mk:3: Replacing \".\" with \".  \".",
+               "AUTOFIX: ~/filename.mk:4: Replacing \".\" with \".    \".",
+               "AUTOFIX: ~/filename.mk:5: Replacing \".\" with \".    \".",
+               "AUTOFIX: ~/filename.mk:6: Replacing \".\" with \".  \".")
+       t.CheckFileLines("filename.mk",
                "# $"+"NetBSD$",
                ".if defined(A)",
                ".  for a in ${A}",
@@ -140,6 +140,11 @@ func (s *Suite) Test_MkLines__varuse_sh_
        t.CheckOutputEmpty()
 }
 
+// For parameterized variables, the "defined but not used" and
+// the "used but not defined" checks are loosened a bit.
+// When VAR.param1 is defined or used, VAR.param2 is also regarded
+// as defined or used since often in pkgsrc, parameterized variables
+// are not referred to by their exact names but by VAR.${param}.
 func (s *Suite) Test_MkLines__varuse_parameterized(c *check.C) {
        t := s.Init(c)
 
@@ -151,8 +156,9 @@ func (s *Suite) Test_MkLines__varuse_par
 
        mklines.Check()
 
-       // No warnings about defined but not used or vice versa
-       t.CheckOutputEmpty()
+       // No warnings about CONFIGURE_ARGS.* being defined but not used or vice versa.
+       t.CheckOutputLines(
+               "WARN: converters/wv2/Makefile:2: ICONV_TYPE is used but not defined.")
 }
 
 // Even very complicated shell commands are parsed correctly.
@@ -339,7 +345,8 @@ func (s *Suite) Test_MkLines_DetermineDe
        // The OSV.NetBSD variable is used implicitly via the OSV variable, therefore no warning.
        t.CheckOutputLines(
                // FIXME: the below warning is wrong; it's ok to have SUBST blocks in all files, maybe except buildlink3.mk.
-               "WARN: determine-defined-variables.mk:12: The variable SUBST_VARS.subst 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: determine-defined-variables.mk:12: The variable SUBST_VARS.subst 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: determine-defined-variables.mk:16: Unknown shell command \"unknown-command\".")
 }
 
@@ -371,7 +378,7 @@ func (s *Suite) Test_MkLines_DetermineDe
 func (s *Suite) Test_MkLines_DetermineUsedVariables__simple(c *check.C) {
        t := s.Init(c)
 
-       mklines := t.NewMkLines("fileName",
+       mklines := t.NewMkLines("filename",
                "\t${VAR}")
        mkline := mklines.mklines[0]
        G.Mk = mklines
@@ -385,49 +392,57 @@ func (s *Suite) Test_MkLines_DetermineUs
 func (s *Suite) Test_MkLines_DetermineUsedVariables__nested(c *check.C) {
        t := s.Init(c)
 
-       mklines := t.NewMkLines("fileName",
+       mklines := t.NewMkLines("filename.mk",
+               MkRcsID,
+               "",
+               "LHS.${lparam}=\tRHS.${rparam}",
+               "",
+               "target:",
                "\t${outer.${inner}}")
-       mkline := mklines.mklines[0]
+       assignMkline := mklines.mklines[2]
+       shellMkline := mklines.mklines[5]
        G.Mk = mklines
 
        mklines.DetermineUsedVariables()
 
-       c.Check(len(mklines.vars.used), equals, 3)
-       c.Check(mklines.vars.FirstUse("inner"), equals, mkline)
-       c.Check(mklines.vars.FirstUse("outer.*"), equals, mkline)
-       c.Check(mklines.vars.FirstUse("outer.${inner}"), equals, mkline)
+       c.Check(len(mklines.vars.used), equals, 5)
+       c.Check(mklines.vars.FirstUse("lparam"), equals, assignMkline)
+       c.Check(mklines.vars.FirstUse("rparam"), equals, assignMkline)
+       c.Check(mklines.vars.FirstUse("inner"), equals, shellMkline)
+       c.Check(mklines.vars.FirstUse("outer.*"), equals, shellMkline)
+       c.Check(mklines.vars.FirstUse("outer.${inner}"), equals, shellMkline)
 }
 
 func (s *Suite) Test_MkLines__private_tool_undefined(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
-       mklines := t.NewMkLines("fileName",
+       mklines := t.NewMkLines("filename",
                MkRcsID,
                "",
-               "\tmd5sum fileName")
+               "\tmd5sum filename")
 
        mklines.Check()
 
        t.CheckOutputLines(
-               "WARN: fileName:3: Unknown shell command \"md5sum\".")
+               "WARN: filename:3: Unknown shell command \"md5sum\".")
 }
 
 func (s *Suite) Test_MkLines__private_tool_defined(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
-       mklines := t.NewMkLines("fileName",
+       mklines := t.NewMkLines("filename",
                MkRcsID,
                "TOOLS_CREATE+=\tmd5sum",
                "",
-               "\tmd5sum fileName")
+               "\tmd5sum filename")
 
        mklines.Check()
 
        // TODO: Is it necessary to add the tool to USE_TOOLS? If not, why not?
        t.CheckOutputLines(
-               "WARN: fileName:4: The \"md5sum\" tool is used but not added to USE_TOOLS.")
+               "WARN: filename:4: The \"md5sum\" tool is used but not added to USE_TOOLS.")
 }
 
 func (s *Suite) Test_MkLines_Check__indentation(c *check.C) {
@@ -613,12 +628,12 @@ func (s *Suite) Test_MkLines__wip_catego
                "WARN: ~/wip/Makefile:14: Unusual target \"clean-tmpdir\".",
                "",
                "\tIf you want to define your own target, declare it like this:",
-               "\t",
+               "",
                "\t\t.PHONY: my-target",
-               "\t",
-               "\tIn the rare case that you actually want a file-based make(1)",
-               "\ttarget, write it like this:",
-               "\t",
+               "",
+               "\tIn the rare case that you actually want a file-based make(1) target,",
+               "\twrite it like this:",
+               "",
                "\t\t${.CURDIR}/my-file:",
                "")
 }
@@ -971,8 +986,10 @@ func (s *Suite) Test_MkLines_Check__MAST
        G.Mk.Check()
 
        t.CheckOutputLines(
-               "WARN: devel/catch/Makefile:2: HOMEPAGE should not be defined in terms of MASTER_SITEs. Use https://github.com/philsquared/Catch/ directly.",
-               "WARN: devel/catch/Makefile:3: HOMEPAGE should not be defined in terms of MASTER_SITEs. Use https://github.com/ directly.",
+               "WARN: devel/catch/Makefile:2: HOMEPAGE should not be defined in terms of MASTER_SITEs. "+
+                       "Use https://github.com/philsquared/Catch/ directly.",
+               "WARN: devel/catch/Makefile:3: HOMEPAGE should not be defined in terms of MASTER_SITEs. "+
+                       "Use https://github.com/ directly.",
                "WARN: devel/catch/Makefile:4: HOMEPAGE should not be defined in terms of MASTER_SITEs.",
                "WARN: devel/catch/Makefile:5: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
 }
@@ -1031,7 +1048,7 @@ func (s *Suite) Test_MkLines_Check__extr
 
        t.CheckOutputLines(
                "WARN: options.mk:3: The values for PYTHON_VERSIONS_ACCEPTED should be in decreasing order.",
-               "NOTE: options.mk:5: Please use \"# empty\", \"# none\" or \"yes\" instead of \"# defined\".",
+               "NOTE: options.mk:5: Please use \"# empty\", \"# none\" or \"# yes\" instead of \"# defined\".",
                "WARN: options.mk:7: Please include \"../../mk/bsd.prefs.mk\" before using \"?=\".",
                "WARN: options.mk:11: Building the package should take place entirely inside ${WRKSRC}, not \"${WRKSRC}/..\".",
                "NOTE: options.mk:11: You can use \"../build\" instead of \"${WRKSRC}/../build\".")
@@ -1078,7 +1095,7 @@ func (s *Suite) Test_VaralignBlock_Check
 
        t.SetupCommandLine("-Wspace", "--show-autofix")
 
-       lines := t.NewLines("file.mk",
+       mklines := t.NewMkLines("file.mk",
                "VAR=   value",    // Indentation 7, fixed to 8.
                "",                //
                "VAR=    value",   // Indentation 8, fixed to 8.
@@ -1093,9 +1110,9 @@ func (s *Suite) Test_VaralignBlock_Check
                "",                //
                "VAR=\tvalue")     // Already aligned with tabs only, left unchanged.
 
-       varalign := &VaralignBlock{}
-       for _, line := range lines.Lines {
-               varalign.Check(NewMkLine(line))
+       var varalign VaralignBlock
+       for _, line := range mklines.mklines {
+               varalign.Check(line)
        }
        varalign.Finish()
 
@@ -1114,10 +1131,11 @@ func (s *Suite) Test_VaralignBlock_Check
                "AUTOFIX: file.mk:11: Replacing \"    \\t\" with \"\\t\\t\".")
 }
 
+// When the lines of a paragraph are inconsistently aligned,
+// they are realigned to the minimum required width.
 func (s *Suite) Test_VaralignBlock_Check__reduce_indentation(c *check.C) {
        t := s.Init(c)
 
-       t.SetupCommandLine("-Wspace")
        mklines := t.NewMkLines("file.mk",
                "VAR= \tvalue",
                "VAR=    \tvalue",
@@ -1127,7 +1145,7 @@ func (s *Suite) Test_VaralignBlock_Check
                "VAR=\t\t\tdeep",
                "VAR=\t\t\tindentation")
 
-       varalign := new(VaralignBlock)
+       var varalign VaralignBlock
        for _, mkline := range mklines.mklines {
                varalign.Check(mkline)
        }
@@ -1139,6 +1157,9 @@ func (s *Suite) Test_VaralignBlock_Check
                "NOTE: file.mk:3: This variable value should be aligned to column 9.")
 }
 
+// For every variable assignment, there is at least one space or tab between the variable
+// name and the value. Even if it is the longest line, and even if the value would start
+// exactly at a tab stop.
 func (s *Suite) Test_VaralignBlock_Check__longest_line_no_space(c *check.C) {
        t := s.Init(c)
 
@@ -1147,9 +1168,9 @@ func (s *Suite) Test_VaralignBlock_Check
                "SUBST_CLASSES+= aaaaaaaa",
                "SUBST_STAGE.aaaaaaaa= pre-configure",
                "SUBST_FILES.aaaaaaaa= *.pl",
-               "SUBST_FILTER_CMD.aaaaaaaa=cat")
+               "SUBST_FILTER_CMD.aaaaaa=cat")
 
-       varalign := new(VaralignBlock)
+       var varalign VaralignBlock
        for _, mkline := range mklines.mklines {
                varalign.Check(mkline)
        }
@@ -1172,7 +1193,7 @@ func (s *Suite) Test_VaralignBlock_Check
                "SUBST_FILES.aaaaaaaa= *.pl",
                "SUBST_FILTER_CMD.aaaaaaaa= cat")
 
-       varalign := new(VaralignBlock)
+       var varalign VaralignBlock
        for _, mkline := range mklines.mklines {
                varalign.Check(mkline)
        }

Index: pkgsrc/pkgtools/pkglint/files/mkshparser.go
diff -u pkgsrc/pkgtools/pkglint/files/mkshparser.go:1.8 pkgsrc/pkgtools/pkglint/files/mkshparser.go:1.9
--- pkgsrc/pkgtools/pkglint/files/mkshparser.go:1.8     Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/mkshparser.go Sun Dec  2 01:57:48 2018
@@ -12,7 +12,7 @@ func parseShellProgram(line Line, progra
 
        tokens, rest := splitIntoShellTokens(line, program)
        lexer := NewShellLexer(tokens, rest)
-       parser := &shyyParserImpl{}
+       parser := shyyParserImpl{}
 
        succeeded := parser.Parse(lexer)
 

Index: pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go
diff -u pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go:1.4 pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go:1.5
--- pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go:1.4        Wed Oct  3 22:27:53 2018
+++ pkgsrc/pkgtools/pkglint/files/mkshwalker_test.go    Sun Dec  2 01:57:48 2018
@@ -87,7 +87,9 @@ func (s *Suite) Test_MkShWalker_Walk(c *
                        "        Pipeline with 1 commands",
                        "         Command ",
                        "   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",
+                       "            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",
@@ -164,7 +166,9 @@ func (s *Suite) Test_MkShWalker_Walk(c *
                        "        Pipeline with 1 commands",
                        "         Command ",
                        "   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",
+                       "            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 >&",

Index: pkgsrc/pkgtools/pkglint/files/package.go
diff -u pkgsrc/pkgtools/pkglint/files/package.go:1.38 pkgsrc/pkgtools/pkglint/files/package.go:1.39
--- pkgsrc/pkgtools/pkglint/files/package.go:1.38       Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/package.go    Sun Dec  2 01:57:48 2018
@@ -26,7 +26,7 @@ type Package struct {
 
        vars                  Scope
        bl3                   map[string]Line // buildlink3.mk name => line; contains only buildlink3.mk files that are directly included.
-       included              map[string]Line // fileName => line
+       included              map[string]Line // filename => line
        seenMakefileCommon    bool            // Does the package have any .includes?
        conditionalIncludes   map[string]MkLine
        unconditionalIncludes map[string]MkLine
@@ -40,7 +40,7 @@ func NewPackage(dir string) *Package {
                panic(fmt.Sprintf("Package directory %q must be two subdirectories below the pkgsrc root %q.", dir, G.Pkgsrc.File(".")))
        }
 
-       pkg := &Package{
+       pkg := Package{
                dir:                   dir,
                Pkgpath:               pkgpath,
                Pkgdir:                ".",
@@ -64,7 +64,7 @@ func NewPackage(dir string) *Package {
        pkg.vars.Fallback("PGSQL_VERSION", "95")
        pkg.vars.Fallback(".CURDIR", ".") // FIXME: In reality, this is an absolute pathname.
 
-       return pkg
+       return &pkg
 }
 
 // File returns the (possibly absolute) path to relativeFileName,
@@ -98,7 +98,7 @@ func (pkg *Package) checkPossibleDowngra
                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, mkline.Line.RefTo(change.Line), pkgversion)
-                       Explain(
+                       G.Explain(
                                "The files in doc/CHANGES-*, in which all version changes are",
                                "recorded, have a higher version number than what the package says.",
                                "This is unusual, since packages are typically upgraded instead of",
@@ -116,8 +116,8 @@ func (pkg *Package) checklinesBuildlink3
        includedFiles := make(map[string]MkLine)
        for _, mkline := range mklines.mklines {
                if mkline.IsInclude() {
-                       file := mkline.IncludeFile()
-                       if m, bl3 := match1(file, `^\.\./\.\./(.*)/buildlink3\.mk`); m {
+                       includedFile := mkline.IncludedFile()
+                       if m, bl3 := match1(includedFile, `^\.\./\.\./(.*)/buildlink3\.mk`); m {
                                includedFiles[bl3] = mkline
                                if pkg.bl3[bl3] == nil {
                                        mkline.Warnf("%s/buildlink3.mk is included by this file but not by the package.", bl3)
@@ -136,22 +136,22 @@ func (pkg *Package) checklinesBuildlink3
 }
 
 func (pkg *Package) loadPackageMakefile() MkLines {
-       fileName := pkg.File("Makefile")
+       filename := pkg.File("Makefile")
        if trace.Tracing {
-               defer trace.Call1(fileName)()
+               defer trace.Call1(filename)()
        }
 
-       mainLines := NewMkLines(NewLines(fileName, nil))
+       mainLines := NewMkLines(NewLines(filename, nil))
        allLines := NewMkLines(NewLines("", nil))
-       if _, result := pkg.readMakefile(fileName, mainLines, allLines, ""); !result {
-               LoadMk(fileName, NotEmpty|LogErrors) // Just for the LogErrors.
+       if _, result := pkg.readMakefile(filename, mainLines, allLines, ""); !result {
+               LoadMk(filename, NotEmpty|LogErrors) // Just for the LogErrors.
                return nil
        }
 
        if G.Opts.DumpMakefile {
-               G.logOut.WriteLine("Whole Makefile (with all included files) follows:")
+               G.out.WriteLine("Whole Makefile (with all included files) follows:")
                for _, line := range allLines.lines.Lines {
-                       G.logOut.WriteLine(line.String())
+                       G.out.WriteLine(line.String())
                }
        }
 
@@ -193,12 +193,12 @@ func (pkg *Package) loadPackageMakefile(
        return mainLines
 }
 
-func (pkg *Package) readMakefile(fileName string, mainLines MkLines, allLines MkLines, includingFnameForUsedCheck string) (exists bool, result bool) {
+func (pkg *Package) readMakefile(filename string, mainLines MkLines, allLines MkLines, includingFnameForUsedCheck string) (exists bool, result bool) {
        if trace.Tracing {
-               defer trace.Call1(fileName)()
+               defer trace.Call1(filename)()
        }
 
-       fileMklines := LoadMk(fileName, NotEmpty)
+       fileMklines := LoadMk(filename, NotEmpty)
        if fileMklines == nil {
                return false, false
        }
@@ -215,22 +215,21 @@ func (pkg *Package) readMakefile(fileNam
                allLines.mklines = append(allLines.mklines, mkline)
                allLines.lines.Lines = append(allLines.lines.Lines, mkline.Line)
 
-               var includeFile, incDir, incBase string
+               var includedFile, incDir, incBase string
                if mkline.IsInclude() {
-                       inc := mkline.IncludeFile()
-                       includeFile = resolveVariableRefs(mkline.ResolveVarsInRelativePath(inc, true))
-                       if containsVarRef(includeFile) {
-                               if !contains(fileName, "/mk/") {
-                                       mkline.Notef("Skipping include file %q. This may result in false warnings.", includeFile)
+                       includedFile = resolveVariableRefs(mkline.ResolveVarsInRelativePath(mkline.IncludedFile(), true))
+                       if containsVarRef(includedFile) {
+                               if !contains(filename, "/mk/") {
+                                       mkline.Notef("Skipping include file %q. This may result in false warnings.", includedFile)
                                }
-                               includeFile = ""
+                               includedFile = ""
                        }
-                       incDir, incBase = path.Split(includeFile)
+                       incDir, incBase = path.Split(includedFile)
                }
 
-               if includeFile != "" {
+               if includedFile != "" {
                        if mkline.Basename != "buildlink3.mk" {
-                               if m, bl3File := match1(includeFile, `^\.\./\.\./(.*)/buildlink3\.mk$`); m {
+                               if m, bl3File := match1(includedFile, `^\.\./\.\./(.*)/buildlink3\.mk$`); m {
                                        pkg.bl3[bl3File] = mkline.Line
                                        if trace.Tracing {
                                                trace.Step1("Buildlink3 file in package: %q", bl3File)
@@ -239,35 +238,35 @@ func (pkg *Package) readMakefile(fileNam
                        }
                }
 
-               if includeFile != "" && pkg.included[includeFile] == nil {
-                       pkg.included[includeFile] = mkline.Line
+               if includedFile != "" && pkg.included[includedFile] == nil {
+                       pkg.included[includedFile] = mkline.Line
 
-                       if matches(includeFile, `^\.\./[^./][^/]*/[^/]+`) {
+                       if matches(includedFile, `^\.\./[^./][^/]*/[^/]+`) {
                                mkline.Warnf("References to other packages should look like \"../../category/package\", not \"../package\".")
                                mkline.ExplainRelativeDirs()
                        }
 
                        if mkline.Basename == "Makefile" && !hasPrefix(incDir, "../../mk/") && incBase != "buildlink3.mk" && incBase != "builtin.mk" && incBase != "options.mk" {
                                if trace.Tracing {
-                                       trace.Step1("Including %q sets seenMakefileCommon.", includeFile)
+                                       trace.Step1("Including %q sets seenMakefileCommon.", includedFile)
                                }
                                pkg.seenMakefileCommon = true
                        }
 
-                       skip := contains(fileName, "/mk/") || hasSuffix(includeFile, "/bsd.pkg.mk") || IsPrefs(includeFile)
+                       skip := contains(filename, "/mk/") || hasSuffix(includedFile, "/bsd.pkg.mk") || IsPrefs(includedFile)
                        if !skip {
-                               dirname, _ := path.Split(fileName)
+                               dirname, _ := path.Split(filename)
                                dirname = cleanpath(dirname)
 
-                               fullIncluded := dirname + "/" + includeFile
+                               fullIncluded := dirname + "/" + includedFile
                                if trace.Tracing {
                                        trace.Step1("Including %q.", fullIncluded)
                                }
-                               fullIncluding := ifelseStr(incBase == "Makefile.common" && incDir != "", fileName, "")
+                               fullIncluding := ifelseStr(incBase == "Makefile.common" && incDir != "", filename, "")
                                innerExists, innerResult := pkg.readMakefile(fullIncluded, mainLines, allLines, fullIncluding)
 
                                if !innerExists {
-                                       if fileMklines.indentation.IsCheckedFile(includeFile) {
+                                       if fileMklines.indentation.IsCheckedFile(includedFile) {
                                                return true // See https://github.com/rillig/pkglint/issues/1
                                        }
 
@@ -280,12 +279,12 @@ func (pkg *Package) readMakefile(fileNam
                                        if dirname != pkgBasedir { // Prevent unnecessary syscalls
                                                dirname = pkgBasedir
 
-                                               fullIncludedFallback := dirname + "/" + includeFile
+                                               fullIncludedFallback := dirname + "/" + includedFile
                                                innerExists, innerResult = pkg.readMakefile(fullIncludedFallback, mainLines, allLines, fullIncluding)
                                        }
 
                                        if !innerExists {
-                                               mkline.Errorf("Cannot read %q.", includeFile)
+                                               mkline.Errorf("Cannot read %q.", includedFile)
                                        }
                                }
 
@@ -315,12 +314,21 @@ func (pkg *Package) readMakefile(fileNam
                fileMklines.CheckForUsedComment(G.Pkgsrc.ToRel(includingFnameForUsedCheck))
        }
 
+       // For every included buildlink3.mk, include the corresponding builtin.mk
+       // automatically since the pkgsrc infrastructure does the same.
+       if false && path.Base(filename) == "buildlink3.mk" {
+               builtin := path.Join(path.Dir(filename), "builtin.mk")
+               if fileExists(builtin) {
+                       pkg.readMakefile(builtin, mainLines, allLines, "")
+               }
+       }
+
        return
 }
 
-func (pkg *Package) checkfilePackageMakefile(fileName string, mklines MkLines) {
+func (pkg *Package) checkfilePackageMakefile(filename string, mklines MkLines) {
        if trace.Tracing {
-               defer trace.Call1(fileName)()
+               defer trace.Call1(filename)()
        }
 
        vars := pkg.vars
@@ -329,7 +337,7 @@ func (pkg *Package) checkfilePackageMake
                !vars.Defined("META_PACKAGE") &&
                !fileExists(pkg.File(pkg.Pkgdir+"/PLIST")) &&
                !fileExists(pkg.File(pkg.Pkgdir+"/PLIST.common")) {
-               NewLineWhole(fileName).Warnf("Neither PLIST nor PLIST.common exist, and PLIST_SRC is unset.")
+               NewLineWhole(filename).Warnf("Neither PLIST nor PLIST.common exist, and PLIST_SRC is unset.")
        }
 
        if (vars.Defined("NO_CHECKSUM") || vars.Defined("META_PACKAGE")) && isEmptyDir(pkg.File(pkg.Patchdir)) {
@@ -348,7 +356,7 @@ func (pkg *Package) checkfilePackageMake
        }
 
        if !vars.Defined("LICENSE") && !vars.Defined("META_PACKAGE") && pkg.once.FirstTime("LICENSE") {
-               NewLineWhole(fileName).Errorf("Each package must define its LICENSE.")
+               NewLineWhole(filename).Errorf("Each package must define its LICENSE.")
        }
 
        pkg.checkGnuConfigureUseLanguages()
@@ -356,11 +364,11 @@ func (pkg *Package) checkfilePackageMake
        pkg.checkPossibleDowngrade()
 
        if !vars.Defined("COMMENT") {
-               NewLineWhole(fileName).Warnf("No COMMENT given.")
+               NewLineWhole(filename).Warnf("No COMMENT given.")
        }
 
        if imake, x11 := vars.FirstDefinition("USE_IMAKE"), vars.FirstDefinition("USE_X11"); imake != nil && x11 != nil {
-               if !hasSuffix(x11.FileName, "/mk/x11.buildlink3.mk") {
+               if !hasSuffix(x11.Filename, "/mk/x11.buildlink3.mk") {
                        imake.Notef("USE_IMAKE makes USE_X11 in %s superfluous.", imake.RefTo(x11))
                }
        }
@@ -445,7 +453,7 @@ func (pkg *Package) determineEffectivePk
 }
 
 func (pkg *Package) pkgnameFromDistname(pkgname, distname string) string {
-       tokens := NewMkParser(dummyLine, pkgname, false).MkTokens()
+       tokens := NewMkParser(nil, pkgname, false).MkTokens()
 
        result := ""
        for _, token := range tokens {
@@ -486,7 +494,7 @@ func (pkg *Package) checkUpdate() {
                        switch {
                        case cmp < 0:
                                pkgnameLine.Warnf("This package should be updated to %s%s.", sugg.Version, comment)
-                               Explain(
+                               G.Explain(
                                        "The wishlist for package updates in doc/TODO mentions that a newer",
                                        "version of this package is available.")
                        case cmp > 0:
@@ -699,17 +707,17 @@ func (pkg *Package) CheckVarorder(mkline
 
        mkline := mklines.mklines[firstRelevant]
        mkline.Warnf("The canonical order of the variables is %s.", strings.Join(canonical, ", "))
-       Explain(
+       G.Explain(
                "In simple package Makefiles, some common variables should be",
                "arranged in a specific order.",
                "",
-               "See doc/Makefile-example or the pkgsrc guide, section",
-               "\"Package components\", subsection \"Makefile\" for more information.")
+               "See doc/Makefile-example for an example Makefile.",
+               seeGuide("Package components, Makefile", "components.Makefile"))
 }
 
-func (pkg *Package) checkLocallyModified(fileName string) {
+func (pkg *Package) checkLocallyModified(filename string) {
        if trace.Tracing {
-               defer trace.Call(fileName)()
+               defer trace.Call(filename)()
        }
 
        owner, _ := pkg.vars.Value("OWNER")
@@ -730,16 +738,15 @@ func (pkg *Package) checkLocallyModified
                return
        }
 
-       if isLocallyModified(fileName) {
+       if isLocallyModified(filename) {
                if owner != "" {
-                       NewLineWhole(fileName).Warnf("Don't commit changes to this file without asking the OWNER, %s.", owner)
-                       Explain(
-                               "See the pkgsrc guide, section \"Package components\",",
-                               "keyword \"owner\", for more information.")
+                       NewLineWhole(filename).Warnf("Don't commit changes to this file without asking the OWNER, %s.", owner)
+                       G.Explain(
+                               seeGuide("Package components, Makefile", "components.Makefile"))
                }
                if maintainer != "" {
-                       NewLineWhole(fileName).Notef("Please only commit changes that %s would approve.", maintainer)
-                       Explain(
+                       NewLineWhole(filename).Notef("Please only commit changes that %s would approve.", maintainer)
+                       G.Explain(
                                "See the pkgsrc guide, section \"Package components\",",
                                "keyword \"maintainer\", for more information.")
                }
@@ -748,27 +755,32 @@ func (pkg *Package) checkLocallyModified
 
 func (pkg *Package) CheckInclude(mkline MkLine, indentation *Indentation) {
        conditionalVars := mkline.ConditionalVars()
-       if conditionalVars == "" {
+       if len(conditionalVars) == 0 {
                conditionalVars = indentation.Varnames()
                mkline.SetConditionalVars(conditionalVars)
        }
 
-       if path.Dir(abspath(mkline.FileName)) == abspath(pkg.File(".")) {
-               includefile := mkline.IncludeFile()
+       if path.Dir(abspath(mkline.Filename)) == abspath(pkg.File(".")) {
+               includedFile := mkline.IncludedFile()
 
                if indentation.IsConditional() {
-                       pkg.conditionalIncludes[includefile] = mkline
-                       if other := pkg.unconditionalIncludes[includefile]; other != nil {
+                       pkg.conditionalIncludes[includedFile] = mkline
+                       if other := pkg.unconditionalIncludes[includedFile]; other != nil {
                                mkline.Warnf("%q is included conditionally here (depending on %s) and unconditionally in %s.",
-                                       cleanpath(includefile), mkline.ConditionalVars(), mkline.RefTo(other))
+                                       cleanpath(includedFile), strings.Join(mkline.ConditionalVars(), ", "), mkline.RefTo(other))
                        }
                } else {
-                       pkg.unconditionalIncludes[includefile] = mkline
-                       if other := pkg.conditionalIncludes[includefile]; other != nil {
+                       pkg.unconditionalIncludes[includedFile] = mkline
+                       if other := pkg.conditionalIncludes[includedFile]; other != nil {
                                mkline.Warnf("%q is included unconditionally here and conditionally in %s (depending on %s).",
-                                       cleanpath(includefile), mkline.RefTo(other), other.ConditionalVars())
+                                       cleanpath(includedFile), mkline.RefTo(other), strings.Join(other.ConditionalVars(), ", "))
                        }
                }
+
+               // TODO: Check whether the conditional variables are the same on both places.
+               // Ideally they should match, but there may be some differences in internal
+               // variables, which need to be filtered out before comparing them, like it is
+               // already done with *_MK variables.
        }
 }
 

Index: pkgsrc/pkgtools/pkglint/files/package_test.go
diff -u pkgsrc/pkgtools/pkglint/files/package_test.go:1.32 pkgsrc/pkgtools/pkglint/files/package_test.go:1.33
--- pkgsrc/pkgtools/pkglint/files/package_test.go:1.32  Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/package_test.go       Sun Dec  2 01:57:48 2018
@@ -23,7 +23,7 @@ func (s *Suite) Test_Package_checklinesB
 
        t.CreateFileLines("category/dependency/buildlink3.mk")
        G.Pkg = NewPackage(t.File("category/package"))
-       G.Pkg.bl3["../../category/dependency/buildlink3.mk"] = t.NewLine("fileName", 1, "")
+       G.Pkg.bl3["../../category/dependency/buildlink3.mk"] = t.NewLine("filename", 1, "")
        mklines := t.NewMkLines("category/package/buildlink3.mk",
                MkRcsID)
 

Index: pkgsrc/pkgtools/pkglint/files/parser.go
diff -u pkgsrc/pkgtools/pkglint/files/parser.go:1.11 pkgsrc/pkgtools/pkglint/files/parser.go:1.12
--- pkgsrc/pkgtools/pkglint/files/parser.go:1.11        Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/parser.go     Sun Dec  2 01:57:48 2018
@@ -7,45 +7,48 @@ import (
 
 type Parser struct {
        Line         Line
-       repl         *textproc.PrefixReplacer
+       lexer        *textproc.Lexer
        EmitWarnings bool
 }
 
 func NewParser(line Line, s string, emitWarnings bool) *Parser {
-       return &Parser{line, G.NewPrefixReplacer(s), emitWarnings}
+       return &Parser{line, textproc.NewLexer(s), emitWarnings}
 }
 
 func (p *Parser) EOF() bool {
-       return p.repl.Rest() == ""
+       return p.lexer.EOF()
 }
 
 func (p *Parser) Rest() string {
-       return p.repl.Rest()
+       return p.lexer.Rest()
 }
 
 func (p *Parser) PkgbasePattern() (pkgbase string) {
-       repl := p.repl
+       lexer := p.lexer
 
        for {
-               if repl.AdvanceRegexp(`^\$\{\w+\}`) ||
-                       repl.AdvanceRegexp(`^[\w.*+,{}]+`) ||
-                       repl.AdvanceRegexp(`^\[[\d-]+\]`) {
-                       pkgbase += repl.Str()
+               mark := lexer.Mark()
+
+               if lexer.SkipRegexp(G.res.Compile(`^\$\{\w+\}`)) ||
+                       lexer.SkipRegexp(G.res.Compile(`^[\w.*+,{}]+`)) ||
+                       lexer.SkipRegexp(G.res.Compile(`^\[[\d-]+\]`)) {
+                       pkgbase += lexer.Since(mark)
                        continue
                }
 
-               mark := repl.Mark()
-               if repl.AdvanceStr("-") {
-                       if repl.AdvanceRegexp(`^\d`) ||
-                               repl.AdvanceRegexp(`^\$\{\w*VER\w*\}`) ||
-                               repl.AdvanceStr("[") {
-                               repl.Reset(mark)
+               if lexer.SkipByte('-') {
+                       if lexer.SkipRegexp(G.res.Compile(`^\d`)) ||
+                               lexer.SkipRegexp(G.res.Compile(`^\$\{\w*VER\w*\}`)) ||
+                               lexer.SkipByte('[') {
+                               lexer.Reset(mark)
                                return
                        }
                        pkgbase += "-"
-               } else {
-                       return
+                       continue
                }
+
+               lexer.Reset(mark)
+               return
        }
 }
 
@@ -59,39 +62,47 @@ type DependencyPattern struct {
 }
 
 func (p *Parser) Dependency() *DependencyPattern {
-       repl := p.repl
+       lexer := p.lexer
 
        var dp DependencyPattern
-       mark := repl.Mark()
+       mark := lexer.Mark()
        dp.Pkgbase = p.PkgbasePattern()
        if dp.Pkgbase == "" {
                return nil
        }
 
-       mark2 := repl.Mark()
-       if repl.AdvanceStr(">=") || repl.AdvanceStr(">") {
-               op := repl.Str()
-               if repl.AdvanceRegexp(`^(?:(?:\$\{\w+\})+|\d[\w.]*)`) {
+       mark2 := lexer.Mark()
+       op := lexer.NextString(">=")
+       if op == "" {
+               op = lexer.NextString(">")
+       }
+       if op != "" {
+               if m := lexer.NextRegexp(G.res.Compile(`^(?:(?:\$\{\w+\})+|\d[\w.]*)`)); m != nil {
                        dp.LowerOp = op
-                       dp.Lower = repl.Str()
+                       dp.Lower = m[0]
                } else {
-                       repl.Reset(mark2)
+                       lexer.Reset(mark2)
                }
        }
-       if repl.AdvanceStr("<=") || repl.AdvanceStr("<") {
-               op := repl.Str()
-               if repl.AdvanceRegexp(`^(?:(?:\$\{\w+\})+|\d[\w.]*)`) {
+
+       op = lexer.NextString("<=")
+       if op == "" {
+               op = lexer.NextString("<")
+       }
+       if op != "" {
+               if m := lexer.NextRegexp(G.res.Compile(`^(?:(?:\$\{\w+\})+|\d[\w.]*)`)); m != nil {
                        dp.UpperOp = op
-                       dp.Upper = repl.Str()
+                       dp.Upper = m[0]
                } else {
-                       repl.Reset(mark2)
+                       lexer.Reset(mark2)
                }
        }
        if dp.LowerOp != "" || dp.UpperOp != "" {
                return &dp
        }
-       if repl.AdvanceStr("-") && repl.Rest() != "" {
-               dp.Wildcard = repl.AdvanceRest()
+       if lexer.SkipByte('-') && lexer.Rest() != "" {
+               dp.Wildcard = lexer.Rest()
+               lexer.Skip(len(lexer.Rest()))
                return &dp
        }
        if hasPrefix(dp.Pkgbase, "${") && hasSuffix(dp.Pkgbase, "}") {
@@ -103,6 +114,6 @@ func (p *Parser) Dependency() *Dependenc
                return &dp
        }
 
-       repl.Reset(mark)
+       lexer.Reset(mark)
        return nil
 }

Index: pkgsrc/pkgtools/pkglint/files/patches.go
diff -u pkgsrc/pkgtools/pkglint/files/patches.go:1.24 pkgsrc/pkgtools/pkglint/files/patches.go:1.25
--- pkgsrc/pkgtools/pkglint/files/patches.go:1.24       Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/patches.go    Sun Dec  2 01:57:48 2018
@@ -33,7 +33,7 @@ func (ck *PatchChecker) Check() {
                defer trace.Call0()()
        }
 
-       if CheckLineRcsid(ck.lines.Lines[0], ``, "") {
+       if ck.lines.CheckRcsID(0, ``, "") {
                ck.exp.Advance()
        }
        if ck.exp.EOF() {
@@ -168,7 +168,7 @@ func (ck *PatchChecker) checkUnifiedDiff
                line := ck.exp.CurrentLine()
                if !ck.isEmptyLine(line.Text) && !matches(line.Text, rePatchUniFileDel) {
                        line.Warnf("Empty line or end of file expected.")
-                       Explain(
+                       G.Explain(
                                "This line is not part of the patch anymore, although it may",
                                "look so.  To make this situation clear, there should be an",
                                "empty line before this line.  If the line doesn't contain",
@@ -184,7 +184,7 @@ func (ck *PatchChecker) checkBeginDiff(l
 
        if !ck.seenDocumentation && patchedFiles == 0 {
                line.Errorf("Each patch must be documented.")
-               Explain(
+               G.Explain(
                        "Pkgsrc tries to have as few patches as possible.  Therefore, each",
                        "patch must document why it is necessary.  Typical reasons are",
                        "portability or security.  A typical documented patch looks like",
@@ -241,7 +241,7 @@ func (ck *PatchChecker) checklineAdded(a
        case ftConfigure:
                if hasSuffix(addedText, ": Avoid regenerating within pkgsrc") {
                        line.Errorf("This code must not be included in patches.")
-                       Explain(
+                       G.Explain(
                                "It is generated automatically by pkgsrc after the patch phase.",
                                "",
                                "For more details, look for \"configure-scripts-override\" in",
@@ -314,12 +314,12 @@ func (ft FileType) String() string {
 }
 
 // This is used to select the proper subroutine for detecting absolute pathnames.
-func guessFileType(fileName string) (fileType FileType) {
+func guessFileType(filename string) (fileType FileType) {
        if trace.Tracing {
-               defer trace.Call(fileName, trace.Result(&fileType))()
+               defer trace.Call(filename, trace.Result(&fileType))()
        }
 
-       basename := path.Base(fileName)
+       basename := path.Base(filename)
        basename = strings.TrimSuffix(basename, ".in") // doesn't influence the content type
        ext := strings.ToLower(strings.TrimLeft(path.Ext(basename), "."))
 
@@ -342,7 +342,7 @@ func guessFileType(fileName string) (fil
        }
 
        if trace.Tracing {
-               trace.Step1("Unknown file type for %q", fileName)
+               trace.Step1("Unknown file type for %q", filename)
        }
        return ftUnknown
 }
@@ -365,7 +365,7 @@ func (ck *PatchChecker) checklineSourceA
                        // ok; Python example: libdir = prefix + '/lib'
 
                default:
-                       CheckwordAbsolutePathname(line, str)
+                       LineChecker{line}.CheckWordAbsolutePathname(str)
                }
        }
 }
@@ -378,7 +378,7 @@ func (ck *PatchChecker) checklineOtherAb
        if hasPrefix(text, "#") && !hasPrefix(text, "#!") {
                // Don't warn for absolute pathnames in comments, except for shell interpreters.
 
-       } else if m, before, path, _ := match3(text, `^(.*?)((?:/[\w.]+)*/(?:bin|dev|etc|home|lib|mnt|opt|proc|sbin|tmp|usr|var)\b[\w./\-]*)(.*)$`); m {
+       } else if m, before, dir, _ := match3(text, `^(.*?)((?:/[\w.]+)*/(?:bin|dev|etc|home|lib|mnt|opt|proc|sbin|tmp|usr|var)\b[\w./\-]*)(.*)$`); m {
                switch {
                case matches(before, `[\w).@}]$`) && !matches(before, `DESTDIR.$`):
                        // Example: $prefix/bin
@@ -396,7 +396,7 @@ func (ck *PatchChecker) checklineOtherAb
                        if trace.Tracing {
                                trace.Step1("before=%q", before)
                        }
-                       CheckwordAbsolutePathname(line, path)
+                       LineChecker{line}.CheckWordAbsolutePathname(dir)
                }
        }
 }

Index: pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go
diff -u pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.10 pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.11
--- pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go:1.10   Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/pkgsrc_test.go        Sun Dec  2 01:57:48 2018
@@ -48,6 +48,37 @@ func (s *Suite) Test_Pkgsrc_parseSuggest
                {lines.Lines[6], "freeciv-client", "2.5.0", "(urgent)"}})
 }
 
+func (s *Suite) Test_Pkgsrc_checkToplevelUnusedLicenses(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupPkgsrc()
+       t.CreateFileLines("mk/misc/category.mk")
+       t.CreateFileLines("licenses/2-clause-bsd")
+       t.CreateFileLines("licenses/gnu-gpl-v3")
+
+       t.CreateFileLines("Makefile",
+               MkRcsID,
+               "SUBDIR+=\tcategory")
+
+       t.CreateFileLines("category/Makefile",
+               MkRcsID,
+               "COMMENT=\tExample category",
+               "",
+               "SUBDIR+=\tpackage",
+               "",
+               ".include \"../mk/misc/category.mk\"")
+
+       t.SetupPackage("category/package",
+               "LICENSE=\t2-clause-bsd")
+
+       G.Main("pkglint", "-r", "-Cglobal", t.File("."))
+
+       t.CheckOutputLines(
+               "WARN: ~/licenses/gnu-gpl-v2: This license seems to be unused.", // Added by Tester.SetupPkgsrc
+               "WARN: ~/licenses/gnu-gpl-v3: This license seems to be unused.",
+               "0 errors and 2 warnings found.")
+}
+
 func (s *Suite) Test_Pkgsrc_loadTools(c *check.C) {
        t := s.Init(c)
 
@@ -162,7 +193,7 @@ func (s *Suite) Test_Pkgsrc_loadDocChang
        c.Check(*changes[6], equals, Change{changes[6].Line, "Downgraded", "category/package", "1.2", "author7", "2018-01-07"})
 
        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:1: Year 2015 for category/package does not match the filename ~/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:12: Unknown doc/CHANGES line: \tAdded another [new package]")
 }
@@ -212,17 +243,21 @@ func (s *Suite) Test_Pkgsrc__deprecated(
        t := s.Init(c)
 
        t.SetupTool("echo", "ECHO", AtRunTime)
+       t.SetupVartypes()
        G.Pkgsrc.initDeprecatedVars()
        mklines := t.NewMkLines("Makefile",
-               "USE_PERL5=\tyes",
-               "SUBST_POSTCMD.class=${ECHO}")
+               MkRcsID,
+               "USE_PERL5=\t\tyes",
+               "SUBST_POSTCMD.class=\t${ECHO}",
+               "CPPFLAGS+=\t\t${BUILDLINK_CPPFLAGS.${PKG_JVM}}")
 
-       MkLineChecker{mklines.mklines[0]}.checkVarassign()
-       MkLineChecker{mklines.mklines[1]}.checkVarassign()
+       mklines.Check()
 
        t.CheckOutputLines(
-               "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.")
+               "WARN: Makefile:2: Definition of USE_PERL5 is deprecated. Use USE_TOOLS+=perl or USE_TOOLS+=perl:run instead.",
+               "WARN: Makefile:3: Definition of SUBST_POSTCMD.class is deprecated. Has been removed, as it seemed unused.",
+               "WARN: Makefile:4: Use of \"PKG_JVM\" is deprecated. Use PKG_DEFAULT_JVM instead.",
+               "WARN: Makefile:4: BUILDLINK_CPPFLAGS.${PKG_JVM} may not be used in any file; it is a write-only variable.")
 }
 
 func (s *Suite) Test_Pkgsrc_ListVersions__no_basedir(c *check.C) {
@@ -427,9 +462,9 @@ func (s *Suite) Test_Pkgsrc_VariableType
        }
 
        checkType("_PERL5_PACKLIST_AWK_STRIP_DESTDIR", "")
-       checkType("SOME_DIR", "PathName (guessed)")
-       checkType("SOMEDIR", "PathName (guessed)")
-       checkType("SEARCHPATHS", "ShellList of PathName (guessed)")
+       checkType("SOME_DIR", "Pathname (guessed)")
+       checkType("SOMEDIR", "Pathname (guessed)")
+       checkType("SEARCHPATHS", "ShellList of Pathname (guessed)")
        checkType("MYPACKAGE_USER", "UserGroupName (guessed)")
        checkType("MYPACKAGE_GROUP", "UserGroupName (guessed)")
        checkType("MY_CMD_ENV", "ShellList of ShellWord (guessed)")
@@ -462,16 +497,22 @@ func (s *Suite) Test_Pkgsrc_VariableType
 func (s *Suite) Test_Pkgsrc_VariableType__from_mk(c *check.C) {
        t := s.Init(c)
 
+       // The type of OSNAME.* cannot be guessed from the variable name,
+       // but it is a known variable since the pkgsrc infrastructure uses
+       // it. But still, its type is unknown.
+
        t.SetupPkgsrc()
        t.CreateFileLines("mk/sys-vars.mk",
                MkRcsID,
                "",
                "PKGSRC_MAKE_ENV?=\t# none",
-               "CPPPATH?=\tcpp")
+               "CPPPATH?=\tcpp",
+               "OSNAME.Linux?=\tLinux")
 
        pkg := t.SetupPackage("category/package",
                "PKGSRC_MAKE_ENV+=\tCPP=${CPPPATH:Q}",
-               "PKGSRC_UNKNOWN_ENV+=\tCPP=${ABCPATH:Q}")
+               "PKGSRC_UNKNOWN_ENV+=\tCPP=${ABCPATH:Q}",
+               "OSNAME.SunOS=\t\t${OSNAME.Other}")
 
        G.Main("pkglint", "-Wall", pkg)
 
@@ -483,6 +524,10 @@ func (s *Suite) Test_Pkgsrc_VariableType
                c.Check(typ.String(), equals, "Pathlist (guessed)")
        }
 
+       if typ := G.Pkgsrc.VariableType("OSNAME.Other"); c.Check(typ, check.NotNil) {
+               c.Check(typ.String(), equals, "Unknown")
+       }
+
        // No warnings about "defined but not used" or "used but not defined"
        // (which both rely on VariableType) may appear here for PKGSRC_MAKE_ENV
        // and CPPPATH since these two variables are defined somewhere in the
Index: pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.10 pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.11
--- pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go:1.10      Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/shtokenizer_test.go   Sun Dec  2 01:57:48 2018
@@ -20,9 +20,9 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                return p.Rest()
        }
 
-       // check ensures that the given string is parsed to the expected
+       // test 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) {
+       test := func(str string, expected ...*ShAtom) {
                rest := checkRest(str, expected...)
                c.Check(rest, equals, "")
                t.CheckOutputEmpty()
@@ -43,7 +43,8 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                varuse := NewMkVarUse(varname, modifiers...)
                return &ShAtom{shtVaruse, text, shqPlain, varuse}
        }
-       text := func(s string) *ShAtom { return atom(shtWord, s) }
+       shvar := func(text, varname string) *ShAtom { return &ShAtom{shtShVarUse, text, shqPlain, varname} }
+       text := func(s string) *ShAtom { return atom(shtText, s) }
        whitespace := func(s string) *ShAtom { return atom(shtSpace, s) }
 
        space := whitespace(" ")
@@ -68,26 +69,27 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
 
        // Ignore unused functions; useful for deleting some of the tests during debugging.
        use := func(args ...interface{}) {}
-       use(checkRest, check)
+       use(checkRest, test)
        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 */)
+       test("" /* none */)
 
-       check("$$var",
-               text("$$var"))
+       test("$$var",
+               shvar("$$var", "var"))
 
-       check("$$var$$var",
-               text("$$var$$var"))
+       test("$$var$$var",
+               shvar("$$var", "var"),
+               shvar("$$var", "var"))
 
-       check("$$var;;",
-               text("$$var"),
+       test("$$var;;",
+               shvar("$$var", "var"),
                operator(";;"))
 
-       check("'single-quoted'",
+       test("'single-quoted'",
                squot(text("'")),
                squot(text("single-quoted")),
                text("'"))
@@ -95,44 +97,45 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
        rest := checkRest("\"" /* none */)
        c.Check(rest, equals, "\"")
 
-       check("$${file%.c}.o",
-               text("$${file%.c}.o"))
+       test("$${file%.c}.o",
+               shvar("$${file%.c}", "file"),
+               text(".o"))
 
-       check("hello",
+       test("hello",
                text("hello"))
 
-       check("hello, world",
+       test("hello, world",
                text("hello,"),
                space,
                text("world"))
 
-       check("\"",
+       test("\"",
                dquot(text("\"")))
 
-       check("`",
+       test("`",
                backt(text("`")))
 
-       check("`cat fileName`",
+       test("`cat filename`",
                backt(text("`")),
                backt(text("cat")),
                backt(space),
-               backt(text("fileName")),
+               backt(text("filename")),
                text("`"))
 
-       check("hello, \"world\"",
+       test("hello, \"world\"",
                text("hello,"),
                space,
                dquot(text("\"")),
                dquot(text("world")),
                text("\""))
 
-       check("set -e;",
+       test("set -e;",
                text("set"),
                space,
                text("-e"),
                semicolon)
 
-       check("cd ${WRKSRC}/doc/man/man3; PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\";",
+       test("cd ${WRKSRC}/doc/man/man3; PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\";",
                text("cd"),
                space,
                mkvar("WRKSRC"),
@@ -159,13 +162,13 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                text("\""),
                semicolon)
 
-       check("ls -1 | ${SED} -e 's,3qt$$,3,'",
+       test("ls -1 | ${SED} -e 's,3qt$$,3,'",
                text("ls"), space, text("-1"), space,
                pipe, space,
                mkvar("SED"), space, text("-e"), space,
                squot(text("'")), squot(text("s,3qt$$,3,")), text("'"))
 
-       check("(for PAGE in $$PAGES; do ",
+       test("(for PAGE in $$PAGES; do ",
                operator("("),
                text("for"),
                space,
@@ -173,13 +176,13 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                space,
                text("in"),
                space,
-               text("$$PAGES"),
+               shvar("$$PAGES", "PAGES"),
                semicolon,
                space,
                text("do"),
                space)
 
-       check("    ${ECHO} installing ${DESTDIR}${QTPREFIX}/man/man3/$${PAGE}; ",
+       test("    ${ECHO} installing ${DESTDIR}${QTPREFIX}/man/man3/$${PAGE}; ",
                whitespace("    "),
                mkvar("ECHO"),
                space,
@@ -187,11 +190,12 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                space,
                mkvar("DESTDIR"),
                mkvar("QTPREFIX"),
-               text("/man/man3/$${PAGE}"),
+               text("/man/man3/"),
+               shvar("$${PAGE}", "PAGE"),
                semicolon,
                space)
 
-       check("    set - X `head -1 $${PAGE}qt`; ",
+       test("    set - X `head -1 $${PAGE}qt`; ",
                whitespace("    "),
                text("set"),
                space,
@@ -204,34 +208,35 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                backt(space),
                backt(text("-1")),
                backt(space),
-               backt(text("$${PAGE}qt")),
+               backt(shvar("$${PAGE}", "PAGE")),
+               backt(text("qt")),
                text("`"),
                semicolon,
                space)
 
-       check("`\"one word\"`",
+       test("`\"one word\"`",
                backt(text("`")),
                backtDquot(text("\"")),
                backtDquot(text("one word")),
                backt(text("\"")),
                text("`"))
 
-       check("$$var \"$$var\" '$$var' `$$var`",
-               text("$$var"),
+       test("$$var \"$$var\" '$$var' `$$var`",
+               shvar("$$var", "var"),
                space,
                dquot(text("\"")),
-               dquot(text("$$var")),
+               dquot(shvar("$$var", "var")),
                text("\""),
                space,
                squot(text("'")),
-               squot(text("$$var")),
+               squot(shvar("$$var", "var")),
                text("'"),
                space,
                backt(text("`")),
-               backt(text("$$var")),
+               backt(shvar("$$var", "var")),
                text("`"))
 
-       check("\"`'echo;echo'`\"",
+       test("\"`'echo;echo'`\"",
                dquot(text("\"")),
                dquotBackt(text("`")),
                dquotBacktSquot(text("'")),
@@ -240,24 +245,27 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                dquot(text("`")),
                text("\""))
 
-       check("cat<file",
+       test("cat<file",
                text("cat"),
                operator("<"),
                text("file"))
 
-       check("-e \"s,\\$$sysconfdir/jabberd,\\$$sysconfdir,g\"",
+       test("\\$$escaped",
+               text("\\$$escaped"))
+
+       test("-e \"s,\\$$sysconfdir/jabberd,\\$$sysconfdir,g\"",
                text("-e"),
                space,
                dquot(text("\"")),
                dquot(text("s,\\$$sysconfdir/jabberd,\\$$sysconfdir,g")),
                text("\""))
 
-       check("echo $$, $$- $$/ $$; $$| $$,$$/$$;$$-",
+       test("echo $$, $$- $$/ $$; $$| $$,$$/$$;$$-",
                text("echo"),
                space,
                text("$$,"),
                space,
-               text("$$-"),
+               shvar("$$-", "-"),
                space,
                text("$$/"),
                space,
@@ -269,19 +277,19 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                space,
                text("$$,$$/$$"),
                semicolon,
-               text("$$-"))
+               shvar("$$-", "-"))
 
        rest = checkRest("COMMENT=\t\\Make $$$$ fast\"",
                text("COMMENT="),
                whitespace("\t"),
                text("\\Make"),
                space,
-               text("$$$$"),
+               shvar("$$$$", "$"),
                space,
                text("fast"))
        c.Check(rest, equals, "\"")
 
-       check("var=`echo;echo|echo&echo||echo&&echo>echo`",
+       test("var=`echo;echo|echo&echo||echo&&echo>echo`",
                text("var="),
                backt(text("`")),
                backt(text("echo")),
@@ -299,22 +307,22 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                backt(text("echo")),
                text("`"))
 
-       check("# comment",
+       test("# comment",
                comment("# comment"))
-       check("no#comment",
+       test("no#comment",
                text("no#comment"))
-       check("`# comment`continue",
+       test("`# comment`continue",
                backt(text("`")),
                backt(comment("# comment")),
                text("`"),
                text("continue"))
-       check("`no#comment`continue",
+       test("`no#comment`continue",
                backt(text("`")),
                backt(text("no#comment")),
                text("`"),
                text("continue"))
 
-       check("var=`tr 'A-Z' 'a-z'`",
+       test("var=`tr 'A-Z' 'a-z'`",
                text("var="),
                backt(text("`")),
                backt(text("tr")),
@@ -328,7 +336,7 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                backt(text("'")),
                text("`"))
 
-       check("var=\"`echo \"\\`echo foo\\`\"`\"",
+       test("var=\"`echo \"\\`echo foo\\`\"`\"",
                text("var="),
                dquot(text("\"")),
                dquotBackt(text("`")),
@@ -340,7 +348,7 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                dquot(text("`")),
                text("\""))
 
-       check("if cond1; then action1; elif cond2; then action2; else action3; fi",
+       test("if cond1; then action1; elif cond2; then action2; else action3; fi",
                text("if"), space, text("cond1"), semicolon, space,
                text("then"), space, text("action1"), semicolon, space,
                text("elif"), space, text("cond2"), semicolon, space,
@@ -348,12 +356,12 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                text("else"), space, text("action3"), semicolon, space,
                text("fi"))
 
-       check("$$(cat)",
+       test("$$(cat)",
                subsh(subshell),
                subsh(text("cat")),
                text(")"))
 
-       check("$$(cat 'file')",
+       test("$$(cat 'file')",
                subsh(subshell),
                subsh(text("cat")),
                subsh(space),
@@ -362,14 +370,14 @@ func (s *Suite) Test_ShTokenizer_ShAtom(
                subsh(text("'")),
                text(")"))
 
-       check("$$(# comment) arg",
+       test("$$(# comment) arg",
                subsh(subshell),
                subsh(comment("# comment")),
                text(")"),
                space,
                text("arg"))
 
-       check("$$(echo \"first\" 'second')",
+       test("$$(echo \"first\" 'second')",
                subsh(subshell),
                subsh(text("echo")),
                subsh(space),
@@ -487,6 +495,56 @@ func (s *Suite) Test_ShTokenizer_ShToken
                "id=`${AWK} '{print}' < ${WRKSRC}/idfile`")
 }
 
+func (s *Suite) Test_ShTokenizer_shVarUse(c *check.C) {
+
+       test := func(input string, output *ShAtom, rest string) {
+               tok := NewShTokenizer(nil, input, false)
+               actual := tok.shVarUse(shqPlain)
+
+               c.Check(actual, deepEquals, output)
+               c.Check(tok.Rest(), equals, rest)
+       }
+
+       shvar := func(text, varname string) *ShAtom {
+               return &ShAtom{shtShVarUse, text, shqPlain, varname}
+       }
+
+       test("$", nil, "$")
+       test("$$", nil, "$$")
+       test("${MKVAR}", nil, "${MKVAR}")
+
+       test("$$a", shvar("$$a", "a"), "")
+       test("$$a.", shvar("$$a", "a"), ".")
+       test("$$a_b_123:", shvar("$$a_b_123", "a_b_123"), ":")
+       test("$$123", shvar("$$1", "1"), "23")
+
+       test("$${varname}", shvar("$${varname}", "varname"), "")
+       test("$${varname}.", shvar("$${varname}", "varname"), ".")
+       test("$${0123}.", shvar("$${0123}", "0123"), ".")
+       test("$${varname", nil, "$${varname")
+
+       test("$${var:=value}", shvar("$${var:=value}", "var"), "")
+       test("$${var#value}", shvar("$${var#value}", "var"), "")
+       test("$${var##value}", shvar("$${var##value}", "var"), "")
+       test("$${var##*}", shvar("$${var##*}", "var"), "")
+       test("$${var%\".gz\"}", shvar("$${var%\".gz\"}", "var"), "")
+
+       // TODO: allow variables in patterns.
+       test("$${var%.${ext}}", nil, "$${var%.${ext}}")
+
+       test("$${var##*", nil, "$${var##*")
+       test("$${var\"", nil, "$${var\"")
+
+       // TODO: test("$${var%${EXT}}", shvar("$${var%${EXT}}", "var"), "")
+       test("$${var%${EXT}}", nil, "$${var%${EXT}}")
+
+       // TODO: length of var
+       test("$${#var}", nil, "$${#var}")
+
+       test("$${/}", nil, "$${/}")
+       test("$${\\}", nil, "$${\\}")
+}
+
 func (s *Suite) Test_ShTokenizer__examples_from_fuzzing(c *check.C) {
        t := s.Init(c)
 
@@ -517,13 +575,13 @@ func (s *Suite) Test_ShTokenizer__exampl
                // Covers shAtomSubshDquot: return nil
                "\t"+"$$(\"'",
 
-               // Covers shAtomSubsh: case repl.AdvanceStr("`")
+               // Covers shAtomSubsh: case lexer.AdvanceStr("`")
                "\t"+"$$(`",
 
                // Covers shAtomSubshSquot: return nil
                "\t"+"$$('$)",
 
-               // Covers shAtomDquotBackt: case repl.AdvanceRegexp("^#[^`]*")
+               // Covers shAtomDquotBackt: case lexer.AdvanceRegexp("^#[^`]*")
                "\t"+"\"`# comment")
 
        mklines.Check()
@@ -537,7 +595,6 @@ func (s *Suite) Test_ShTokenizer__exampl
                "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).",
@@ -547,7 +604,6 @@ func (s *Suite) Test_ShTokenizer__exampl
                "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).",
Index: pkgsrc/pkgtools/pkglint/files/shtypes.go
diff -u pkgsrc/pkgtools/pkglint/files/shtypes.go:1.10 pkgsrc/pkgtools/pkglint/files/shtypes.go:1.11
--- pkgsrc/pkgtools/pkglint/files/shtypes.go:1.10       Wed Oct  3 22:27:53 2018
+++ pkgsrc/pkgtools/pkglint/files/shtypes.go    Sun Dec  2 01:57:48 2018
@@ -11,7 +11,8 @@ type ShAtomType uint8
 const (
        shtSpace    ShAtomType = iota
        shtVaruse              // ${PREFIX}
-       shtWord                // while, cat, ENV=value
+       shtShVarUse            // $${var:-pol}
+       shtText                // while, cat, ENV=value
        shtOperator            // (, ;, |
        shtComment             // # ...
        shtSubshell            // $$(
@@ -21,16 +22,20 @@ func (t ShAtomType) String() string {
        return [...]string{
                "space",
                "varuse",
-               "word",
+               "shvaruse",
+               "text",
                "operator",
                "comment",
                "subshell",
        }[t]
 }
 
+// IsWord checks whether the atom counts as text.
+// Makefile variables, shell variables and other text counts,
+// but keywords, operators and separators don't.
 func (t ShAtomType) IsWord() bool {
        switch t {
-       case shtVaruse, shtWord:
+       case shtVaruse, shtShVarUse, shtText:
                return true
        }
        return false
@@ -44,7 +49,7 @@ type ShAtom struct {
 }
 
 func (atom *ShAtom) String() string {
-       if atom.Type == shtWord && atom.Quoting == shqPlain && atom.data == nil {
+       if atom.Type == shtText && atom.Quoting == shqPlain && atom.data == nil {
                return fmt.Sprintf("%q", atom.MkText)
        }
        if atom.Type == shtVaruse {
@@ -62,6 +67,12 @@ func (atom *ShAtom) VarUse() *MkVarUse {
        return nil
 }
 
+// ShVarname applies to shell variable atoms like $$varname or $${varname:-modifier}
+// and returns the name of the shell variable.
+func (atom *ShAtom) ShVarname() string {
+       return atom.data.(string)
+}
+
 // ShQuoting describes the context in which a string appears
 // and how it must be unescaped to get its literal value.
 type ShQuoting uint8
Index: pkgsrc/pkgtools/pkglint/files/vartype_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.10 pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.11
--- pkgsrc/pkgtools/pkglint/files/vartype_test.go:1.10  Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/vartype_test.go       Sun Dec  2 01:57:48 2018
@@ -10,7 +10,7 @@ func (s *Suite) Test_Vartype_EffectivePe
        t.SetupVartypes()
 
        if typ := G.Pkgsrc.vartypes["PREFIX"]; c.Check(typ, check.NotNil) {
-               c.Check(typ.basicType.name, equals, "PathName")
+               c.Check(typ.basicType.name, equals, "Pathname")
                c.Check(typ.aclEntries, check.DeepEquals, []ACLEntry{{glob: "*", permissions: aclpUse}})
                c.Check(typ.EffectivePermissions("Makefile"), equals, aclpUse)
        }

Index: pkgsrc/pkgtools/pkglint/files/plist.go
diff -u pkgsrc/pkgtools/pkglint/files/plist.go:1.31 pkgsrc/pkgtools/pkglint/files/plist.go:1.32
--- pkgsrc/pkgtools/pkglint/files/plist.go:1.31 Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/plist.go      Sun Dec  2 01:57:48 2018
@@ -11,11 +11,11 @@ func ChecklinesPlist(lines Lines) {
                defer trace.Call1(lines.FileName)()
        }
 
-       CheckLineRcsid(lines.Lines[0], `@comment `, "@comment ")
+       lines.CheckRcsID(0, `@comment `, "@comment ")
 
        if lines.Len() == 1 {
                lines.Lines[0].Warnf("PLIST files shouldn't be empty.")
-               Explain(
+               G.Explain(
                        "One reason for empty PLISTs is that this is a newly created package",
                        "and that the author didn't run \"bmake print-PLIST\" after installing",
                        "the files.",
@@ -27,7 +27,7 @@ func ChecklinesPlist(lines Lines) {
                        "Meta packages also don't need a PLIST file.")
        }
 
-       ck := &PlistChecker{
+       ck := PlistChecker{
                make(map[string]*PlistLine),
                make(map[string]*PlistLine),
                "",
@@ -53,7 +53,7 @@ func (ck *PlistChecker) Check(plainLines
        ck.collectFilesAndDirs(plines)
 
        if plines[0].Basename == "PLIST.common_end" {
-               commonLines := Load(strings.TrimSuffix(plines[0].FileName, "_end"), NotEmpty)
+               commonLines := Load(strings.TrimSuffix(plines[0].Filename, "_end"), NotEmpty)
                if commonLines != nil {
                        ck.collectFilesAndDirs(ck.NewLines(commonLines))
                }
@@ -192,7 +192,7 @@ func (ck *PlistChecker) checkpath(pline 
        }
        if hasSuffix(text, "/perllocal.pod") {
                pline.Warnf("perllocal.pod files should not be in the PLIST.")
-               Explain(
+               G.Explain(
                        "This file is handled automatically by the INSTALL/DEINSTALL scripts,",
                        "since its contents changes frequently.")
        }
@@ -204,9 +204,9 @@ func (ck *PlistChecker) checkpath(pline 
 func (ck *PlistChecker) checkSorted(pline *PlistLine) {
        if text := pline.text; G.Opts.WarnPlistSort && hasAlnumPrefix(text) && !containsVarRef(text) {
                if ck.lastFname != "" {
-                       if ck.lastFname > text && !G.Opts.Autofix {
+                       if ck.lastFname > text && !G.Logger.Opts.Autofix {
                                pline.Warnf("%q should be sorted before %q.", text, ck.lastFname)
-                               Explain(
+                               G.Explain(
                                        "The files in the PLIST should be sorted alphabetically.",
                                        "To fix this, run \"pkglint -F PLIST\".")
                        }
@@ -227,7 +227,7 @@ func (ck *PlistChecker) checkDuplicate(p
        }
 
        fix := pline.Autofix()
-       fix.Errorf("Duplicate file name %q, already appeared in %s.", text, pline.RefTo(prev.Line))
+       fix.Errorf("Duplicate filename %q, already appeared in %s.", text, pline.RefTo(prev.Line))
        fix.Delete()
        fix.Apply()
 }
@@ -235,7 +235,7 @@ func (ck *PlistChecker) checkDuplicate(p
 func (ck *PlistChecker) checkpathBin(pline *PlistLine, dirname, basename string) {
        if contains(dirname, "/") {
                pline.Warnf("The bin/ directory should not have subdirectories.")
-               Explain(
+               G.Explain(
                        "The programs in bin/ are collected there to be executable by the",
                        "user without having to type an absolute path.  This advantage does",
                        "not apply to programs in subdirectories of bin/.  These programs",
@@ -298,7 +298,7 @@ func (ck *PlistChecker) checkpathLib(pli
 func (ck *PlistChecker) checkpathMan(pline *PlistLine) {
        m, catOrMan, section, manpage, ext, gz := match5(pline.text, `^man/(cat|man)(\w+)/(.*?)\.(\w+)(\.gz)?$`)
        if !m {
-               // maybe: line.Warnf("Invalid file name %q for manual page.", text)
+               // maybe: line.Warnf("Invalid filename %q for manual page.", text)
                return
        }
 
@@ -346,7 +346,7 @@ func (ck *PlistChecker) checkpathShare(p
 
                if text == "share/icons/hicolor/icon-theme.cache" && G.Pkg.Pkgpath != "graphics/hicolor-icon-theme" {
                        pline.Errorf("The file icon-theme.cache must not appear in any PLIST file.")
-                       Explain(
+                       G.Explain(
                                "Remove this line and add the following line to the package Makefile.",
                                "",
                                ".include \"../../graphics/hicolor-icon-theme/buildlink3.mk\"")
@@ -356,7 +356,7 @@ func (ck *PlistChecker) checkpathShare(p
                        f := "../../graphics/gnome-icon-theme/buildlink3.mk"
                        if G.Pkg.included[f] == nil {
                                pline.Errorf("The package Makefile must include %q.", f)
-                               Explain(
+                               G.Explain(
                                        "Packages that install GNOME icons must maintain the icon theme",
                                        "cache.")
                        }
@@ -377,7 +377,7 @@ func (ck *PlistChecker) checkpathShare(p
 
        case hasPrefix(text, "share/info/"):
                pline.Warnf("Info pages should be installed into info/, not share/info/.")
-               Explain(
+               G.Explain(
                        "To fix this, add INFO_FILES=yes to the package Makefile.")
 
        case hasPrefix(text, "share/locale/") && hasSuffix(text, ".mo"):
@@ -390,9 +390,9 @@ func (ck *PlistChecker) checkpathShare(p
 
 func (pline *PlistLine) CheckTrailingWhitespace() {
        if hasSuffix(pline.text, " ") || hasSuffix(pline.text, "\t") {
-               pline.Errorf("pkgsrc does not support filenames ending in white-space.")
-               Explain(
-                       "Each character in the PLIST is relevant, even trailing white-space.")
+               pline.Errorf("pkgsrc does not support filenames ending in whitespace.")
+               G.Explain(
+                       "Each character in the PLIST is relevant, even trailing whitespace.")
        }
 }
 
@@ -420,10 +420,10 @@ func (pline *PlistLine) CheckDirective(c
 
        case "dirrm":
                pline.Warnf("@dirrm is obsolete. Please remove this line.")
-               Explain(
+               G.Explain(
                        "Directories are removed automatically when they are empty.",
                        "When a package needs an empty directory, it can use the @pkgdir",
-                       "command in the PLIST")
+                       "command in the PLIST.")
 
        case "imake-man":
                args := fields(arg)
@@ -444,7 +444,7 @@ func (pline *PlistLine) CheckDirective(c
 
 func (pline *PlistLine) warnImakeMannewsuffix() {
        pline.Warnf("IMAKE_MANNEWSUFFIX is not meant to appear in PLISTs.")
-       Explain(
+       G.Explain(
                "This is the result of a print-PLIST call that has not been edited",
                "manually by the package maintainer.  Please replace the",
                "IMAKE_MANNEWSUFFIX with:",
@@ -491,13 +491,13 @@ func NewPlistLineSorter(plines []*PlistL
 
 func (s *plistLineSorter) Sort() {
        if line := s.unsortable; line != nil {
-               if G.Opts.ShowAutofix || G.Opts.Autofix {
+               if G.Logger.IsAutofix() {
                        trace.Stepf("%s: This line prevents pkglint from sorting the PLIST automatically.", line)
                }
                return
        }
 
-       if !shallBeLogged("%q should be sorted before %q.") {
+       if !G.shallBeLogged("%q should be sorted before %q.") {
                return
        }
        if len(s.middle) == 0 {
@@ -536,5 +536,5 @@ func (s *plistLineSorter) Sort() {
                lines = append(lines, pline.Line)
        }
 
-       s.autofixed = SaveAutofixChanges(NewLines(lines[0].FileName, lines))
+       s.autofixed = SaveAutofixChanges(NewLines(lines[0].Filename, lines))
 }
Index: pkgsrc/pkgtools/pkglint/files/util.go
diff -u pkgsrc/pkgtools/pkglint/files/util.go:1.31 pkgsrc/pkgtools/pkglint/files/util.go:1.32
--- pkgsrc/pkgtools/pkglint/files/util.go:1.31  Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/util.go       Sun Dec  2 01:57:48 2018
@@ -2,8 +2,10 @@ package main
 
 import (
        "fmt"
+       "hash/crc64"
        "io/ioutil"
        "netbsd.org/pkglint/regex"
+       "netbsd.org/pkglint/textproc"
        "os"
        "path"
        "path/filepath"
@@ -117,9 +119,9 @@ func mustMatch(s string, re regex.Patter
        panic(fmt.Sprintf("mustMatch %q %q", s, re))
 }
 
-func isEmptyDir(fileName string) bool {
-       dirents, err := ioutil.ReadDir(fileName)
-       if err != nil || hasSuffix(fileName, "/CVS") {
+func isEmptyDir(filename string) bool {
+       dirents, err := ioutil.ReadDir(filename)
+       if err != nil || hasSuffix(filename, "/CVS") {
                return true
        }
        for _, dirent := range dirents {
@@ -127,7 +129,7 @@ func isEmptyDir(fileName string) bool {
                if isIgnoredFilename(name) {
                        continue
                }
-               if dirent.IsDir() && isEmptyDir(fileName+"/"+name) {
+               if dirent.IsDir() && isEmptyDir(filename+"/"+name) {
                        continue
                }
                return false
@@ -135,24 +137,24 @@ func isEmptyDir(fileName string) bool {
        return true
 }
 
-func getSubdirs(fileName string) []string {
-       dirents, err := ioutil.ReadDir(fileName)
+func getSubdirs(filename string) []string {
+       dirents, err := ioutil.ReadDir(filename)
        if err != nil {
-               NewLineWhole(fileName).Fatalf("Cannot be read: %s", err)
+               NewLineWhole(filename).Fatalf("Cannot be read: %s", err)
        }
 
        var subdirs []string
        for _, dirent := range dirents {
                name := dirent.Name()
-               if dirent.IsDir() && !isIgnoredFilename(name) && !isEmptyDir(fileName+"/"+name) {
+               if dirent.IsDir() && !isIgnoredFilename(name) && !isEmptyDir(filename+"/"+name) {
                        subdirs = append(subdirs, name)
                }
        }
        return subdirs
 }
 
-func isIgnoredFilename(fileName string) bool {
-       switch fileName {
+func isIgnoredFilename(filename string) bool {
+       switch filename {
        case ".", "..", "CVS", ".svn", ".git", ".hg":
                return true
        }
@@ -160,12 +162,12 @@ func isIgnoredFilename(fileName string) 
 }
 
 // Checks whether a file is already committed to the CVS repository.
-func isCommitted(fileName string) bool {
-       lines := loadCvsEntries(fileName)
+func isCommitted(filename string) bool {
+       lines := loadCvsEntries(filename)
        if lines == nil {
                return false
        }
-       needle := "/" + path.Base(fileName) + "/"
+       needle := "/" + path.Base(filename) + "/"
        for _, line := range lines.Lines {
                if hasPrefix(line.Text, needle) {
                        return true
@@ -174,10 +176,10 @@ func isCommitted(fileName string) bool {
        return false
 }
 
-func isLocallyModified(fileName string) bool {
-       baseName := path.Base(fileName)
+func isLocallyModified(filename string) bool {
+       baseName := path.Base(filename)
 
-       lines := loadCvsEntries(fileName)
+       lines := loadCvsEntries(filename)
        if lines == nil {
                return false
        }
@@ -185,14 +187,14 @@ func isLocallyModified(fileName string) 
        for _, line := range lines.Lines {
                fields := strings.Split(line.Text, "/")
                if 3 < len(fields) && fields[1] == baseName {
-                       st, err := os.Stat(fileName)
+                       st, err := os.Stat(filename)
                        if err != nil {
                                return true
                        }
 
                        // According to http://cvsman.com/cvs-1.12.12/cvs_19.php, format both timestamps.
                        cvsModTime := fields[3]
-                       fsModTime := st.ModTime().Format(time.ANSIC)
+                       fsModTime := st.ModTime().UTC().Format(time.ANSIC)
                        if trace.Tracing {
                                trace.Stepf("cvs.time=%q fs.time=%q", cvsModTime, fsModTime)
                        }
@@ -203,8 +205,8 @@ func isLocallyModified(fileName string) 
        return false
 }
 
-func loadCvsEntries(fileName string) Lines {
-       dir := path.Dir(fileName)
+func loadCvsEntries(filename string) Lines {
+       dir := path.Dir(filename)
        if dir == G.CvsEntriesDir {
                return G.CvsEntriesLines
        }
@@ -249,7 +251,6 @@ func shorten(s string, maxChars int) str
        for i := range s {
                if chars >= maxChars {
                        return s[:i] + "..."
-                       break
                }
                chars++
        }
@@ -302,13 +303,13 @@ func varIsUsedSimilar(varname string) bo
                G.Pkg != nil && G.Pkg.vars.UsedSimilar(varname)
 }
 
-func fileExists(fileName string) bool {
-       st, err := os.Stat(fileName)
+func fileExists(filename string) bool {
+       st, err := os.Stat(filename)
        return err == nil && st.Mode().IsRegular()
 }
 
-func dirExists(fileName string) bool {
-       st, err := os.Stat(fileName)
+func dirExists(filename string) bool {
+       st, err := os.Stat(filename)
        return err == nil && st.Mode().IsDir()
 }
 
@@ -367,77 +368,72 @@ func relpath(from, to string) string {
        return result
 }
 
-func abspath(fileName string) string {
-       abs, err := filepath.Abs(fileName)
-       G.Assertf(err == nil, "abspath %q.", fileName)
+func abspath(filename string) string {
+       abs, err := filepath.Abs(filename)
+       G.Assertf(err == nil, "abspath %q.", filename)
        return filepath.ToSlash(abs)
 }
 
 // Differs from path.Clean in that only "../../" is replaced, not "../".
 // Also, the initial directory is always kept.
 // This is to provide the package path as context in recursive invocations of pkglint.
-func cleanpath(fileName string) string {
-       tmp := fileName
-       for len(tmp) > 2 && hasPrefix(tmp, "./") {
-               tmp = tmp[2:]
-       }
-       for contains(tmp, "/./") {
-               tmp = strings.Replace(tmp, "/./", "/", -1)
-       }
-       if len(tmp) > 2 && hasSuffix(tmp, "/.") {
-               tmp = tmp[:len(tmp)-2]
-       }
-       for contains(tmp, "//") {
-               tmp = strings.Replace(tmp, "//", "/", -1)
+func cleanpath(filename string) string {
+       parts := make([]string, 0, 5)
+       lex := textproc.NewLexer(filename)
+       for lex.SkipString("./") {
        }
 
-       // 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
+       for !lex.EOF() {
+               part := lex.NextBytesFunc(func(b byte) bool { return b != '/' })
+               parts = append(parts, part)
+               n := len(parts)
+               if n >= 5 && parts[n-1] == ".." && parts[n-2] == ".." && parts[n-3] != ".." && parts[n-4] != ".." {
+                       parts = parts[:n-4]
+               }
+               if lex.SkipByte('/') {
+                       for lex.SkipByte('/') || lex.SkipString("./") {
                        }
                }
        }
 
-       tmp = strings.TrimSuffix(tmp, "/")
-       return tmp
+       if len(parts) == 0 {
+               return "."
+       }
+       return strings.Join(parts, "/")
 }
 
 func containsVarRef(s string) bool {
        return contains(s, "${")
 }
 
-func hasAlnumPrefix(s string) bool {
-       if s == "" {
-               return false
-       }
-       b := s[0]
-       return '0' <= b && b <= '9' || 'A' <= b && b <= 'Z' || b == '_' || 'a' <= b && b <= 'z'
-}
+func hasAlnumPrefix(s string) bool { return s != "" && textproc.AlnumU.Contains(s[0]) }
 
 // Once remembers with which arguments its FirstTime method has been called
 // and only returns true on each first call.
 type Once struct {
-       seen map[string]bool
+       seen map[uint64]bool
 }
 
 func (o *Once) FirstTime(what string) bool {
-       if o.seen == nil {
-               o.seen = make(map[string]bool)
+       return o.check(crc64.Checksum([]byte(what), crc64.MakeTable(crc64.ECMA)))
+}
+
+func (o *Once) FirstTimeSlice(whats ...string) bool {
+       crc := crc64.New(crc64.MakeTable(crc64.ECMA))
+       for _, what := range whats {
+               _, _ = crc.Write([]byte(what))
        }
-       if _, ok := o.seen[what]; ok {
+       return o.check(crc.Sum64())
+}
+
+func (o *Once) check(key uint64) bool {
+       if _, ok := o.seen[key]; ok {
                return false
        }
-       o.seen[what] = true
+       if o.seen == nil {
+               o.seen = make(map[uint64]bool)
+       }
+       o.seen[key] = true
        return true
 }
 
@@ -596,8 +592,8 @@ func naturalLess(str1, str2 string) bool
 
        idx := 0
        len1, len2 := len(str1), len(str2)
-       len := len1 + len2 - imax(len1, len2)
-       for idx < len {
+       minLen := len1 + len2 - imax(len1, len2)
+       for idx < minLen {
                c1, c2 := str1[idx], str2[idx]
                dig1, dig2 := isDigit(c1), isDigit(c2)
                switch {
@@ -727,8 +723,8 @@ func (s *RedundantScope) Handle(mkline M
 
 // IsPrefs returns whether the given file, when included, loads the user
 // preferences.
-func IsPrefs(fileName string) bool {
-       switch path.Base(fileName) {
+func IsPrefs(filename string) bool {
+       switch path.Base(filename) {
        case // See https://github.com/golang/go/issues/28057
                "bsd.prefs.mk",         // in mk/
                "bsd.fast.prefs.mk",    // in mk/
@@ -742,7 +738,7 @@ func IsPrefs(fileName string) bool {
 
 func isalnum(s string) bool {
        for _, ch := range []byte(s) {
-               if !('0' <= ch && ch <= '9' || 'A' <= ch && ch <= 'Z' || ch == '_' || 'a' <= ch && ch <= 'z') {
+               if !textproc.AlnumU.Contains(ch) {
                        return false
                }
        }
@@ -773,8 +769,8 @@ func NewFileCache(size int) *FileCache {
                0}
 }
 
-func (c *FileCache) Put(fileName string, options LoadOptions, lines Lines) {
-       key := c.key(fileName)
+func (c *FileCache) Put(filename string, options LoadOptions, lines Lines) {
+       key := c.key(filename)
 
        entry := c.mapping[key]
        if entry == nil {
@@ -798,7 +794,9 @@ func (c *FileCache) removeOldEntries() {
 
        if G.Testing {
                for _, e := range c.table {
-                       G.logOut.Printf("FileCache %q with count %d.\n", e.key, e.count)
+                       if trace.Tracing {
+                               trace.Stepf("FileCache %q with count %d.", e.key, e.count)
+                       }
                }
        }
 
@@ -806,8 +804,8 @@ func (c *FileCache) removeOldEntries() {
        newLen := len(c.table)
        for newLen > 0 && c.table[newLen-1].count == minCount {
                e := c.table[newLen-1]
-               if G.Testing {
-                       G.logOut.Printf("FileCache.Evict %q with count %d.\n", e.key, e.count)
+               if trace.Tracing {
+                       trace.Stepf("FileCache.Evict %q with count %d.", e.key, e.count)
                }
                delete(c.mapping, e.key)
                newLen--
@@ -816,15 +814,15 @@ func (c *FileCache) removeOldEntries() {
 
        // To avoid files getting stuck in the cache.
        for _, e := range c.table {
-               if G.Testing {
-                       G.logOut.Printf("FileCache.Halve %q with count %d.\n", e.key, e.count)
+               if trace.Tracing {
+                       trace.Stepf("FileCache.Halve %q with count %d.", e.key, e.count)
                }
                e.count /= 2
        }
 }
 
-func (c *FileCache) Get(fileName string, options LoadOptions) Lines {
-       key := c.key(fileName)
+func (c *FileCache) Get(filename string, options LoadOptions) Lines {
+       key := c.key(filename)
        entry, found := c.mapping[key]
        if found && entry.options == options {
                c.hits++
@@ -832,16 +830,16 @@ func (c *FileCache) Get(fileName string,
 
                lines := make([]Line, entry.lines.Len())
                for i, line := range entry.lines.Lines {
-                       lines[i] = NewLineMulti(fileName, int(line.firstLine), int(line.lastLine), line.Text, line.raw)
+                       lines[i] = NewLineMulti(filename, int(line.firstLine), int(line.lastLine), line.Text, line.raw)
                }
-               return NewLines(fileName, lines)
+               return NewLines(filename, lines)
        }
        c.misses++
        return nil
 }
 
-func (c *FileCache) Evict(fileName string) {
-       key := c.key(fileName)
+func (c *FileCache) Evict(filename string) {
+       key := c.key(filename)
        entry, found := c.mapping[key]
        if found {
                delete(c.mapping, key)
@@ -853,10 +851,89 @@ func (c *FileCache) Evict(fileName strin
        }
 }
 
-func (c *FileCache) key(fileName string) string {
-       return path.Clean(fileName)
+func (c *FileCache) key(filename string) string {
+       return path.Clean(filename)
 }
 
 func makeHelp(topic string) string { return bmake("help topic=" + topic) }
 
 func bmake(target string) string { return sprintf("%s %s", confMake, target) }
+
+func seeGuide(sectionName, sectionID string) string {
+       return sprintf("See the pkgsrc guide, section %q: https://www.NetBSD.org/docs/pkgsrc/pkgsrc.html#%s";,
+               sectionName, sectionID)
+}
+
+func wrap(max int, lines ...string) []string {
+       var wrapped []string
+       var buf strings.Builder
+       nonSpace := textproc.Space.Inverse()
+
+       for _, line := range lines {
+               if line == "" || isHspace(line[0]) || line[0] == '*' {
+                       if buf.Len() > 0 {
+                               wrapped = append(wrapped, buf.String())
+                               buf.Reset()
+                       }
+                       wrapped = append(wrapped, line)
+                       continue
+               }
+
+               lexer := textproc.NewLexer(line)
+               for !lexer.EOF() {
+                       bol := len(lexer.Rest()) == len(line)
+                       space := lexer.NextBytesSet(textproc.Space)
+                       word := lexer.NextBytesSet(nonSpace)
+
+                       if bol && space == "" && buf.Len() > 0 {
+                               space = " "
+                       }
+
+                       if buf.Len() > 0 && buf.Len()+len(space)+len(word) > max {
+                               wrapped = append(wrapped, buf.String())
+                               buf.Reset()
+                               if hasPrefix(space, " ") {
+                                       space = ""
+                               }
+                       }
+
+                       buf.WriteString(space)
+                       buf.WriteString(word)
+               }
+       }
+
+       if buf.Len() > 0 {
+               wrapped = append(wrapped, buf.String())
+       }
+
+       return wrapped
+}
+
+// escapePrintable returns an ASCII-only string that represents the given string
+// very closely, but without putting any physical terminal or terminal emulator
+// at the risk of interpreting malicious data from the files checked by pkglint.
+// This escaping is not reversible, and it doesn't need to.
+func escapePrintable(s string) string {
+       i := 0
+       for i < len(s) && textproc.XPrint.Contains(s[i]) {
+               i++
+       }
+       if i == len(s) {
+               return s
+       }
+
+       var escaped strings.Builder
+       escaped.WriteString(s[:i])
+       rest := s[i:]
+       for j, r := range rest {
+               switch {
+               case rune(byte(r)) == r && textproc.XPrint.Contains(byte(rest[j])):
+                       escaped.WriteByte(byte(r))
+               case r == 0xFFFD && !hasPrefix(rest[j:], "\uFFFD"):
+                       _, _ = fmt.Fprintf(&escaped, "\\x%02X", rest[j])
+               default:
+                       _, _ = fmt.Fprintf(&escaped, "%U", r)
+               }
+       }
+       return escaped.String()
+}

Index: pkgsrc/pkgtools/pkglint/files/shell.y
diff -u pkgsrc/pkgtools/pkglint/files/shell.y:1.2 pkgsrc/pkgtools/pkglint/files/shell.y:1.3
--- pkgsrc/pkgtools/pkglint/files/shell.y:1.2   Sun Jul 10 21:24:47 2016
+++ pkgsrc/pkgtools/pkglint/files/shell.y       Sun Dec  2 01:57:48 2018
@@ -153,9 +153,9 @@ term : term separator and_or {
 
 for_clause : tkFOR tkWORD linebreak do_group {
        args := NewShToken("\"$$@\"",
-               &ShAtom{shtWord, "\"",shqDquot, nil},
-               &ShAtom{shtWord, "$$@",shqDquot, nil},
-               &ShAtom{shtWord,"\"",shqPlain, nil})
+               &ShAtom{shtText, "\"", shqDquot, nil},
+               &ShAtom{shtShVarUse, "$$@", shqDquot, "@"},
+               &ShAtom{shtText, "\"", shqPlain, nil})
        $$ = &MkShForClause{$2.MkText, []*ShToken{args}, $4}
 }
 for_clause : tkFOR tkWORD linebreak tkIN sequential_sep do_group {

Index: pkgsrc/pkgtools/pkglint/files/shell_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shell_test.go:1.33 pkgsrc/pkgtools/pkglint/files/shell_test.go:1.34
--- pkgsrc/pkgtools/pkglint/files/shell_test.go:1.33    Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/shell_test.go Sun Dec  2 01:57:48 2018
@@ -2,6 +2,7 @@ package main
 
 import (
        "gopkg.in/check.v1"
+       "strings"
 )
 
 func (s *Suite) Test_splitIntoShellTokens__line_continuation(c *check.C) {
@@ -85,6 +86,16 @@ func (s *Suite) Test_splitIntoShellToken
        c.Check(rest, equals, "")
 }
 
+// Two shell variables, next to each other,
+// are two separate atoms but count as a single token.
+func (s *Suite) Test_splitIntoShellTokens__two_shell_variables(c *check.C) {
+       code := "echo $$i$$j"
+       words, rest := splitIntoShellTokens(dummyLine, code)
+
+       c.Check(words, deepEquals, []string{"echo", "$$i$$j"})
+       c.Check(rest, equals, "")
+}
+
 func (s *Suite) Test_splitIntoMkWords__semicolons(c *check.C) {
        words, rest := splitIntoMkWords(dummyLine, "word1 word2;;;")
 
@@ -142,7 +153,7 @@ func (s *Suite) Test_ShellLine_CheckShel
        t.SetupTool("unzip", "UNZIP_CMD", AtRunTime)
 
        checkShellCommandLine := func(shellCommand string) {
-               G.Mk = t.NewMkLines("fileName",
+               G.Mk = t.NewMkLines("filename",
                        "\t"+shellCommand)
                shline := NewShellLine(G.Mk.mklines[0])
 
@@ -158,10 +169,10 @@ func (s *Suite) Test_ShellLine_CheckShel
        checkShellCommandLine("uname=`uname`; echo $$uname; echo; ${PREFIX}/bin/command")
 
        t.CheckOutputLines(
-               "WARN: fileName:1: Unknown shell command \"uname\".",
-               "WARN: fileName:1: Please switch to \"set -e\" mode before using a semicolon (after \"uname=`uname`\") to separate commands.",
-               "WARN: fileName:1: Unknown shell command \"echo\".",
-               "WARN: fileName:1: Unknown shell command \"echo\".")
+               "WARN: filename:1: Unknown shell command \"uname\".",
+               "WARN: filename:1: Please switch to \"set -e\" mode before using a semicolon (after \"uname=`uname`\") to separate commands.",
+               "WARN: filename:1: Unknown shell command \"echo\".",
+               "WARN: filename:1: Unknown shell command \"echo\".")
 
        t.SetupTool("echo", "", AtRunTime)
        t.SetupVartypes()
@@ -169,39 +180,41 @@ func (s *Suite) Test_ShellLine_CheckShel
        checkShellCommandLine("echo ${PKGNAME:Q}") // vucQuotPlain
 
        t.CheckOutputLines(
-               "WARN: fileName:1: PKGNAME may not be used in this file; it would be ok in Makefile, Makefile.*, *.mk.",
-               "NOTE: fileName:1: The :Q operator isn't necessary for ${PKGNAME} here.")
+               "WARN: filename:1: PKGNAME may not be used in this file; it would be ok in Makefile, Makefile.*, *.mk.",
+               "NOTE: filename:1: The :Q operator isn't necessary for ${PKGNAME} here.")
 
        checkShellCommandLine("echo \"${CFLAGS:Q}\"") // vucQuotDquot
 
        t.CheckOutputLines(
-               "WARN: fileName:1: Please don't use the :Q operator in double quotes.",
-               "WARN: fileName:1: CFLAGS may not be used in this file; "+
+               "WARN: filename:1: Please don't use the :Q operator in double quotes.",
+               "WARN: filename:1: CFLAGS may not be used in this file; "+
                        "it would be ok in Makefile, Makefile.common, options.mk, *.mk.",
-               "WARN: fileName:1: Please use ${CFLAGS:M*:Q} instead of ${CFLAGS:Q} "+
+               "WARN: filename:1: Please use ${CFLAGS:M*:Q} instead of ${CFLAGS:Q} "+
                        "and make sure the variable appears outside of any quoting characters.")
 
        checkShellCommandLine("echo '${COMMENT:Q}'") // vucQuotSquot
 
        t.CheckOutputLines(
-               "WARN: fileName:1: COMMENT may not be used in any file; it is a write-only variable.",
-               "WARN: fileName:1: Please move ${COMMENT:Q} outside of any quoting characters.")
+               "WARN: filename:1: COMMENT may not be used in any file; it is a write-only variable.",
+               "WARN: filename:1: Please move ${COMMENT:Q} outside of any quoting characters.")
 
        checkShellCommandLine("echo target=$@ exitcode=$$? '$$' \"\\$$\"")
 
        t.CheckOutputLines(
-               "WARN: fileName:1: Please use \"${.TARGET}\" instead of \"$@\".",
-               "WARN: fileName:1: The $? shell variable is often not available in \"set -e\" mode.")
+               "WARN: filename:1: Please use \"${.TARGET}\" instead of \"$@\".",
+               "WARN: filename:1: The $? shell variable is often not available in \"set -e\" mode.")
 
        checkShellCommandLine("echo $$@")
 
        t.CheckOutputLines(
-               "WARN: fileName:1: The $@ shell variable should only be used in double quotes.")
+               "WARN: filename:1: The $@ shell variable should only be used in double quotes.")
 
        checkShellCommandLine("echo \"$$\"") // As seen by make(1); the shell sees: echo "$"
 
-       t.CheckOutputLines(
-               "WARN: fileName:1: Unescaped $ or strange shell variable found.")
+       // No warning about a possibly missed variable name.
+       // This occurs only rarely, and typically as part of a regular expression
+       // where it is used intentionally.
+       t.CheckOutputEmpty()
 
        checkShellCommandLine("echo \"\\n\"")
 
@@ -219,7 +232,8 @@ func (s *Suite) Test_ShellLine_CheckShel
        checkShellCommandLine("${RUN} subdir=\"`unzip -c \"$$e\" install.rdf | awk '/re/ { print \"hello\" }'`\"")
 
        t.CheckOutputLines(
-               "WARN: fileName:1: The exitcode of \"unzip\" at the left of the | operator is ignored.")
+               "WARN: filename:1: Double quotes inside backticks inside double quotes are error prone.",
+               "WARN: filename:1: The exitcode of \"unzip\" at the left of the | operator is ignored.")
 
        // From mail/thunderbird/Makefile, rev. 1.159
        checkShellCommandLine("" +
@@ -232,8 +246,9 @@ func (s *Suite) Test_ShellLine_CheckShel
                "done")
 
        t.CheckOutputLines(
-               "WARN: fileName:1: XPI_FILES is used but not defined.",
-               "WARN: fileName:1: The exitcode of \"${UNZIP_CMD}\" at the left of the | operator is ignored.")
+               "WARN: filename:1: XPI_FILES is used but not defined.",
+               "WARN: filename:1: Double quotes inside backticks inside double quotes are error prone.",
+               "WARN: filename:1: The exitcode of \"${UNZIP_CMD}\" at the left of the | operator is ignored.")
 
        // From x11/wxGTK28/Makefile
        checkShellCommandLine("" +
@@ -244,25 +259,25 @@ func (s *Suite) Test_ShellLine_CheckShel
                "done")
 
        t.CheckOutputLines(
-               "WARN: fileName:1: WRKSRC may not be used in this file; it would be ok in Makefile, Makefile.*, *.mk.",
-               "WARN: fileName:1: Unknown shell command \"[\".",
-               "WARN: fileName:1: Unknown shell command \"${TOOLS_PATH.msgfmt}\".")
+               "WARN: filename:1: WRKSRC may not be used in this file; it would be ok in Makefile, Makefile.*, *.mk.",
+               "WARN: filename:1: Unknown shell command \"[\".",
+               "WARN: filename:1: Unknown shell command \"${TOOLS_PATH.msgfmt}\".")
 
        checkShellCommandLine("@cp from to")
 
        t.CheckOutputLines(
-               "WARN: fileName:1: The shell command \"cp\" should not be hidden.")
+               "WARN: filename:1: The shell command \"cp\" should not be hidden.")
 
        checkShellCommandLine("-cp from to")
 
        t.CheckOutputLines(
-               "WARN: fileName:1: Using a leading \"-\" to suppress errors is deprecated.")
+               "WARN: filename:1: Using a leading \"-\" to suppress errors is deprecated.")
 
        checkShellCommandLine("-${MKDIR} deeply/nested/subdir")
 
        t.CheckOutputLines(
-               "NOTE: fileName:1: You don't need to use \"-\" before \"${MKDIR} deeply/nested/subdir\".",
-               "WARN: fileName:1: Using a leading \"-\" to suppress errors is deprecated.")
+               "NOTE: filename:1: You don't need to use \"-\" before \"${MKDIR} deeply/nested/subdir\".",
+               "WARN: filename:1: Using a leading \"-\" to suppress errors is deprecated.")
 
        G.Pkg = NewPackage(t.File("category/pkgbase"))
        G.Pkg.Plist.Dirs["share/pkgbase"] = true
@@ -271,15 +286,15 @@ func (s *Suite) Test_ShellLine_CheckShel
        checkShellCommandLine("${RUN} ${INSTALL_DATA_DIR} share/pkgbase ${PREFIX}/share/pkgbase")
 
        t.CheckOutputLines(
-               "NOTE: fileName:1: You can use AUTO_MKDIRS=yes or \"INSTALLATION_DIRS+= share/pkgbase\" "+
+               "NOTE: filename:1: You can use AUTO_MKDIRS=yes or \"INSTALLATION_DIRS+= share/pkgbase\" "+
                        "instead of \"${INSTALL_DATA_DIR}\".",
-               "WARN: fileName:1: The INSTALL_*_DIR commands can only handle one directory at a time.")
+               "WARN: filename:1: The INSTALL_*_DIR commands can only handle one directory at a time.")
 
        // A directory that is not found in the PLIST.
        checkShellCommandLine("${RUN} ${INSTALL_DATA_DIR} ${PREFIX}/share/other")
 
        t.CheckOutputLines(
-               "NOTE: fileName:1: You can use \"INSTALLATION_DIRS+= share/other\" instead of \"${INSTALL_DATA_DIR}\".")
+               "NOTE: filename:1: You can use \"INSTALLATION_DIRS+= share/other\" instead of \"${INSTALL_DATA_DIR}\".")
 
        G.Pkg = nil
 
@@ -293,7 +308,7 @@ func (s *Suite) Test_ShellLine_CheckShel
        t := s.Init(c)
 
        checkShellCommandLine := func(shellCommand string) {
-               G.Mk = t.NewMkLines("fileName",
+               G.Mk = t.NewMkLines("filename",
                        "\t"+shellCommand)
 
                G.Mk.ForEach(func(mkline MkLine) {
@@ -305,8 +320,8 @@ func (s *Suite) Test_ShellLine_CheckShel
        checkShellCommandLine("${STRIP} executable")
 
        t.CheckOutputLines(
-               "WARN: fileName:1: Unknown shell command \"${STRIP}\".",
-               "WARN: fileName:1: STRIP is used but not defined.")
+               "WARN: filename:1: Unknown shell command \"${STRIP}\".",
+               "WARN: filename:1: STRIP is used but not defined.")
 
        t.SetupVartypes()
 
@@ -363,7 +378,7 @@ func (s *Suite) Test_ShellProgramChecker
                "\t cat | right-side",
                "\t cat | echo | right-side",
                "\t echo | cat | right-side",
-               "\t sed s,s,s, fileName | right-side",
+               "\t sed s,s,s, filename | right-side",
                "\t sed s,s,s < input | right-side",
                "\t ./unknown | right-side",
                "\t var=value | right-side",
@@ -404,7 +419,7 @@ func (s *Suite) Test_ShellLine_CheckShel
        t := s.Init(c)
 
        t.SetupVartypes()
-       G.Mk = t.NewMkLines("fileName",
+       G.Mk = t.NewMkLines("filename",
                "# dummy")
        shline := NewShellLine(G.Mk.mklines[0])
 
@@ -419,13 +434,13 @@ func (s *Suite) Test_ShellLine_CheckShel
        G.Mk.ForEach(func(mkline MkLine) { shline.CheckWord(text, false, RunTime) })
 
        t.CheckOutputLines(
-               "WARN: fileName:1: Unknown shell command \"echo\".")
+               "WARN: filename:1: Unknown shell command \"echo\".")
 
        G.Mk.ForEach(func(mkline MkLine) { shline.CheckShellCommandLine(text) })
 
        // No parse errors
        t.CheckOutputLines(
-               "WARN: fileName:1: Unknown shell command \"echo\".")
+               "WARN: filename:1: Unknown shell command \"echo\".")
 }
 
 func (s *Suite) Test_ShellLine_CheckShellCommandLine__dollar_without_variable(c *check.C) {
@@ -433,7 +448,7 @@ func (s *Suite) Test_ShellLine_CheckShel
 
        t.SetupVartypes()
        t.SetupTool("pax", "", AtRunTime)
-       G.Mk = t.NewMkLines("fileName",
+       G.Mk = t.NewMkLines("filename",
                "# dummy")
        shline := NewShellLine(G.Mk.mklines[0])
 
@@ -457,11 +472,17 @@ func (s *Suite) Test_ShellLine_CheckWord
 
        checkWord("${${list}}", false)
 
-       t.CheckOutputEmpty() // No warning for variables that are completely indirect.
+       // No warning for the outer variable since it is completely indirect.
+       // The inner variable ${list} must still be defined, though.
+       t.CheckOutputLines(
+               "WARN: dummy.mk:1: list is used but not defined.",
+               "WARN: dummy.mk:1: list is used but not defined.")
 
        checkWord("${SED_FILE.${id}}", false)
 
-       t.CheckOutputEmpty() // No warning for variables that are partly indirect.
+       // No warning for variables that are partly indirect.
+       t.CheckOutputLines(
+               "WARN: dummy.mk:1: id is used but not defined.")
 
        // The unquoted $@ takes a different code path in pkglint than the quoted $@.
        checkWord("$@", false)
@@ -508,7 +529,7 @@ func (s *Suite) Test_ShellLine_CheckWord
 func (s *Suite) Test_ShellLine_CheckWord__dollar_without_variable(c *check.C) {
        t := s.Init(c)
 
-       shline := t.NewShellLine("fileName", 1, "# dummy")
+       shline := t.NewShellLine("filename", 1, "# dummy")
 
        shline.CheckWord("/.*~$$//g", false, RunTime) // Typical argument to pax(1).
 
@@ -519,50 +540,51 @@ func (s *Suite) Test_ShellLine_CheckWord
        t := s.Init(c)
 
        t.SetupTool("find", "FIND", AtRunTime)
-       shline := t.NewShellLine("fileName", 1, "\tfind . -exec rm -rf {} \\+")
+       shline := t.NewShellLine("filename", 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: fileName:1: Pkglint parse error in ShellLine.CheckWord at \"\\\\+\" (quoting=plain), rest: \\+")
+       // A backslash before any other character than " \ ` is discarded by the parser.
+       t.CheckOutputEmpty()
 }
 
 func (s *Suite) Test_ShellLine_CheckWord__squot_dollar(c *check.C) {
        t := s.Init(c)
 
-       shline := t.NewShellLine("fileName", 1, "\t'$")
+       shline := t.NewShellLine("filename", 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: fileName:1: Pkglint parse error in ShellLine.CheckWord at \"'$\" (quoting=s), rest: $")
+               "WARN: filename:1: Pkglint parse error in ShTokenizer.ShAtom at \"$\" (quoting=s).",
+               "WARN: filename: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("fileName", 1, "\t\"$")
+       shline := t.NewShellLine("filename", 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: fileName:1: Pkglint parse error in ShellLine.CheckWord at \"\\\"$\" (quoting=d), rest: $")
+               "WARN: filename:1: Pkglint parse error in ShTokenizer.ShAtom at \"$\" (quoting=d).",
+               "WARN: filename: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)
 
-       shline := t.NewShellLine("fileName", 1, "\t$$(echo output)")
+       shline := t.NewShellLine("filename", 1, "\t$$(echo output)")
 
        shline.CheckWord(shline.mkline.ShellCommand(), false, RunTime)
 
        t.CheckOutputLines(
-               "WARN: fileName:1: Invoking subshells via $(...) is not portable enough.")
+               "WARN: filename:1: Invoking subshells via $(...) is not portable enough.")
 }
 
 func (s *Suite) Test_ShellLine_CheckWord__PKGMANDIR(c *check.C) {
@@ -585,7 +607,7 @@ func (s *Suite) Test_ShellLine_CheckWord
 func (s *Suite) Test_ShellLine_unescapeBackticks__unfinished(c *check.C) {
        t := s.Init(c)
 
-       mklines := t.NewMkLines("fileName.mk",
+       mklines := t.NewMkLines("filename.mk",
                MkRcsID,
                "",
                "pre-configure:",
@@ -599,30 +621,64 @@ func (s *Suite) Test_ShellLine_unescapeB
 
        // FIXME: Mention the unfinished backquote.
        t.CheckOutputLines(
-               "WARN: fileName.mk:4: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}",
-               "WARN: fileName.mk:5: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"echo\"}")
+               "WARN: filename.mk:4: Pkglint ShellLine.CheckShellCommand: parse error at []string{\"\"}",
+               "WARN: filename.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)
 
+       mkline := t.NewMkLine("dummy.mk", 123, "\t# shell command")
+
        // 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)
+       atoms := []*ShAtom{
+               NewShAtom(shtText, "`", shqBackt)}
+       NewShellLine(mkline).
+               unescapeBackticks(&atoms, shqBackt)
 
        t.CheckOutputLines(
-               "ERROR: Unfinished backquotes: ")
+               "ERROR: dummy.mk:123: Unfinished backticks after \"\".")
 }
 
 func (s *Suite) Test_ShellLine_variableNeedsQuoting(c *check.C) {
+
+       test := func(shVarname string, expected bool) {
+               c.Check((*ShellLine).variableNeedsQuoting(nil, shVarname), equals, expected)
+       }
+
+       test("#", false) // A length is always an integer.
+       test("?", false) // The exit code is always an integer.
+       test("$", false) // The PID is always an integer.
+
+       // In most cases, file and directory names don't contain special characters,
+       // and if they do, the package will probably not build. Therefore pkglint
+       // doesn't require them to be quoted, but doing so does not hurt.
+       test("d", false)    // Typically used for directories.
+       test("f", false)    // Typically used for files.
+       test("i", false)    // Typically used for literal values without special characters.
+       test("id", false)   // Identifiers usually don't use special characters.
+       test("dir", false)  // See d above.
+       test("file", false) // See f above.
+       test("src", false)  // Typically used when copying files or directories.
+       test("dst", false)  // Typically used when copying files or directories.
+
+       test("bindir", false) // A typical GNU-style directory.
+       test("mandir", false) // A typical GNU-style directory.
+       test("prefix", false) //
+
+       test("bindirs", true) // A list of directories is typically separated by spaces.
+       test("var", true)     // Other variables are unknown, so they should be quoted.
+       test("0", true)       // The program name may contain special characters when given as full path.
+       test("1", true)       // Command line arguments can be arbitrary strings.
+}
+
+func (s *Suite) Test_ShellLine_variableNeedsQuoting__integration(c *check.C) {
        t := s.Init(c)
 
        t.SetupVartypes()
        t.SetupTool("cp", "", AtRunTime)
-       mklines := t.NewMkLines("fileName.mk",
+       mklines := t.NewMkLines("filename.mk",
                MkRcsID,
                "",
                // It's a bit silly to use shell variables in CONFIGURE_ARGS,
@@ -636,7 +692,7 @@ func (s *Suite) Test_ShellLine_variableN
        // Quoting check is currently disabled for real shell commands.
        // See ShellLine.CheckShellCommand, spc.checkWord.
        t.CheckOutputLines(
-               "WARN: fileName.mk:3: Unquoted shell variable \"target\".")
+               "WARN: filename.mk:3: Unquoted shell variable \"target\".")
 }
 
 func (s *Suite) Test_ShellLine_CheckShellCommandLine__echo(c *check.C) {
@@ -644,9 +700,9 @@ func (s *Suite) Test_ShellLine_CheckShel
 
        echo := t.SetupTool("echo", "ECHO", AtRunTime)
        echo.MustUseVarForm = true
-       G.Mk = t.NewMkLines("fileName",
+       G.Mk = t.NewMkLines("filename",
                "# dummy")
-       mkline := t.NewMkLine("fileName", 3, "# dummy")
+       mkline := t.NewMkLine("filename", 3, "# dummy")
 
        MkLineChecker{mkline}.checkText("echo \"hello, world\"")
 
@@ -655,7 +711,7 @@ func (s *Suite) Test_ShellLine_CheckShel
        NewShellLine(mkline).CheckShellCommandLine("echo \"hello, world\"")
 
        t.CheckOutputLines(
-               "WARN: fileName:3: Please use \"${ECHO}\" instead of \"echo\".")
+               "WARN: filename:3: Please use \"${ECHO}\" instead of \"echo\".")
 }
 
 func (s *Suite) Test_ShellLine_CheckShellCommandLine__shell_variables(c *check.C) {
@@ -697,21 +753,21 @@ func (s *Suite) Test_ShellLine_CheckShel
 func (s *Suite) Test_ShellLine_checkInstallCommand(c *check.C) {
        t := s.Init(c)
 
-       G.Mk = t.NewMkLines("fileName",
+       G.Mk = t.NewMkLines("filename",
                "# dummy")
        G.Mk.target = "do-install"
 
-       shline := t.NewShellLine("fileName", 1, "\tdummy")
+       shline := t.NewShellLine("filename", 1, "\tdummy")
 
        shline.checkInstallCommand("sed")
 
        t.CheckOutputLines(
-               "WARN: fileName:1: The shell command \"sed\" should not be used in the install phase.")
+               "WARN: filename:1: The shell command \"sed\" should not be used in the install phase.")
 
        shline.checkInstallCommand("cp")
 
        t.CheckOutputLines(
-               "WARN: fileName:1: ${CP} should not be used to install files.")
+               "WARN: filename:1: ${CP} should not be used to install files.")
 }
 
 func (s *Suite) Test_splitIntoMkWords(c *check.C) {
@@ -739,7 +795,7 @@ func (s *Suite) Test_ShellLine_CheckShel
        t.SetupVartypes()
        t.SetupTool("sed", "SED", AtRunTime)
        t.SetupTool("mv", "MV", AtRunTime)
-       shline := t.NewShellLine("Makefile", 85, "\t${RUN} ${SED} 's,#,// comment:,g' fileName > fileName.tmp; ${MV} fileName.tmp fileName")
+       shline := t.NewShellLine("Makefile", 85, "\t${RUN} ${SED} 's,#,// comment:,g' filename > filename.tmp; ${MV} filename.tmp filename")
 
        shline.CheckShellCommandLine(shline.mkline.ShellCommand())
 
@@ -813,34 +869,123 @@ func (s *Suite) Test_ShellLine__shell_co
                "WARN: ~/Makefile:3--4: A shell comment does not stop at the end of line.")
 }
 
+func (s *Suite) Test_ShellLine_checkWordQuoting(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupVartypes()
+       t.SetupTool("grep", "GREP", AtRunTime)
+
+       test := func(lineno int, input string) {
+               shline := t.NewShellLine("module.mk", lineno, "\t"+input)
+
+               shline.checkWordQuoting(shline.mkline.ShellCommand(), true, RunTime)
+       }
+
+       test(101, "socklen=`${GREP} 'expr' ${WRKSRC}/config.h`")
+
+       test(102, "s,$$from,$$to,")
+
+       // This variable is typically defined by GNU Configure,
+       // which cannot handle directories with special characters.
+       // Therefore using it unquoted is considered safe.
+       test(103, "${PREFIX}/$$bindir/program")
+
+       test(104, "$$@")
+
+       // TODO: Add separate tests for "set +e" and "set -e".
+       test(105, "$$?")
+
+       test(106, "$$(cat /bin/true)")
+
+       test(107, "\"$$\"")
+
+       test(108, "$$$$")
+
+       // TODO: The $ variable in line 108 doesn't need quoting.
+       t.CheckOutputLines(
+               "WARN: module.mk:102: Unquoted shell variable \"from\".",
+               "WARN: module.mk:102: Unquoted shell variable \"to\".",
+               "WARN: module.mk:104: The $@ shell variable should only be used in double quotes.",
+               "WARN: module.mk:105: The $? shell variable is often not available in \"set -e\" mode.",
+               "WARN: module.mk:106: Invoking subshells via $(...) is not portable enough.")
+}
+
 func (s *Suite) Test_ShellLine_unescapeBackticks(c *check.C) {
        t := s.Init(c)
 
-       shline := t.NewShellLine("dummy.mk", 13, "# dummy")
-       // foobar="`echo \"foo   bar\" "\ " "three"`"
-       text := "foobar=\"`echo \\\"foo   bar\\\" \"\\ \" \"three\"`\""
-       repl := G.NewPrefixReplacer(text)
-       repl.AdvanceStr("foobar=\"`")
+       test := func(lineno int, input string, expectedOutput string, expectedRest string) {
+               shline := t.NewShellLine("dummy.mk", lineno, "# dummy")
+
+               tok := NewShTokenizer(nil, input, false)
+               atoms := tok.ShAtoms()
 
-       backtCommand, newQuoting := shline.unescapeBackticks(text, repl, shqDquotBackt)
+               // Set up the correct quoting mode for the test by skipping
+               // uninteresting atoms at the beginning.
+               q := shqPlain
+               for atoms[0].MkText != "`" {
+                       q = atoms[0].Quoting
+                       atoms = atoms[1:]
+               }
+               c.Check(tok.Rest(), equals, "")
+
+               backtCommand := shline.unescapeBackticks(&atoms, q)
+
+               var actualRest strings.Builder
+               for _, atom := range atoms {
+                       actualRest.WriteString(atom.MkText)
+               }
 
-       c.Check(backtCommand, equals, "echo \"foo   bar\" \"\\ \" \"three\"")
-       c.Check(newQuoting, equals, shqDquot)
-       c.Check(repl.Rest(), equals, "\"")
+               c.Check(backtCommand, equals, expectedOutput)
+               c.Check(actualRest.String(), equals, expectedRest)
+       }
+
+       // The 1xx test cases are in shqPlain mode.
+
+       test(100, "`echo`end", "echo", "end")
+       test(101, "`echo $$var`end", "echo $$var", "end")
+       test(102, "``end", "", "end")
+       test(103, "`echo \"hello\"`end", "echo \"hello\"", "end")
+       test(104, "`echo 'hello'`end", "echo 'hello'", "end")
+       test(105, "`echo '\\\\\\\\'`end", "echo '\\\\'", "end")
+
+       // Only the characters " $ ` \ are unescaped. All others stay the same.
+       test(120, "`echo '\\n'`end", "echo '\\n'", "end")
+       test(121, "\tsocklen=`${GREP} 'expr' ${WRKSRC}/config.h`", "${GREP} 'expr' ${WRKSRC}/config.h", "")
+
+       // TODO: Add more details regarding which backslash is meant.
+       t.CheckOutputLines(
+               "WARN: dummy.mk:120: Backslashes should be doubled inside backticks.")
 
+       // The 2xx test cases are in shqDquot mode.
+
+       test(200, "\"`echo`\"", "echo", "\"")
+       test(201, "\"`echo \"\"`\"", "echo \"\"", "\"")
+
+       t.CheckOutputLines(
+               "WARN: dummy.mk:201: Double quotes inside backticks inside double quotes are error prone.")
+
+       // varname="`echo \"one   two\" "\ " "three"`"
+       test(202,
+               "varname=\"`echo \\\"one   two\\\" \"\\ \" \"three\"`\"",
+               "echo \"one   two\" \"\\ \" \"three\"",
+               "\"")
+
+       // TODO: Add more details regarding which backslash and backtick is meant.
        t.CheckOutputLines(
-               "WARN: dummy.mk:13: Backslashes should be doubled inside backticks.")
+               "WARN: dummy.mk:202: Backslashes should be doubled inside backticks.",
+               "WARN: dummy.mk:202: Double quotes inside backticks inside double quotes are error prone.",
+               "WARN: dummy.mk:202: Double quotes inside backticks inside double quotes are error prone.")
 }
 
 func (s *Suite) Test_ShellLine_unescapeBackticks__dquotBacktDquot(c *check.C) {
        t := s.Init(c)
 
-       mkline := t.NewMkLine("dummy.mk", 13, "\t var=\"`\"\"`\"")
+       t.SetupTool("echo", "", AtRunTime)
+       mkline := t.NewMkLine("dummy.mk", 13, "\t var=\"`echo \"\"`\"")
 
        MkLineChecker{mkline}.Check()
 
        t.CheckOutputLines(
-               "WARN: dummy.mk:13: Double quotes inside backticks inside double quotes are error prone.",
                "WARN: dummy.mk:13: Double quotes inside backticks inside double quotes are error prone.")
 }
 
@@ -1157,6 +1302,7 @@ func (s *Suite) Test_ShellProgramChecker
 
        t.SetupVartypes()
        t.SetupTool("echo", "", AtRunTime)
+       t.SetupTool("env", "", AtRunTime)
        t.SetupTool("grep", "GREP", AtRunTime)
        t.SetupTool("sed", "", AtRunTime)
        t.SetupTool("touch", "", AtRunTime)
@@ -1178,7 +1324,8 @@ func (s *Suite) Test_ShellProgramChecker
                "\techo 'starting'; echo 'done.'",
                "\techo 'logging' > log; echo 'done.'",
                "\techo 'to stderr' 1>&2; echo 'done.'",
-               "\techo 'hello' | tr -d 'aeiou'")
+               "\techo 'hello' | tr -d 'aeiou'",
+               "\tenv | grep '^PATH='")
 
        mklines.Check()
 

Index: pkgsrc/pkgtools/pkglint/files/shtypes_test.go
diff -u pkgsrc/pkgtools/pkglint/files/shtypes_test.go:1.5 pkgsrc/pkgtools/pkglint/files/shtypes_test.go:1.6
--- pkgsrc/pkgtools/pkglint/files/shtypes_test.go:1.5   Wed Sep  5 17:56:22 2018
+++ pkgsrc/pkgtools/pkglint/files/shtypes_test.go       Sun Dec  2 01:57:48 2018
@@ -8,10 +8,6 @@ func NewShAtom(typ ShAtomType, text stri
        return &ShAtom{typ, text, quoting, nil}
 }
 
-func NewShAtomVaruse(text string, quoting ShQuoting, varname string, modifiers ...string) *ShAtom {
-       return &ShAtom{shtVaruse, text, quoting, NewMkVarUse(varname, modifiers...)}
-}
-
 func (s *Suite) Test_ShAtomType_String(c *check.C) {
        c.Check(shtComment.String(), equals, "comment")
 }
@@ -24,8 +20,8 @@ func (s *Suite) Test_ShAtom_String(c *ch
        c.Check(len(atoms), equals, 5)
        c.Check(atoms[0].String(), equals, "varuse(\"ECHO\")")
        c.Check(atoms[1].String(), equals, "ShAtom(space, \" \", plain)")
-       c.Check(atoms[2].String(), equals, "ShAtom(word, \"\\\"\", d)")
-       c.Check(atoms[3].String(), equals, "ShAtom(word, \"hello, world\", d)")
+       c.Check(atoms[2].String(), equals, "ShAtom(text, \"\\\"\", d)")
+       c.Check(atoms[3].String(), equals, "ShAtom(text, \"hello, world\", d)")
        c.Check(atoms[4].String(), equals, "\"\\\"\"")
 }
 
@@ -37,5 +33,5 @@ func (s *Suite) Test_ShToken_String(c *c
        tokenizer := NewShTokenizer(dummyLine, "${ECHO} \"hello, world\"", false)
 
        c.Check(tokenizer.ShToken().String(), equals, "ShToken([varuse(\"ECHO\")])")
-       c.Check(tokenizer.ShToken().String(), equals, "ShToken([ShAtom(word, \"\\\"\", d) ShAtom(word, \"hello, world\", d) \"\\\"\"])")
+       c.Check(tokenizer.ShToken().String(), equals, "ShToken([ShAtom(text, \"\\\"\", d) ShAtom(text, \"hello, world\", d) \"\\\"\"])")
 }

Index: pkgsrc/pkgtools/pkglint/files/substcontext.go
diff -u pkgsrc/pkgtools/pkglint/files/substcontext.go:1.15 pkgsrc/pkgtools/pkglint/files/substcontext.go:1.16
--- pkgsrc/pkgtools/pkglint/files/substcontext.go:1.15  Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/substcontext.go       Sun Dec  2 01:57:48 2018
@@ -1,5 +1,7 @@
 package main
 
+import "netbsd.org/pkglint/textproc"
+
 // SubstContext records the state of a block of variable assignments
 // that make up a SUBST class (see `mk/subst.mk`).
 type SubstContext struct {
@@ -135,27 +137,33 @@ func (ctx *SubstContext) Varassign(mklin
                        if noConfigureLine := G.Pkg.vars.FirstDefinition("NO_CONFIGURE"); noConfigureLine != nil {
                                mkline.Warnf("SUBST_STAGE %s has no effect when NO_CONFIGURE is set (in %s).",
                                        value, mkline.RefTo(noConfigureLine))
-                               Explain(
+                               G.Explain(
                                        "To fix this properly, remove the definition of NO_CONFIGURE.")
                        }
                }
 
        case "SUBST_MESSAGE.*":
                ctx.dupString(mkline, &ctx.message, varname, value)
+
        case "SUBST_FILES.*":
                ctx.dupBool(mkline, &ctx.curr.seenFiles, varname, op, value)
+
        case "SUBST_SED.*":
                ctx.dupBool(mkline, &ctx.curr.seenSed, varname, op, value)
                ctx.curr.seenTransform = true
+
+               ctx.suggestSubstVars(mkline)
+
        case "SUBST_VARS.*":
                ctx.dupBool(mkline, &ctx.curr.seenVars, varname, op, value)
                ctx.curr.seenTransform = true
-               for _, substVar := range mkline.ValueSplit(value, "") {
+               for _, substVar := range mkline.ValueFields(value) {
                        if ctx.vars == nil {
                                ctx.vars = make(map[string]bool)
                        }
                        ctx.vars[substVar] = true
                }
+
        case "SUBST_FILTER_CMD.*":
                ctx.dupString(mkline, &ctx.filterCmd, varname, value)
                ctx.curr.seenTransform = true
@@ -230,3 +238,53 @@ func (ctx *SubstContext) dupBool(mkline 
        }
        *flag = true
 }
+
+func (ctx *SubstContext) suggestSubstVars(mkline MkLine) {
+
+       tokens, _ := splitIntoShellTokens(mkline.Line, mkline.Value())
+       for _, token := range tokens {
+
+               parser := NewMkParser(nil, token, false)
+               lexer := parser.lexer
+               if !lexer.SkipByte('s') {
+                       continue
+               }
+
+               separator := lexer.NextByteSet(textproc.XPrint) // Really any character works
+               if separator == -1 {
+                       continue
+               }
+
+               if !lexer.SkipByte('@') {
+                       continue
+               }
+
+               varname := parser.Varname()
+               if !lexer.SkipByte('@') || !lexer.SkipByte(byte(separator)) {
+                       continue
+               }
+
+               varuse := parser.VarUse()
+               if varuse == nil || varuse.varname != varname {
+                       continue
+               }
+
+               switch varuse.Mod() {
+               case "", ":Q":
+                       break
+               default:
+                       continue
+               }
+
+               if !lexer.SkipByte(byte(separator)) {
+                       continue
+               }
+
+               mkline.Notef("The substitution command %q can be replaced with \"SUBST_VARS.%s+= %s\".", token, ctx.id, varname)
+               mkline.Explain(
+                       "Replacing @VAR@ with ${VAR} is such a typical pattern that pkgsrc has built-in support for it,",
+                       "requiring only the variable name instead of the full sed command.")
+       }
+
+       // TODO: Autofix
+}

Index: pkgsrc/pkgtools/pkglint/files/vardefs.go
diff -u pkgsrc/pkgtools/pkglint/files/vardefs.go:1.49 pkgsrc/pkgtools/pkglint/files/vardefs.go:1.50
--- pkgsrc/pkgtools/pkglint/files/vardefs.go:1.49       Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/vardefs.go    Sun Dec  2 01:57:48 2018
@@ -23,13 +23,13 @@ func (src *Pkgsrc) InitVartypes() {
                m := mustMatch(varname, `^([A-Z_.][A-Z0-9_]*|@)(|\*|\.\*)$`)
                varbase, varparam := m[1], m[2]
 
-               vartype := &Vartype{kindOfList, checker, parseACLEntries(varname, aclEntries), false}
+               vartype := Vartype{kindOfList, checker, parseACLEntries(varname, aclEntries), false}
 
                if varparam == "" || varparam == "*" {
-                       src.vartypes[varbase] = vartype
+                       src.vartypes[varbase] = &vartype
                }
                if varparam == "*" || varparam == ".*" {
-                       src.vartypes[varbase+".*"] = vartype
+                       src.vartypes[varbase+".*"] = &vartype
                }
        }
 
@@ -116,8 +116,8 @@ func (src *Pkgsrc) InitVartypes() {
        //
        // If the file cannot be found, the allowed values are taken from
        // defval. This is mostly useful when testing pkglint.
-       enumFrom := func(fileName string, defval string, varcanons ...string) *BasicType {
-               mklines := LoadMk(src.File(fileName), NotEmpty)
+       enumFrom := func(filename string, defval string, varcanons ...string) *BasicType {
+               mklines := LoadMk(src.File(filename), NotEmpty)
                values := make(map[string]bool)
 
                if mklines != nil {
@@ -141,7 +141,7 @@ func (src *Pkgsrc) InitVartypes() {
                if len(values) > 0 {
                        joined := keysJoined(values)
                        if trace.Tracing {
-                               trace.Stepf("Enum from %s in: %s", strings.Join(varcanons, " "), fileName, joined)
+                               trace.Stepf("Enum from %s in: %s", strings.Join(varcanons, " "), filename, joined)
                        }
                        return enum(joined)
                }

Index: pkgsrc/pkgtools/pkglint/files/vartypecheck.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.43 pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.44
--- pkgsrc/pkgtools/pkglint/files/vartypecheck.go:1.43  Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck.go       Sun Dec  2 01:57:48 2018
@@ -221,7 +221,7 @@ func (cv *VartypeCheck) Comment() {
        }
        if m, isA := match1(value, ` (is a|is an) `); m {
                cv.Warnf("COMMENT should not contain %q.", isA)
-               Explain(
+               G.Explain(
                        "The words \"package is a\" are redundant.  Since every package comment",
                        "could start with them, it is better to remove this redundancy in all",
                        "cases.")
@@ -230,7 +230,7 @@ func (cv *VartypeCheck) Comment() {
                pkgbase := G.Pkg.EffectivePkgbase
                if hasPrefix(strings.ToLower(value), strings.ToLower(pkgbase+" ")) {
                        cv.Warnf("COMMENT should not start with the package name.")
-                       Explain(
+                       G.Explain(
                                "The COMMENT is usually displayed together with the package name.",
                                "Therefore it does not need to repeat the package name but should",
                                "provide additional information instead.")
@@ -262,11 +262,11 @@ func (cv *VartypeCheck) ConfFiles() {
        }
 
        for i, word := range words {
-               cv.WithValue(word).PathName()
+               cv.WithValue(word).Pathname()
 
                if i%2 == 1 && !hasPrefix(word, "${") {
                        cv.Warnf("The destination file %q should start with a variable reference.", word)
-                       Explain(
+                       G.Explain(
                                "Since pkgsrc can be installed in different locations, the",
                                "configuration files will also end up in different locations.",
                                "Typical variables that are used for configuration files are",
@@ -282,7 +282,7 @@ func (cv *VartypeCheck) Dependency() {
        deppat := parser.Dependency()
        if deppat != nil && deppat.Wildcard == "" && (parser.Rest() == "{,nb*}" || parser.Rest() == "{,nb[0-9]*}") {
                cv.Warnf("Dependency patterns of the form pkgbase>=1.0 don't need the \"{,nb*}\" extension.")
-               Explain(
+               G.Explain(
                        "The \"{,nb*}\" extension is only necessary for dependencies of the",
                        "form \"pkgbase-1.2\", since the pattern \"pkgbase-1.2\" doesn't match",
                        "the version \"pkgbase-1.2nb5\".  For dependency patterns using the",
@@ -290,7 +290,7 @@ func (cv *VartypeCheck) Dependency() {
 
        } else if deppat == nil || !parser.EOF() {
                cv.Warnf("Invalid dependency pattern %q.", value)
-               Explain(
+               G.Explain(
                        "Typical dependencies have the following forms:",
                        "",
                        "\tpackage>=2.5",
@@ -304,7 +304,7 @@ func (cv *VartypeCheck) Dependency() {
        if m, inside := match1(wildcard, `^\[(.*)\]\*$`); m {
                if inside != "0-9" {
                        cv.Warnf("Only [0-9]* is allowed in the numeric part of a dependency.")
-                       Explain(
+                       G.Explain(
                                "The pattern -[0-9] means any version.  All other version patterns",
                                "should be expressed using the comparison operators like < or >= or",
                                "even >=2<3.",
@@ -317,21 +317,21 @@ func (cv *VartypeCheck) Dependency() {
        } else if m, ver, suffix := match2(wildcard, `^(\d\w*(?:\.\w+)*)(\.\*|\{,nb\*\}|\{,nb\[0-9\]\*\}|\*|)$`); m {
                if suffix == "" {
                        cv.Warnf("Please use %q instead of %q as the version pattern.", ver+"{,nb*}", ver)
-                       Explain(
+                       G.Explain(
                                "Without the \"{,nb*}\" suffix, this version pattern only matches",
                                "package versions that don't have a PKGREVISION (which is the part",
                                "after the \"nb\").")
                }
                if suffix == "*" {
                        cv.Warnf("Please use %q instead of %q as the version pattern.", ver+".*", ver+"*")
-                       Explain(
+                       G.Explain(
                                "For example, the version \"1*\" also matches \"10.0.0\", which is",
                                "probably not intended.")
                }
 
        } else if wildcard == "*" {
                cv.Warnf("Please use \"%[1]s-[0-9]*\" instead of \"%[1]s-*\".", deppat.Pkgbase)
-               Explain(
+               G.Explain(
                        "If you use a * alone, the package specification may match other",
                        "packages that have the same prefix but a longer name.  For example,",
                        "foo-* matches foo-1.2 but also foo-client-1.2 and foo-server-1.2.")
@@ -340,7 +340,7 @@ func (cv *VartypeCheck) Dependency() {
        withoutCharClasses := replaceAll(wildcard, `\[[\d-]+\]`, "")
        if contains(withoutCharClasses, "-") {
                cv.Warnf("The version pattern %q should not contain a hyphen.", wildcard)
-               Explain(
+               G.Explain(
                        "Pkgsrc interprets package names with version numbers like this:",
                        "",
                        "\t\"foo-2.0-2.1.x\" => pkgbase \"foo\", version \"2.0-2.1.x\"",
@@ -368,7 +368,7 @@ func (cv *VartypeCheck) DependencyWithPa
                        cv.Warnf("Please use USE_TOOLS+=gmake instead of this dependency.")
                }
 
-               MkLineChecker{cv.MkLine}.CheckVartypePrimitive(cv.Varname, BtDependency, cv.Op, pattern, cv.MkComment, cv.Guessed)
+               MkLineChecker{cv.MkLine}.CheckVartypeBasic(cv.Varname, BtDependency, cv.Op, pattern, cv.MkComment, cv.Guessed)
                return
        }
 
@@ -379,7 +379,7 @@ func (cv *VartypeCheck) DependencyWithPa
        }
 
        cv.Warnf("Invalid dependency pattern with path %q.", value)
-       Explain(
+       G.Explain(
                "Examples for valid dependency patterns with path are:",
                "  package-[0-9]*:../../category/package",
                "  package>=3.41:../../category/package",
@@ -403,7 +403,7 @@ func (cv *VartypeCheck) EmulPlatform() {
                enumEmulArch.checker(archCv)
        } else {
                cv.Warnf("%q is not a valid emulation platform.", cv.Value)
-               Explain(
+               G.Explain(
                        "An emulation platform has the form <OPSYS>-<MACHINE_ARCH>.",
                        "OPSYS is the lower-case name of the operating system, and",
                        "MACHINE_ARCH is the hardware architecture.",
@@ -439,7 +439,7 @@ func (cv *VartypeCheck) Enum(vmap map[st
 }
 
 func (cv *VartypeCheck) FetchURL() {
-       MkLineChecker{cv.MkLine}.CheckVartypePrimitive(cv.Varname, BtURL, cv.Op, cv.Value, cv.MkComment, cv.Guessed)
+       MkLineChecker{cv.MkLine}.CheckVartypeBasic(cv.Varname, BtURL, cv.Op, cv.Value, cv.MkComment, cv.Guessed)
 
        for siteURL, siteName := range G.Pkgsrc.MasterSiteURLToVar {
                if hasPrefix(cv.Value, siteURL) {
@@ -465,17 +465,17 @@ func (cv *VartypeCheck) FetchURL() {
        }
 }
 
-// See PathName.
+// Filename checks that filenames use only limited special characters.
 //
 // See http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_169
-func (cv *VartypeCheck) FileName() {
+func (cv *VartypeCheck) Filename() {
        switch {
        case cv.Op == opUseMatch:
                break
        case contains(cv.ValueNoVar, "/"):
-               cv.Warnf("A file name should not contain a slash.")
+               cv.Warnf("A filename should not contain a slash.")
        case !matches(cv.ValueNoVar, `^[-0-9@A-Za-z.,_~+%]*$`):
-               cv.Warnf("%q is not a valid file name.", cv.Value)
+               cv.Warnf("%q is not a valid filename.", cv.Value)
        }
 }
 
@@ -484,9 +484,9 @@ func (cv *VartypeCheck) FileMask() {
        case cv.Op == opUseMatch:
                break
        case contains(cv.ValueNoVar, "/"):
-               cv.Warnf("A file name mask should not contain a slash.")
+               cv.Warnf("A filename mask should not contain a slash.")
        case !matches(cv.ValueNoVar, `^[#%*+\-./0-9?@A-Z\[\]_a-z~]*$`):
-               cv.Warnf("%q is not a valid file name mask.", cv.Value)
+               cv.Warnf("%q is not a valid filename mask.", cv.Value)
        }
 }
 
@@ -520,7 +520,7 @@ func (cv *VartypeCheck) GccReqd() {
 }
 
 func (cv *VartypeCheck) Homepage() {
-       MkLineChecker{cv.MkLine}.CheckVartypePrimitive(cv.Varname, BtURL, cv.Op, cv.Value, cv.MkComment, cv.Guessed)
+       MkLineChecker{cv.MkLine}.CheckVartypeBasic(cv.Varname, BtURL, cv.Op, cv.Value, cv.MkComment, cv.Guessed)
 
        if m, wrong, sitename, subdir := match3(cv.Value, `^(\$\{(MASTER_SITE\w+)(?::=([\w\-/]+))?\})`); m {
                baseURL := G.Pkgsrc.MasterSiteVarToURL[sitename]
@@ -604,8 +604,7 @@ func (cv *VartypeCheck) LdFlag() {
 }
 
 func (cv *VartypeCheck) License() {
-       licenseChecker := &LicenseChecker{cv.MkLine}
-       licenseChecker.Check(cv.Value, cv.Op)
+       (&LicenseChecker{cv.MkLine}).Check(cv.Value, cv.Op)
 }
 
 func (cv *VartypeCheck) MachineGnuPlatform() {
@@ -637,7 +636,7 @@ func (cv *VartypeCheck) MachineGnuPlatfo
 
        } else {
                cv.Warnf("%q is not a valid platform pattern.", cv.Value)
-               Explain(
+               G.Explain(
                        "A platform pattern has the form <OPSYS>-<OS_VERSION>-<MACHINE_ARCH>.",
                        "Each of these components may be a shell globbing expression.",
                        "",
@@ -675,7 +674,7 @@ func (cv *VartypeCheck) Message() {
 
        if matches(value, `^[\"'].*[\"']$`) {
                cv.Warnf("%s should not be quoted.", varname)
-               Explain(
+               G.Explain(
                        "The quoting is only needed for variables which are interpreted as",
                        "multiple words (or, generally speaking, a list of something).  A",
                        "single text message does not belong to this class, since it is only",
@@ -692,13 +691,13 @@ func (cv *VartypeCheck) Option() {
        }
 
        if m, optname := match1(value, `^-?([a-z][-0-9a-z+]*)$`); m {
-               if G.Mk != nil && !G.Mk.FirstTime("option:"+optname) {
+               if G.Mk != nil && !G.Mk.FirstTimeSlice("option:", optname) {
                        return
                }
 
                if _, found := G.Pkgsrc.PkgOptions[optname]; !found { // There's a difference between empty and absent here.
                        cv.Warnf("Unknown option %q.", optname)
-                       Explain(
+                       G.Explain(
                                "This option is not documented in the mk/defaults/options.description",
                                "file.  Please think of a brief but precise description and either",
                                "update that file yourself or suggest a description for this option",
@@ -722,22 +721,24 @@ func (cv *VartypeCheck) Pathlist() {
        // Sometimes, variables called PATH contain a single pathname,
        // especially those with auto-guessed type from MkLineImpl.VariableType.
        if !contains(value, ":") && cv.Guessed {
-               cv.PathName()
+               cv.Pathname()
                return
        }
 
-       for _, path := range cv.MkLine.ValueSplit(value, ":") {
-               if hasPrefix(path, "${") {
-                       continue
-               }
+       for _, dir := range cv.MkLine.ValueSplit(value, ":") {
 
-               pathNoVar := cv.MkLine.WithoutMakeVariables(path)
-               if !matches(pathNoVar, `^[-0-9A-Za-z._~+%/]*$`) {
-                       cv.Warnf("%q is not a valid pathname.", path)
+               dirNoVar := cv.MkLine.WithoutMakeVariables(dir)
+               if !matches(dirNoVar, `^[-0-9A-Za-z._~+%/]*$`) {
+                       cv.Warnf("%q is not a valid pathname.", dir)
                }
 
-               if !hasPrefix(path, "/") {
-                       cv.Warnf("All components of %s (in this case %q) should be absolute paths.", cv.Varname, path)
+               if !hasPrefix(dir, "/") && !hasPrefix(dir, "${") {
+                       cv.Errorf("The component %q of %s must be an absolute path.", dir, cv.Varname)
+                       G.Explain(
+                               "Relative paths in the PATH variable are a security risk.",
+                               "They also make the execution unreliable since they are",
+                               "evaluated relative to the current directory of the process,",
+                               "which means that after a \"cd\", different commands might be run.")
                }
        }
 }
@@ -752,22 +753,22 @@ func (cv *VartypeCheck) PathMask() {
        if !matches(cv.ValueNoVar, `^[#%*+\-./0-9?@A-Z\[\]_a-z~]*$`) {
                cv.Warnf("%q is not a valid pathname mask.", cv.Value)
        }
-       CheckLineAbsolutePathname(cv.Line, cv.Value)
+       LineChecker{cv.Line}.CheckAbsolutePathname(cv.Value)
 }
 
-// PathName checks for pathnames.
+// Pathname checks for pathnames.
 //
-// Like FileName, but including slashes.
+// Like Filename, but including slashes.
 //
 // See http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_266
-func (cv *VartypeCheck) PathName() {
+func (cv *VartypeCheck) Pathname() {
        if cv.Op == opUseMatch {
                return
        }
        if !matches(cv.ValueNoVar, `^[#\-0-9A-Za-z._~+%/]*$`) {
                cv.Warnf("%q is not a valid pathname.", cv.Value)
        }
-       CheckLineAbsolutePathname(cv.Line, cv.Value)
+       LineChecker{cv.Line}.CheckAbsolutePathname(cv.Value)
 }
 
 func (cv *VartypeCheck) Perl5Packlist() {
@@ -791,7 +792,7 @@ func (cv *VartypeCheck) Pkgname() {
 
        if cv.Op != opUseMatch && value == cv.ValueNoVar && !matches(value, rePkgname) {
                cv.Warnf("%q is not a valid package name.", value)
-               Explain(
+               G.Explain(
                        "A valid package name has the form packagename-version, where version",
                        "consists only of digits, letters and dots.")
        }
@@ -802,7 +803,7 @@ func (cv *VartypeCheck) PkgOptionsVar() 
 
        if matches(cv.Value, `\$\{PKGBASE[:\}]`) {
                cv.Errorf("PKGBASE must not be used in PKG_OPTIONS_VAR.")
-               Explain(
+               G.Explain(
                        "PKGBASE is defined in bsd.pkg.mk, which is included as the",
                        "very last file, but PKG_OPTIONS_VAR is evaluated earlier.",
                        "Use ${PKGNAME:C/-[0-9].*//} instead.")
@@ -817,7 +818,7 @@ func (cv *VartypeCheck) PkgOptionsVar() 
 // PkgPath checks a directory name relative to the top-level pkgsrc directory.
 // Despite its name, it is more similar to RelativePkgDir than to RelativePkgPath.
 func (cv *VartypeCheck) PkgPath() {
-       pkgsrcdir := relpath(path.Dir(cv.MkLine.FileName), G.Pkgsrc.File("."))
+       pkgsrcdir := relpath(path.Dir(cv.MkLine.Filename), G.Pkgsrc.File("."))
        MkLineChecker{cv.MkLine}.CheckRelativePkgdir(pkgsrcdir + "/" + cv.Value)
 }
 
@@ -827,7 +828,7 @@ func (cv *VartypeCheck) PkgRevision() {
        }
        if cv.Line.Basename != "Makefile" {
                cv.Errorf("%s only makes sense directly in the package Makefile.", cv.Varname)
-               Explain(
+               G.Explain(
                        "Usually, different packages using the same Makefile.common have",
                        "different dependencies and will be bumped at different times (e.g.",
                        "for shlib major bumps) and thus the PKGREVISIONs must be in the",
@@ -866,7 +867,7 @@ func (cv *VartypeCheck) MachinePlatformP
 
        } else {
                cv.Warnf("%q is not a valid platform pattern.", cv.Value)
-               Explain(
+               G.Explain(
                        "A platform pattern has the form <OPSYS>-<OS_VERSION>-<MACHINE_ARCH>.",
                        "Each of these components may be a shell globbing expression.",
                        "",
@@ -893,7 +894,7 @@ func (cv *VartypeCheck) PythonDependency
                cv.Warnf("Python dependencies should not contain variables.")
        } else if !matches(cv.ValueNoVar, `^[+\-.0-9A-Z_a-z]+(?:|:link|:build)$`) {
                cv.Warnf("Invalid Python dependency %q.", cv.Value)
-               Explain(
+               G.Explain(
                        "Python dependencies must be an identifier for a package, as",
                        "specified in lang/python/versioned_dependencies.mk.  This",
                        "identifier may be followed by :build for a build-time only",
@@ -917,22 +918,19 @@ func (cv *VartypeCheck) RelativePkgPath(
 func (cv *VartypeCheck) Restricted() {
        if cv.Value != "${RESTRICTED}" {
                cv.Warnf("The only valid value for %s is ${RESTRICTED}.", cv.Varname)
-               Explain(
+               G.Explain(
                        "These variables are used to control which files may be mirrored on",
                        "FTP servers or CD-ROM collections.  They are not intended to mark",
                        "packages whose only MASTER_SITES are on ftp.NetBSD.org.")
        }
 }
 
-func (cv *VartypeCheck) SedCommand() {
-}
-
 func (cv *VartypeCheck) SedCommands() {
        tokens, rest := splitIntoShellTokens(cv.Line, cv.Value)
        if rest != "" {
                if contains(cv.Line.Text, "#") {
                        cv.Errorf("Invalid shell words %q in sed commands.", rest)
-                       Explain(
+                       G.Explain(
                                "When sed commands have embedded \"#\" characters, they need to be",
                                "escaped with a backslash, otherwise make(1) will interpret them as a",
                                "comment, no matter if they occur in single or double quotes or",
@@ -955,7 +953,7 @@ func (cv *VartypeCheck) SedCommands() {
                                ncommands++
                                if ncommands > 1 {
                                        cv.Notef("Each sed command should appear in an assignment of its own.")
-                                       Explain(
+                                       G.Explain(
                                                "For example, instead of",
                                                "    SUBST_SED.foo+=        -e s,command1,, -e s,command2,,",
                                                "use",
@@ -964,7 +962,6 @@ func (cv *VartypeCheck) SedCommands() {
                                                "",
                                                "This way, short sed commands cannot be hidden at the end of a line.")
                                }
-                               MkLineChecker{cv.MkLine}.CheckVartypePrimitive(cv.Varname, BtSedCommand, cv.Op, tokens[i], cv.MkComment, cv.Guessed)
                        } else {
                                cv.Errorf("The -e option to sed requires an argument.")
                        }
@@ -991,7 +988,7 @@ func (cv *VartypeCheck) ShellCommand() {
        NewShellLine(cv.MkLine).CheckShellCommand(cv.Value, &setE, RunTime)
 }
 
-// Zero or more shell commands, each terminated with a semicolon.
+// ShellCommands checks for zero or more shell commands, each terminated with a semicolon.
 func (cv *VartypeCheck) ShellCommands() {
        NewShellLine(cv.MkLine).CheckShellCommands(cv.Value, RunTime)
 }
@@ -1023,7 +1020,7 @@ func (cv *VartypeCheck) Tool() {
                }
        } else if cv.Op != opUseMatch && cv.Value == cv.ValueNoVar {
                cv.Errorf("Malformed tool dependency: %q.", cv.Value)
-               Explain(
+               G.Explain(
                        "A tool dependency typically looks like \"sed\" or \"sed:run\".")
        }
 }
@@ -1077,7 +1074,7 @@ func (cv *VartypeCheck) UserGroupName() 
 func (cv *VartypeCheck) VariableName() {
        if cv.Value == cv.ValueNoVar && !matches(cv.Value, `^[A-Z_][0-9A-Z_]*(?:[.].*)?$`) {
                cv.Warnf("%q is not a valid variable name.", cv.Value)
-               Explain(
+               G.Explain(
                        "Variable names are restricted to only uppercase letters and the",
                        "underscore in the basename, and arbitrary characters in the",
                        "parameterized part, following the dot.",
@@ -1102,7 +1099,7 @@ func (cv *VartypeCheck) Version() {
                if m, ver, suffix := match2(value, `^(`+digit+alnum+`*(?:\.`+alnum+`+)*)(\.\*|\*|)$`); m {
                        if suffix == "*" && ver != "[0-9]" {
                                cv.Warnf("Please use %q instead of %q as the version pattern.", ver+".*", ver+"*")
-                               Explain(
+                               G.Explain(
                                        "For example, the version \"1*\" also matches \"10.0.0\", which is",
                                        "probably not intended.")
                        }
@@ -1134,17 +1131,18 @@ func (cv *VartypeCheck) WrapperTransform
 }
 
 func (cv *VartypeCheck) WrkdirSubdirectory() {
-       MkLineChecker{cv.MkLine}.CheckVartypePrimitive(cv.Varname, BtPathname, cv.Op, cv.Value, cv.MkComment, cv.Guessed)
+       MkLineChecker{cv.MkLine}.CheckVartypeBasic(cv.Varname, BtPathname, cv.Op, cv.Value, cv.MkComment, cv.Guessed)
 }
 
-// A directory relative to ${WRKSRC}, for use in CONFIGURE_DIRS and similar variables.
+// WrksrcSubdirectory checks a directory relative to ${WRKSRC},
+// for use in CONFIGURE_DIRS and similar variables.
 func (cv *VartypeCheck) WrksrcSubdirectory() {
        if m, _, rest := match2(cv.Value, `^(\$\{WRKSRC\})(?:/(.*))?`); m {
                if rest == "" {
                        rest = "."
                }
                cv.Notef("You can use %q instead of %q.", rest, cv.Value)
-               Explain(
+               G.Explain(
                        "These directories are interpreted relative to ${WRKSRC}.")
 
        } else if cv.Value != "" && cv.ValueNoVar == "" {
@@ -1159,7 +1157,7 @@ func (cv *VartypeCheck) Yes() {
        switch cv.Op {
        case opUseMatch:
                cv.Warnf("%s should only be used in a \".if defined(...)\" condition.", cv.Varname)
-               Explain(
+               G.Explain(
                        "This variable can have only two values: defined or undefined.",
                        "When it is defined, it means \"yes\", even when its value is",
                        "\"no\" or the empty string.",
@@ -1170,7 +1168,7 @@ func (cv *VartypeCheck) Yes() {
        default:
                if !matches(cv.Value, `^(?:YES|yes)(?:[\t ]+#.*)?$`) {
                        cv.Warnf("%s should be set to YES or yes.", cv.Varname)
-                       Explain(
+                       G.Explain(
                                "This variable means \"yes\" if it is defined, and \"no\" if it is",
                                "undefined.  Even when it has the value \"no\", this means \"yes\".",
                                "Therefore when it is defined, its value should correspond to its",
@@ -1194,7 +1192,7 @@ func (cv *VartypeCheck) YesNo() {
                }
        } else if cv.Op == opUseCompare {
                cv.Warnf("%s should be matched against %q or %q, not compared with %q.", cv.Varname, yes1, no1, cv.Value)
-               Explain(
+               G.Explain(
                        "The yes/no value can be written in either upper or lower case, and",
                        "both forms are actually used.  As long as this is the case, when",
                        "checking the variable value, both must be accepted.")

Index: pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go
diff -u pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.35 pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.36
--- pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go:1.35     Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/vartypecheck_test.go  Sun Dec  2 01:57:48 2018
@@ -16,7 +16,7 @@ func (s *Suite) Test_VartypeCheck_AwkCom
                "{print $$0}")
 
        vt.Output(
-               "WARN: fileName:1: $0 is ambiguous. Use ${0} if you mean a Makefile variable or $$0 if you mean a shell variable.")
+               "WARN: filename:1: $0 is ambiguous. Use ${0} if you mean a Makefile variable or $$0 if you mean a shell variable.")
 }
 
 func (s *Suite) Test_VartypeCheck_BasicRegularExpression(c *check.C) {
@@ -28,7 +28,7 @@ func (s *Suite) Test_VartypeCheck_BasicR
                ".*\\.pl$$")
 
        vt.Output(
-               "WARN: fileName:1: Pkglint parse error in MkLine.Tokenize at \"$\".")
+               "WARN: filename:1: Pkglint parse error in MkLine.Tokenize at \"$\".")
 }
 
 func (s *Suite) Test_VartypeCheck_BuildlinkDepmethod(c *check.C) {
@@ -41,7 +41,7 @@ func (s *Suite) Test_VartypeCheck_Buildl
                "unknown")
 
        vt.Output(
-               "WARN: fileName:2: Invalid dependency method \"unknown\". Valid methods are \"build\" or \"full\".")
+               "WARN: filename:2: Invalid dependency method \"unknown\". Valid methods are \"build\" or \"full\".")
 }
 
 func (s *Suite) Test_VartypeCheck_Category(c *check.C) {
@@ -61,8 +61,8 @@ func (s *Suite) Test_VartypeCheck_Catego
                "wip")
 
        vt.Output(
-               "ERROR: fileName:2: Invalid category \"arabic\".",
-               "ERROR: fileName:4: Invalid category \"wip\".")
+               "ERROR: filename:2: Invalid category \"arabic\".",
+               "ERROR: filename:4: Invalid category \"wip\".")
 }
 
 func (s *Suite) Test_VartypeCheck_CFlag(c *check.C) {
@@ -79,9 +79,9 @@ func (s *Suite) Test_VartypeCheck_CFlag(
                "`pkg-config pidgin --cflags`")
 
        vt.Output(
-               "WARN: fileName:2: Compiler flag \"/W3\" should start with a hyphen.",
-               "WARN: fileName:3: Compiler flag \"target:sparc64\" should start with a hyphen.",
-               "WARN: fileName:5: Unknown compiler flag \"-XX:+PrintClassHistogramAfterFullGC\".")
+               "WARN: filename:2: Compiler flag \"/W3\" should start with a hyphen.",
+               "WARN: filename:3: Compiler flag \"target:sparc64\" should start with a hyphen.",
+               "WARN: filename:5: Unknown compiler flag \"-XX:+PrintClassHistogramAfterFullGC\".")
 
        vt.Op(opUseMatch)
        vt.Values(
@@ -111,17 +111,17 @@ func (s *Suite) Test_VartypeCheck_Commen
                "Converter converts between measurement units")
 
        vt.Output(
-               "ERROR: fileName:2: COMMENT must be set.",
-               "WARN: fileName:3: COMMENT should not begin with \"A\".",
-               "WARN: fileName:3: COMMENT should not end with a period.",
-               "WARN: fileName:4: COMMENT should start with a capital letter.",
-               "WARN: fileName:4: COMMENT should not be longer than 70 characters.",
-               "WARN: fileName:5: COMMENT should not be enclosed in quotes.",
-               "WARN: fileName:6: COMMENT should not be enclosed in quotes.",
-               "WARN: fileName:7: COMMENT should not contain \"is a\".",
-               "WARN: fileName:8: COMMENT should not contain \"is an\".",
-               "WARN: fileName:9: COMMENT should not contain \"is a\".",
-               "WARN: fileName:10: COMMENT should not start with the package name.")
+               "ERROR: filename:2: COMMENT must be set.",
+               "WARN: filename:3: COMMENT should not begin with \"A\".",
+               "WARN: filename:3: COMMENT should not end with a period.",
+               "WARN: filename:4: COMMENT should start with a capital letter.",
+               "WARN: filename:4: COMMENT should not be longer than 70 characters.",
+               "WARN: filename:5: COMMENT should not be enclosed in quotes.",
+               "WARN: filename:6: COMMENT should not be enclosed in quotes.",
+               "WARN: filename:7: COMMENT should not contain \"is a\".",
+               "WARN: filename:8: COMMENT should not contain \"is an\".",
+               "WARN: filename:9: COMMENT should not contain \"is a\".",
+               "WARN: filename:10: COMMENT should not start with the package name.")
 }
 
 func (s *Suite) Test_VartypeCheck_ConfFiles(c *check.C) {
@@ -137,10 +137,10 @@ func (s *Suite) Test_VartypeCheck_ConfFi
                "share/etc/bootrc /etc/bootrc")
 
        vt.Output(
-               "WARN: fileName:1: Values for CONF_FILES should always be pairs of paths.",
-               "WARN: fileName:3: Values for CONF_FILES should always be pairs of paths.",
-               "WARN: fileName:5: Found absolute pathname: /etc/bootrc",
-               "WARN: fileName:5: The destination file \"/etc/bootrc\" should start with a variable reference.")
+               "WARN: filename:1: Values for CONF_FILES should always be pairs of paths.",
+               "WARN: filename:3: Values for CONF_FILES should always be pairs of paths.",
+               "WARN: filename:5: Found absolute pathname: /etc/bootrc",
+               "WARN: filename:5: The destination file \"/etc/bootrc\" should start with a variable reference.")
 }
 
 func (s *Suite) Test_VartypeCheck_Dependency(c *check.C) {
@@ -172,16 +172,16 @@ func (s *Suite) Test_VartypeCheck_Depend
                "gnome-control-center>=2.20.1{,nb*}")
 
        vt.Output(
-               "WARN: fileName:1: Invalid dependency pattern \"Perl\".",
-               "WARN: fileName:3: Please use \"perl5-[0-9]*\" instead of \"perl5-*\".",
-               "WARN: fileName:5: Only [0-9]* is allowed in the numeric part of a dependency.",
-               "WARN: fileName:5: The version pattern \"[5.10-5.22]*\" should not contain a hyphen.",
-               "WARN: fileName:6: Invalid dependency pattern \"py-docs\".",
-               "WARN: fileName:10: Please use \"5.22{,nb*}\" instead of \"5.22\" as the version pattern.",
-               "WARN: fileName:11: Please use \"5.*\" instead of \"5*\" as the version pattern.",
-               "WARN: fileName:12: The version pattern \"2.0-[0-9]*\" should not contain a hyphen.",
-               "WARN: fileName:20: The version pattern \"[0-9]*,openssh-[0-9]*}\" should not contain a hyphen.", // XXX
-               "WARN: fileName:21: Dependency patterns of the form pkgbase>=1.0 don't need the \"{,nb*}\" extension.")
+               "WARN: filename:1: Invalid dependency pattern \"Perl\".",
+               "WARN: filename:3: Please use \"perl5-[0-9]*\" instead of \"perl5-*\".",
+               "WARN: filename:5: Only [0-9]* is allowed in the numeric part of a dependency.",
+               "WARN: filename:5: The version pattern \"[5.10-5.22]*\" should not contain a hyphen.",
+               "WARN: filename:6: Invalid dependency pattern \"py-docs\".",
+               "WARN: filename:10: Please use \"5.22{,nb*}\" instead of \"5.22\" as the version pattern.",
+               "WARN: filename:11: Please use \"5.*\" instead of \"5*\" as the version pattern.",
+               "WARN: filename:12: The version pattern \"2.0-[0-9]*\" should not contain a hyphen.",
+               "WARN: filename:20: The version pattern \"[0-9]*,openssh-[0-9]*}\" should not contain a hyphen.", // XXX
+               "WARN: filename:21: Dependency patterns of the form pkgbase>=1.0 don't need the \"{,nb*}\" extension.")
 }
 
 func (s *Suite) Test_VartypeCheck_DependencyWithPath(c *check.C) {
@@ -196,7 +196,7 @@ func (s *Suite) Test_VartypeCheck_Depend
 
        vt.Varname("DEPENDS")
        vt.Op(opAssignAppend)
-       vt.File(G.Pkg.File("fileName.mk"))
+       vt.File(G.Pkg.File("filename.mk"))
        vt.Values(
                "Perl",
                "perl5>=5.22:../perl5",
@@ -214,21 +214,21 @@ func (s *Suite) Test_VartypeCheck_Depend
                "gmake-[0-9]*:../../devel/gmake")
 
        vt.Output(
-               "WARN: ~/category/package/fileName.mk:1: Invalid dependency pattern with path \"Perl\".",
-               "WARN: ~/category/package/fileName.mk:2: Dependencies should have the form \"../../category/package\".",
-               "ERROR: ~/category/package/fileName.mk:3: \"../../lang/perl5\" does not exist.",
-               "ERROR: ~/category/package/fileName.mk:3: There is no package in \"lang/perl5\".",
-               "WARN: ~/category/package/fileName.mk:3: Please use USE_TOOLS+=perl:run instead of this dependency.",
-               "WARN: ~/category/package/fileName.mk:4: Invalid dependency pattern \"broken0.12.1\".",
-               "WARN: ~/category/package/fileName.mk:5: Invalid dependency pattern \"broken[0-9]*\".",
-               "WARN: ~/category/package/fileName.mk:6: Invalid dependency pattern with path \"broken[0-9]*../../x11/alacarte\".",
-               "WARN: ~/category/package/fileName.mk:7: Invalid dependency pattern \"broken>=\".",
-               "WARN: ~/category/package/fileName.mk:8: Invalid dependency pattern \"broken=0\".",
-               "WARN: ~/category/package/fileName.mk:9: Invalid dependency pattern \"broken=\".",
-               "WARN: ~/category/package/fileName.mk:10: Invalid dependency pattern \"broken-\".",
-               "WARN: ~/category/package/fileName.mk:11: Invalid dependency pattern \"broken>\".",
-               "WARN: ~/category/package/fileName.mk:13: Please use USE_TOOLS+=msgfmt instead of this dependency.",
-               "WARN: ~/category/package/fileName.mk:14: Please use USE_TOOLS+=gmake instead of this dependency.")
+               "WARN: ~/category/package/filename.mk:1: Invalid dependency pattern with path \"Perl\".",
+               "WARN: ~/category/package/filename.mk:2: Dependencies should have the form \"../../category/package\".",
+               "ERROR: ~/category/package/filename.mk:3: \"../../lang/perl5\" does not exist.",
+               "ERROR: ~/category/package/filename.mk:3: There is no package in \"lang/perl5\".",
+               "WARN: ~/category/package/filename.mk:3: Please use USE_TOOLS+=perl:run instead of this dependency.",
+               "WARN: ~/category/package/filename.mk:4: Invalid dependency pattern \"broken0.12.1\".",
+               "WARN: ~/category/package/filename.mk:5: Invalid dependency pattern \"broken[0-9]*\".",
+               "WARN: ~/category/package/filename.mk:6: Invalid dependency pattern with path \"broken[0-9]*../../x11/alacarte\".",
+               "WARN: ~/category/package/filename.mk:7: Invalid dependency pattern \"broken>=\".",
+               "WARN: ~/category/package/filename.mk:8: Invalid dependency pattern \"broken=0\".",
+               "WARN: ~/category/package/filename.mk:9: Invalid dependency pattern \"broken=\".",
+               "WARN: ~/category/package/filename.mk:10: Invalid dependency pattern \"broken-\".",
+               "WARN: ~/category/package/filename.mk:11: Invalid dependency pattern \"broken>\".",
+               "WARN: ~/category/package/filename.mk:13: Please use USE_TOOLS+=msgfmt instead of this dependency.",
+               "WARN: ~/category/package/filename.mk:14: Please use USE_TOOLS+=gmake instead of this dependency.")
 }
 
 func (s *Suite) Test_VartypeCheck_DistSuffix(c *check.C) {
@@ -240,7 +240,7 @@ func (s *Suite) Test_VartypeCheck_DistSu
                ".tar.bz2")
 
        vt.Output(
-               "NOTE: fileName:1: EXTRACT_SUFX is \".tar.gz\" by default, so this definition may be redundant.")
+               "NOTE: filename:1: EXTRACT_SUFX is \".tar.gz\" by default, so this definition may be redundant.")
 }
 
 func (s *Suite) Test_VartypeCheck_EmulPlatform(c *check.C) {
@@ -253,12 +253,12 @@ func (s *Suite) Test_VartypeCheck_EmulPl
                "${LINUX}")
 
        vt.Output(
-               "WARN: fileName:2: \"nextbsd\" is not valid for the operating system part of EMUL_PLATFORM. "+
+               "WARN: filename:2: \"nextbsd\" is not valid for the operating system part of EMUL_PLATFORM. "+
                        "Use one of "+
                        "{ bitrig bsdos cygwin darwin dragonfly freebsd haiku hpux "+
                        "interix irix linux mirbsd netbsd openbsd osf1 solaris sunos "+
                        "} instead.",
-               "WARN: fileName:2: \"8087\" is not valid for the hardware architecture part of EMUL_PLATFORM. "+
+               "WARN: filename:2: \"8087\" is not valid for the hardware architecture part of EMUL_PLATFORM. "+
                        "Use one of "+
                        "{ aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 cobalt coldfire convex dreamcast "+
                        "earm earmeb earmhf earmhfeb earmv4 earmv4eb earmv5 earmv5eb earmv6 earmv6eb earmv6hf "+
@@ -266,7 +266,7 @@ func (s *Suite) Test_VartypeCheck_EmulPl
                        "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 "+
                        "} instead.",
-               "WARN: fileName:3: \"${LINUX}\" is not a valid emulation platform.")
+               "WARN: filename:3: \"${LINUX}\" is not a valid emulation platform.")
 }
 
 func (s *Suite) Test_VartypeCheck_Enum(c *check.C) {
@@ -282,9 +282,9 @@ func (s *Suite) Test_VartypeCheck_Enum(c
                "[")
 
        vt.Output(
-               "WARN: fileName:3: The pattern \"sun-jdk*\" cannot match any of { jdk1 jdk2 jdk4 } for JDK.",
-               "WARN: fileName:5: Invalid match pattern \"[\".",
-               "WARN: fileName:5: The pattern \"[\" cannot match any of { jdk1 jdk2 jdk4 } for JDK.")
+               "WARN: filename:3: The pattern \"sun-jdk*\" cannot match any of { jdk1 jdk2 jdk4 } for JDK.",
+               "WARN: filename:5: Invalid match pattern \"[\".",
+               "WARN: filename:5: The pattern \"[\" cannot match any of { jdk1 jdk2 jdk4 } for JDK.")
 }
 
 func (s *Suite) Test_VartypeCheck_Enum__use_match(c *check.C) {
@@ -324,45 +324,45 @@ func (s *Suite) Test_VartypeCheck_FetchU
                "${MASTER_SITE_INVALID:=subdir/}")
 
        vt.Output(
-               "WARN: fileName:1: Please use ${MASTER_SITE_GITHUB:=example/} "+
+               "WARN: filename:1: Please use ${MASTER_SITE_GITHUB:=example/} "+
                        "instead of \"https://github.com/example/project/\"; "+
                        "and run \""+confMake+" help topic=github\" for further tips.",
-               "WARN: fileName:2: Please use ${MASTER_SITE_GNU:=bison} instead of \"http://ftp.gnu.org/pub/gnu/bison\".";,
-               "ERROR: fileName:3: The subdirectory in MASTER_SITE_GNU must end with a slash.",
-               "ERROR: fileName:4: The site MASTER_SITE_INVALID does not exist.")
+               "WARN: filename:2: Please use ${MASTER_SITE_GNU:=bison} instead of \"http://ftp.gnu.org/pub/gnu/bison\".";,
+               "ERROR: filename:3: The subdirectory in MASTER_SITE_GNU must end with a slash.",
+               "ERROR: filename:4: The site MASTER_SITE_INVALID does not exist.")
 
        // PR 46570, keyword gimp-fix-ca
        vt.Values(
-               "https://example.org/download.cgi?fileName=fileName&sha1=12341234";)
+               "https://example.org/download.cgi?filename=filename&sha1=12341234";)
 
        t.CheckOutputEmpty()
 
        vt.Values(
                "http://example.org/distfiles/";,
-               "http://example.org/download?fileName=distfile;version=1.0";,
-               "http://example.org/download?fileName=<distfile>;version=<version>")
+               "http://example.org/download?filename=distfile;version=1.0";,
+               "http://example.org/download?filename=<distfile>;version=<version>")
 
        vt.Output(
-               "WARN: fileName:8: \"http://example.org/download?fileName=<distfile>;version=<version>\" is not a valid URL.")
+               "WARN: filename:8: \"http://example.org/download?filename=<distfile>;version=<version>\" is not a valid URL.")
 }
 
-func (s *Suite) Test_VartypeCheck_FileName(c *check.C) {
-       vt := NewVartypeCheckTester(s.Init(c), (*VartypeCheck).FileName)
+func (s *Suite) Test_VartypeCheck_Filename(c *check.C) {
+       vt := NewVartypeCheckTester(s.Init(c), (*VartypeCheck).Filename)
 
        vt.Varname("FNAME")
        vt.Values(
-               "FileName with spaces.docx",
+               "Filename with spaces.docx",
                "OS/2-manual.txt")
 
        vt.Output(
-               "WARN: fileName:1: \"FileName with spaces.docx\" is not a valid file name.",
-               "WARN: fileName:2: A file name should not contain a slash.")
+               "WARN: filename:1: \"Filename with spaces.docx\" is not a valid filename.",
+               "WARN: filename:2: A filename should not contain a slash.")
 
        vt.Op(opUseMatch)
        vt.Values(
-               "FileName with spaces.docx")
+               "Filename with spaces.docx")
 
-       // There's no guarantee that a file name only contains [A-Za-z0-9.].
+       // There's no guarantee that a filename only contains [A-Za-z0-9.].
        // Therefore there are no useful checks in this situation.
        vt.OutputEmpty()
 }
@@ -376,14 +376,14 @@ func (s *Suite) Test_VartypeCheck_FileMa
                "OS/2-manual.txt")
 
        vt.Output(
-               "WARN: fileName:1: \"FileMask with spaces.docx\" is not a valid file name mask.",
-               "WARN: fileName:2: A file name mask should not contain a slash.")
+               "WARN: filename:1: \"FileMask with spaces.docx\" is not a valid filename mask.",
+               "WARN: filename:2: A filename mask should not contain a slash.")
 
        vt.Op(opUseMatch)
        vt.Values(
                "FileMask with spaces.docx")
 
-       // There's no guarantee that a file name only contains [A-Za-z0-9.].
+       // There's no guarantee that a filename only contains [A-Za-z0-9.].
        // Therefore there are no useful checks in this situation.
        vt.OutputEmpty()
 }
@@ -397,20 +397,22 @@ func (s *Suite) Test_VartypeCheck_FileMo
                "0600",
                "1234",
                "12345",
-               "${OTHER_PERMS}")
+               "${OTHER_PERMS}",
+               "")
 
        vt.Output(
-               "WARN: fileName:1: Invalid file mode \"u+rwx\".",
-               "WARN: fileName:4: Invalid file mode \"12345\".")
+               "WARN: filename:1: Invalid file mode \"u+rwx\".",
+               "WARN: filename:4: Invalid file mode \"12345\".",
+               "WARN: filename:6: Invalid file mode \"\".")
 
        vt.Op(opUseMatch)
        vt.Values(
                "u+rwx")
 
-       // There's no guarantee that a file name only contains [A-Za-z0-9.].
+       // There's no guarantee that a filename only contains [A-Za-z0-9.].
        // Therefore there are no useful checks in this situation.
        vt.Output(
-               "WARN: fileName:11: Invalid file mode \"u+rwx\".")
+               "WARN: filename:11: Invalid file mode \"u+rwx\".")
 }
 
 func (s *Suite) Test_VartypeCheck_GccReqd(c *check.C) {
@@ -427,8 +429,8 @@ func (s *Suite) Test_VartypeCheck_GccReq
                "6",
                "7.3")
        vt.Output(
-               "WARN: fileName:5: GCC version numbers should only contain the major version (5).",
-               "WARN: fileName:7: GCC version numbers should only contain the major version (7).")
+               "WARN: filename:5: GCC version numbers should only contain the major version (5).",
+               "WARN: filename:7: GCC version numbers should only contain the major version (7).")
 }
 
 func (s *Suite) Test_VartypeCheck_Homepage(c *check.C) {
@@ -440,7 +442,7 @@ func (s *Suite) Test_VartypeCheck_Homepa
                "${MASTER_SITES}")
 
        vt.Output(
-               "WARN: fileName:1: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
+               "WARN: filename:1: HOMEPAGE should not be defined in terms of MASTER_SITEs.")
 
        G.Pkg = NewPackage(t.File("category/package"))
        G.Pkg.vars.Define("MASTER_SITES", t.NewMkLine(G.Pkg.File("Makefile"), 5, "MASTER_SITES=\thttps://cdn.NetBSD.org/pub/pkgsrc/distfiles/";))
@@ -449,7 +451,7 @@ func (s *Suite) Test_VartypeCheck_Homepa
                "${MASTER_SITES}")
 
        vt.Output(
-               "WARN: fileName:2: HOMEPAGE should not be defined in terms of MASTER_SITEs. Use https://cdn.NetBSD.org/pub/pkgsrc/distfiles/ directly.")
+               "WARN: filename:2: HOMEPAGE should not be defined in terms of MASTER_SITEs. Use https://cdn.NetBSD.org/pub/pkgsrc/distfiles/ directly.")
 }
 
 func (s *Suite) Test_VartypeCheck_Identifier(c *check.C) {
@@ -467,9 +469,9 @@ func (s *Suite) Test_VartypeCheck_Identi
                "A*B")
 
        vt.Output(
-               "WARN: fileName:2: Invalid identifier \"identifiers cannot contain spaces\".",
-               "WARN: fileName:3: Invalid identifier \"id/cannot/contain/slashes\".",
-               "WARN: fileName:11: Invalid identifier pattern \"[A-Z]\" for SUBST_CLASSES.")
+               "WARN: filename:2: Invalid identifier \"identifiers cannot contain spaces\".",
+               "WARN: filename:3: Invalid identifier \"id/cannot/contain/slashes\".",
+               "WARN: filename:11: Invalid identifier pattern \"[A-Z]\" for SUBST_CLASSES.")
 }
 
 func (s *Suite) Test_VartypeCheck_Integer(c *check.C) {
@@ -484,8 +486,8 @@ func (s *Suite) Test_VartypeCheck_Intege
                "11111111111111111111111111111111111111111111111")
 
        vt.Output(
-               "WARN: fileName:1: Invalid integer \"${OTHER_VAR}\".",
-               "WARN: fileName:3: Invalid integer \"-13\".")
+               "WARN: filename:1: Invalid integer \"${OTHER_VAR}\".",
+               "WARN: filename:3: Invalid integer \"-13\".")
 }
 
 func (s *Suite) Test_VartypeCheck_LdFlag(c *check.C) {
@@ -499,28 +501,44 @@ func (s *Suite) Test_VartypeCheck_LdFlag
                "`pkg-config pidgin --ldflags`",
                "-unknown",
                "no-hyphen",
-               "-Wl,--rpath,/usr/lib64")
+               "-Wl,--rpath,/usr/lib64",
+               "-pthread",
+               "-static",
+               "-static-something",
+               "${LDFLAGS.NetBSD}",
+               "-l${LIBNCURSES}")
        vt.Op(opUseMatch)
        vt.Values(
                "anything")
 
        vt.Output(
-               "WARN: fileName:4: Unknown linker flag \"-unknown\".",
-               "WARN: fileName:5: Linker flag \"no-hyphen\" should start with a hyphen.",
-               "WARN: fileName:6: Please use \"${COMPILER_RPATH_FLAG}\" instead of \"-Wl,--rpath\".")
+               "WARN: filename:4: Unknown linker flag \"-unknown\".",
+               "WARN: filename:5: Linker flag \"no-hyphen\" should start with a hyphen.",
+               "WARN: filename:6: Please use \"${COMPILER_RPATH_FLAG}\" instead of \"-Wl,--rpath\".")
 }
 
 func (s *Suite) Test_VartypeCheck_License(c *check.C) {
-       vt := NewVartypeCheckTester(s.Init(c), (*VartypeCheck).License)
+       t := s.Init(c)
+       t.SetupPkgsrc() // Adds the gnu-gpl-v2 and 2-clause-bsd licenses
+
+       G.Mk = t.NewMkLines("perl5.mk",
+               MkRcsID,
+               "PERL5_LICENSE= gnu-gpl-v2 OR artistic")
+       G.Mk.DetermineDefinedVariables()
+
+       vt := NewVartypeCheckTester(t, (*VartypeCheck).License)
 
        vt.Varname("LICENSE")
        vt.Values(
                "gnu-gpl-v2",
-               "AND mit")
+               "AND mit",
+               "${PERL5_LICENSE}", // Is properly resolved, see perl5.mk above.
+               "${UNKNOWN_LICENSE}")
 
        vt.Output(
-               "WARN: fileName:1: License file ~/licenses/gnu-gpl-v2 does not exist.",
-               "ERROR: fileName:2: Parse error for license condition \"AND mit\".")
+               "ERROR: filename:2: Parse error for license condition \"AND mit\".",
+               "WARN: filename:3: License file ~/licenses/artistic does not exist.",
+               "ERROR: filename:4: Parse error for license condition \"${UNKNOWN_LICENSE}\".")
 
        vt.Op(opAssignAppend)
        vt.Values(
@@ -528,8 +546,8 @@ func (s *Suite) Test_VartypeCheck_Licens
                "AND mit")
 
        vt.Output(
-               "ERROR: fileName:11: Parse error for appended license condition \"gnu-gpl-v2\".",
-               "WARN: fileName:12: License file ~/licenses/mit does not exist.")
+               "ERROR: filename:11: Parse error for appended license condition \"gnu-gpl-v2\".",
+               "WARN: filename:12: License file ~/licenses/mit does not exist.")
 }
 
 func (s *Suite) Test_VartypeCheck_MachineGnuPlatform(c *check.C) {
@@ -545,17 +563,17 @@ func (s *Suite) Test_VartypeCheck_Machin
                "${OTHER_VAR}")
 
        vt.Output(
-               "WARN: fileName:2: The pattern \"Cygwin\" cannot match any of "+
+               "WARN: filename:2: The pattern \"Cygwin\" cannot match any of "+
                        "{ aarch64 aarch64_be alpha amd64 arc arm armeb armv4 armv4eb armv6 armv6eb armv7 armv7eb "+
                        "cobalt convex dreamcast hpcmips hpcsh hppa hppa64 i386 i486 ia64 m5407 m68010 m68k m88k "+
                        "mips mips64 mips64el mipseb mipsel mipsn32 mlrisc ns32k pc532 pmax powerpc powerpc64 "+
                        "rs6000 s390 sh shle sparc sparc64 vax x86_64 "+
                        "} for the hardware architecture part of MACHINE_GNU_PLATFORM.",
-               "WARN: fileName:2: The pattern \"amd64\" cannot match any of "+
+               "WARN: filename:2: The pattern \"amd64\" cannot match any of "+
                        "{ bitrig bsdos cygwin darwin dragonfly freebsd haiku hpux interix irix linux mirbsd "+
                        "netbsd openbsd osf1 solaris sunos } "+
                        "for the operating system part of MACHINE_GNU_PLATFORM.",
-               "WARN: fileName:4: \"*-*-*-*\" is not a valid platform pattern.")
+               "WARN: filename:4: \"*-*-*-*\" is not a valid platform pattern.")
 }
 
 func (s *Suite) Test_VartypeCheck_MailAddress(c *check.C) {
@@ -569,10 +587,10 @@ func (s *Suite) Test_VartypeCheck_MailAd
                "user1%example.org@localhost,user2%example.org@localhost")
 
        vt.Output(
-               "WARN: fileName:1: Please write \"NetBSD.org\" instead of \"netbsd.org\".",
-               "ERROR: fileName:2: This mailing list address is obsolete. Use pkgsrc-users%NetBSD.org@localhost instead.",
-               "ERROR: fileName:3: This mailing list address is obsolete. Use pkgsrc-users%NetBSD.org@localhost instead.",
-               "WARN: fileName:4: \"user1%example.org@localhost,user2%example.org@localhost\" is not a valid mail address.")
+               "WARN: filename:1: Please write \"NetBSD.org\" instead of \"netbsd.org\".",
+               "ERROR: filename:2: This mailing list address is obsolete. Use pkgsrc-users%NetBSD.org@localhost instead.",
+               "ERROR: filename:3: This mailing list address is obsolete. Use pkgsrc-users%NetBSD.org@localhost instead.",
+               "WARN: filename:4: \"user1%example.org@localhost,user2%example.org@localhost\" is not a valid mail address.")
 }
 
 func (s *Suite) Test_VartypeCheck_Message(c *check.C) {
@@ -584,7 +602,7 @@ func (s *Suite) Test_VartypeCheck_Messag
                "Correct paths")
 
        vt.Output(
-               "WARN: fileName:1: SUBST_MESSAGE.id should not be quoted.")
+               "WARN: filename:1: SUBST_MESSAGE.id should not be quoted.")
 }
 
 func (s *Suite) Test_VartypeCheck_Option(c *check.C) {
@@ -602,9 +620,9 @@ func (s *Suite) Test_VartypeCheck_Option
                "UPPER")
 
        vt.Output(
-               "WARN: fileName:3: Unknown option \"unknown\".",
-               "WARN: fileName:4: Use of the underscore character in option names is deprecated.",
-               "ERROR: fileName:5: Invalid option name \"UPPER\". "+
+               "WARN: filename:3: Unknown option \"unknown\".",
+               "WARN: filename:4: Use of the underscore character in option names is deprecated.",
+               "ERROR: filename:5: Invalid option name \"UPPER\". "+
                        "Option names must start with a lowercase letter and be all-lowercase.")
 }
 
@@ -613,13 +631,14 @@ func (s *Suite) Test_VartypeCheck_Pathli
 
        vt.Varname("PATH")
        vt.Values(
-               "/usr/bin:/usr/sbin:.::${LOCALBASE}/bin:${HOMEPAGE:S,https://,,}";,
+               "/usr/bin:/usr/sbin:.::${LOCALBASE}/bin:${HOMEPAGE:S,https://,,}:${TMPDIR}:${PREFIX}/!!!";,
                "/directory with spaces")
 
        vt.Output(
-               "WARN: fileName:1: All components of PATH (in this case \".\") should be absolute paths.",
-               "WARN: fileName:1: All components of PATH (in this case \"\") should be absolute paths.",
-               "WARN: fileName:2: \"/directory with spaces\" is not a valid pathname.")
+               "ERROR: filename:1: The component \".\" of PATH must be an absolute path.",
+               "ERROR: filename:1: The component \"\" of PATH must be an absolute path.",
+               "WARN: filename:1: \"${PREFIX}/!!!\" is not a valid pathname.",
+               "WARN: filename:2: \"/directory with spaces\" is not a valid pathname.")
 }
 
 func (s *Suite) Test_VartypeCheck_PathMask(c *check.C) {
@@ -632,8 +651,8 @@ func (s *Suite) Test_VartypeCheck_PathMa
                "src/*/*")
 
        vt.Output(
-               "WARN: fileName:1: Found absolute pathname: /home/user/*",
-               "WARN: fileName:2: \"src/*&*\" is not a valid pathname mask.")
+               "WARN: filename:1: Found absolute pathname: /home/user/*",
+               "WARN: filename:2: \"src/*&*\" is not a valid pathname mask.")
 
        vt.Op(opUseMatch)
        vt.Values("any")
@@ -641,8 +660,8 @@ func (s *Suite) Test_VartypeCheck_PathMa
        vt.OutputEmpty()
 }
 
-func (s *Suite) Test_VartypeCheck_PathName(c *check.C) {
-       vt := NewVartypeCheckTester(s.Init(c), (*VartypeCheck).PathName)
+func (s *Suite) Test_VartypeCheck_Pathname(c *check.C) {
+       vt := NewVartypeCheckTester(s.Init(c), (*VartypeCheck).Pathname)
 
        vt.Varname("EGDIR")
        vt.Values(
@@ -655,8 +674,8 @@ func (s *Suite) Test_VartypeCheck_PathNa
                "anything")
 
        vt.Output(
-               "WARN: fileName:1: \"${PREFIX}/*\" is not a valid pathname.",
-               "WARN: fileName:4: Found absolute pathname: /bin")
+               "WARN: filename:1: \"${PREFIX}/*\" is not a valid pathname.",
+               "WARN: filename:4: Found absolute pathname: /bin")
 }
 
 func (s *Suite) Test_VartypeCheck_Perl5Packlist(c *check.C) {
@@ -668,7 +687,7 @@ func (s *Suite) Test_VartypeCheck_Perl5P
                "anything else")
 
        vt.Output(
-               "WARN: fileName:1: PERL5_PACKLIST should not depend on other variables.")
+               "WARN: filename:1: PERL5_PACKLIST should not depend on other variables.")
 }
 
 func (s *Suite) Test_VartypeCheck_Perms(c *check.C) {
@@ -683,7 +702,7 @@ func (s *Suite) Test_VartypeCheck_Perms(
                "${REAL_ROOT_USER}")
 
        vt.Output(
-               "ERROR: fileName:2: ROOT_USER must not be used in permission definitions. Use REAL_ROOT_USER instead.")
+               "ERROR: filename:2: ROOT_USER must not be used in permission definitions. Use REAL_ROOT_USER instead.")
 }
 
 func (s *Suite) Test_VartypeCheck_Pkgname(c *check.C) {
@@ -702,7 +721,7 @@ func (s *Suite) Test_VartypeCheck_Pkgnam
                "pkgbase-3.1.4.1.5.9.2.6.5.3.5.8.9.7.9")
 
        vt.Output(
-               "WARN: fileName:8: \"pkgbase-z1\" is not a valid package name.")
+               "WARN: filename:8: \"pkgbase-z1\" is not a valid package name.")
 }
 
 func (s *Suite) Test_VartypeCheck_PkgOptionsVar(c *check.C) {
@@ -715,8 +734,8 @@ func (s *Suite) Test_VartypeCheck_PkgOpt
                "PKG_OPTS.mc")
 
        vt.Output(
-               "ERROR: fileName:1: PKGBASE must not be used in PKG_OPTIONS_VAR.",
-               "ERROR: fileName:3: PKG_OPTIONS_VAR must be of the form \"PKG_OPTIONS.*\", not \"PKG_OPTS.mc\".")
+               "ERROR: filename:1: PKGBASE must not be used in PKG_OPTIONS_VAR.",
+               "ERROR: filename:3: PKG_OPTIONS_VAR must be of the form \"PKG_OPTIONS.*\", not \"PKG_OPTS.mc\".")
 }
 
 func (s *Suite) Test_VartypeCheck_PkgPath(c *check.C) {
@@ -734,10 +753,10 @@ func (s *Suite) Test_VartypeCheck_PkgPat
                "../../invalid/relative")
 
        vt.Output(
-               "ERROR: fileName:3: \"../../invalid\" does not exist.",
-               "WARN: fileName:3: \"../../invalid\" is not a valid relative package directory.",
-               "ERROR: fileName:4: \"../../../../invalid/relative\" does not exist.",
-               "WARN: fileName:4: \"../../../../invalid/relative\" is not a valid relative package directory.")
+               "ERROR: filename:3: \"../../invalid\" does not exist.",
+               "WARN: filename:3: \"../../invalid\" is not a valid relative package directory.",
+               "ERROR: filename:4: \"../../../../invalid/relative\" does not exist.",
+               "WARN: filename:4: \"../../../../invalid/relative\" is not a valid relative package directory.")
 }
 
 func (s *Suite) Test_VartypeCheck_PkgRevision(c *check.C) {
@@ -748,8 +767,8 @@ func (s *Suite) Test_VartypeCheck_PkgRev
                "3a")
 
        vt.Output(
-               "WARN: fileName:1: PKGREVISION must be a positive integer number.",
-               "ERROR: fileName:1: PKGREVISION only makes sense directly in the package Makefile.")
+               "WARN: filename:1: PKGREVISION must be a positive integer number.",
+               "ERROR: filename:1: PKGREVISION only makes sense directly in the package Makefile.")
 
        vt.File("Makefile")
        vt.Values(
@@ -774,31 +793,31 @@ func (s *Suite) Test_VartypeCheck_Machin
                "NetBSD-[0-1]*-*")
 
        vt.Output(
-               "WARN: fileName:1: \"linux-i386\" is not a valid platform pattern.",
-               "WARN: fileName:2: The pattern \"nextbsd\" cannot match any of "+
+               "WARN: filename:1: \"linux-i386\" is not a valid platform pattern.",
+               "WARN: filename:2: The pattern \"nextbsd\" cannot match any of "+
                        "{ AIX BSDOS Bitrig Cygwin Darwin DragonFly FreeBSD FreeMiNT GNUkFreeBSD HPUX Haiku "+
                        "IRIX Interix Linux Minix MirBSD NetBSD OSF1 OpenBSD QNX SCO_SV SunOS UnixWare "+
                        "} for the operating system part of ONLY_FOR_PLATFORM.",
-               "WARN: fileName:2: The pattern \"8087\" cannot match any of "+
+               "WARN: filename:2: The pattern \"8087\" cannot match any of "+
                        "{ aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 cobalt coldfire convex dreamcast "+
                        "earm earmeb earmhf earmhfeb earmv4 earmv4eb earmv5 earmv5eb earmv6 earmv6eb earmv6hf "+
                        "earmv6hfeb earmv7 earmv7eb earmv7hf earmv7hfeb evbarm hpcmips hpcsh hppa hppa64 "+
                        "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: fileName:3: The pattern \"netbsd\" cannot match any of "+
+               "WARN: filename:3: The pattern \"netbsd\" cannot match any of "+
                        "{ AIX BSDOS Bitrig Cygwin Darwin DragonFly FreeBSD FreeMiNT GNUkFreeBSD HPUX Haiku "+
                        "IRIX Interix Linux Minix MirBSD NetBSD OSF1 OpenBSD QNX SCO_SV SunOS UnixWare "+
                        "} for the operating system part of ONLY_FOR_PLATFORM.",
-               "WARN: fileName:3: The pattern \"l*\" cannot match any of "+
+               "WARN: filename:3: The pattern \"l*\" cannot match any of "+
                        "{ aarch64 aarch64eb alpha amd64 arc arm arm26 arm32 cobalt coldfire convex dreamcast "+
                        "earm earmeb earmhf earmhfeb earmv4 earmv4eb earmv5 earmv5eb earmv6 earmv6eb earmv6hf "+
                        "earmv6hfeb earmv7 earmv7eb earmv7hf earmv7hfeb evbarm hpcmips hpcsh hppa hppa64 "+
                        "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: fileName:5: \"FreeBSD*\" is not a valid platform pattern.",
-               "WARN: fileName:8: Please use \"[0-1].*\" instead of \"[0-1]*\" as the version pattern.")
+               "WARN: filename:5: \"FreeBSD*\" is not a valid platform pattern.",
+               "WARN: filename:8: Please use \"[0-1].*\" instead of \"[0-1]*\" as the version pattern.")
 }
 
 func (s *Suite) Test_VartypeCheck_PythonDependency(c *check.C) {
@@ -811,8 +830,8 @@ func (s *Suite) Test_VartypeCheck_Python
                "cairo,X")
 
        vt.Output(
-               "WARN: fileName:2: Python dependencies should not contain variables.",
-               "WARN: fileName:3: Invalid Python dependency \"cairo,X\".")
+               "WARN: filename:2: Python dependencies should not contain variables.",
+               "WARN: filename:3: Invalid Python dependency \"cairo,X\".")
 }
 
 func (s *Suite) Test_VartypeCheck_PrefixPathname(c *check.C) {
@@ -824,7 +843,7 @@ func (s *Suite) Test_VartypeCheck_Prefix
                "share/locale")
 
        vt.Output(
-               "WARN: fileName:1: Please use \"${PKGMANDIR}/man1\" instead of \"man/man1\".")
+               "WARN: filename:1: Please use \"${PKGMANDIR}/man1\" instead of \"man/man1\".")
 }
 
 func (s *Suite) Test_VartypeCheck_RelativePkgPath(c *check.C) {
@@ -843,9 +862,9 @@ func (s *Suite) Test_VartypeCheck_Relati
                "../../invalid/relative")
 
        vt.Output(
-               "ERROR: fileName:1: \"category/other-package\" does not exist.",
-               "ERROR: fileName:4: \"invalid\" does not exist.",
-               "ERROR: fileName:5: \"../../invalid/relative\" does not exist.")
+               "ERROR: filename:1: \"category/other-package\" does not exist.",
+               "ERROR: filename:4: \"invalid\" does not exist.",
+               "ERROR: filename:5: \"../../invalid/relative\" does not exist.")
 }
 
 func (s *Suite) Test_VartypeCheck_Restricted(c *check.C) {
@@ -856,7 +875,7 @@ func (s *Suite) Test_VartypeCheck_Restri
                "May only be distributed free of charge")
 
        vt.Output(
-               "WARN: fileName:1: The only valid value for NO_BIN_ON_CDROM is ${RESTRICTED}.")
+               "WARN: filename:1: The only valid value for NO_BIN_ON_CDROM is ${RESTRICTED}.")
 }
 
 func (s *Suite) Test_VartypeCheck_SedCommands(c *check.C) {
@@ -875,12 +894,12 @@ func (s *Suite) Test_VartypeCheck_SedCom
                "-e")
 
        vt.Output(
-               "NOTE: fileName:1: Please always use \"-e\" in sed commands, even if there is only one substitution.",
-               "NOTE: fileName:2: Each sed command should appear in an assignment of its own.",
-               "WARN: fileName:3: The # character starts a comment.",
-               "ERROR: fileName:3: Invalid shell words \"\\\"s,\" in sed commands.",
-               "WARN: fileName:8: Unknown sed command \"1d\".",
-               "ERROR: fileName:9: The -e option to sed requires an argument.")
+               "NOTE: filename:1: Please always use \"-e\" in sed commands, even if there is only one substitution.",
+               "NOTE: filename:2: Each sed command should appear in an assignment of its own.",
+               "WARN: filename:3: The # character starts a comment.",
+               "ERROR: filename:3: Invalid shell words \"\\\"s,\" in sed commands.",
+               "WARN: filename:8: Unknown sed command \"1d\".",
+               "ERROR: filename:9: The -e option to sed requires an argument.")
 }
 
 func (s *Suite) Test_VartypeCheck_ShellCommand(c *check.C) {
@@ -892,6 +911,9 @@ func (s *Suite) Test_VartypeCheck_ShellC
        vt.Values(
                "${INSTALL_DATA} -m 0644 ${WRKDIR}/source ${DESTDIR}${PREFIX}/target")
 
+       vt.Op(opUseMatch)
+       vt.Values("*")
+
        vt.OutputEmpty()
 }
 
@@ -907,7 +929,7 @@ func (s *Suite) Test_VartypeCheck_ShellC
                "echo bin/program;")
 
        vt.Output(
-               "WARN: fileName:1: This shell command list should end with a semicolon.")
+               "WARN: filename:1: This shell command list should end with a semicolon.")
 }
 
 func (s *Suite) Test_VartypeCheck_Stage(c *check.C) {
@@ -920,7 +942,7 @@ func (s *Suite) Test_VartypeCheck_Stage(
                "pre-test")
 
        vt.Output(
-               "WARN: fileName:2: Invalid stage name \"post-modern\". " +
+               "WARN: filename:2: Invalid stage name \"post-modern\". " +
                        "Use one of {pre,do,post}-{extract,patch,configure,build,test,install}.")
 }
 
@@ -942,10 +964,10 @@ func (s *Suite) Test_VartypeCheck_Tool(c
                "unknown")
 
        vt.Output(
-               "ERROR: fileName:2: Unknown tool dependency \"unknown\". "+
+               "ERROR: filename:2: Unknown tool dependency \"unknown\". "+
                        "Use one of \"bootstrap\", \"build\", \"pkgsrc\", \"run\" or \"test\".",
-               "ERROR: fileName:4: Malformed tool dependency: \"mal:formed:tool\".",
-               "ERROR: fileName:5: Unknown tool \"unknown\".")
+               "ERROR: filename:4: Malformed tool dependency: \"mal:formed:tool\".",
+               "ERROR: filename:5: Unknown tool \"unknown\".")
 
        vt.Varname("USE_TOOLS.NetBSD")
        vt.Op(opAssignAppend)
@@ -954,7 +976,7 @@ func (s *Suite) Test_VartypeCheck_Tool(c
                "tool2:unknown")
 
        vt.Output(
-               "ERROR: fileName:12: Unknown tool dependency \"unknown\". " +
+               "ERROR: filename:12: Unknown tool dependency \"unknown\". " +
                        "Use one of \"bootstrap\", \"build\", \"pkgsrc\", \"run\" or \"test\".")
 
        vt.Varname("TOOLS_NOOP")
@@ -981,12 +1003,12 @@ func (s *Suite) Test_VartypeCheck_URL(c 
                "string with spaces")
 
        vt.Output(
-               "WARN: fileName:3: Please write NetBSD.org instead of www.netbsd.org.",
-               "WARN: fileName:4: \"mailto:someone%example.org@localhost\"; is not a valid URL.",
-               "WARN: fileName:5: \"httpxs://www.example.org\" is not a valid URL. Only ftp, gopher, http, and https URLs are allowed here.",
-               "NOTE: fileName:6: For consistency, please add a trailing slash to \"https://www.example.org\".";,
-               "WARN: fileName:7: \"https://www.example.org/path with spaces\" is not a valid URL.",
-               "WARN: fileName:8: \"string with spaces\" is not a valid URL.")
+               "WARN: filename:3: Please write NetBSD.org instead of www.netbsd.org.",
+               "WARN: filename:4: \"mailto:someone%example.org@localhost\"; is not a valid URL.",
+               "WARN: filename:5: \"httpxs://www.example.org\" is not a valid URL. Only ftp, gopher, http, and https URLs are allowed here.",
+               "NOTE: filename:6: For consistency, please add a trailing slash to \"https://www.example.org\".";,
+               "WARN: filename:7: \"https://www.example.org/path with spaces\" is not a valid URL.",
+               "WARN: filename:8: \"string with spaces\" is not a valid URL.")
 }
 
 func (s *Suite) Test_VartypeCheck_UserGroupName(c *check.C) {
@@ -1002,8 +1024,8 @@ func (s *Suite) Test_VartypeCheck_UserGr
                "${OTHER_VAR}")
 
        vt.Output(
-               "WARN: fileName:1: Invalid user or group name \"user with spaces\".",
-               "WARN: fileName:4: Invalid user or group name \"domain\\\\user\".")
+               "WARN: filename:1: Invalid user or group name \"user with spaces\".",
+               "WARN: filename:4: Invalid user or group name \"domain\\\\user\".")
 }
 
 func (s *Suite) Test_VartypeCheck_VariableName(c *check.C) {
@@ -1017,7 +1039,7 @@ func (s *Suite) Test_VartypeCheck_Variab
                "${INDIRECT}")
 
        vt.Output(
-               "WARN: fileName:2: \"VarBase\" is not a valid variable name.")
+               "WARN: filename:2: \"VarBase\" is not a valid variable name.")
 }
 
 func (s *Suite) Test_VartypeCheck_Version(c *check.C) {
@@ -1032,7 +1054,7 @@ func (s *Suite) Test_VartypeCheck_Versio
                "4.1-SNAPSHOT",
                "4pre7")
        vt.Output(
-               "WARN: fileName:4: Invalid version number \"4.1-SNAPSHOT\".")
+               "WARN: filename:4: Invalid version number \"4.1-SNAPSHOT\".")
 
        vt.Op(opUseMatch)
        vt.Values(
@@ -1044,10 +1066,10 @@ func (s *Suite) Test_VartypeCheck_Versio
                "1.[2-7].*",
                "[0-9]*")
        vt.Output(
-               "WARN: fileName:11: Invalid version number pattern \"a*\".",
-               "WARN: fileName:12: Invalid version number pattern \"1.2/456\".",
-               "WARN: fileName:13: Please use \"4.*\" instead of \"4*\" as the version pattern.",
-               "WARN: fileName:15: Please use \"1.[234].*\" instead of \"1.[234]*\" as the version pattern.")
+               "WARN: filename:11: Invalid version number pattern \"a*\".",
+               "WARN: filename:12: Invalid version number pattern \"1.2/456\".",
+               "WARN: filename:13: Please use \"4.*\" instead of \"4*\" as the version pattern.",
+               "WARN: filename:15: Please use \"1.[234].*\" instead of \"1.[234]*\" as the version pattern.")
 }
 
 func (s *Suite) Test_VartypeCheck_WrapperReorder(c *check.C) {
@@ -1060,8 +1082,8 @@ func (s *Suite) Test_VartypeCheck_Wrappe
                "reorder:l:first",
                "omit:first")
        vt.Output(
-               "WARN: fileName:2: Unknown wrapper reorder command \"reorder:l:first\".",
-               "WARN: fileName:3: Unknown wrapper reorder command \"omit:first\".")
+               "WARN: filename:2: Unknown wrapper reorder command \"reorder:l:first\".",
+               "WARN: filename:3: Unknown wrapper reorder command \"omit:first\".")
 }
 
 func (s *Suite) Test_VartypeCheck_WrapperTransform(c *check.C) {
@@ -1079,8 +1101,8 @@ func (s *Suite) Test_VartypeCheck_Wrappe
                "rpath:/usr/lib",
                "unknown")
        vt.Output(
-               "WARN: fileName:7: Unknown wrapper transform command \"rpath:/usr/lib\".",
-               "WARN: fileName:8: Unknown wrapper transform command \"unknown\".")
+               "WARN: filename:7: Unknown wrapper transform command \"rpath:/usr/lib\".",
+               "WARN: filename:8: Unknown wrapper transform command \"unknown\".")
 }
 
 func (s *Suite) Test_VartypeCheck_WrksrcSubdirectory(c *check.C) {
@@ -1097,12 +1119,12 @@ func (s *Suite) Test_VartypeCheck_Wrksrc
                "${WRKSRC}/directory with spaces",
                "directory with spaces")
        vt.Output(
-               "NOTE: fileName:1: You can use \".\" instead of \"${WRKSRC}\".",
-               "NOTE: fileName:2: You can use \".\" instead of \"${WRKSRC}/\".",
-               "NOTE: fileName:3: You can use \".\" instead of \"${WRKSRC}/.\".",
-               "NOTE: fileName:4: You can use \"subdir\" instead of \"${WRKSRC}/subdir\".",
-               "NOTE: fileName:6: You can use \"directory with spaces\" instead of \"${WRKSRC}/directory with spaces\".",
-               "WARN: fileName:7: \"directory with spaces\" is not a valid subdirectory of ${WRKSRC}.")
+               "NOTE: filename:1: You can use \".\" instead of \"${WRKSRC}\".",
+               "NOTE: filename:2: You can use \".\" instead of \"${WRKSRC}/\".",
+               "NOTE: filename:3: You can use \".\" instead of \"${WRKSRC}/.\".",
+               "NOTE: filename:4: You can use \"subdir\" instead of \"${WRKSRC}/subdir\".",
+               "NOTE: filename:6: You can use \"directory with spaces\" instead of \"${WRKSRC}/directory with spaces\".",
+               "WARN: filename:7: \"directory with spaces\" is not a valid subdirectory of ${WRKSRC}.")
 }
 
 func (s *Suite) Test_VartypeCheck_Yes(c *check.C) {
@@ -1115,8 +1137,8 @@ func (s *Suite) Test_VartypeCheck_Yes(c 
                "${YESVAR}")
 
        vt.Output(
-               "WARN: fileName:2: APACHE_MODULE should be set to YES or yes.",
-               "WARN: fileName:3: APACHE_MODULE should be set to YES or yes.")
+               "WARN: filename:2: APACHE_MODULE should be set to YES or yes.",
+               "WARN: filename:3: APACHE_MODULE should be set to YES or yes.")
 
        vt.Varname("PKG_DEVELOPER")
        vt.Op(opUseMatch)
@@ -1126,9 +1148,9 @@ func (s *Suite) Test_VartypeCheck_Yes(c 
                "${YESVAR}")
 
        vt.Output(
-               "WARN: fileName:11: PKG_DEVELOPER should only be used in a \".if defined(...)\" condition.",
-               "WARN: fileName:12: PKG_DEVELOPER should only be used in a \".if defined(...)\" condition.",
-               "WARN: fileName:13: PKG_DEVELOPER should only be used in a \".if defined(...)\" condition.")
+               "WARN: filename:11: PKG_DEVELOPER should only be used in a \".if defined(...)\" condition.",
+               "WARN: filename:12: PKG_DEVELOPER should only be used in a \".if defined(...)\" condition.",
+               "WARN: filename:13: PKG_DEVELOPER should only be used in a \".if defined(...)\" condition.")
 }
 
 func (s *Suite) Test_VartypeCheck_YesNo(c *check.C) {
@@ -1142,8 +1164,8 @@ func (s *Suite) Test_VartypeCheck_YesNo(
                "${YESVAR}")
 
        vt.Output(
-               "WARN: fileName:3: GNU_CONFIGURE should be set to YES, yes, NO, or no.",
-               "WARN: fileName:4: GNU_CONFIGURE should be set to YES, yes, NO, or no.")
+               "WARN: filename:3: GNU_CONFIGURE should be set to YES, yes, NO, or no.",
+               "WARN: filename:4: GNU_CONFIGURE should be set to YES, yes, NO, or no.")
 }
 
 func (s *Suite) Test_VartypeCheck_YesNoIndirectly(c *check.C) {
@@ -1157,28 +1179,28 @@ func (s *Suite) Test_VartypeCheck_YesNoI
                "${YESVAR}")
 
        vt.Output(
-               "WARN: fileName:3: GNU_CONFIGURE should be set to YES, yes, NO, or no.")
+               "WARN: filename:3: GNU_CONFIGURE should be set to YES, yes, NO, or no.")
 }
 
 // VartypeCheckTester helps to test the many different checks in VartypeCheck.
-// It keeps track of the current variable, operator, file name, line number,
+// It keeps track of the current variable, operator, filename, line number,
 // so that the test can focus on the interesting details.
 type VartypeCheckTester struct {
        tester   *Tester
        checker  func(cv *VartypeCheck)
-       fileName string
+       filename string
        lineno   int
        varname  string
        op       MkOperator
 }
 
-// NewVartypeCheckTester starts the test with a file name of "fileName", at line 1,
+// NewVartypeCheckTester starts the test with a filename of "filename", at line 1,
 // with "=" as the operator. The variable has to be initialized explicitly.
 func NewVartypeCheckTester(t *Tester, checker func(cv *VartypeCheck)) *VartypeCheckTester {
        return &VartypeCheckTester{
                t,
                checker,
-               "fileName",
+               "filename",
                1,
                "",
                opAssign}
@@ -1189,8 +1211,8 @@ func (vt *VartypeCheckTester) Varname(va
        vt.nextSection()
 }
 
-func (vt *VartypeCheckTester) File(fileName string) {
-       vt.fileName = fileName
+func (vt *VartypeCheckTester) File(filename string) {
+       vt.filename = filename
        vt.lineno = 1
 }
 
@@ -1224,7 +1246,7 @@ func (vt *VartypeCheckTester) Values(val
                        panic("Invalid operator: " + opStr)
                }
 
-               mkline := vt.tester.NewMkLine(vt.fileName, vt.lineno, text)
+               mkline := vt.tester.NewMkLine(vt.filename, vt.lineno, text)
                comment := ""
                if mkline.IsVarassign() {
                        mkline.Tokenize(value, true) // Produce some warnings as side-effects.
@@ -1237,8 +1259,8 @@ func (vt *VartypeCheckTester) Values(val
                }
 
                valueNovar := mkline.WithoutMakeVariables(effectiveValue)
-               vc := &VartypeCheck{mkline, mkline.Line, varname, op, effectiveValue, valueNovar, comment, false}
-               vt.checker(vc)
+               vc := VartypeCheck{mkline, mkline.Line, varname, op, effectiveValue, valueNovar, comment, false}
+               vt.checker(&vc)
 
                vt.lineno++
        }

Index: pkgsrc/pkgtools/pkglint/files/getopt/getopt.go
diff -u pkgsrc/pkgtools/pkglint/files/getopt/getopt.go:1.6 pkgsrc/pkgtools/pkglint/files/getopt/getopt.go:1.7
--- pkgsrc/pkgtools/pkglint/files/getopt/getopt.go:1.6  Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/getopt/getopt.go      Sun Dec  2 01:57:48 2018
@@ -29,43 +29,55 @@ func NewOptions() *Options {
 //  warnings.AddFlagVar("extra", &extra, false, "Print extra warnings")
 func (o *Options) AddFlagGroup(shortName rune, longName, argsName, description string) *FlagGroup {
        grp := new(FlagGroup)
-       opt := &option{shortName, longName, argsName, description, grp}
-       o.options = append(o.options, opt)
+       opt := option{shortName, longName, argsName, description, grp}
+       o.options = append(o.options, &opt)
        return grp
 }
 
 func (o *Options) AddFlagVar(shortName rune, longName string, pflag *bool, defval bool, description string) {
        *pflag = defval
-       opt := &option{shortName, longName, "", description, pflag}
-       o.options = append(o.options, opt)
+       opt := option{shortName, longName, "", description, pflag}
+       o.options = append(o.options, &opt)
+}
+
+func (o *Options) AddStrVar(shortName rune, longName string, pstr *string, defval string, description string) {
+       *pstr = defval
+       opt := option{shortName, longName, "", description, pstr}
+       o.options = append(o.options, &opt)
 }
 
 func (o *Options) AddStrList(shortName rune, longName string, plist *[]string, description string) {
        *plist = []string{}
-       opt := &option{shortName, longName, "", description, plist}
-       o.options = append(o.options, opt)
+       opt := option{shortName, longName, "", description, plist}
+       o.options = append(o.options, &opt)
 }
 
 // Parse extracts the command line options from the given arguments.
 // args[0] is the program name, as in os.Args.
 func (o *Options) Parse(args []string) (remainingArgs []string, err error) {
        var skip int
+
        for i := 1; i < len(args) && err == nil; i++ {
                arg := args[i]
                switch {
+
                case arg == "--":
                        remainingArgs = append(remainingArgs, args[i+1:]...)
                        return
+
                case strings.HasPrefix(arg, "--"):
                        skip, err = o.parseLongOption(args, i, arg[2:])
                        i += skip
+
                case strings.HasPrefix(arg, "-"):
                        skip, err = o.parseShortOptions(args, i, arg[1:])
                        i += skip
+
                default:
                        remainingArgs = append(remainingArgs, arg)
                }
        }
+
        if err != nil {
                err = optErr(args[0] + ": " + err.Error())
        }
@@ -75,6 +87,7 @@ func (o *Options) Parse(args []string) (
 func (o *Options) parseLongOption(args []string, i int, argRest string) (skip int, err error) {
        parts := strings.SplitN(argRest, "=", 2)
        argname := parts[0]
+
        var argval *string
        if 1 < len(parts) {
                argval = &parts[1]
@@ -96,6 +109,7 @@ func (o *Options) parseLongOption(args [
                        }
                }
        }
+
        if prefixOpt != nil {
                return o.handleLongOption(args, i, prefixOpt, argval)
        }
@@ -104,6 +118,7 @@ func (o *Options) parseLongOption(args [
 
 func (o *Options) handleLongOption(args []string, i int, opt *option, argval *string) (skip int, err error) {
        switch data := opt.data.(type) {
+
        case *bool:
                if argval == nil {
                        *data = true
@@ -118,6 +133,19 @@ func (o *Options) handleLongOption(args 
                        }
                }
                return 0, nil
+
+       case *string:
+               switch {
+               case argval != nil:
+                       *data = *argval
+                       return 0, nil
+               case i+1 < len(args):
+                       *data = args[i+1]
+                       return 1, nil
+               default:
+                       return 0, optErr("option requires an argument: --" + opt.longName)
+               }
+
        case *[]string:
                switch {
                case argval != nil:
@@ -129,6 +157,7 @@ func (o *Options) handleLongOption(args 
                default:
                        return 0, optErr("option requires an argument: --" + opt.longName)
                }
+
        case *FlagGroup:
                switch {
                case argval != nil:
@@ -139,6 +168,7 @@ func (o *Options) handleLongOption(args 
                        return 0, optErr("option requires an argument: --" + opt.longName)
                }
        }
+
        panic("getopt: internal error: unknown option type")
 }
 
@@ -148,10 +178,24 @@ optchar:
                for _, opt := range o.options {
                        if optchar == opt.shortName {
                                switch data := opt.data.(type) {
+
                                case *bool:
                                        *data = true
                                        continue optchar
 
+                               case *string:
+                                       argarg := optchars[ai+utf8.RuneLen(optchar):]
+                                       switch {
+                                       case argarg != "":
+                                               *data = argarg
+                                               return 0, nil
+                                       case i+1 < len(args):
+                                               *data = args[i+1]
+                                               return 1, nil
+                                       default:
+                                               return 0, optErr("option requires an argument: -" + string([]rune{optchar}))
+                                       }
+
                                case *[]string:
                                        argarg := optchars[ai+utf8.RuneLen(optchar):]
                                        switch {
@@ -178,8 +222,10 @@ optchar:
                                }
                        }
                }
+
                return 0, optErr("unknown option: -" + string([]rune{optchar}))
        }
+
        return 0, nil
 }
 
@@ -218,6 +264,7 @@ func (o *Options) Help(out io.Writer, ge
                        rowf("  Flags for -%c, --%s:", opt.shortName, opt.longName)
                        rowf("    all\t all of the following")
                        rowf("    none\t none of the following")
+
                        for _, flag := range flagGroup.flags {
                                state := "disabled"
                                if *flag.value {
@@ -225,9 +272,11 @@ func (o *Options) Help(out io.Writer, ge
                                }
                                rowf("    %s\t %s (%v)", flag.name, flag.description, state)
                        }
+
                        finishTable()
                }
        }
+
        if hasFlagGroups {
                rowf("")
                rowf("  (Prefix a flag with \"no-\" to disable it.)")
@@ -254,8 +303,8 @@ type groupFlag struct {
 }
 
 func (fg *FlagGroup) AddFlagVar(name string, flag *bool, defval bool, description string) {
-       opt := &groupFlag{name, flag, description}
-       fg.flags = append(fg.flags, opt)
+       opt := groupFlag{name, flag, description}
+       fg.flags = append(fg.flags, &opt)
        *flag = defval
 }
 

Index: pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go
diff -u pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go:1.8 pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go:1.9
--- pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go:1.8     Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/getopt/getopt_test.go Sun Dec  2 01:57:48 2018
@@ -25,6 +25,36 @@ func (s *Suite) Test_Options_Parse__shor
        c.Check(help, check.Equals, true)
 }
 
+func (s *Suite) Test_Options_Parse__short_string(c *check.C) {
+       opts := NewOptions()
+       var help bool
+       var src string
+       var dst string
+       opts.AddFlagVar('h', "help", &help, false, "prints a help page")
+       opts.AddStrVar('s', "src", &src, "", "source of the copy operation")
+       opts.AddStrVar('d', "dst", &dst, "", "destination of the copy operation")
+
+       args, err := opts.Parse([]string{"copy", "-hssource", "-d", "destination"})
+
+       c.Assert(err, check.IsNil)
+       c.Check(args, check.IsNil)
+       c.Check(help, check.Equals, true)
+       c.Check(src, check.Equals, "source")
+       c.Check(dst, check.Equals, "destination")
+}
+
+func (s *Suite) Test_Options_Parse__short_string_unfinished(c *check.C) {
+       opts := NewOptions()
+       var unfinished string
+       opts.AddStrVar('u', "unfinished", &unfinished, "", "demo")
+
+       args, err := opts.Parse([]string{"program", "-u"})
+
+       c.Assert(err.Error(), check.Equals, "program: option requires an argument: -u")
+       c.Check(args, check.IsNil)
+       c.Check(unfinished, check.Equals, "")
+}
+
 func (s *Suite) Test_Options_Parse__unknown_short(c *check.C) {
        opts := NewOptions()
 
@@ -33,6 +63,14 @@ func (s *Suite) Test_Options_Parse__unkn
        c.Check(err.Error(), check.Equals, "progname: unknown option: -z")
 }
 
+func (s *Suite) Test_Options_Parse__unknown_short_with_argument(c *check.C) {
+       opts := NewOptions()
+
+       _, err := opts.Parse([]string{"progname", "-z", "arg"})
+
+       c.Check(err.Error(), check.Equals, "progname: unknown option: -z")
+}
+
 func (s *Suite) Test_Options_Parse__unknown_long(c *check.C) {
        opts := NewOptions()
 
@@ -180,6 +218,47 @@ func (s *Suite) Test_Options_Parse__long
        c.Check(err.Error(), check.Equals, "progname: invalid argument for option --other1")
 }
 
+func (s *Suite) Test_Options_Parse__long_string(c *check.C) {
+       opts := NewOptions()
+       var src, dst string
+       opts.AddStrVar('s', "src", &src, "", "source of the copy operation")
+       opts.AddStrVar('d', "dst", &dst, "", "destination of the copy operation")
+
+       args, err := opts.Parse([]string{"copy", "--src=source", "--dst", "destination", "arg"})
+
+       c.Assert(err, check.IsNil)
+       c.Check(args, check.DeepEquals, []string{"arg"})
+       c.Check(src, check.Equals, "source")
+       c.Check(dst, check.Equals, "destination")
+}
+
+func (s *Suite) Test_Options_Parse__long_string_unfinished(c *check.C) {
+       opts := NewOptions()
+       var unfinished string
+       opts.AddStrVar('u', "unfinished", &unfinished, "", "unfinished option")
+
+       args, err := opts.Parse([]string{"program", "--unfinished"})
+
+       c.Check(err.Error(), check.Equals, "program: option requires an argument: --unfinished")
+       c.Check(args, check.IsNil)
+       c.Check(unfinished, check.Equals, "")
+}
+
+func (s *Suite) Test_Options_handleLongOption__string(c *check.C) {
+       var extra bool
+
+       opts := NewOptions()
+
+       group := opts.AddFlagGroup('W', "warnings", "warning,...", "Print selected warnings")
+       group.AddFlagVar("extra", &extra, false, "Print extra warnings")
+
+       args, err := opts.Parse([]string{"progname", "--warnings"})
+
+       c.Check(args, check.IsNil)
+       c.Check(err.Error(), check.Equals, "progname: option requires an argument: --warnings")
+       c.Check(extra, check.Equals, false)
+}
+
 func (s *Suite) Test_Options_handleLongOption__flag_group_without_argument(c *check.C) {
        var extra bool
 
@@ -208,18 +287,44 @@ func (s *Suite) Test_Options_handleLongO
        c.Check(extra, check.Equals, true)
 }
 
-func (s *Suite) Test_Options_handleLongOption__flag_group_negated(c *check.C) {
-       var extra bool
+func (s *Suite) Test_Options_handleLongOption__flag_group_all_then_disable(c *check.C) {
+       var false1, false2, true1, true2 bool
 
        opts := NewOptions()
-       group := opts.AddFlagGroup('W', "warnings", "warning,...", "Print selected warnings")
-       group.AddFlagVar("extra", &extra, true, "Print extra warnings")
+       group := opts.AddFlagGroup('a', "answers", "answer,...", "Choose the answers")
+       group.AddFlagVar("false1", &false1, false, "A")
+       group.AddFlagVar("false2", &false2, false, "B")
+       group.AddFlagVar("true1", &true1, true, "C")
+       group.AddFlagVar("true2", &true2, true, "C")
 
-       args, err := opts.Parse([]string{"progname", "--warnings", "all,no-extra"})
+       args, err := opts.Parse([]string{"progname", "--answers", "all,no-false1,no-true1", "arg"})
 
-       c.Check(args, check.IsNil)
        c.Check(err, check.IsNil)
-       c.Check(extra, check.Equals, false)
+       c.Check(args, check.DeepEquals, []string{"arg"})
+       c.Check(false1, check.Equals, false)
+       c.Check(false2, check.Equals, true)
+       c.Check(true1, check.Equals, false)
+       c.Check(true2, check.Equals, true)
+}
+
+func (s *Suite) Test_Options_handleLongOption__flag_group_none_then_enable(c *check.C) {
+       var false1, false2, true1, true2 bool
+
+       opts := NewOptions()
+       group := opts.AddFlagGroup('a', "answers", "answer,...", "Choose the answers")
+       group.AddFlagVar("false1", &false1, false, "A")
+       group.AddFlagVar("false2", &false2, false, "B")
+       group.AddFlagVar("true1", &true1, true, "C")
+       group.AddFlagVar("true2", &true2, true, "C")
+
+       args, err := opts.Parse([]string{"progname", "--answers", "none,false1,true1", "arg"})
+
+       c.Check(err, check.IsNil)
+       c.Check(args, check.DeepEquals, []string{"arg"})
+       c.Check(false1, check.Equals, true)
+       c.Check(false2, check.Equals, false)
+       c.Check(true1, check.Equals, true)
+       c.Check(true2, check.Equals, false)
 }
 
 func (s *Suite) Test_Options_handleLongOption__internal_error(c *check.C) {
@@ -253,10 +358,30 @@ func (s *Suite) Test_Options_parseShortO
 }
 
 func (s *Suite) Test_Options_Help(c *check.C) {
+       var verbose bool
+       var name string
+
+       opts := NewOptions()
+       opts.AddFlagVar('v', "verbose", &verbose, false, "Print a detailed log")
+       opts.AddStrVar('n', "name", &name, "", "Name of the print job")
+
+       var out strings.Builder
+       opts.Help(&out, "progname [options] args")
+
+       c.Check(out.String(), check.Equals, ""+
+               "usage: progname [options] args\n"+
+               "\n"+
+               "  -v, --verbose   Print a detailed log\n"+
+               "  -n, --name      Name of the print job\n")
+}
+
+func (s *Suite) Test_Options_Help__with_flag_group(c *check.C) {
        var verbose, basic, extra bool
+       var name string
 
        opts := NewOptions()
        opts.AddFlagVar('v', "verbose", &verbose, false, "Print a detailed log")
+       opts.AddStrVar('n', "name", &name, "", "Name of the print job")
        group := opts.AddFlagGroup('W', "warnings", "warning,...", "Print selected warnings")
        group.AddFlagVar("basic", &basic, true, "Print basic warnings")
        group.AddFlagVar("extra", &extra, false, "Print extra warnings")
@@ -268,6 +393,7 @@ func (s *Suite) Test_Options_Help(c *che
                "usage: progname [options] args\n"+
                "\n"+
                "  -v, --verbose                Print a detailed log\n"+
+               "  -n, --name                   Name of the print job\n"+
                "  -W, --warnings=warning,...   Print selected warnings\n"+
                "\n"+
                "  Flags for -W, --warnings:\n"+

Index: pkgsrc/pkgtools/pkglint/files/intqa/testnames.go
diff -u pkgsrc/pkgtools/pkglint/files/intqa/testnames.go:1.1 pkgsrc/pkgtools/pkglint/files/intqa/testnames.go:1.2
--- pkgsrc/pkgtools/pkglint/files/intqa/testnames.go:1.1        Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/intqa/testnames.go    Sun Dec  2 01:57:48 2018
@@ -31,7 +31,7 @@ type TestNameChecker struct {
 
 type testeePrefix struct {
        prefix   string
-       fileName string
+       filename string
 }
 
 // testeeElement is an element of the source code that can be tested.
@@ -94,27 +94,9 @@ func (ck *TestNameChecker) addWarning(fo
        ck.warnings = append(ck.warnings, "W: "+fmt.Sprintf(format, args...))
 }
 
-func newElement(typeName, funcName, fileName string) *testeeElement {
-       typeName = strings.TrimSuffix(typeName, "Impl")
-
-       e := &testeeElement{File: fileName, Type: typeName, Func: funcName}
-
-       e.FullName = e.Type + ifelseStr(e.Type != "" && e.Func != "", ".", "") + e.Func
-
-       e.Test = strings.HasSuffix(e.File, "_test.go") && e.Type != "" && strings.HasPrefix(e.Func, "Test")
-
-       if e.Test {
-               e.Prefix = strings.Split(strings.TrimPrefix(e.Func, "Test"), "__")[0]
-       } else {
-               e.Prefix = e.Type + ifelseStr(e.Type != "" && e.Func != "", "_", "") + e.Func
-       }
-
-       return e
-}
-
 // addElement adds a single type or function declaration
 // to the known elements.
-func (ck *TestNameChecker) addElement(elements *[]*testeeElement, decl ast.Decl, fileName string) {
+func (ck *TestNameChecker) addElement(elements *[]*testeeElement, decl ast.Decl, filename string) {
        switch decl := decl.(type) {
 
        case *ast.GenDecl:
@@ -122,7 +104,7 @@ func (ck *TestNameChecker) addElement(el
                        switch spec := spec.(type) {
                        case *ast.TypeSpec:
                                typeName := spec.Name.Name
-                               *elements = append(*elements, newElement(typeName, "", fileName))
+                               *elements = append(*elements, newElement(typeName, "", filename))
                        }
                }
 
@@ -136,7 +118,7 @@ func (ck *TestNameChecker) addElement(el
                                typeName = typeExpr.(*ast.Ident).Name
                        }
                }
-               *elements = append(*elements, newElement(typeName, decl.Name.Name, fileName))
+               *elements = append(*elements, newElement(typeName, decl.Name.Name, filename))
        }
 }
 
@@ -145,12 +127,12 @@ func (ck *TestNameChecker) addElement(el
 //
 // It doesn't really belong to this type (TestNameChecker) but
 // merely uses its infrastructure.
-func (ck *TestNameChecker) fixTabs(fileName string) {
-       if ck.isIgnored(fileName) {
+func (ck *TestNameChecker) fixTabs(filename string) {
+       if ck.isIgnored(filename) {
                return
        }
 
-       readBytes, err := ioutil.ReadFile(fileName)
+       readBytes, err := ioutil.ReadFile(filename)
        ck.c.Assert(err, check.IsNil)
 
        var fixed bytes.Buffer
@@ -161,10 +143,10 @@ func (ck *TestNameChecker) fixTabs(fileN
        }
 
        if fixed.String() != string(readBytes) {
-               tmpName := fileName + ".tmp"
+               tmpName := filename + ".tmp"
                err = ioutil.WriteFile(tmpName, fixed.Bytes(), 0666)
                ck.c.Assert(err, check.IsNil)
-               err = os.Rename(tmpName, fileName)
+               err = os.Rename(tmpName, filename)
                ck.c.Assert(err, check.IsNil)
        }
 }
@@ -181,25 +163,14 @@ func (ck *TestNameChecker) loadAllElemen
 
        var elements []*testeeElement
        for _, pkg := range pkgs {
-               for fileName, file := range pkg.Files {
+               for filename, file := range pkg.Files {
                        for _, decl := range file.Decls {
-                               ck.addElement(&elements, decl, fileName)
+                               ck.addElement(&elements, decl, filename)
                        }
                }
        }
 
-       sort.Slice(elements, func(i, j int) bool {
-               ti := elements[i]
-               tj := elements[j]
-               switch {
-               case ti.Type != tj.Type:
-                       return ti.Type < tj.Type
-               case ti.Func != tj.Func:
-                       return ti.Func < tj.Func
-               default:
-                       return ti.File < tj.File
-               }
-       })
+       sort.Slice(elements, func(i, j int) bool { return elements[i].Less(elements[j]) })
 
        return elements
 }
@@ -219,7 +190,7 @@ func (ck *TestNameChecker) collectTestee
        }
 
        for _, p := range ck.prefixes {
-               prefixes[p.prefix] = newElement(p.prefix, "", p.fileName)
+               prefixes[p.prefix] = newElement(p.prefix, "", p.filename)
        }
 
        return prefixes
@@ -233,7 +204,7 @@ func (ck *TestNameChecker) checkTestName
        } else if !strings.HasSuffix(testee.File, "_test.go") {
                correctTestFile := strings.TrimSuffix(testee.File, ".go") + "_test.go"
                if correctTestFile != test.File {
-                       ck.addWarning("Test %q for %q should be in %s instead of %s.",
+                       ck.addError("Test %q for %q must be in %s instead of %s.",
                                test.FullName, testee.FullName, correctTestFile, test.File)
                }
        }
@@ -300,9 +271,9 @@ func (ck *TestNameChecker) Check() {
        }
 }
 
-func (ck *TestNameChecker) isIgnored(fileName string) bool {
+func (ck *TestNameChecker) isIgnored(filename string) bool {
        for _, mask := range ck.ignore {
-               ok, err := filepath.Match(mask, fileName)
+               ok, err := filepath.Match(mask, filename)
                if err != nil {
                        panic(err)
                }
@@ -313,6 +284,35 @@ func (ck *TestNameChecker) isIgnored(fil
        return false
 }
 
+func newElement(typeName, funcName, filename string) *testeeElement {
+       typeName = strings.TrimSuffix(typeName, "Impl")
+
+       e := &testeeElement{File: filename, Type: typeName, Func: funcName}
+
+       e.FullName = e.Type + ifelseStr(e.Type != "" && e.Func != "", ".", "") + e.Func
+
+       e.Test = strings.HasSuffix(e.File, "_test.go") && e.Type != "" && strings.HasPrefix(e.Func, "Test")
+
+       if e.Test {
+               e.Prefix = strings.Split(strings.TrimPrefix(e.Func, "Test"), "__")[0]
+       } else {
+               e.Prefix = e.Type + ifelseStr(e.Type != "" && e.Func != "", "_", "") + e.Func
+       }
+
+       return e
+}
+
+func (el *testeeElement) Less(other *testeeElement) bool {
+       switch {
+       case el.Type != other.Type:
+               return el.Type < other.Type
+       case el.Func != other.Func:
+               return el.Func < other.Func
+       default:
+               return el.File < other.File
+       }
+}
+
 func ifelseStr(cond bool, a, b string) string {
        if cond {
                return a

Index: pkgsrc/pkgtools/pkglint/files/licenses/licenses.go
diff -u pkgsrc/pkgtools/pkglint/files/licenses/licenses.go:1.5 pkgsrc/pkgtools/pkglint/files/licenses/licenses.go:1.6
--- pkgsrc/pkgtools/pkglint/files/licenses/licenses.go:1.5      Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/licenses/licenses.go  Sun Dec  2 01:57:48 2018
@@ -18,8 +18,8 @@ type Condition struct {
 }
 
 func Parse(licenses string) *Condition {
-       lexer := &licenseLexer{lexer: textproc.NewLexer(licenses)}
-       result := liyyNewParser().Parse(lexer)
+       lexer := licenseLexer{lexer: textproc.NewLexer(licenses)}
+       result := liyyNewParser().Parse(&lexer)
        if result != 0 || !lexer.lexer.EOF() {
                return nil
        }
@@ -70,18 +70,18 @@ type licenseLexer struct {
 var licenseNameChars = textproc.NewByteSet("A-Za-z0-9---.")
 
 func (lexer *licenseLexer) Lex(llval *liyySymType) int {
-       repl := lexer.lexer
-       repl.NextHspace()
+       lex := lexer.lexer
+       lex.NextHspace()
        switch {
-       case repl.EOF():
+       case lex.EOF():
                return 0
-       case repl.NextByte('('):
+       case lex.SkipByte('('):
                return ltOPEN
-       case repl.NextByte(')'):
+       case lex.SkipByte(')'):
                return ltCLOSE
        }
 
-       word := repl.NextBytesSet(licenseNameChars)
+       word := lex.NextBytesSet(licenseNameChars)
        switch word {
        case "AND":
                return ltAND

Index: pkgsrc/pkgtools/pkglint/files/textproc/lexer.go
diff -u pkgsrc/pkgtools/pkglint/files/textproc/lexer.go:1.1 pkgsrc/pkgtools/pkglint/files/textproc/lexer.go:1.2
--- pkgsrc/pkgtools/pkglint/files/textproc/lexer.go:1.1 Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/textproc/lexer.go     Sun Dec  2 01:57:48 2018
@@ -1,6 +1,10 @@
 package textproc
 
-import "strings"
+import (
+       "fmt"
+       "regexp"
+       "strings"
+)
 
 // Lexer provides a flexible way of splitting a string into several parts
 // by repeatedly chopping off a prefix that matches a string, a function
@@ -20,7 +24,7 @@ type LexerMark string
 // It cannot match Unicode code points individually and is therefore
 // usually used with ASCII characters.
 type ByteSet struct {
-       bits [4]uint64
+       bits [256]bool
 }
 
 func NewLexer(text string) *Lexer {
@@ -42,8 +46,9 @@ func (l *Lexer) PeekByte() int {
 }
 
 // Skip skips the next n bytes.
-func (l *Lexer) Skip(n int) {
+func (l *Lexer) Skip(n int) bool {
        l.rest = l.rest[n:]
+       return n > 0
 }
 
 // NextString tests whether the remaining string has the given prefix,
@@ -56,6 +61,34 @@ func (l *Lexer) NextString(prefix string
        return ""
 }
 
+// SkipString skips over the given string, if the remaining string starts
+// with it. It returns whether it actually skipped.
+func (l *Lexer) SkipString(prefix string) bool {
+       skipped := strings.HasPrefix(l.rest, prefix)
+       if skipped {
+               l.rest = l.rest[len(prefix):]
+       }
+       return skipped
+}
+
+// SkipHspace chops off the longest prefix (possibly empty) consisting
+// solely of horizontal whitespace, which is the ASCII space (U+0020)
+// and tab (U+0009).
+func (l *Lexer) SkipHspace() bool {
+       // Very similar code as in NextHspace, inlined here for performance reasons.
+       // As of Go 1.11, the compiler does not inline a call to NextHspace here.
+       i := 0
+       rest := l.rest
+       for i < len(rest) && (rest[i] == ' ' || rest[i] == '\t') {
+               i++
+       }
+       if i > 0 {
+               l.rest = rest[i:]
+               return true
+       }
+       return false
+}
+
 // NextHspace chops off the longest prefix (possibly empty) consisting
 // solely of horizontal whitespace, which is the ASCII space (U+0020)
 // and tab (U+0009).
@@ -73,12 +106,9 @@ func (l *Lexer) NextHspace() string {
        return rest[:i]
 }
 
-// NextByte returns true if the remaining string starts with the given byte,
+// SkipByte returns true if the remaining string starts with the given byte,
 // and in that case, chops it off.
-//
-// The return type differs from the other methods since creating a string
-// would be too much work for such a simple operation.
-func (l *Lexer) NextByte(b byte) bool {
+func (l *Lexer) SkipByte(b byte) bool {
        if len(l.rest) > 0 && l.rest[0] == b {
                l.rest = l.rest[1:]
                return true
@@ -104,7 +134,7 @@ func (l *Lexer) NextBytesFunc(fn func(b 
 // otherwise -1.
 func (l *Lexer) NextByteSet(set *ByteSet) int {
        rest := l.rest
-       if 0 < len(rest) && set.bits[rest[0]/64]&(1<<(rest[0]%64)) != 0 {
+       if 0 < len(rest) && set.Contains(rest[0]) {
                l.rest = rest[1:]
                return int(rest[0])
        }
@@ -118,7 +148,7 @@ func (l *Lexer) NextBytesSet(bytes *Byte
        // As of Go 1.11, the compiler does not inline variable function arguments.
        i := 0
        rest := l.rest
-       for i < len(rest) && bytes.bits[rest[i]/64]&(1<<(rest[i]%64)) != 0 {
+       for i < len(rest) && bytes.Contains(rest[i]) {
                i++
        }
        if i != 0 {
@@ -127,6 +157,34 @@ func (l *Lexer) NextBytesSet(bytes *Byte
        return rest[:i]
 }
 
+// SkipRegexp returns true if the remaining string matches the given regular
+// expression, and in that case, chops it off.
+func (l *Lexer) SkipRegexp(re *regexp.Regexp) bool {
+       if !strings.HasPrefix(re.String(), "^") {
+               panic(fmt.Sprintf("Lexer.SkipRegexp: regular expression %q must have prefix %q.", re, "^"))
+       }
+       str := re.FindString(l.rest)
+       if str != "" {
+               l.Skip(len(str))
+       }
+       return str != ""
+}
+
+// NextRegexp tests whether the remaining string matches the given regular
+// expression, and in that case, skips over it and returns the matched substrings,
+// as in regexp.FindStringSubmatch.
+// If the regular expression does not match, returns nil.
+func (l *Lexer) NextRegexp(re *regexp.Regexp) []string {
+       if !strings.HasPrefix(re.String(), "^") {
+               panic(fmt.Sprintf("Lexer.NextRegexp: regular expression %q must have prefix %q.", re, "^"))
+       }
+       m := re.FindStringSubmatch(l.rest)
+       if m != nil {
+               l.Skip(len(m[0]))
+       }
+       return m
+}
+
 // Mark returns the current position of the lexer,
 // which can later be restored by calling Reset.
 func (l *Lexer) Mark() LexerMark {
@@ -164,26 +222,12 @@ func NewByteSet(chars string) *ByteSet {
                case i+2 < len(chars) && chars[i+1] == '-':
                        min := uint(chars[i])
                        max := uint(chars[i+2]) // inclusive
-                       for j := uint(0); j < 4; j++ {
-                               minBit := 64 * j
-                               if min < minBit+64 && minBit <= max {
-                                       loMask := ^uint64(0)
-                                       if minBit < min {
-                                               loMask <<= min - minBit
-                                       }
-
-                                       hiMask := ^uint64(0)
-                                       if minBit+63 > max {
-                                               hiMask >>= minBit + 63 - max
-                                       }
-
-                                       set.bits[j] |= loMask & hiMask
-                               }
+                       for c := min; c <= max; c++ {
+                               set.bits[c] = true
                        }
                        i += 3
                default:
-                       ch := chars[i]
-                       set.bits[ch/64] |= 1 << (ch % 64)
+                       set.bits[chars[i]] = true
                        i++
                }
        }
@@ -192,9 +236,16 @@ func NewByteSet(chars string) *ByteSet {
 
 // Inverse returns a byte set that matches the inverted set of bytes.
 func (bs *ByteSet) Inverse() *ByteSet {
-       return &ByteSet{[4]uint64{^bs.bits[0], ^bs.bits[1], ^bs.bits[2], ^bs.bits[3]}}
+       var inv ByteSet
+       for i := 0; i < 256; i++ {
+               inv.bits[i] = !bs.Contains(byte(i))
+       }
+       return &inv
 }
 
+// Contains tests whether the byte set contains the given byte.
+func (bs *ByteSet) Contains(b byte) bool { return bs.bits[b] }
+
 // Predefined byte sets for parsing ASCII text.
 var (
        Alnum  = NewByteSet("A-Za-z0-9")  // Alphanumerical, without underscore
@@ -202,4 +253,5 @@ var (
        Digit  = NewByteSet("0-9")        // The digits zero to nine
        Space  = NewByteSet("\t\n ")      // Tab, newline, space
        Hspace = NewByteSet("\t ")        // Tab, space
+       XPrint = NewByteSet("\n\t -~")    // Printable ASCII, plus tab and newline
 )
Index: pkgsrc/pkgtools/pkglint/files/textproc/lexer_test.go
diff -u pkgsrc/pkgtools/pkglint/files/textproc/lexer_test.go:1.1 pkgsrc/pkgtools/pkglint/files/textproc/lexer_test.go:1.2
--- pkgsrc/pkgtools/pkglint/files/textproc/lexer_test.go:1.1    Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/textproc/lexer_test.go        Sun Dec  2 01:57:48 2018
@@ -3,6 +3,8 @@ package textproc
 import (
        "gopkg.in/check.v1"
        "netbsd.org/pkglint/intqa"
+       "regexp"
+       "strings"
        "testing"
        "unicode"
 )
@@ -59,7 +61,11 @@ func (s *Suite) Test_Lexer_PeekByte(c *c
 func (s *Suite) Test_Lexer_Skip(c *check.C) {
        lexer := NewLexer("example text")
 
-       lexer.Skip(7)
+       c.Check(lexer.Skip(7), equals, true)
+
+       c.Check(lexer.Rest(), equals, " text")
+
+       c.Check(lexer.Skip(0), equals, false)
 
        c.Check(lexer.Rest(), equals, " text")
 
@@ -80,25 +86,48 @@ func (s *Suite) Test_Lexer_NextString(c 
        c.Check(lexer.NextString("xt"), equals, "xt")
 }
 
+func (s *Suite) Test_Lexer_SkipString(c *check.C) {
+       lexer := NewLexer("text")
+
+       c.Check(lexer.SkipString("te"), equals, true)
+       c.Check(lexer.SkipString("st"), equals, false)
+       c.Check(lexer.SkipString("xt"), equals, true)
+}
+
+func (s *Suite) Test_Lexer_SkipHspace(c *check.C) {
+       lexer := NewLexer("spaces   \t \t  and tabs\n\t ")
+
+       c.Check(lexer.NextString("spaces"), equals, "spaces")
+       c.Check(lexer.SkipHspace(), equals, true)
+       c.Check(lexer.Rest(), equals, "and tabs\n\t ")
+       c.Check(lexer.SkipHspace(), equals, false) // No space left.
+       c.Check(lexer.NextString("and tabs"), equals, "and tabs")
+       c.Check(lexer.SkipHspace(), equals, false) // Newline is not a horizontal space.
+       c.Check(lexer.NextString("\n"), equals, "\n")
+       c.Check(lexer.SkipHspace(), equals, true)
+}
+
 func (s *Suite) Test_Lexer_NextHspace(c *check.C) {
-       lexer := NewLexer("spaces   \t \t  and tabs\n")
+       lexer := NewLexer("spaces   \t \t  and tabs\n\t ")
 
        c.Check(lexer.NextString("spaces"), equals, "spaces")
        c.Check(lexer.NextHspace(), equals, "   \t \t  ")
        c.Check(lexer.NextHspace(), equals, "") // No space left.
        c.Check(lexer.NextString("and tabs"), equals, "and tabs")
        c.Check(lexer.NextHspace(), equals, "") // Newline is not a horizontal space.
+       c.Check(lexer.NextString("\n"), equals, "\n")
+       c.Check(lexer.NextHspace(), equals, "\t ")
 }
 
-func (s *Suite) Test_Lexer_NextByte(c *check.C) {
+func (s *Suite) Test_Lexer_SkipByte(c *check.C) {
        lexer := NewLexer("byte")
 
-       c.Check(lexer.NextByte('b'), equals, true)
-       c.Check(lexer.NextByte('b'), equals, false) // The b is already chopped off.
-       c.Check(lexer.NextByte('y'), equals, true)
-       c.Check(lexer.NextByte('t'), equals, true)
-       c.Check(lexer.NextByte('e'), equals, true)
-       c.Check(lexer.NextByte(0), equals, false) // This is not a C string.
+       c.Check(lexer.SkipByte('b'), equals, true)
+       c.Check(lexer.SkipByte('b'), equals, false) // The b is already chopped off.
+       c.Check(lexer.SkipByte('y'), equals, true)
+       c.Check(lexer.SkipByte('t'), equals, true)
+       c.Check(lexer.SkipByte('e'), equals, true)
+       c.Check(lexer.SkipByte(0), equals, false) // This is not a C string.
 }
 
 func (s *Suite) Test_Lexer_NextBytesFunc(c *check.C) {
@@ -117,6 +146,7 @@ func (s *Suite) Test_Lexer_NextByteSet(c
        c.Check(lexer.NextByteSet(Alnum), equals, int('a'))
        c.Check(lexer.NextByteSet(Alnum), equals, int('n'))
        c.Check(lexer.NextByteSet(Space), equals, int(' '))
+       c.Check(lexer.NextByteSet(Space), equals, -1)
        c.Check(lexer.NextByteSet(Alnum), equals, int('a'))
        c.Check(lexer.NextByteSet(Space), equals, int('\n'))
        c.Check(lexer.NextByteSet(Alnum), equals, -1)
@@ -137,6 +167,48 @@ func (s *Suite) Test_Lexer_NextBytesSet(
        c.Check(lexer.NextBytesSet(Space), equals, "\n")
 }
 
+func (s *Suite) Test_Lexer_SkipRegexp(c *check.C) {
+       lexer := NewLexer("an alphanumerical 90_ \tstring\t\t \n")
+
+       c.Check(lexer.SkipRegexp(regexp.MustCompile(`^\w+`)), equals, true)
+       c.Check(lexer.Rest(), equals, " alphanumerical 90_ \tstring\t\t \n")
+       c.Check(lexer.SkipRegexp(regexp.MustCompile(`^.+`)), equals, true)
+       c.Check(lexer.Rest(), equals, "\n")
+       c.Check(lexer.SkipRegexp(regexp.MustCompile(`^.+`)), equals, false)
+       // This call returns false since the matched string was empty.
+       c.Check(lexer.SkipRegexp(regexp.MustCompile(`^.*`)), equals, false)
+       c.Check(lexer.Rest(), equals, "\n")
+}
+
+func (s *Suite) Test_Lexer_SkipRegexp__panic(c *check.C) {
+       lexer := NewLexer("an alphanumerical 90_ \tstring\t\t \n")
+
+       c.Check(
+               func() { lexer.SkipRegexp(regexp.MustCompile(`\w+`)) },
+               check.Panics,
+               "Lexer.SkipRegexp: regular expression \"\\\\w+\" must have prefix \"^\".")
+}
+
+func (s *Suite) Test_Lexer_NextRegexp(c *check.C) {
+       lexer := NewLexer("an alphanumerical 90_ \tstring\t\t \n")
+
+       c.Check(lexer.NextRegexp(regexp.MustCompile(`^\w+`))[0], equals, "an")
+       c.Check(lexer.NextRegexp(regexp.MustCompile(`^[\w ]+`))[0], equals, " alphanumerical 90_ ")
+       c.Check(lexer.NextRegexp(regexp.MustCompile(`^.+`))[0], equals, "\tstring\t\t ")
+       c.Check(lexer.NextRegexp(regexp.MustCompile(`^.+`)), check.IsNil)
+       c.Check(lexer.NextRegexp(regexp.MustCompile(`^.*`))[0], equals, "")
+       c.Check(lexer.Rest(), equals, "\n")
+}
+
+func (s *Suite) Test_Lexer_NextRegexp__panic(c *check.C) {
+       lexer := NewLexer("an alphanumerical 90_ \tstring\t\t \n")
+
+       c.Check(
+               func() { lexer.NextRegexp(regexp.MustCompile(`\w+`)) },
+               check.Panics,
+               "Lexer.NextRegexp: regular expression \"\\\\w+\" must have prefix \"^\".")
+}
+
 func (s *Suite) Test_Lexer_Mark__beginning(c *check.C) {
        lexer := NewLexer("text")
 
@@ -241,49 +313,69 @@ func (s *Suite) Test_Lexer_Commit(c *che
 func (s *Suite) Test_NewByteSet(c *check.C) {
        set := NewByteSet("A-Za-z0-9_\xFC")
 
-       c.Check(set.bits, equals, [4]uint64{
-               0x03ff000000000000, // 9-0
-               0x07fffffe87fffffe, // z-a _ Z-A
-               0x0000000000000000,
-               0x1000000000000000}) // \xFC
-}
-
-// Ensures that the bit manipulations work when a range spans
-// multiple of the uint64 words.
-func (s *Suite) Test_NewByteSet__large_range(c *check.C) {
-       set := NewByteSet("\x01-\xFE")
-
-       c.Check(set.bits, equals, [4]uint64{
-               0xfffffffffffffffe,
-               0xffffffffffffffff,
-               0xffffffffffffffff,
-               0x7fffffffffffffff})
+       expected := "" +
+               "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
+               "abcdefghijklmnopqrstuvwxyz" +
+               "0123456789_\xFC"
+       for i := 0; i < 256; i++ {
+               c.Check(
+                       set.Contains(byte(i)),
+                       equals,
+                       strings.IndexByte(expected, byte(i)) != -1)
+       }
 }
 
 // Demonstrates how to specify a byte set that includes a hyphen,
 // since that is also used for byte ranges.
-// The hyphen must be written as ---, and it must be at the beginning.
+// The hyphen must be written as ---, which is a range from hyphen to hyphen.
 func (s *Suite) Test_NewByteSet__range_hyphen(c *check.C) {
        set := NewByteSet("---a-z")
 
-       c.Check(set.bits, equals, [4]uint64{
-               0x0000200000000000,
-               0x07fffffe00000000,
-               0x0000000000000000,
-               0x0000000000000000})
+       expected := "abcdefghijklmnopqrstuvwxyz-"
+       for i := 0; i < 256; i++ {
+               c.Check(
+                       set.Contains(byte(i)),
+                       equals,
+                       strings.IndexByte(expected, byte(i)) != -1)
+       }
 }
 
 func (s *Suite) Test_ByteSet_Inverse(c *check.C) {
        set := NewByteSet("A-Za-z0-9_\xFC")
        inverse := set.Inverse()
 
-       c.Check(inverse.bits, equals, [4]uint64{
-               0xfc00ffffffffffff,
-               0xf800000178000001,
-               0xffffffffffffffff,
-               0xefffffffffffffff})
+       for i := 0; i < 256; i++ {
+               c.Check(
+                       inverse.Contains(byte(i)),
+                       equals,
+                       !set.Contains(byte(i)))
+       }
+}
+
+func (s *Suite) Test_ByteSet_Contains(c *check.C) {
+       set := NewByteSet("A-Za-z0-9_\xFC")
 
-       c.Check(inverse.Inverse().bits, equals, set.bits)
+       c.Check(set.Contains(0x00), equals, false)
+       c.Check(set.Contains('-'), equals, false)
+       c.Check(set.Contains('A'), equals, true)
+       c.Check(set.Contains('Z'), equals, true)
+       c.Check(set.Contains('['), equals, false)
+       c.Check(set.Contains(0xFC), equals, true)
+       c.Check(set.Contains(0xFD), equals, false)
+}
+
+func (s *Suite) Test__XPrint(c *check.C) {
+       set := XPrint
+
+       c.Check(set.Contains(0x00), equals, false)
+       c.Check(set.Contains(0x08), equals, false)
+       c.Check(set.Contains('\t'), equals, true)
+       c.Check(set.Contains('\n'), equals, true)
+       c.Check(set.Contains('\v'), equals, false)
+       c.Check(set.Contains(' '), equals, true)
+       c.Check(set.Contains('~'), equals, true)
+       c.Check(set.Contains(0x7F), equals, false)
+       c.Check(set.Contains(0xA0), equals, false)
 }
 
 func (s *Suite) Test__test_names(c *check.C) {

Index: pkgsrc/pkgtools/pkglint/files/trace/tracing.go
diff -u pkgsrc/pkgtools/pkglint/files/trace/tracing.go:1.3 pkgsrc/pkgtools/pkglint/files/trace/tracing.go:1.4
--- pkgsrc/pkgtools/pkglint/files/trace/tracing.go:1.3  Wed Nov  7 20:58:23 2018
+++ pkgsrc/pkgtools/pkglint/files/trace/tracing.go      Sun Dec  2 01:57:48 2018
@@ -9,16 +9,23 @@ import (
        "strings"
 )
 
+// Tracer produces a hierarchical structure of log events.
 type Tracer struct {
        Tracing bool
        Out     io.Writer
        depth   int
 }
 
+// Result marks an argument of Tracer.Call as a result of that function.
+// It is only logged when exiting from the function but not when entering it.
+type Result struct {
+       pointer interface{}
+}
+
 func (t *Tracer) Stepf(format string, args ...interface{}) {
        if t.Tracing {
                msg := fmt.Sprintf(format, args...)
-               io.WriteString(t.Out, fmt.Sprintf("TRACE: %s  %s\n", t.traceIndent(), msg))
+               _, _ = io.WriteString(t.Out, fmt.Sprintf("TRACE: %s  %s\n", t.traceIndent(), msg))
        }
 }
 
@@ -96,30 +103,26 @@ func (t *Tracer) traceCall(args ...inter
                }
        }
        indent := t.traceIndent()
-       io.WriteString(t.Out, fmt.Sprintf("TRACE: %s+ %s(%s)\n", indent, funcname, argsStr(withoutResults(args))))
+       _, _ = io.WriteString(t.Out, fmt.Sprintf("TRACE: %s+ %s(%s)\n", indent, funcname, argsStr(withoutResults(args))))
        t.depth++
 
        return func() {
                t.depth--
-               io.WriteString(t.Out, fmt.Sprintf("TRACE: %s- %s(%s)\n", indent, funcname, argsStr(withResults(args))))
+               _, _ = io.WriteString(t.Out, fmt.Sprintf("TRACE: %s- %s(%s)\n", indent, funcname, argsStr(withResults(args))))
        }
 }
 
-type result struct {
-       pointer interface{}
-}
-
 // Result marks an argument as a result and is only logged when the function returns.
-func (t *Tracer) Result(rv interface{}) result {
+func (t *Tracer) Result(rv interface{}) Result {
        if reflect.ValueOf(rv).Kind() != reflect.Ptr {
                panic(fmt.Sprintf("Result must be called with a pointer to the result, not %#v.", rv))
        }
-       return result{rv}
+       return Result{rv}
 }
 
 func withoutResults(args []interface{}) []interface{} {
        for i, arg := range args {
-               if _, ok := arg.(result); ok {
+               if _, ok := arg.(Result); ok {
                        return args[0:i]
                }
        }
@@ -128,12 +131,12 @@ func withoutResults(args []interface{}) 
 
 func withResults(args []interface{}) []interface{} {
        for i, arg := range args {
-               if _, ok := arg.(result); ok {
+               if _, ok := arg.(Result); ok {
                        var awr []interface{}
                        awr = append(awr, args[0:i]...)
                        awr = append(awr, "=>")
                        for _, res := range args[i:] {
-                               pointer := reflect.ValueOf(res.(result).pointer)
+                               pointer := reflect.ValueOf(res.(Result).pointer)
                                actual := reflect.Indirect(pointer).Interface()
                                awr = append(awr, actual)
                        }

Added files:

Index: pkgsrc/pkgtools/pkglint/files/lines_test.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/lines_test.go:1.1
--- /dev/null   Sun Dec  2 01:57:49 2018
+++ pkgsrc/pkgtools/pkglint/files/lines_test.go Sun Dec  2 01:57:48 2018
@@ -0,0 +1,54 @@
+package main
+
+import "gopkg.in/check.v1"
+
+func (s *Suite) Test_Lines_CheckRcsID(c *check.C) {
+       t := s.Init(c)
+
+       lines := t.NewLines("filename",
+               "$"+"NetBSD: dummy $",
+               "$"+"NetBSD$",
+               "$"+"Id: dummy $",
+               "$"+"Id$",
+               "$"+"FreeBSD$")
+
+       for i := range lines.Lines {
+               lines.CheckRcsID(i, ``, "")
+       }
+
+       t.CheckOutputLines(
+               "ERROR: filename:3: Expected \"$"+"NetBSD$\".",
+               "ERROR: filename:4: Expected \"$"+"NetBSD$\".",
+               "ERROR: filename:5: Expected \"$"+"NetBSD$\".")
+}
+
+// Since pkgsrc-wip uses Git as version control system, the CVS-specific
+// IDs don't make sense there. More often than not, the expanded form
+// "$NetBSD:" is a copy-and-paste mistake rather than an intentional
+// documentation of the file's history. Therefore, pkgsrc-wip files should
+// only use the unexpanded form.
+func (s *Suite) Test_Lines_CheckRcsID__wip(c *check.C) {
+       t := s.Init(c)
+
+       t.SetupPkgsrc()
+       t.SetupPackage("wip/package",
+               "CATEGORIES=\tchinese")
+       t.CreateFileLines("wip/package/file1.mk",
+               "# $"+"NetBSD: dummy $")
+       t.CreateFileLines("wip/package/file2.mk",
+               "# $"+"NetBSD$")
+       t.CreateFileLines("wip/package/file3.mk",
+               "# $"+"Id: dummy $")
+       t.CreateFileLines("wip/package/file4.mk",
+               "# $"+"Id$")
+       t.CreateFileLines("wip/package/file5.mk",
+               "# $"+"FreeBSD$")
+
+       G.CheckDirent(t.File("wip/package"))
+
+       t.CheckOutputLines(
+               "ERROR: ~/wip/package/file1.mk:1: Expected exactly \"# $"+"NetBSD$\".",
+               "ERROR: ~/wip/package/file3.mk:1: Expected \"# $"+"NetBSD$\".",
+               "ERROR: ~/wip/package/file4.mk:1: Expected \"# $"+"NetBSD$\".",
+               "ERROR: ~/wip/package/file5.mk:1: Expected \"# $"+"NetBSD$\".")
+}

Index: pkgsrc/pkgtools/pkglint/files/intqa/ideas.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/intqa/ideas.go:1.1
--- /dev/null   Sun Dec  2 01:57:49 2018
+++ pkgsrc/pkgtools/pkglint/files/intqa/ideas.go        Sun Dec  2 01:57:48 2018
@@ -0,0 +1,17 @@
+package intqa
+
+// XXX: It might be nice to check all comments of the form "See XYZ"
+// to see whether XYZ actually exists. The scope should be the current type,
+// then the current package, then a package-qualified identifier.
+// As if there were a "_ = XYZ" at the beginning of the function.
+
+// XXX: All methods should be defined in the same file as their receiver type.
+// If that is not possible, there should only be a small list of exceptions.
+
+// XXX: All tests should be in the same order as their corresponding elements in the
+// main code.
+
+// XXX: All tests for a single testee should be grouped together.
+
+// XXX: If there is a constructor for a type, only that constructor may be used
+// for constructing objects. All other forms (var x Type; x := &Type{}) should be forbidden.

Index: pkgsrc/pkgtools/pkglint/files/textproc/lexer_bench_test.go
diff -u /dev/null pkgsrc/pkgtools/pkglint/files/textproc/lexer_bench_test.go:1.1
--- /dev/null   Sun Dec  2 01:57:49 2018
+++ pkgsrc/pkgtools/pkglint/files/textproc/lexer_bench_test.go  Sun Dec  2 01:57:48 2018
@@ -0,0 +1,189 @@
+package textproc
+
+import (
+       "fmt"
+       "testing"
+)
+
+func validate(n int, sum int) {
+       var expected int
+       switch n {
+       case 1:
+               expected = 0
+       case 100:
+               expected = 40
+       case 10000:
+               expected = 2457
+       case 1000000:
+               expected = 246088
+       case 100000000:
+               expected = 24609375
+       case 10000000000:
+               expected = 246093750
+       default:
+               return
+       }
+       if sum != expected {
+               panic(fmt.Sprintf("expected %d for n = %d, got %d", expected, n, sum))
+       }
+}
+
+type ByteSetBool struct {
+       bits [256]bool
+}
+
+func NewByteSetBool(other *ByteSet) *ByteSetBool {
+       var s ByteSetBool
+       for i := 0; i < 256; i++ {
+               s.bits[i] = other.Contains(byte(i))
+       }
+       return &s
+}
+
+func (s *ByteSetBool) Contains(b byte) bool { return s.bits[b] }
+
+func Benchmark_ByteSetBool_Contains(b *testing.B) {
+       set := NewByteSetBool(AlnumU)
+       var sum int
+       for i := 0; i < b.N; i++ {
+               if set.Contains(byte(i)) {
+                       sum++
+               }
+       }
+       validate(b.N, sum)
+}
+
+type ByteSetUint8 struct {
+       bits [32]uint8
+}
+
+func NewByteSetUint8(other *ByteSet) *ByteSetUint8 {
+       var s8 ByteSetUint8
+       for i := uint(0); i < 256; i++ {
+               if other.Contains(byte(i)) {
+                       s8.bits[i/8] |= 1 << (i % 8)
+               }
+       }
+       return &s8
+}
+
+func (s *ByteSetUint8) Contains(b byte) bool { return s.bits[b/8]&(1<<(b%8)) != 0 }
+
+func Benchmark_ByteSetUint8_Contains(b *testing.B) {
+       set := NewByteSetUint8(AlnumU)
+       var sum int
+       for i := 0; i < b.N; i++ {
+               if set.Contains(byte(i)) {
+                       sum++
+               }
+       }
+       validate(b.N, sum)
+}
+
+type ByteSetUint16 struct {
+       bits [16]uint16
+}
+
+func NewByteSetUint16(other *ByteSet) *ByteSetUint16 {
+       var s ByteSetUint16
+       for i := uint(0); i < 256; i++ {
+               if other.Contains(byte(i)) {
+                       s.bits[i/16] |= 1 << (i % 16)
+               }
+       }
+       return &s
+}
+
+func (s *ByteSetUint16) Contains(b byte) bool { return s.bits[b/16]&(1<<(b%16)) != 0 }
+
+func Benchmark_ByteSet16_Contains(b *testing.B) {
+       set := NewByteSetUint16(AlnumU)
+       var sum int
+       for i := 0; i < b.N; i++ {
+               if set.Contains(byte(i)) {
+                       sum++
+               }
+       }
+       validate(b.N, sum)
+}
+
+type ByteSetUint64 struct {
+       bits [64]uint64
+}
+
+func NewByteSetUint64(other *ByteSet) *ByteSetUint64 {
+       var s ByteSetUint64
+       for i := uint(0); i < 256; i++ {
+               if other.Contains(byte(i)) {
+                       s.bits[i/64] |= 1 << (i % 64)
+               }
+       }
+       return &s
+}
+
+func (s *ByteSetUint64) Contains(b byte) bool { return s.bits[b/64]&(1<<(b%64)) != 0 }
+
+func Benchmark_ByteSet64_Contains(b *testing.B) {
+       set := NewByteSetUint64(AlnumU)
+       var sum int
+       for i := 0; i < b.N; i++ {
+               if set.Contains(byte(i)) {
+                       sum++
+               }
+       }
+       validate(b.N, sum)
+}
+
+func Benchmark_Direct_Compare(b *testing.B) {
+       var sum int
+       for i := 0; i < b.N; i++ {
+               i := byte(i)
+               if 'A' <= i && i <= 'Z' || 'a' <= i && i <= 'z' || '0' <= i && i <= '9' || i == '_' {
+                       sum++
+               }
+       }
+       validate(b.N, sum)
+}
+
+func Benchmark_Direct_Compare_binary_search(b *testing.B) {
+       var sum int
+       for i := 0; i < b.N; i++ {
+               i := byte(i)
+               if i >= 'A' {
+                       if i >= 'a' {
+                               if i <= 'z' {
+                                       sum++
+                               }
+                       } else if i <= 'Z' {
+                               sum++
+                       } else if i == '_' {
+                               sum++
+                       }
+               } else if i >= '0' && i <= '9' {
+                       sum++
+               }
+       }
+       validate(b.N, sum)
+}
+
+func Benchmark_Direct_Compare_reverse(b *testing.B) {
+       var sum int
+       for i := 0; i < b.N; i++ {
+               i := byte(i)
+               if i == '_' || '0' <= i && i <= '9' || 'a' <= i && i <= 'z' || 'A' <= i && i <= 'Z' {
+                       sum++
+               }
+       }
+       validate(b.N, sum)
+}
+
+func Benchmark_Direct_Compare_fold_case(b *testing.B) {
+       var sum int
+       for i := 0; i < b.N; i++ {
+               i := byte(i)
+               if 'A' <= (i&^0x20) && (i&^0x20) <= 'Z' || '0' <= i && i <= '9' || i == '_' {
+                       sum++
+               }
+       }
+       validate(b.N, sum)
+}



Home | Main Index | Thread Index | Old Index