[BACK]Return to shell.go CVS log [TXT][DIR] Up to [cvs.NetBSD.org] / pkgsrc / pkgtools / pkglint / files

Annotation of pkgsrc/pkgtools/pkglint/files/shell.go, Revision 1.18

1.1       rillig      1: package main
                      2:
                      3: // Parsing and checking shell commands embedded in Makefiles
                      4:
                      5: import (
1.15      rillig      6:        "netbsd.org/pkglint/textproc"
                      7:        "netbsd.org/pkglint/trace"
1.1       rillig      8:        "path"
                      9:        "strings"
                     10: )
                     11:
                     12: const (
1.9       rillig     13:        reShVarname      = `(?:[!#*\-\d?@]|\$\$|[A-Za-z_]\w*)`
                     14:        reShVarexpansion = `(?:(?:#|##|%|%%|:-|:=|:\?|:\+|\+)[^$\\{}]*)`
                     15:        reShVaruse       = `\$\$` + `(?:` + reShVarname + `|` + `\{` + reShVarname + `(?:` + reShVarexpansion + `)?` + `\})`
                     16:        reShDollar       = `\\\$\$|` + reShVaruse + `|\$\$[,\-/|]`
1.1       rillig     17: )
                     18:
1.8       rillig     19: type ShellLine struct {
1.16      rillig     20:        mkline MkLine
1.1       rillig     21: }
                     22:
1.16      rillig     23: func NewShellLine(mkline MkLine) *ShellLine {
                     24:        return &ShellLine{mkline}
1.1       rillig     25: }
                     26:
