903 lines
26 KiB
Go
903 lines
26 KiB
Go
package main
|
|
|
|
// Parsing and checking shell commands embedded in Makefiles
|
|
|
|
import (
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
reShVarname = `(?:[!#*\-\d?@]|\$\$|[A-Za-z_]\w*)`
|
|
reShVarexpansion = `(?:(?:#|##|%|%%|:-|:=|:\?|:\+|\+)[^$\\{}]*)`
|
|
reShVaruse = `\$\$` + `(?:` + reShVarname + `|` + `\{` + reShVarname + `(?:` + reShVarexpansion + `)?` + `\})`
|
|
reShDollar = `\\\$\$|` + reShVaruse + `|\$\$[,\-/|]`
|
|
)
|
|
|
|
type ShellLine struct {
|
|
line *Line
|
|
mkline *MkLine
|
|
}
|
|
|
|
func NewShellLine(mkline *MkLine) *ShellLine {
|
|
return &ShellLine{mkline.Line, mkline}
|
|
}
|
|
|
|
var shellcommandsContextType = &Vartype{lkNone, BtShellCommands, []AclEntry{{"*", aclpAllRuntime}}, false}
|
|
var shellwordVuc = &VarUseContext{shellcommandsContextType, vucTimeUnknown, vucQuotPlain, false}
|
|
|
|
func (shline *ShellLine) CheckWord(token string, checkQuoting bool) {
|
|
if G.opts.Debug {
|
|
defer tracecall(token, checkQuoting)()
|
|
}
|
|
|
|
if token == "" || hasPrefix(token, "#") {
|
|
return
|
|
}
|
|
|
|
line := shline.line
|
|
|
|
p := NewMkParser(line, token, false)
|
|
if varuse := p.VarUse(); varuse != nil && p.EOF() {
|
|
shline.mkline.CheckVaruse(varuse, shellwordVuc)
|
|
return
|
|
}
|
|
|
|
if matches(token, `\$\{PREFIX\}/man(?:$|/)`) {
|
|
line.Warn0("Please use ${PKGMANDIR} instead of \"man\".")
|
|
}
|
|
if contains(token, "etc/rc.d") {
|
|
line.Warn0("Please use the RCD_SCRIPTS mechanism to install rc.d scripts automatically to ${RCD_SCRIPTS_EXAMPLEDIR}.")
|
|
}
|
|
|
|
parser := NewMkParser(line, token, false)
|
|
repl := parser.repl
|
|
quoting := shqPlain
|
|
outer:
|
|
for !parser.EOF() {
|
|
if G.opts.Debug {
|
|
traceStep("shell state %s: %q", quoting, parser.Rest())
|
|
}
|
|
|
|
switch {
|
|
// When parsing inside backticks, it is more
|
|
// reasonable to check the whole shell command
|
|
// recursively, instead of splitting off the first
|
|
// make(1) variable.
|
|
case quoting == shqBackt || quoting == shqDquotBackt:
|
|
var backtCommand string
|
|
backtCommand, quoting = shline.unescapeBackticks(token, repl, quoting)
|
|
setE := true
|
|
shline.CheckShellCommand(backtCommand, &setE)
|
|
|
|
// Make(1) variables have the same syntax, no matter in which state we are currently.
|
|
case shline.checkVaruseToken(parser, quoting):
|
|
break
|
|
|
|
case quoting == shqPlain:
|
|
switch {
|
|
case repl.AdvanceRegexp(`^[!#\%&\(\)*+,\-.\/0-9:;<=>?@A-Z\[\]^_a-z{|}~]+`),
|
|
repl.AdvanceRegexp(`^\\(?:[ !"#'\(\)*./;?\\^{|}]|\$\$)`):
|
|
case repl.AdvanceStr("'"):
|
|
quoting = shqSquot
|
|
case repl.AdvanceStr("\""):
|
|
quoting = shqDquot
|
|
case repl.AdvanceStr("`"):
|
|
quoting = shqBackt
|
|
case repl.AdvanceRegexp(`^\$\$([0-9A-Z_a-z]+|#)`),
|
|
repl.AdvanceRegexp(`^\$\$\{([0-9A-Z_a-z]+|#)\}`),
|
|
repl.AdvanceRegexp(`^\$\$(\$)\$`):
|
|
shvarname := repl.m[1]
|
|
if G.opts.WarnQuoting && checkQuoting && shline.variableNeedsQuoting(shvarname) {
|
|
line.Warn1("Unquoted shell variable %q.", shvarname)
|
|
Explain(
|
|
"When a shell variable contains white-space, it is expanded (split",
|
|
"into multiple words) when it is written as $variable in a shell",
|
|
"script. If that is not intended, you should add quotation marks",
|
|
"around it, like \"$variable\". Then, the variable will always expand",
|
|
"to a single word, preserving all white-space and other special",
|
|
"characters.",
|
|
"",
|
|
"Example:",
|
|
"\tfname=\"Curriculum vitae.doc\"",
|
|
"\tcp $fname /tmp",
|
|
"\t# tries to copy the two files \"Curriculum\" and \"Vitae.doc\"",
|
|
"\tcp \"$fname\" /tmp",
|
|
"\t# copies one file, as intended")
|
|
}
|
|
case repl.AdvanceStr("$@"):
|
|
line.Warn2("Please use %q instead of %q.", "${.TARGET}", "$@")
|
|
Explain2(
|
|
"It is more readable and prevents confusion with the shell variable of",
|
|
"the same name.")
|
|
|
|
case repl.AdvanceStr("$$@"):
|
|
line.Warn0("The $@ shell variable should only be used in double quotes.")
|
|
|
|
case repl.AdvanceStr("$$?"):
|
|
line.Warn0("The $? shell variable is often not available in \"set -e\" mode.")
|
|
|
|
case repl.AdvanceStr("$$("):
|
|
line.Warn0("Invoking subshells via $(...) is not portable enough.")
|
|
Explain2(
|
|
"The Solaris /bin/sh does not know this way to execute a command in a",
|
|
"subshell. Please use backticks (`...`) as a replacement.")
|
|
|
|
case repl.AdvanceStr("$$"): // Not part of a variable.
|
|
break
|
|
|
|
default:
|
|
break outer
|
|
}
|
|
|
|
case quoting == shqSquot:
|
|
switch {
|
|
case repl.AdvanceRegexp(`^'`):
|
|
quoting = shqPlain
|
|
case repl.AdvanceRegexp(`^[^\$\']+`):
|
|
// just skip
|
|
case repl.AdvanceRegexp(`^\$\$`):
|
|
// just skip
|
|
default:
|
|
break outer
|
|
}
|
|
|
|
case quoting == shqDquot:
|
|
switch {
|
|
case repl.AdvanceStr("\""):
|
|
quoting = shqPlain
|
|
case repl.AdvanceStr("`"):
|
|
quoting = shqDquotBackt
|
|
case repl.AdvanceRegexp("^[^$\"\\\\`]+"):
|
|
break
|
|
case repl.AdvanceStr("\\$$"):
|
|
break
|
|
case repl.AdvanceRegexp(`^\\.`): // See http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_02_01
|
|
break
|
|
case repl.AdvanceRegexp(`^\$\$\{\w+[#%+\-:]*[^{}]*\}`),
|
|
repl.AdvanceRegexp(`^\$\$(?:\w+|[!#?@]|\$\$)`):
|
|
break
|
|
case repl.AdvanceStr("$$"):
|
|
line.Warn0("Unescaped $ or strange shell variable found.")
|
|
default:
|
|
break outer
|
|
}
|
|
}
|
|
}
|
|
|
|
if strings.TrimSpace(parser.Rest()) != "" {
|
|
line.Warnf("Pkglint parse error in ShellLine.CheckWord at %q (quoting=%s, rest=%q)", token, quoting, parser.Rest())
|
|
}
|
|
}
|
|
|
|
func (shline *ShellLine) checkVaruseToken(parser *MkParser, quoting ShQuoting) bool {
|
|
if G.opts.Debug {
|
|
defer tracecall(parser.Rest(), quoting)()
|
|
}
|
|
|
|
varuse := parser.VarUse()
|
|
if varuse == nil {
|
|
return false
|
|
}
|
|
varname := varuse.varname
|
|
|
|
if varname == "@" {
|
|
shline.line.Warn0("Please use \"${.TARGET}\" instead of \"$@\".")
|
|
Explain2(
|
|
"The variable $@ can easily be confused with the shell variable of",
|
|
"the same name, which has a completely different meaning.")
|
|
varname = ".TARGET"
|
|
varuse = &MkVarUse{varname, varuse.modifiers}
|
|
}
|
|
|
|
switch {
|
|
case quoting == shqPlain && varuse.IsQ():
|
|
// Fine.
|
|
case quoting == shqBackt:
|
|
// Don't check anything here, to avoid false positives for tool names.
|
|
case (quoting == shqSquot || quoting == shqDquot) && matches(varname, `^(?:.*DIR|.*FILE|.*PATH|.*_VAR|PREFIX|.*BASE|PKGNAME)$`):
|
|
// This is ok if we don't allow these variables to have embedded [\$\\\"\'\`].
|
|
case quoting == shqDquot && varuse.IsQ():
|
|
shline.line.Warn0("Please don't use the :Q operator in double quotes.")
|
|
Explain2(
|
|
"Either remove the :Q or the double quotes. In most cases, it is",
|
|
"more appropriate to remove the double quotes.")
|
|
}
|
|
|
|
if varname != "@" {
|
|
vucstate := quoting.ToVarUseContext()
|
|
vuc := &VarUseContext{shellcommandsContextType, vucTimeUnknown, vucstate, true}
|
|
shline.mkline.CheckVaruse(varuse, vuc)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Scan for the end of the backticks, checking for single backslashes
|
|
// and removing one level of backslashes. Backslashes are only removed
|
|
// before a dollar, a backslash or a backtick.
|
|
//
|
|
// See http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_03
|
|
func (shline *ShellLine) unescapeBackticks(shellword string, repl *PrefixReplacer, quoting ShQuoting) (unescaped string, newQuoting ShQuoting) {
|
|
if G.opts.Debug {
|
|
defer tracecall(shellword, quoting, "=>", ref(&unescaped))()
|
|
}
|
|
|
|
line := shline.line
|
|
for repl.rest != "" {
|
|
switch {
|
|
case repl.AdvanceStr("`"):
|
|
if quoting == shqBackt {
|
|
quoting = shqPlain
|
|
} else {
|
|
quoting = shqDquot
|
|
}
|
|
return unescaped, quoting
|
|
|
|
case repl.AdvanceRegexp("^\\\\([\"\\\\`$])"):
|
|
unescaped += repl.m[1]
|
|
|
|
case repl.AdvanceStr("\\"):
|
|
line.Warn0("Backslashes should be doubled inside backticks.")
|
|
unescaped += "\\"
|
|
|
|
case quoting == shqDquotBackt && repl.AdvanceStr("\""):
|
|
line.Warn0("Double quotes inside backticks inside double quotes are error prone.")
|
|
Explain4(
|
|
"According to the SUSv3, they produce undefined results.",
|
|
"",
|
|
"See the paragraph starting \"Within the backquoted ...\" in",
|
|
"http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html")
|
|
|
|
case repl.AdvanceRegexp("^([^\\\\`]+)"):
|
|
unescaped += repl.m[1]
|
|
|
|
default:
|
|
line.Errorf("Internal pkglint error in ShellLine.unescapeBackticks at %q (rest=%q)", shellword, repl.rest)
|
|
}
|
|
}
|
|
line.Error1("Unfinished backquotes: rest=%q", repl.rest)
|
|
return unescaped, quoting
|
|
}
|
|
|
|
func (shline *ShellLine) variableNeedsQuoting(shvarname string) bool {
|
|
switch shvarname {
|
|
case "#", "?":
|
|
return false // Definitely ok
|
|
case "d", "f", "i", "dir", "file", "src", "dst":
|
|
return false // Probably ok
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (shline *ShellLine) CheckShellCommandLine(shelltext string) {
|
|
if G.opts.Debug {
|
|
defer tracecall1(shelltext)()
|
|
}
|
|
|
|
line := shline.line
|
|
|
|
if contains(shelltext, "${SED}") && contains(shelltext, "${MV}") {
|
|
line.Note0("Please use the SUBST framework instead of ${SED} and ${MV}.")
|
|
Explain(
|
|
"Using the SUBST framework instead of explicit commands is easier",
|
|
"to understand, since all the complexity of using sed and mv is",
|
|
"hidden behind the scenes.",
|
|
"",
|
|
"Run \"bmake help topic=subst\" for more information.")
|
|
if contains(shelltext, "#") {
|
|
Explain(
|
|
"When migrating to the SUBST framework, pay attention to \"#\"",
|
|
"characters. In shell commands, make(1) does not interpret them as",
|
|
"comment character, but in variable assignments it does. Therefore,",
|
|
"instead of the shell command",
|
|
"",
|
|
"\tsed -e 's,#define foo,,'",
|
|
"",
|
|
"you need to write",
|
|
"",
|
|
"\tSUBST_SED.foo+=\t's,\\#define foo,,'")
|
|
}
|
|
}
|
|
|
|
if m, cmd := match1(shelltext, `^@*-(.*(?:MKDIR|INSTALL.*-d|INSTALL_.*_DIR).*)`); m {
|
|
line.Note1("You don't need to use \"-\" before %q.", cmd)
|
|
}
|
|
|
|
repl := NewPrefixReplacer(shelltext)
|
|
repl.AdvanceRegexp(`^\s+`)
|
|
if repl.AdvanceRegexp(`^[-@]+`) {
|
|
shline.checkHiddenAndSuppress(repl.m[0], repl.rest)
|
|
}
|
|
setE := false
|
|
if repl.AdvanceStr("${RUN}") {
|
|
setE = true
|
|
} else {
|
|
repl.AdvanceStr("${_PKG_SILENT}${_PKG_DEBUG}")
|
|
}
|
|
|
|
shline.CheckShellCommand(repl.rest, &setE)
|
|
}
|
|
|
|
func (shline *ShellLine) CheckShellCommand(shellcmd string, pSetE *bool) {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
program, err := parseShellProgram(shline.line, shellcmd)
|
|
if err != nil && contains(shellcmd, "$$(") { // Hack until the shell parser can handle subshells.
|
|
shline.line.Warn0("Invoking subshells via $(...) is not portable enough.")
|
|
return
|
|
}
|
|
if err != nil {
|
|
shline.line.Warnf("Pkglint ShellLine.CheckShellCommand: %s", err)
|
|
return
|
|
}
|
|
|
|
spc := &ShellProgramChecker{shline}
|
|
spc.checkConditionalCd(program)
|
|
|
|
(*MkShWalker).Walk(nil, program, func(node interface{}) {
|
|
if cmd, ok := node.(*MkShSimpleCommand); ok {
|
|
scc := NewSimpleCommandChecker(shline, cmd)
|
|
scc.Check()
|
|
if scc.strcmd.Name == "set" && scc.strcmd.AnyArgMatches(`^-.*e`) {
|
|
*pSetE = true
|
|
}
|
|
}
|
|
|
|
if cmd, ok := node.(*MkShList); ok {
|
|
spc.checkSetE(cmd, pSetE)
|
|
}
|
|
|
|
if cmd, ok := node.(*MkShPipeline); ok {
|
|
spc.checkPipeExitcode(shline.line, cmd)
|
|
}
|
|
|
|
if word, ok := node.(*ShToken); ok {
|
|
spc.checkWord(word, false)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (shline *ShellLine) CheckShellCommands(shellcmds string) {
|
|
setE := true
|
|
shline.CheckShellCommand(shellcmds, &setE)
|
|
if !hasSuffix(shellcmds, ";") {
|
|
shline.line.Warn0("This shell command list should end with a semicolon.")
|
|
}
|
|
}
|
|
|
|
func (shline *ShellLine) checkHiddenAndSuppress(hiddenAndSuppress, rest string) {
|
|
if G.opts.Debug {
|
|
defer tracecall(hiddenAndSuppress, rest)()
|
|
}
|
|
|
|
switch {
|
|
case !contains(hiddenAndSuppress, "@"):
|
|
// Nothing is hidden at all.
|
|
|
|
case hasPrefix(G.Mk.target, "show-") || hasSuffix(G.Mk.target, "-message"):
|
|
// In these targets, all commands may be hidden.
|
|
|
|
case hasPrefix(rest, "#"):
|
|
// Shell comments may be hidden, since they cannot have side effects.
|
|
|
|
default:
|
|
tokens, _ := splitIntoShellTokens(shline.line, rest)
|
|
if len(tokens) > 0 {
|
|
cmd := tokens[0]
|
|
switch cmd {
|
|
case "${DELAYED_ERROR_MSG}", "${DELAYED_WARNING_MSG}",
|
|
"${DO_NADA}",
|
|
"${ECHO}", "${ECHO_MSG}", "${ECHO_N}", "${ERROR_CAT}", "${ERROR_MSG}",
|
|
"${FAIL_MSG}",
|
|
"${PHASE_MSG}", "${PRINTF}",
|
|
"${SHCOMMENT}", "${STEP_MSG}",
|
|
"${WARNING_CAT}", "${WARNING_MSG}":
|
|
break
|
|
default:
|
|
shline.line.Warn1("The shell command %q should not be hidden.", cmd)
|
|
Explain(
|
|
"Hidden shell commands do not appear on the terminal or in the log",
|
|
"file when they are executed. When they fail, the error message",
|
|
"cannot be assigned to the command, which is very difficult to debug.",
|
|
"",
|
|
"It is better to insert ${RUN} at the beginning of the whole command",
|
|
"line. This will hide the command by default, but shows it when",
|
|
"PKG_DEBUG_LEVEL is set.")
|
|
}
|
|
}
|
|
}
|
|
|
|
if contains(hiddenAndSuppress, "-") {
|
|
shline.line.Warn0("Using a leading \"-\" to suppress errors is deprecated.")
|
|
Explain2(
|
|
"If you really want to ignore any errors from this command, append",
|
|
"\"|| ${TRUE}\" to the command.")
|
|
}
|
|
}
|
|
|
|
type SimpleCommandChecker struct {
|
|
shline *ShellLine
|
|
cmd *MkShSimpleCommand
|
|
strcmd *StrCommand
|
|
}
|
|
|
|
func NewSimpleCommandChecker(shline *ShellLine, cmd *MkShSimpleCommand) *SimpleCommandChecker {
|
|
strcmd := NewStrCommand(cmd)
|
|
return &SimpleCommandChecker{shline, cmd, strcmd}
|
|
|
|
}
|
|
|
|
func (scc *SimpleCommandChecker) Check() {
|
|
if G.opts.Debug {
|
|
defer tracecall(scc.strcmd)()
|
|
}
|
|
|
|
scc.checkCommandStart()
|
|
scc.checkAbsolutePathnames()
|
|
scc.checkAutoMkdirs()
|
|
scc.checkInstallMulti()
|
|
scc.checkPaxPe()
|
|
scc.checkEchoN()
|
|
}
|
|
|
|
func (scc *SimpleCommandChecker) checkCommandStart() {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
shellword := scc.strcmd.Name
|
|
switch {
|
|
case shellword == "${RUN}" || shellword == "":
|
|
case scc.handleForbiddenCommand():
|
|
case scc.handleTool():
|
|
case scc.handleCommandVariable():
|
|
case matches(shellword, `^(?::|break|cd|continue|eval|exec|exit|export|read|set|shift|umask|unset)$`):
|
|
case hasPrefix(shellword, "./"): // All commands from the current directory are fine.
|
|
case hasPrefix(shellword, "${PKGSRCDIR"): // With or without the :Q modifier
|
|
case scc.handleComment():
|
|
default:
|
|
if G.opts.WarnExtra && !(G.Mk != nil && G.Mk.indentation.DependsOn("OPSYS")) {
|
|
scc.shline.line.Warn1("Unknown shell command %q.", shellword)
|
|
Explain3(
|
|
"If you want your package to be portable to all platforms that pkgsrc",
|
|
"supports, you should only use shell commands that are covered by the",
|
|
"tools framework.")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (scc *SimpleCommandChecker) handleTool() bool {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
shellword := scc.strcmd.Name
|
|
tool, localTool := G.globalData.Tools.byName[shellword], false
|
|
if tool == nil && G.Mk != nil {
|
|
tool, localTool = G.Mk.toolRegistry.byName[shellword], true
|
|
}
|
|
if tool == nil {
|
|
return false
|
|
}
|
|
|
|
if !localTool && !G.Mk.tools[shellword] && !G.Mk.tools["g"+shellword] {
|
|
scc.shline.line.Warn1("The %q tool is used but not added to USE_TOOLS.", shellword)
|
|
}
|
|
|
|
if tool.MustUseVarForm {
|
|
scc.shline.line.Warn2("Please use \"${%s}\" instead of %q.", tool.Varname, shellword)
|
|
}
|
|
|
|
scc.shline.checkCommandUse(shellword)
|
|
return true
|
|
}
|
|
|
|
func (scc *SimpleCommandChecker) handleForbiddenCommand() bool {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
shellword := scc.strcmd.Name
|
|
switch path.Base(shellword) {
|
|
case "ktrace", "mktexlsr", "strace", "texconfig", "truss":
|
|
scc.shline.line.Error1("%q must not be used in Makefiles.", shellword)
|
|
Explain3(
|
|
"This command must appear in INSTALL scripts, not in the package",
|
|
"Makefile, so that the package also works if it is installed as a binary",
|
|
"package via pkg_add.")
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (scc *SimpleCommandChecker) handleCommandVariable() bool {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
shellword := scc.strcmd.Name
|
|
if m, varname := match1(shellword, `^\$\{([\w_]+)\}$`); m {
|
|
|
|
if tool := G.globalData.Tools.byVarname[varname]; tool != nil {
|
|
if !G.Mk.tools[tool.Name] {
|
|
scc.shline.line.Warn1("The %q tool is used but not added to USE_TOOLS.", tool.Name)
|
|
}
|
|
scc.shline.checkCommandUse(shellword)
|
|
return true
|
|
}
|
|
|
|
if vartype := scc.shline.mkline.getVariableType(varname); vartype != nil && vartype.basicType.name == "ShellCommand" {
|
|
scc.shline.checkCommandUse(shellword)
|
|
return true
|
|
}
|
|
|
|
// When the package author has explicitly defined a command
|
|
// variable, assume it to be valid.
|
|
if G.Pkg != nil && G.Pkg.vardef[varname] != nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (scc *SimpleCommandChecker) handleComment() bool {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
shellword := scc.strcmd.Name
|
|
if G.opts.Debug {
|
|
defer tracecall1(shellword)()
|
|
}
|
|
|
|
if !hasPrefix(shellword, "#") {
|
|
return false
|
|
}
|
|
|
|
semicolon := contains(shellword, ";")
|
|
multiline := scc.shline.line.IsMultiline()
|
|
|
|
if semicolon {
|
|
scc.shline.line.Warn0("A shell comment should not contain semicolons.")
|
|
}
|
|
if multiline {
|
|
scc.shline.line.Warn0("A shell comment does not stop at the end of line.")
|
|
}
|
|
|
|
if semicolon || multiline {
|
|
Explain(
|
|
"When you split a shell command into multiple lines that are",
|
|
"continued with a backslash, they will nevertheless be converted to",
|
|
"a single line before the shell sees them. That means that even if",
|
|
"it _looks_ like that the comment only spans one line in the",
|
|
"Makefile, in fact it spans until the end of the whole shell command.",
|
|
"",
|
|
"To insert a comment into shell code, you can write it like this:",
|
|
"",
|
|
"\t"+"${SHCOMMENT} \"The following command might fail; this is ok.\"",
|
|
"",
|
|
"Note that any special characters in the comment are still",
|
|
"interpreted by the shell.")
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (scc *SimpleCommandChecker) checkAbsolutePathnames() {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
cmdname := scc.strcmd.Name
|
|
isSubst := false
|
|
for _, arg := range scc.strcmd.Args {
|
|
if !isSubst {
|
|
scc.shline.line.CheckAbsolutePathname(arg)
|
|
}
|
|
if false && isSubst && !matches(arg, `"^[\"\'].*[\"\']$`) {
|
|
scc.shline.line.Warn1("Substitution commands like %q should always be quoted.", arg)
|
|
Explain3(
|
|
"Usually these substitution commands contain characters like '*' or",
|
|
"other shell metacharacters that might lead to lookup of matching",
|
|
"filenames and then expand to more than one word.")
|
|
}
|
|
isSubst = cmdname == "${PAX}" && arg == "-s" || cmdname == "${SED}" && arg == "-e"
|
|
}
|
|
}
|
|
|
|
func (scc *SimpleCommandChecker) checkAutoMkdirs() {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
cmdname := scc.strcmd.Name
|
|
switch {
|
|
case cmdname == "${MKDIR}":
|
|
break
|
|
case cmdname == "${INSTALL}" && scc.strcmd.HasOption("-d"):
|
|
cmdname = "${INSTALL} -d"
|
|
case matches(cmdname, `^\$\{INSTALL_.*_DIR\}$`):
|
|
break
|
|
default:
|
|
return
|
|
}
|
|
|
|
for _, arg := range scc.strcmd.Args {
|
|
if !contains(arg, "$$") && !matches(arg, `\$\{[_.]*[a-z]`) {
|
|
if m, dirname := match1(arg, `^(?:\$\{DESTDIR\})?\$\{PREFIX(?:|:Q)\}/(.*)`); m {
|
|
scc.shline.line.Note2("You can use AUTO_MKDIRS=yes or \"INSTALLATION_DIRS+= %s\" instead of %q.", dirname, cmdname)
|
|
Explain(
|
|
"Many packages include a list of all needed directories in their",
|
|
"PLIST file. In such a case, you can just set AUTO_MKDIRS=yes and",
|
|
"be done. The pkgsrc infrastructure will then create all directories",
|
|
"in advance.",
|
|
"",
|
|
"To create directories that are not mentioned in the PLIST file, it",
|
|
"is easier to just list them in INSTALLATION_DIRS than to execute the",
|
|
"commands explicitly. That way, you don't have to think about which",
|
|
"of the many INSTALL_*_DIR variables is appropriate, since",
|
|
"INSTALLATION_DIRS takes care of that.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (scc *SimpleCommandChecker) checkInstallMulti() {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
cmd := scc.strcmd
|
|
|
|
if hasPrefix(cmd.Name, "${INSTALL_") && hasSuffix(cmd.Name, "_DIR}") {
|
|
prevdir := ""
|
|
for i, arg := range cmd.Args {
|
|
switch {
|
|
case hasPrefix(arg, "-"):
|
|
break
|
|
case i > 0 && (cmd.Args[i-1] == "-m" || cmd.Args[i-1] == "-o" || cmd.Args[i-1] == "-g"):
|
|
break
|
|
default:
|
|
if prevdir != "" {
|
|
scc.shline.line.Warn0("The INSTALL_*_DIR commands can only handle one directory at a time.")
|
|
Explain2(
|
|
"Many implementations of install(1) can handle more, but pkgsrc aims",
|
|
"at maximum portability.")
|
|
return
|
|
}
|
|
prevdir = arg
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (scc *SimpleCommandChecker) checkPaxPe() {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
if scc.strcmd.Name == "${PAX}" && scc.strcmd.HasOption("-pe") {
|
|
scc.shline.line.Warn0("Please use the -pp option to pax(1) instead of -pe.")
|
|
Explain3(
|
|
"The -pe option tells pax to preserve the ownership of the files, which",
|
|
"means that the installed files will belong to the user that has built",
|
|
"the package.")
|
|
}
|
|
}
|
|
|
|
func (scc *SimpleCommandChecker) checkEchoN() {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
if scc.strcmd.Name == "${ECHO}" && scc.strcmd.HasOption("-n") {
|
|
scc.shline.line.Warn0("Please use ${ECHO_N} instead of \"echo -n\".")
|
|
}
|
|
}
|
|
|
|
type ShellProgramChecker struct {
|
|
shline *ShellLine
|
|
}
|
|
|
|
func (spc *ShellProgramChecker) checkConditionalCd(list *MkShList) {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
getSimple := func(list *MkShList) *MkShSimpleCommand {
|
|
if len(list.AndOrs) == 1 {
|
|
if len(list.AndOrs[0].Pipes) == 1 {
|
|
if len(list.AndOrs[0].Pipes[0].Cmds) == 1 {
|
|
return list.AndOrs[0].Pipes[0].Cmds[0].Simple
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
checkConditionalCd := func(cmd *MkShSimpleCommand) {
|
|
if NewStrCommand(cmd).Name == "cd" {
|
|
spc.shline.line.Error0("The Solaris /bin/sh cannot handle \"cd\" inside conditionals.")
|
|
Explain3(
|
|
"When the Solaris shell is in \"set -e\" mode and \"cd\" fails, the",
|
|
"shell will exit, no matter if it is protected by an \"if\" or the",
|
|
"\"||\" operator.")
|
|
}
|
|
}
|
|
|
|
(*MkShWalker).Walk(nil, list, func(node interface{}) {
|
|
if cmd, ok := node.(*MkShIfClause); ok {
|
|
for _, cond := range cmd.Conds {
|
|
if simple := getSimple(cond); simple != nil {
|
|
checkConditionalCd(simple)
|
|
}
|
|
}
|
|
}
|
|
if cmd, ok := node.(*MkShLoopClause); ok {
|
|
if simple := getSimple(cmd.Cond); simple != nil {
|
|
checkConditionalCd(simple)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func (spc *ShellProgramChecker) checkWords(words []*ShToken, checkQuoting bool) {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
for _, word := range words {
|
|
spc.checkWord(word, checkQuoting)
|
|
}
|
|
}
|
|
|
|
func (spc *ShellProgramChecker) checkWord(word *ShToken, checkQuoting bool) {
|
|
if G.opts.Debug {
|
|
defer tracecall(word.MkText)()
|
|
}
|
|
|
|
spc.shline.CheckWord(word.MkText, checkQuoting)
|
|
}
|
|
|
|
func (scc *ShellProgramChecker) checkPipeExitcode(line *Line, pipeline *MkShPipeline) {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
if G.opts.WarnExtra && len(pipeline.Cmds) > 1 {
|
|
line.Warn0("The exitcode of the left-hand-side command of the pipe operator is ignored.")
|
|
Explain(
|
|
"In a shell command like \"cat *.txt | grep keyword\", if the command",
|
|
"on the left side of the \"|\" fails, this failure is ignored.",
|
|
"",
|
|
"If you need to detect the failure of the left-hand-side command, use",
|
|
"temporary files to save the output of the command.")
|
|
}
|
|
}
|
|
|
|
func (scc *ShellProgramChecker) checkSetE(list *MkShList, eflag *bool) {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
// Disabled until the shell parser can recognize "command || exit 1" reliably.
|
|
if false && G.opts.WarnExtra && !*eflag && "the current token" == ";" {
|
|
*eflag = true
|
|
scc.shline.line.Warn1("Please switch to \"set -e\" mode before using a semicolon (the one after %q) to separate commands.", "previous token")
|
|
Explain(
|
|
"Normally, when a shell command fails (returns non-zero), the",
|
|
"remaining commands are still executed. For example, the following",
|
|
"commands would remove all files from the HOME directory:",
|
|
"",
|
|
"\tcd \"$HOME\"; cd /nonexistent; rm -rf *",
|
|
"",
|
|
"To fix this warning, you can:",
|
|
"",
|
|
"* insert ${RUN} at the beginning of the line",
|
|
" (which among other things does \"set -e\")",
|
|
"* insert \"set -e\" explicitly at the beginning of the line",
|
|
"* use \"&&\" instead of \";\" to separate the commands")
|
|
}
|
|
}
|
|
|
|
// Some shell commands should not be used in the install phase.
|
|
func (shline *ShellLine) checkCommandUse(shellcmd string) {
|
|
if G.opts.Debug {
|
|
defer tracecall()()
|
|
}
|
|
|
|
if G.Mk == nil || !matches(G.Mk.target, `^(?:pre|do|post)-install$`) {
|
|
return
|
|
}
|
|
|
|
line := shline.line
|
|
switch shellcmd {
|
|
case "${INSTALL}",
|
|
"${INSTALL_DATA}", "${INSTALL_DATA_DIR}",
|
|
"${INSTALL_LIB}", "${INSTALL_LIB_DIR}",
|
|
"${INSTALL_MAN}", "${INSTALL_MAN_DIR}",
|
|
"${INSTALL_PROGRAM}", "${INSTALL_PROGRAM_DIR}",
|
|
"${INSTALL_SCRIPT}",
|
|
"${LIBTOOL}",
|
|
"${LN}",
|
|
"${PAX}":
|
|
return
|
|
|
|
case "sed", "${SED}",
|
|
"tr", "${TR}":
|
|
line.Warn1("The shell command %q should not be used in the install phase.", shellcmd)
|
|
Explain3(
|
|
"In the install phase, the only thing that should be done is to",
|
|
"install the prepared files to their final location. The file's",
|
|
"contents should not be changed anymore.")
|
|
|
|
case "cp", "${CP}":
|
|
line.Warn0("${CP} should not be used to install files.")
|
|
Explain(
|
|
"The ${CP} command is highly platform dependent and cannot overwrite",
|
|
"read-only files. Please use ${PAX} instead.",
|
|
"",
|
|
"For example, instead of",
|
|
"\t${CP} -R ${WRKSRC}/* ${PREFIX}/foodir",
|
|
"you should use",
|
|
"\tcd ${WRKSRC} && ${PAX} -wr * ${PREFIX}/foodir")
|
|
}
|
|
}
|
|
|
|
// Example: "word1 word2;;;" => "word1", "word2", ";;", ";"
|
|
func splitIntoShellTokens(line *Line, text string) (tokens []string, rest string) {
|
|
if G.opts.Debug {
|
|
defer tracecall(line, text)()
|
|
}
|
|
|
|
word := ""
|
|
emit := func() {
|
|
if word != "" {
|
|
tokens = append(tokens, word)
|
|
word = ""
|
|
}
|
|
}
|
|
p := NewShTokenizer(line, text, false)
|
|
atoms := p.ShAtoms()
|
|
q := shqPlain
|
|
for _, atom := range atoms {
|
|
q = atom.Quoting
|
|
if atom.Type == shtSpace && q == shqPlain {
|
|
emit()
|
|
} else if atom.Type == shtWord || atom.Type == shtVaruse || atom.Quoting != shqPlain {
|
|
word += atom.MkText
|
|
} else {
|
|
emit()
|
|
tokens = append(tokens, atom.MkText)
|
|
}
|
|
}
|
|
emit()
|
|
return tokens, word + p.mkp.Rest()
|
|
}
|
|
|
|
// Example: "word1 word2;;;" => "word1", "word2;;;"
|
|
// Compare devel/bmake/files/str.c, function brk_string.
|
|
func splitIntoMkWords(line *Line, text string) (words []string, rest string) {
|
|
if G.opts.Debug {
|
|
defer tracecall(line, text)()
|
|
}
|
|
|
|
p := NewShTokenizer(line, text, false)
|
|
atoms := p.ShAtoms()
|
|
word := ""
|
|
for _, atom := range atoms {
|
|
if atom.Type == shtSpace && atom.Quoting == shqPlain {
|
|
words = append(words, word)
|
|
word = ""
|
|
} else {
|
|
word += atom.MkText
|
|
}
|
|
}
|
|
if word != "" && atoms[len(atoms)-1].Quoting == shqPlain {
|
|
words = append(words, word)
|
|
word = ""
|
|
}
|
|
return words, word + p.mkp.Rest()
|
|
}
|