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>