1.12      rillig     27: var shellcommandsContextType = &Vartype{lkNone, BtShellCommands, []AclEntry{{"*", aclpAllRuntime}}, false}
1.11      rillig     28: var shellwordVuc = &VarUseContext{shellcommandsContextType, vucTimeUnknown, vucQuotPlain, false}
1.8       rillig     29:
1.9       rillig     30: func (shline *ShellLine) CheckWord(token string, checkQuoting bool) {
1.15      rillig     31:        if trace.Tracing {
                     32:                defer trace.Call(token, checkQuoting)()
1.8       rillig     33:        }
1.1       rillig     34:
1.8       rillig     35:        if token == "" || hasPrefix(token, "#") {
1.1       rillig     36:                return
                     37:        }
                     38:
1.18    ! rillig     39:        var line = shline.mkline.Line
1.1       rillig     40:
1.9       rillig     41:        p := NewMkParser(line, token, false)
                     42:        if varuse := p.VarUse(); varuse != nil && p.EOF() {
1.15      rillig     43:                MkLineChecker{shline.mkline}.CheckVaruse(varuse, shellwordVuc)
1.1       rillig     44:                return
                     45:        }
                     46:
1.8       rillig     47:        if matches(token, `\$\{PREFIX\}/man(?:$|/)`) {
1.14      rillig     48:                line.Warnf("Please use ${PKGMANDIR} instead of \"man\".")
1.1       rillig     49:        }
1.8       rillig     50:        if contains(token, "etc/rc.d") {
1.14      rillig     51:                line.Warnf("Please use the RCD_SCRIPTS mechanism to install rc.d scripts automatically to ${RCD_SCRIPTS_EXAMPLEDIR}.")
1.1       rillig     52:        }
                     53:
1.9       rillig     54:        parser := NewMkParser(line, token, false)
                     55:        repl := parser.repl
                     56:        quoting := shqPlain
1.1       rillig     57: outer:
1.9       rillig     58:        for !parser.EOF() {
1.15      rillig     59:                if trace.Tracing {
                     60:                        trace.Stepf("shell state %s: %q", quoting, parser.Rest())
1.8       rillig     61:                }
1.1       rillig     62:
                     63:                switch {
                     64:                // When parsing inside backticks, it is more
                     65:                // reasonable to check the whole shell command
                     66:                // recursively, instead of splitting off the first
                     67:                // make(1) variable.
1.9       rillig     68:                case quoting == shqBackt || quoting == shqDquotBackt:
1.7       rillig     69:                        var backtCommand string
1.9       rillig     70:                        backtCommand, quoting = shline.unescapeBackticks(token, repl, quoting)
1.8       rillig     71:                        setE := true
                     72:                        shline.CheckShellCommand(backtCommand, &setE)
1.1       rillig     73:
1.9       rillig     74:                        // Make(1) variables have the same syntax, no matter in which state we are currently.
                     75:                case shline.checkVaruseToken(parser, quoting):
                     76:                        break
1.1       rillig     77:
1.9       rillig     78:                case quoting == shqPlain:
1.1       rillig     79:                        switch {
1.8       rillig     80:                        case repl.AdvanceRegexp(`^[!#\%&\(\)*+,\-.\/0-9:;<=>?@A-Z\[\]^_a-z{|}~]+`),
                     81:                                repl.AdvanceRegexp(`^\\(?:[ !"#'\(\)*./;?\\^{|}]|\$\$)`):
                     82:                        case repl.AdvanceStr("'"):
1.9       rillig     83:                                quoting = shqSquot
1.8       rillig     84:                        case repl.AdvanceStr("\""):
1.9       rillig     85:                                quoting = shqDquot
1.8       rillig     86:                        case repl.AdvanceStr("`"):
1.9       rillig     87:                                quoting = shqBackt
1.8       rillig     88:                        case repl.AdvanceRegexp(`^\$\$([0-9A-Z_a-z]+|#)`),
                     89:                                repl.AdvanceRegexp(`^\$\$\{([0-9A-Z_a-z]+|#)\}`),
                     90:                                repl.AdvanceRegexp(`^\$\$(\$)\$`):
1.15      rillig     91:                                shvarname := repl.Group(1)
1.8       rillig     92:                                if G.opts.WarnQuoting && checkQuoting && shline.variableNeedsQuoting(shvarname) {
1.14      rillig     93:                                        line.Warnf("Unquoted shell variable %q.", shvarname)
1.8       rillig     94:                                        Explain(
                     95:                                                "When a shell variable contains white-space, it is expanded (split",
                     96:                                                "into multiple words) when it is written as $variable in a shell",
                     97:                                                "script.  If that is not intended, you should add quotation marks",
                     98:                                                "around it, like \"$variable\".  Then, the variable will always expand",
                     99:                                                "to a single word, preserving all white-space and other special",
                    100:                                                "characters.",
1.1       rillig    101:                                                "",
                    102:                                                "Example:",
                    103:                                                "\tfname=\"Curriculum vitae.doc\"",
                    104:                                                "\tcp $fname /tmp",
                    105:                                                "\t# tries to copy the two files \"Curriculum\" and \"Vitae.doc\"",
                    106:                                                "\tcp \"$fname\" /tmp",
                    107:                                                "\t# copies one file, as intended")
                    108:                                }
1.8       rillig    109:                        case repl.AdvanceStr("$@"):
1.14      rillig    110:                                line.Warnf("Please use %q instead of %q.", "${.TARGET}", "$@")
                    111:                                Explain(
1.1       rillig    112:                                        "It is more readable and prevents confusion with the shell variable of",
                    113:                                        "the same name.")
                    114:
1.8       rillig    115:                        case repl.AdvanceStr("$$@"):
1.14      rillig    116:                                line.Warnf("The $@ shell variable should only be used in double quotes.")
1.1       rillig    117:
1.8       rillig    118:                        case repl.AdvanceStr("$$?"):
1.14      rillig    119:                                line.Warnf("The $? shell variable is often not available in \"set -e\" mode.")
1.1       rillig    120:
1.8       rillig    121:                        case repl.AdvanceStr("$$("):
1.14      rillig    122:                                line.Warnf("Invoking subshells via $(...) is not portable enough.")
                    123:                                Explain(
1.1       rillig    124:                                        "The Solaris /bin/sh does not know this way to execute a command in a",
1.8       rillig    125:                                        "subshell.  Please use backticks (`...`) as a replacement.")
                    126:
                    127:                        case repl.AdvanceStr("$$"): // Not part of a variable.
                    128:                                break
1.1       rillig    129:
                    130:                        default:
                    131:                                break outer
                    132:                        }
                    133:
1.9       rillig    134:                case quoting == shqSquot:
1.1       rillig    135:                        switch {
1.8       rillig    136:                        case repl.AdvanceRegexp(`^'`):
1.9       rillig    137:                                quoting = shqPlain
1.8       rillig    138:                        case repl.AdvanceRegexp(`^[^\$\']+`):
1.1       rillig    139:                                // just skip
1.8       rillig    140:                        case repl.AdvanceRegexp(`^\$\$`):
1.1       rillig    141:                                // just skip
                    142:                        default:
                    143:                                break outer
                    144:                        }
                    145:
1.9       rillig    146:                case quoting == shqDquot:
1.1       rillig    147:                        switch {
1.8       rillig    148:                        case repl.AdvanceStr("\""):
1.9       rillig    149:                                quoting = shqPlain
1.8       rillig    150:                        case repl.AdvanceStr("`"):
1.9       rillig    151:                                quoting = shqDquotBackt
1.8       rillig    152:                        case repl.AdvanceRegexp("^[^$\"\\\\`]+"):
                    153:                                break
                    154:                        case repl.AdvanceStr("\\$$"):
                    155:                                break
                    156:                        case repl.AdvanceRegexp(`^\\.`): // See http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_02_01
                    157:                                break
                    158:                        case repl.AdvanceRegexp(`^\$\$\{\w+[#%+\-:]*[^{}]*\}`),
                    159:                                repl.AdvanceRegexp(`^\$\$(?:\w+|[!#?@]|\$\$)`):
                    160:                                break
                    161:                        case repl.AdvanceStr("$$"):
1.14      rillig    162:                                line.Warnf("Unescaped $ or strange shell variable found.")
1.1       rillig    163:                        default:
                    164:                                break outer
                    165:                        }
                    166:                }
                    167:        }
                    168:
1.9       rillig    169:        if strings.TrimSpace(parser.Rest()) != "" {
                    170:                line.Warnf("Pkglint parse error in ShellLine.CheckWord at %q (quoting=%s, rest=%q)", token, quoting, parser.Rest())
                    171:        }
                    172: }
                    173:
                    174: func (shline *ShellLine) checkVaruseToken(parser *MkParser, quoting ShQuoting) bool {
1.15      rillig    175:        if trace.Tracing {
                    176:                defer trace.Call(parser.Rest(), quoting)()
1.9       rillig    177:        }
                    178:
                    179:        varuse := parser.VarUse()
                    180:        if varuse == nil {
                    181:                return false
                    182:        }
                    183:        varname := varuse.varname
                    184:
                    185:        if varname == "@" {
1.16      rillig    186:                shline.mkline.Warnf("Please use \"${.TARGET}\" instead of \"$@\".")
1.14      rillig    187:                Explain(
1.9       rillig    188:                        "The variable $@ can easily be confused with the shell variable of",
                    189:                        "the same name, which has a completely different meaning.")
                    190:                varname = ".TARGET"
                    191:                varuse = &MkVarUse{varname, varuse.modifiers}
                    192:        }
                    193:
                    194:        switch {
                    195:        case quoting == shqPlain && varuse.IsQ():
                    196:                // Fine.
                    197:        case quoting == shqBackt:
                    198:                // Don't check anything here, to avoid false positives for tool names.
                    199:        case (quoting == shqSquot || quoting == shqDquot) && matches(varname, `^(?:.*DIR|.*FILE|.*PATH|.*_VAR|PREFIX|.*BASE|PKGNAME)$`):
                    200:                // This is ok if we don't allow these variables to have embedded [\$\\\"\'\`].
                    201:        case quoting == shqDquot && varuse.IsQ():
1.16      rillig    202:                shline.mkline.Warnf("Please don't use the :Q operator in double quotes.")
1.14      rillig    203:                Explain(
1.9       rillig    204:                        "Either remove the :Q or the double quotes.  In most cases, it is",
                    205:                        "more appropriate to remove the double quotes.")
                    206:        }
                    207:
                    208:        if varname != "@" {
                    209:                vucstate := quoting.ToVarUseContext()
1.11      rillig    210:                vuc := &VarUseContext{shellcommandsContextType, vucTimeUnknown, vucstate, true}
1.15      rillig    211:                MkLineChecker{shline.mkline}.CheckVaruse(varuse, vuc)
1.7       rillig    212:        }
1.9       rillig    213:        return true
1.7       rillig    214: }
                    215:
                    216: // Scan for the end of the backticks, checking for single backslashes
                    217: // and removing one level of backslashes. Backslashes are only removed
                    218: // before a dollar, a backslash or a backtick.
                    219: //
                    220: // See http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_03
1.15      rillig    221: func (shline *ShellLine) unescapeBackticks(shellword string, repl *textproc.PrefixReplacer, quoting ShQuoting) (unescaped string, newQuoting ShQuoting) {
                    222:        if trace.Tracing {
                    223:                defer trace.Call(shellword, quoting, "=>", trace.Ref(&unescaped))()
1.9       rillig    224:        }
                    225:
1.18    ! rillig    226:        line := shline.mkline.Line
1.15      rillig    227:        for !repl.EOF() {
1.7       rillig    228:                switch {
1.8       rillig    229:                case repl.AdvanceStr("`"):
1.9       rillig    230:                        if quoting == shqBackt {
                    231:                                quoting = shqPlain
1.7       rillig    232:                        } else {
1.9       rillig    233:                                quoting = shqDquot
1.7       rillig    234:                        }
1.9       rillig    235:                        return unescaped, quoting
1.7       rillig    236:
1.9       rillig    237:                case repl.AdvanceRegexp("^\\\\([\"\\\\`$])"):
1.15      rillig    238:                        unescaped += repl.Group(1)
1.7       rillig    239:
1.8       rillig    240:                case repl.AdvanceStr("\\"):
1.14      rillig    241:                        line.Warnf("Backslashes should be doubled inside backticks.")
1.8       rillig    242:                        unescaped += "\\"
                    243:
1.9       rillig    244:                case quoting == shqDquotBackt && repl.AdvanceStr("\""):
1.14      rillig    245:                        line.Warnf("Double quotes inside backticks inside double quotes are error prone.")
                    246:                        Explain(
1.7       rillig    247:                                "According to the SUSv3, they produce undefined results.",
                    248:                                "",
                    249:                                "See the paragraph starting \"Within the backquoted ...\" in",
                    250:                                "http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html")
                    251:
1.8       rillig    252:                case repl.AdvanceRegexp("^([^\\\\`]+)"):
1.15      rillig    253:                        unescaped += repl.Group(1)
1.7       rillig    254:
                    255:                default:
1.15      rillig    256:                        line.Errorf("Internal pkglint error in ShellLine.unescapeBackticks at %q (rest=%q)", shellword, repl.Rest())
1.7       rillig    257:                }
1.1       rillig    258:        }
1.15      rillig    259:        line.Errorf("Unfinished backquotes: rest=%q", repl.Rest())
1.9       rillig    260:        return unescaped, quoting
1.1       rillig    261: }
                    262:
1.8       rillig    263: func (shline *ShellLine) variableNeedsQuoting(shvarname string) bool {
1.1       rillig    264:        switch shvarname {
                    265:        case "#", "?":
                    266:                return false // Definitely ok
                    267:        case "d", "f", "i", "dir", "file", "src", "dst":
                    268:                return false // Probably ok
                    269:        }
                    270:        return true
                    271: }
                    272:
1.8       rillig    273: func (shline *ShellLine) CheckShellCommandLine(shelltext string) {
1.15      rillig    274:        if trace.Tracing {
                    275:                defer trace.Call1(shelltext)()
1.8       rillig    276:        }
1.1       rillig    277:
1.18    ! rillig    278:        line := shline.mkline.Line
1.1       rillig    279:
                    280:        if contains(shelltext, "${SED}") && contains(shelltext, "${MV}") {
1.14      rillig    281:                line.Notef("Please use the SUBST framework instead of ${SED} and ${MV}.")
1.8       rillig    282:                Explain(
                    283:                        "Using the SUBST framework instead of explicit commands is easier",
                    284:                        "to understand, since all the complexity of using sed and mv is",
                    285:                        "hidden behind the scenes.",
1.1       rillig    286:                        "",
1.8       rillig    287:                        "Run \"bmake help topic=subst\" for more information.")
                    288:                if contains(shelltext, "#") {
                    289:                        Explain(
                    290:                                "When migrating to the SUBST framework, pay attention to \"#\"",
                    291:                                "characters.  In shell commands, make(1) does not interpret them as",
                    292:                                "comment character, but in variable assignments it does.  Therefore,",
                    293:                                "instead of the shell command",
                    294:                                "",
                    295:                                "\tsed -e 's,#define foo,,'",
                    296:                                "",
                    297:                                "you need to write",
                    298:                                "",
                    299:                                "\tSUBST_SED.foo+=\t's,\\#define foo,,'")
                    300:                }
1.1       rillig    301:        }
                    302:
                    303:        if m, cmd := match1(shelltext, `^@*-(.*(?:MKDIR|INSTALL.*-d|INSTALL_.*_DIR).*)`); m {
1.14      rillig    304:                line.Notef("You don't need to use \"-\" before %q.", cmd)
1.1       rillig    305:        }
                    306:
1.15      rillig    307:        repl := textproc.NewPrefixReplacer(shelltext)
1.8       rillig    308:        repl.AdvanceRegexp(`^\s+`)
                    309:        if repl.AdvanceRegexp(`^[-@]+`) {
1.15      rillig    310:                shline.checkHiddenAndSuppress(repl.Group(0), repl.Rest())
1.8       rillig    311:        }
1.1       rillig    312:        setE := false
1.8       rillig    313:        if repl.AdvanceStr("${RUN}") {
                    314:                setE = true
                    315:        } else {
                    316:                repl.AdvanceStr("${_PKG_SILENT}${_PKG_DEBUG}")
1.1       rillig    317:        }
                    318:
1.15      rillig    319:        shline.CheckShellCommand(repl.Rest(), &setE)
1.8       rillig    320: }
                    321:
                    322: func (shline *ShellLine) CheckShellCommand(shellcmd string, pSetE *bool) {
1.15      rillig    323:        if trace.Tracing {
                    324:                defer trace.Call()()
1.9       rillig    325:        }
                    326:
1.18    ! rillig    327:        line := shline.mkline.Line
1.16      rillig    328:        program, err := parseShellProgram(line, shellcmd)
1.10      rillig    329:        if err != nil && contains(shellcmd, "$$(") { // Hack until the shell parser can handle subshells.
1.16      rillig    330:                line.Warnf("Invoking subshells via $(...) is not portable enough.")
1.10      rillig    331:                return
                    332:        }
                    333:        if err != nil {
1.16      rillig    334:                line.Warnf("Pkglint ShellLine.CheckShellCommand: %s", err)
1.10      rillig    335:                return
1.9       rillig    336:        }
                    337:
1.10      rillig    338:        spc := &ShellProgramChecker{shline}
                    339:        spc.checkConditionalCd(program)
                    340:
                    341:        (*MkShWalker).Walk(nil, program, func(node interface{}) {
                    342:                if cmd, ok := node.(*MkShSimpleCommand); ok {
                    343:                        scc := NewSimpleCommandChecker(shline, cmd)
                    344:                        scc.Check()
                    345:                        if scc.strcmd.Name == "set" && scc.strcmd.AnyArgMatches(`^-.*e`) {
                    346:                                *pSetE = true
                    347:                        }
1.8       rillig    348:                }
1.1       rillig    349:
1.10      rillig    350:                if cmd, ok := node.(*MkShList); ok {
                    351:                        spc.checkSetE(cmd, pSetE)
1.1       rillig    352:                }
                    353:
1.10      rillig    354:                if cmd, ok := node.(*MkShPipeline); ok {
1.16      rillig    355:                        spc.checkPipeExitcode(line, cmd)
1.1       rillig    356:                }
                    357:
1.10      rillig    358:                if word, ok := node.(*ShToken); ok {
                    359:                        spc.checkWord(word, false)
1.1       rillig    360:                }
1.10      rillig    361:        })
1.8       rillig    362: }
1.1       rillig    363:
1.8       rillig    364: func (shline *ShellLine) CheckShellCommands(shellcmds string) {
                    365:        setE := true
                    366:        shline.CheckShellCommand(shellcmds, &setE)
                    367:        if !hasSuffix(shellcmds, ";") {
1.16      rillig    368:                shline.mkline.Warnf("This shell command list should end with a semicolon.")
1.8       rillig    369:        }
1.1       rillig    370: }
                    371:
1.8       rillig    372: func (shline *ShellLine) checkHiddenAndSuppress(hiddenAndSuppress, rest string) {
1.15      rillig    373:        if trace.Tracing {
                    374:                defer trace.Call(hiddenAndSuppress, rest)()
1.8       rillig    375:        }
1.1       rillig    376:
                    377:        switch {
1.8       rillig    378:        case !contains(hiddenAndSuppress, "@"):
1.1       rillig    379:                // Nothing is hidden at all.
                    380:
1.8       rillig    381:        case hasPrefix(G.Mk.target, "show-") || hasSuffix(G.Mk.target, "-message"):
                    382:                // In these targets, all commands may be hidden.
1.1       rillig    383:
                    384:        case hasPrefix(rest, "#"):
                    385:                // Shell comments may be hidden, since they cannot have side effects.
                    386:
                    387:        default:
1.18    ! rillig    388:                tokens, _ := splitIntoShellTokens(shline.mkline.Line, rest)
1.10      rillig    389:                if len(tokens) > 0 {
                    390:                        cmd := tokens[0]
1.1       rillig    391:                        switch cmd {
                    392:                        case "${DELAYED_ERROR_MSG}", "${DELAYED_WARNING_MSG}",
                    393:                                "${DO_NADA}",
                    394:                                "${ECHO}", "${ECHO_MSG}", "${ECHO_N}", "${ERROR_CAT}", "${ERROR_MSG}",
                    395:                                "${FAIL_MSG}",
                    396:                                "${PHASE_MSG}", "${PRINTF}",
                    397:                                "${SHCOMMENT}", "${STEP_MSG}",
                    398:                                "${WARNING_CAT}", "${WARNING_MSG}":
1.8       rillig    399:                                break
1.1       rillig    400:                        default:
1.16      rillig    401:                                shline.mkline.Warnf("The shell command %q should not be hidden.", cmd)
1.8       rillig    402:                                Explain(
                    403:                                        "Hidden shell commands do not appear on the terminal or in the log",
                    404:                                        "file when they are executed.  When they fail, the error message",
                    405:                                        "cannot be assigned to the command, which is very difficult to debug.",
                    406:                                        "",
                    407:                                        "It is better to insert ${RUN} at the beginning of the whole command",
                    408:                                        "line.  This will hide the command by default, but shows it when",
                    409:                                        "PKG_DEBUG_LEVEL is set.")
1.1       rillig    410:                        }
                    411:                }
                    412:        }
                    413:
1.8       rillig    414:        if contains(hiddenAndSuppress, "-") {
1.16      rillig    415:                shline.mkline.Warnf("Using a leading \"-\" to suppress errors is deprecated.")
1.14      rillig    416:                Explain(
1.8       rillig    417:                        "If you really want to ignore any errors from this command, append",
                    418:                        "\"|| ${TRUE}\" to the command.")
1.1       rillig    419:        }
                    420: }
                    421:
1.10      rillig    422: type SimpleCommandChecker struct {
                    423:        shline *ShellLine
                    424:        cmd    *MkShSimpleCommand
                    425:        strcmd *StrCommand
                    426: }
                    427:
                    428: func NewSimpleCommandChecker(shline *ShellLine, cmd *MkShSimpleCommand) *SimpleCommandChecker {
                    429:        strcmd := NewStrCommand(cmd)
                    430:        return &SimpleCommandChecker{shline, cmd, strcmd}
                    431:
                    432: }
                    433:
1.12      rillig    434: func (scc *SimpleCommandChecker) Check() {
1.15      rillig    435:        if trace.Tracing {
                    436:                defer trace.Call(scc.strcmd)()
1.8       rillig    437:        }
1.1       rillig    438:
1.12      rillig    439:        scc.checkCommandStart()
                    440:        scc.checkAbsolutePathnames()
                    441:        scc.checkAutoMkdirs()
                    442:        scc.checkInstallMulti()
                    443:        scc.checkPaxPe()
                    444:        scc.checkEchoN()
1.10      rillig    445: }
                    446:
1.12      rillig    447: func (scc *SimpleCommandChecker) checkCommandStart() {
1.15      rillig    448:        if trace.Tracing {
                    449:                defer trace.Call()()
1.1       rillig    450:        }
                    451:
1.12      rillig    452:        shellword := scc.strcmd.Name
1.1       rillig    453:        switch {
1.10      rillig    454:        case shellword == "${RUN}" || shellword == "":
1.12      rillig    455:        case scc.handleForbiddenCommand():
                    456:        case scc.handleTool():
                    457:        case scc.handleCommandVariable():
1.10      rillig    458:        case matches(shellword, `^(?::|break|cd|continue|eval|exec|exit|export|read|set|shift|umask|unset)$`):
1.1       rillig    459:        case hasPrefix(shellword, "./"): // All commands from the current directory are fine.
1.9       rillig    460:        case hasPrefix(shellword, "${PKGSRCDIR"): // With or without the :Q modifier
1.12      rillig    461:        case scc.handleComment():
1.1       rillig    462:        default:
1.10      rillig    463:                if G.opts.WarnExtra && !(G.Mk != nil && G.Mk.indentation.DependsOn("OPSYS")) {
1.16      rillig    464:                        scc.shline.mkline.Warnf("Unknown shell command %q.", shellword)
1.14      rillig    465:                        Explain(
1.1       rillig    466:                                "If you want your package to be portable to all platforms that pkgsrc",
                    467:                                "supports, you should only use shell commands that are covered by the",
                    468:                                "tools framework.")
                    469:                }
                    470:        }
                    471: }
                    472:
1.12      rillig    473: func (scc *SimpleCommandChecker) handleTool() bool {
1.15      rillig    474:        if trace.Tracing {
                    475:                defer trace.Call()()
1.8       rillig    476:        }
1.1       rillig    477:
1.12      rillig    478:        shellword := scc.strcmd.Name
1.13      rillig    479:        tool, localTool := G.globalData.Tools.byName[shellword], false
                    480:        if tool == nil && G.Mk != nil {
                    481:                tool, localTool = G.Mk.toolRegistry.byName[shellword], true
                    482:        }
1.9       rillig    483:        if tool == nil {
1.1       rillig    484:                return false
                    485:        }
                    486:
1.13      rillig    487:        if !localTool && !G.Mk.tools[shellword] && !G.Mk.tools["g"+shellword] {
1.16      rillig    488:                scc.shline.mkline.Warnf("The %q tool is used but not added to USE_TOOLS.", shellword)
1.1       rillig    489:        }
                    490:
1.9       rillig    491:        if tool.MustUseVarForm {
1.16      rillig    492:                scc.shline.mkline.Warnf("Please use \"${%s}\" instead of %q.", tool.Varname, shellword)
1.1       rillig    493:        }
                    494:
1.12      rillig    495:        scc.shline.checkCommandUse(shellword)
1.1       rillig    496:        return true
                    497: }
                    498:
1.12      rillig    499: func (scc *SimpleCommandChecker) handleForbiddenCommand() bool {
1.15      rillig    500:        if trace.Tracing {
                    501:                defer trace.Call()()
1.10      rillig    502:        }
                    503:
1.12      rillig    504:        shellword := scc.strcmd.Name
1.10      rillig    505:        switch path.Base(shellword) {
1.1       rillig    506:        case "ktrace", "mktexlsr", "strace", "texconfig", "truss":
1.16      rillig    507:                scc.shline.mkline.Errorf("%q must not be used in Makefiles.", shellword)
1.14      rillig    508:                Explain(
1.9       rillig    509:                        "This command must appear in INSTALL scripts, not in the package",
                    510:                        "Makefile, so that the package also works if it is installed as a binary",
                    511:                        "package via pkg_add.")
                    512:                return true
1.1       rillig    513:        }
1.9       rillig    514:        return false
1.1       rillig    515: }
                    516:
1.12      rillig    517: func (scc *SimpleCommandChecker) handleCommandVariable() bool {
1.15      rillig    518:        if trace.Tracing {
                    519:                defer trace.Call()()
1.8       rillig    520:        }
1.1       rillig    521:
1.12      rillig    522:        shellword := scc.strcmd.Name
1.1       rillig    523:        if m, varname := match1(shellword, `^\$\{([\w_]+)\}$`); m {
                    524:
1.9       rillig    525:                if tool := G.globalData.Tools.byVarname[varname]; tool != nil {
                    526:                        if !G.Mk.tools[tool.Name] {
1.16      rillig    527:                                scc.shline.mkline.Warnf("The %q tool is used but not added to USE_TOOLS.", tool.Name)
1.1       rillig    528:                        }
1.12      rillig    529:                        scc.shline.checkCommandUse(shellword)
1.1       rillig    530:                        return true
                    531:                }
                    532:
1.16      rillig    533:                if vartype := scc.shline.mkline.VariableType(varname); vartype != nil && vartype.basicType.name == "ShellCommand" {
1.12      rillig    534:                        scc.shline.checkCommandUse(shellword)
1.1       rillig    535:                        return true
                    536:                }
                    537:
                    538:                // When the package author has explicitly defined a command
                    539:                // variable, assume it to be valid.
1.8       rillig    540:                if G.Pkg != nil && G.Pkg.vardef[varname] != nil {
1.1       rillig    541:                        return true
                    542:                }
                    543:        }
                    544:        return false
                    545: }
                    546:
1.12      rillig    547: func (scc *SimpleCommandChecker) handleComment() bool {
1.15      rillig    548:        if trace.Tracing {
                    549:                defer trace.Call()()
1.10      rillig    550:        }
                    551:
1.12      rillig    552:        shellword := scc.strcmd.Name
1.15      rillig    553:        if trace.Tracing {
                    554:                defer trace.Call1(shellword)()
1.8       rillig    555:        }
1.1       rillig    556:
                    557:        if !hasPrefix(shellword, "#") {
                    558:                return false
                    559:        }
                    560:
                    561:        semicolon := contains(shellword, ";")
1.16      rillig    562:        multiline := scc.shline.mkline.IsMultiline()
1.1       rillig    563:
                    564:        if semicolon {
1.16      rillig    565:                scc.shline.mkline.Warnf("A shell comment should not contain semicolons.")
1.1       rillig    566:        }
                    567:        if multiline {
1.16      rillig    568:                scc.shline.mkline.Warnf("A shell comment does not stop at the end of line.")
1.1       rillig    569:        }
                    570:
                    571:        if semicolon || multiline {
1.8       rillig    572:                Explain(
                    573:                        "When you split a shell command into multiple lines that are",
                    574:                        "continued with a backslash, they will nevertheless be converted to",
                    575:                        "a single line before the shell sees them.  That means that even if",
                    576:                        "it _looks_ like that the comment only spans one line in the",
                    577:                        "Makefile, in fact it spans until the end of the whole shell command.",
                    578:                        "",
                    579:                        "To insert a comment into shell code, you can write it like this:",
                    580:                        "",
                    581:                        "\t"+"${SHCOMMENT} \"The following command might fail; this is ok.\"",
                    582:                        "",
                    583:                        "Note that any special characters in the comment are still",
                    584:                        "interpreted by the shell.")
1.1       rillig    585:        }
                    586:        return true
                    587: }
                    588:
1.12      rillig    589: func (scc *SimpleCommandChecker) checkAbsolutePathnames() {
1.15      rillig    590:        if trace.Tracing {
                    591:                defer trace.Call()()
1.10      rillig    592:        }
                    593:
1.12      rillig    594:        cmdname := scc.strcmd.Name
1.10      rillig    595:        isSubst := false
1.12      rillig    596:        for _, arg := range scc.strcmd.Args {
1.10      rillig    597:                if !isSubst {
1.18    ! rillig    598:                        CheckLineAbsolutePathname(scc.shline.mkline.Line, arg)
1.10      rillig    599:                }
                    600:                if false && isSubst && !matches(arg, `"^[\"\'].*[\"\']$`) {
1.16      rillig    601:                        scc.shline.mkline.Warnf("Substitution commands like %q should always be quoted.", arg)
1.14      rillig    602:                        Explain(
1.1       rillig    603:                                "Usually these substitution commands contain characters like '*' or",
                    604:                                "other shell metacharacters that might lead to lookup of matching",
                    605:                                "filenames and then expand to more than one word.")
                    606:                }
1.10      rillig    607:                isSubst = cmdname == "${PAX}" && arg == "-s" || cmdname == "${SED}" && arg == "-e"
1.1       rillig    608:        }
                    609: }
                    610:
1.12      rillig    611: func (scc *SimpleCommandChecker) checkAutoMkdirs() {
1.15      rillig    612:        if trace.Tracing {
                    613:                defer trace.Call()()
1.10      rillig    614:        }
                    615:
1.12      rillig    616:        cmdname := scc.strcmd.Name
1.10      rillig    617:        switch {
                    618:        case cmdname == "${MKDIR}":
                    619:                break
1.12      rillig    620:        case cmdname == "${INSTALL}" && scc.strcmd.HasOption("-d"):
1.10      rillig    621:                cmdname = "${INSTALL} -d"
                    622:        case matches(cmdname, `^\$\{INSTALL_.*_DIR\}$`):
                    623:                break
                    624:        default:
                    625:                return
                    626:        }
                    627:
1.12      rillig    628:        for _, arg := range scc.strcmd.Args {
1.10      rillig    629:                if !contains(arg, "$$") && !matches(arg, `\$\{[_.]*[a-z]`) {
                    630:                        if m, dirname := match1(arg, `^(?:\$\{DESTDIR\})?\$\{PREFIX(?:|:Q)\}/(.*)`); m {
1.16      rillig    631:                                scc.shline.mkline.Notef("You can use AUTO_MKDIRS=yes or \"INSTALLATION_DIRS+= %s\" instead of %q.", dirname, cmdname)
1.10      rillig    632:                                Explain(
                    633:                                        "Many packages include a list of all needed directories in their",
                    634:                                        "PLIST file.  In such a case, you can just set AUTO_MKDIRS=yes and",
                    635:                                        "be done.  The pkgsrc infrastructure will then create all directories",
                    636:                                        "in advance.",
                    637:                                        "",
                    638:                                        "To create directories that are not mentioned in the PLIST file, it",
                    639:                                        "is easier to just list them in INSTALLATION_DIRS than to execute the",
                    640:                                        "commands explicitly.  That way, you don't have to think about which",
                    641:                                        "of the many INSTALL_*_DIR variables is appropriate, since",
                    642:                                        "INSTALLATION_DIRS takes care of that.")
                    643:                        }
                    644:                }
                    645:        }
                    646: }
                    647:
1.12      rillig    648: func (scc *SimpleCommandChecker) checkInstallMulti() {
1.15      rillig    649:        if trace.Tracing {
                    650:                defer trace.Call()()
1.10      rillig    651:        }
                    652:
1.12      rillig    653:        cmd := scc.strcmd
1.10      rillig    654:
                    655:        if hasPrefix(cmd.Name, "${INSTALL_") && hasSuffix(cmd.Name, "_DIR}") {
                    656:                prevdir := ""
                    657:                for i, arg := range cmd.Args {
                    658:                        switch {
                    659:                        case hasPrefix(arg, "-"):
                    660:                                break
                    661:                        case i > 0 && (cmd.Args[i-1] == "-m" || cmd.Args[i-1] == "-o" || cmd.Args[i-1] == "-g"):
                    662:                                break
                    663:                        default:
                    664:                                if prevdir != "" {
1.16      rillig    665:                                        scc.shline.mkline.Warnf("The INSTALL_*_DIR commands can only handle one directory at a time.")
1.14      rillig    666:                                        Explain(
1.10      rillig    667:                                                "Many implementations of install(1) can handle more, but pkgsrc aims",
                    668:                                                "at maximum portability.")
                    669:                                        return
                    670:                                }
                    671:                                prevdir = arg
                    672:                        }
                    673:                }
                    674:        }
                    675: }
                    676:
1.12      rillig    677: func (scc *SimpleCommandChecker) checkPaxPe() {
1.15      rillig    678:        if trace.Tracing {
                    679:                defer trace.Call()()
1.10      rillig    680:        }
                    681:
1.12      rillig    682:        if scc.strcmd.Name == "${PAX}" && scc.strcmd.HasOption("-pe") {
1.16      rillig    683:                scc.shline.mkline.Warnf("Please use the -pp option to pax(1) instead of -pe.")
1.14      rillig    684:                Explain(
1.10      rillig    685:                        "The -pe option tells pax to preserve the ownership of the files, which",
                    686:                        "means that the installed files will belong to the user that has built",
                    687:                        "the package.")
                    688:        }
                    689: }
                    690:
1.12      rillig    691: func (scc *SimpleCommandChecker) checkEchoN() {
1.15      rillig    692:        if trace.Tracing {
                    693:                defer trace.Call()()
1.12      rillig    694:        }
                    695:
                    696:        if scc.strcmd.Name == "${ECHO}" && scc.strcmd.HasOption("-n") {
1.16      rillig    697:                scc.shline.mkline.Warnf("Please use ${ECHO_N} instead of \"echo -n\".")
1.12      rillig    698:        }
                    699: }
                    700:
                    701: type ShellProgramChecker struct {
                    702:        shline *ShellLine
                    703: }
                    704:
                    705: func (spc *ShellProgramChecker) checkConditionalCd(list *MkShList) {
1.15      rillig    706:        if trace.Tracing {
                    707:                defer trace.Call()()
1.12      rillig    708:        }
                    709:
                    710:        getSimple := func(list *MkShList) *MkShSimpleCommand {
                    711:                if len(list.AndOrs) == 1 {
                    712:                        if len(list.AndOrs[0].Pipes) == 1 {
                    713:                                if len(list.AndOrs[0].Pipes[0].Cmds) == 1 {
                    714:                                        return list.AndOrs[0].Pipes[0].Cmds[0].Simple
                    715:                                }
                    716:                        }
                    717:                }
                    718:                return nil
                    719:        }
                    720:
                    721:        checkConditionalCd := func(cmd *MkShSimpleCommand) {
                    722:                if NewStrCommand(cmd).Name == "cd" {
1.16      rillig    723:                        spc.shline.mkline.Errorf("The Solaris /bin/sh cannot handle \"cd\" inside conditionals.")
1.14      rillig    724:                        Explain(
1.12      rillig    725:                                "When the Solaris shell is in \"set -e\" mode and \"cd\" fails, the",
                    726:                                "shell will exit, no matter if it is protected by an \"if\" or the",
                    727:                                "\"||\" operator.")
                    728:                }
                    729:        }
                    730:
                    731:        (*MkShWalker).Walk(nil, list, func(node interface{}) {
                    732:                if cmd, ok := node.(*MkShIfClause); ok {
                    733:                        for _, cond := range cmd.Conds {
                    734:                                if simple := getSimple(cond); simple != nil {
                    735:                                        checkConditionalCd(simple)
                    736:                                }
                    737:                        }
                    738:                }
                    739:                if cmd, ok := node.(*MkShLoopClause); ok {
                    740:                        if simple := getSimple(cmd.Cond); simple != nil {
                    741:                                checkConditionalCd(simple)
                    742:                        }
                    743:                }
                    744:        })
                    745: }
                    746:
                    747: func (spc *ShellProgramChecker) checkWords(words []*ShToken, checkQuoting bool) {
1.15      rillig    748:        if trace.Tracing {
                    749:                defer trace.Call()()
1.10      rillig    750:        }
                    751:
1.12      rillig    752:        for _, word := range words {
                    753:                spc.checkWord(word, checkQuoting)
1.1       rillig    754:        }
                    755: }
                    756:
1.12      rillig    757: func (spc *ShellProgramChecker) checkWord(word *ShToken, checkQuoting bool) {
1.15      rillig    758:        if trace.Tracing {
                    759:                defer trace.Call(word.MkText)()
1.12      rillig    760:        }
                    761:
                    762:        spc.shline.CheckWord(word.MkText, checkQuoting)
                    763: }
                    764:
1.18    ! rillig    765: func (scc *ShellProgramChecker) checkPipeExitcode(line Line, pipeline *MkShPipeline) {
1.15      rillig    766:        if trace.Tracing {
                    767:                defer trace.Call()()
1.10      rillig    768:        }
                    769:
                    770:        if G.opts.WarnExtra && len(pipeline.Cmds) > 1 {
1.14      rillig    771:                line.Warnf("The exitcode of the left-hand-side command of the pipe operator is ignored.")
1.8       rillig    772:                Explain(
1.1       rillig    773:                        "In a shell command like \"cat *.txt | grep keyword\", if the command",
                    774:                        "on the left side of the \"|\" fails, this failure is ignored.",
                    775:                        "",
                    776:                        "If you need to detect the failure of the left-hand-side command, use",
                    777:                        "temporary files to save the output of the command.")
                    778:        }
                    779: }
                    780:
1.12      rillig    781: func (scc *ShellProgramChecker) checkSetE(list *MkShList, eflag *bool) {
1.15      rillig    782:        if trace.Tracing {
                    783:                defer trace.Call()()
1.10      rillig    784:        }
                    785:
                    786:        // Disabled until the shell parser can recognize "command || exit 1" reliably.
                    787:        if false && G.opts.WarnExtra && !*eflag && "the current token" == ";" {
1.8       rillig    788:                *eflag = true
1.16      rillig    789:                scc.shline.mkline.Warnf("Please switch to \"set -e\" mode before using a semicolon (the one after %q) to separate commands.", "previous token")
1.8       rillig    790:                Explain(
                    791:                        "Normally, when a shell command fails (returns non-zero), the",
                    792:                        "remaining commands are still executed.  For example, the following",
                    793:                        "commands would remove all files from the HOME directory:",
                    794:                        "",
                    795:                        "\tcd \"$HOME\"; cd /nonexistent; rm -rf *",
                    796:                        "",
                    797:                        "To fix this warning, you can:",
                    798:                        "",
                    799:                        "* insert ${RUN} at the beginning of the line",
                    800:                        "  (which among other things does \"set -e\")",
                    801:                        "* insert \"set -e\" explicitly at the beginning of the line",
                    802:                        "* use \"&&\" instead of \";\" to separate the commands")
1.1       rillig    803:        }
                    804: }
                    805:
                    806: // Some shell commands should not be used in the install phase.
1.8       rillig    807: func (shline *ShellLine) checkCommandUse(shellcmd string) {
1.15      rillig    808:        if trace.Tracing {
                    809:                defer trace.Call()()
1.10      rillig    810:        }
                    811:
1.8       rillig    812:        if G.Mk == nil || !matches(G.Mk.target, `^(?:pre|do|post)-install$`) {
1.1       rillig    813:                return
                    814:        }
                    815:
1.18    ! rillig    816:        line := shline.mkline.Line
1.1       rillig    817:        switch shellcmd {
                    818:        case "${INSTALL}",
                    819:                "${INSTALL_DATA}", "${INSTALL_DATA_DIR}",
                    820:                "${INSTALL_LIB}", "${INSTALL_LIB_DIR}",
                    821:                "${INSTALL_MAN}", "${INSTALL_MAN_DIR}",
                    822:                "${INSTALL_PROGRAM}", "${INSTALL_PROGRAM_DIR}",
                    823:                "${INSTALL_SCRIPT}",
                    824:                "${LIBTOOL}",
                    825:                "${LN}",
                    826:                "${PAX}":
                    827:                return
                    828:
                    829:        case "sed", "${SED}",
                    830:                "tr", "${TR}":
1.14      rillig    831:                line.Warnf("The shell command %q should not be used in the install phase.", shellcmd)
                    832:                Explain(
1.8       rillig    833:                        "In the install phase, the only thing that should be done is to",
                    834:                        "install the prepared files to their final location.  The file's",
                    835:                        "contents should not be changed anymore.")
1.1       rillig    836:
                    837:        case "cp", "${CP}":
1.14      rillig    838:                line.Warnf("${CP} should not be used to install files.")
1.8       rillig    839:                Explain(
1.1       rillig    840:                        "The ${CP} command is highly platform dependent and cannot overwrite",
1.8       rillig    841:                        "read-only files.  Please use ${PAX} instead.",
1.1       rillig    842:                        "",
                    843:                        "For example, instead of",
                    844:                        "\t${CP} -R ${WRKSRC}/* ${PREFIX}/foodir",
                    845:                        "you should use",
                    846:                        "\tcd ${WRKSRC} && ${PAX} -wr * ${PREFIX}/foodir")
                    847:        }
                    848: }
                    849:
1.9       rillig    850: // Example: "word1 word2;;;" => "word1", "word2", ";;", ";"
1.18    ! rillig    851: func splitIntoShellTokens(line Line, text string) (tokens []string, rest string) {
1.15      rillig    852:        if trace.Tracing {
                    853:                defer trace.Call(line, text)()
1.9       rillig    854:        }
                    855:
                    856:        word := ""
                    857:        emit := func() {
                    858:                if word != "" {
                    859:                        tokens = append(tokens, word)
                    860:                        word = ""
                    861:                }
1.8       rillig    862:        }
1.9       rillig    863:        p := NewShTokenizer(line, text, false)
                    864:        atoms := p.ShAtoms()
                    865:        q := shqPlain
                    866:        for _, atom := range atoms {
                    867:                q = atom.Quoting
                    868:                if atom.Type == shtSpace && q == shqPlain {
                    869:                        emit()
                    870:                } else if atom.Type == shtWord || atom.Type == shtVaruse || atom.Quoting != shqPlain {
1.10      rillig    871:                        word += atom.MkText
1.9       rillig    872:                } else {
                    873:                        emit()
1.10      rillig    874:                        tokens = append(tokens, atom.MkText)
1.9       rillig    875:                }
                    876:        }
                    877:        emit()
                    878:        return tokens, word + p.mkp.Rest()
1.8       rillig    879: }
1.1       rillig    880:
1.9       rillig    881: // Example: "word1 word2;;;" => "word1", "word2;;;"
1.8       rillig    882: // Compare devel/bmake/files/str.c, function brk_string.
1.18    ! rillig    883: func splitIntoMkWords(line Line, text string) (words []string, rest string) {
1.15      rillig    884:        if trace.Tracing {
                    885:                defer trace.Call(line, text)()
1.9       rillig    886:        }
                    887:
                    888:        p := NewShTokenizer(line, text, false)
                    889:        atoms := p.ShAtoms()
                    890:        word := ""
                    891:        for _, atom := range atoms {
                    892:                if atom.Type == shtSpace && atom.Quoting == shqPlain {
                    893:                        words = append(words, word)
                    894:                        word = ""
                    895:                } else {
1.10      rillig    896:                        word += atom.MkText
1.9       rillig    897:                }
                    898:        }
                    899:        if word != "" && atoms[len(atoms)-1].Quoting == shqPlain {
                    900:                words = append(words, word)
                    901:                word = ""
                    902:        }
                    903:        return words, word + p.mkp.Rest()
                    904: }

CVSweb <webmaster@jp.NetBSD.org>