version 1.6, 2015/12/05 21:00:42 |
version 1.7, 2015/12/05 22:42:01 |
|
|
import ( |
import ( |
"path" |
"path" |
"strings" |
"strings" |
"unicode" |
|
) |
) |
|
|
const ( |
const ( |
Line 81 func NewMkShellLine(line *Line) *MkShell |
|
Line 80 func NewMkShellLine(line *Line) *MkShell |
|
return &MkShellLine{line} |
return &MkShellLine{line} |
} |
} |
|
|
|
type ShellwordState string |
|
|
|
const ( |
|
swstPlain ShellwordState = "plain" |
|
swstSquot ShellwordState = "squot" |
|
swstDquot ShellwordState = "dquot" |
|
swstDquotBackt ShellwordState = "dquot+backt" |
|
swstBackt ShellwordState = "backt" |
|
) |
|
|
func (msline *MkShellLine) checkShellword(shellword string, checkQuoting bool) { |
func (msline *MkShellLine) checkShellword(shellword string, checkQuoting bool) { |
defer tracecall("MkShellLine.checklineMkShellword", shellword, checkQuoting)() |
defer tracecall("MkShellLine.checklineMkShellword", shellword, checkQuoting)() |
|
|
Line 104 func (msline *MkShellLine) checkShellwor |
|
Line 113 func (msline *MkShellLine) checkShellwor |
|
line.warnf("Please use the RCD_SCRIPTS mechanism to install rc.d scripts automatically to ${RCD_SCRIPTS_EXAMPLEDIR}.") |
line.warnf("Please use the RCD_SCRIPTS mechanism to install rc.d scripts automatically to ${RCD_SCRIPTS_EXAMPLEDIR}.") |
} |
} |
|
|
type ShellwordState string |
repl := NewPrefixReplacer(shellword) |
const ( |
|
swstPlain ShellwordState = "plain" |
|
swstSquot ShellwordState = "squot" |
|
swstDquot ShellwordState = "dquot" |
|
swstDquotBackt ShellwordState = "dquot+backt" |
|
swstBackt ShellwordState = "backt" |
|
) |
|
|
|
rest := shellword |
|
state := swstPlain |
state := swstPlain |
outer: |
outer: |
for rest != "" { |
for repl.rest != "" { |
_ = G.opts.DebugShell && line.debugf("shell state %s: %q", state, rest) |
_ = G.opts.DebugShell && line.debugf("shell state %s: %q", state, repl.rest) |
|
|
var m []string |
|
switch { |
switch { |
// When parsing inside backticks, it is more |
// When parsing inside backticks, it is more |
// reasonable to check the whole shell command |
// reasonable to check the whole shell command |
// recursively, instead of splitting off the first |
// recursively, instead of splitting off the first |
// make(1) variable. |
// make(1) variable. |
case state == swstBackt || state == swstDquotBackt: |
case state == swstBackt || state == swstDquotBackt: |
// Scan for the end of the backticks, checking |
var backtCommand string |
// for single backslashes and removing one level |
backtCommand, state = msline.unescapeBackticks(shellword, repl, state) |
// of backslashes. Backslashes are only removed |
msline.checkShelltext(backtCommand) |
// before a dollar, a backslash or a backtick. |
|
// |
|
// References: |
|
// * http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_03 |
|
stripped := "" |
|
for rest != "" { |
|
switch { |
|
case replacePrefix(&rest, &m, "^`"): |
|
if state == swstBackt { |
|
state = swstPlain |
|
} else { |
|
state = swstDquot |
|
} |
|
goto endOfBackticks |
|
|
|
case replacePrefix(&rest, &m, "^\\\\([\\\\`$])"): |
|
stripped += m[1] |
|
|
|
case replacePrefix(&rest, &m, `^(\\)`): |
|
line.warnf("Backslashes should be doubled inside backticks.") |
|
stripped += m[1] |
|
|
|
case state == swstDquotBackt && replacePrefix(&rest, &m, `^"`): |
|
line.warnf("Double quotes inside backticks inside double quotes are error prone.") |
|
line.explain( |
|
"According to the SUSv3, they produce undefined results.", |
|
"", |
|
"See the paragraph starting \"Within the backquoted ...\" in", |
|
"http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html") |
|
|
|
case replacePrefix(&rest, &m, "^([^\\\\`]+)"): |
|
stripped += m[1] |
|
|
|
default: |
|
line.errorf("Internal pkglint error: checklineMkShellword shellword=%q rest=%q", shellword, rest) |
|
} |
|
} |
|
line.errorf("Unfinished backquotes: rest=%q", rest) |
|
|
|
endOfBackticks: |
|
msline.checkShelltext(stripped) |
|
|
|
// Make(1) variables have the same syntax, no matter in which state we are currently. |
// Make(1) variables have the same syntax, no matter in which state we are currently. |
case replacePrefix(&rest, &m, `^\$\{(`+reVarnameDirect+`|@)(:[^\{]+)?\}`), |
case repl.startsWith(`^\$\{(` + reVarnameDirect + `|@)(:[^\{]+)?\}`), |
replacePrefix(&rest, &m, `^\$\((`+reVarnameDirect+`|@])(:[^\)]+)?\)`), |
repl.startsWith(`^\$\((` + reVarnameDirect + `|@])(:[^\)]+)?\)`), |
replacePrefix(&rest, &m, `^\$([\w@])()`): |
repl.startsWith(`^\$([\w@])()`): |
varname, mod := m[1], m[2] |
varname, mod := repl.m[1], repl.m[2] |
|
|
if varname == "@" { |
if varname == "@" { |
line.warnf("Please use \"${.TARGET}\" instead of \"$@\".") |
line.warnf("Please use \"${.TARGET}\" instead of \"$@\".") |
|
|
// The syntax of the variable modifiers can get quite |
// The syntax of the variable modifiers can get quite |
// hairy. In lack of motivation, we just skip anything |
// hairy. In lack of motivation, we just skip anything |
// complicated, hoping that at least the braces are balanced. |
// complicated, hoping that at least the braces are balanced. |
case replacePrefix(&rest, &m, `^\$\{`): |
case repl.startsWith(`^\$\{`): |
braces := 1 |
braces := 1 |
skip: |
skip: |
for rest != "" && braces > 0 { |
for repl.rest != "" && braces > 0 { |
switch { |
switch { |
case replacePrefix(&rest, &m, `^\}`): |
case repl.startsWith(`^\}`): |
braces-- |
braces-- |
case replacePrefix(&rest, &m, `^\{`): |
case repl.startsWith(`^\{`): |
braces++ |
braces++ |
case replacePrefix(&rest, &m, `^[^{}]+`): |
case repl.startsWith(`^[^{}]+`): |
// skip |
// skip |
default: |
default: |
break skip |
break skip |
|
|
|
|
case state == swstPlain: |
case state == swstPlain: |
switch { |
switch { |
case replacePrefix(&rest, &m, `^[!#\%&\(\)*+,\-.\/0-9:;<=>?@A-Z\[\]^_a-z{|}~]+`), |
case repl.startsWith(`^[!#\%&\(\)*+,\-.\/0-9:;<=>?@A-Z\[\]^_a-z{|}~]+`), |
replacePrefix(&rest, &m, `^\\(?:[ !"#'\(\)*;?\\^{|}]|\$\$)`): |
repl.startsWith(`^\\(?:[ !"#'\(\)*;?\\^{|}]|\$\$)`): |
case replacePrefix(&rest, &m, `^'`): |
case repl.startsWith(`^'`): |
state = swstSquot |
state = swstSquot |
case replacePrefix(&rest, &m, `^"`): |
case repl.startsWith(`^"`): |
state = swstDquot |
state = swstDquot |
case replacePrefix(&rest, &m, "^`"): |
case repl.startsWith("^`"): |
state = swstBackt |
state = swstBackt |
case replacePrefix(&rest, &m, `^\$\$([0-9A-Z_a-z]+|\#)`), |
case repl.startsWith(`^\$\$([0-9A-Z_a-z]+|\#)`), |
replacePrefix(&rest, &m, `^\$\$\{([0-9A-Z_a-z]+|\#)\}`), |
repl.startsWith(`^\$\$\{([0-9A-Z_a-z]+|\#)\}`), |
replacePrefix(&rest, &m, `^\$\$(\$)\$`): |
repl.startsWith(`^\$\$(\$)\$`): |
shvarname := m[1] |
shvarname := repl.m[1] |
if G.opts.WarnQuoting && checkQuoting && msline.variableNeedsQuoting(shvarname) { |
if G.opts.WarnQuoting && checkQuoting && msline.variableNeedsQuoting(shvarname) { |
line.warnf("Unquoted shell variable %q.", shvarname) |
line.warnf("Unquoted shell variable %q.", shvarname) |
line.explain( |
line.explain( |
|
|
"\tcp \"$fname\" /tmp", |
"\tcp \"$fname\" /tmp", |
"\t# copies one file, as intended") |
"\t# copies one file, as intended") |
} |
} |
case replacePrefix(&rest, &m, `^\$@`): |
case repl.startsWith(`^\$@`): |
line.warnf("Please use %q instead of %q.", "${.TARGET}", "$@") |
line.warnf("Please use %q instead of %q.", "${.TARGET}", "$@") |
line.explain( |
line.explain( |
"It is more readable and prevents confusion with the shell variable of", |
"It is more readable and prevents confusion with the shell variable of", |
"the same name.") |
"the same name.") |
|
|
case replacePrefix(&rest, &m, `^\$\$@`): |
case repl.startsWith(`^\$\$@`): |
line.warnf("The $@ shell variable should only be used in double quotes.") |
line.warnf("The $@ shell variable should only be used in double quotes.") |
|
|
case replacePrefix(&rest, &m, `^\$\$\?`): |
case repl.startsWith(`^\$\$\?`): |
line.warnf("The $? shell variable is often not available in \"set -e\" mode.") |
line.warnf("The $? shell variable is often not available in \"set -e\" mode.") |
|
|
case replacePrefix(&rest, &m, `^\$\$\(`): |
case repl.startsWith(`^\$\$\(`): |
line.warnf("Invoking subshells via $(...) is not portable enough.") |
line.warnf("Invoking subshells via $(...) is not portable enough.") |
line.explain( |
line.explain( |
"The Solaris /bin/sh does not know this way to execute a command in a", |
"The Solaris /bin/sh does not know this way to execute a command in a", |
|
|
|
|
case state == swstSquot: |
case state == swstSquot: |
switch { |
switch { |
case replacePrefix(&rest, &m, `^'`): |
case repl.startsWith(`^'`): |
state = swstPlain |
state = swstPlain |
case replacePrefix(&rest, &m, `^[^\$\']+`): |
case repl.startsWith(`^[^\$\']+`): |
// just skip |
// just skip |
case replacePrefix(&rest, &m, `^\$\$`): |
case repl.startsWith(`^\$\$`): |
// just skip |
// just skip |
default: |
default: |
break outer |
break outer |
|
|
|
|
case state == swstDquot: |
case state == swstDquot: |
switch { |
switch { |
case replacePrefix(&rest, &m, `^"`): |
case repl.startsWith(`^"`): |
state = swstPlain |
state = swstPlain |
case replacePrefix(&rest, &m, "^`"): |
case repl.startsWith("^`"): |
state = swstDquotBackt |
state = swstDquotBackt |
case replacePrefix(&rest, &m, "^[^$\"\\\\`]+"): |
case repl.startsWith("^[^$\"\\\\`]+"): |
// just skip |
// just skip |
case replacePrefix(&rest, &m, "^\\\\(?:[\\\\\"`]|\\$\\$)"): |
case repl.startsWith("^\\\\(?:[\\\\\"`]|\\$\\$)"): |
// just skip |
// just skip |
case replacePrefix(&rest, &m, `^\$\$\{([0-9A-Za-z_]+)\}`), |
case repl.startsWith(`^\$\$\{([0-9A-Za-z_]+)\}`), |
replacePrefix(&rest, &m, `^\$\$([0-9A-Z_a-z]+|[!#?@]|\$\$)`): |
repl.startsWith(`^\$\$([0-9A-Z_a-z]+|[!#?@]|\$\$)`): |
shvarname := m[1] |
shvarname := repl.m[1] |
_ = G.opts.DebugShell && line.debugf("checklineMkShellword: found double-quoted variable %q.", shvarname) |
_ = G.opts.DebugShell && line.debugf("checklineMkShellword: found double-quoted variable %q.", shvarname) |
case replacePrefix(&rest, &m, `^\$\$`): |
case repl.startsWith(`^\$\$`): |
line.warnf("Unquoted $ or strange shell variable found.") |
line.warnf("Unquoted $ or strange shell variable found.") |
case replacePrefix(&rest, &m, `^\\(.)`): |
case repl.startsWith(`^\\(.)`): |
char := m[1] |
char := repl.m[1] |
line.warnf("Please use \"%s\" instead of \"%s\".", "\\\\"+char, "\\"+char) |
line.warnf("Please use \"%s\" instead of \"%s\".", "\\\\"+char, "\\"+char) |
line.explain( |
line.explain( |
"Although the current code may work, it is not good style to rely on", |
"Although the current code may work, it is not good style to rely on", |
|
|
} |
} |
} |
} |
|
|
if strings.TrimSpace(rest) != "" { |
if strings.TrimSpace(repl.rest) != "" { |
line.errorf("Internal pkglint error: checklineMkShellword state=%s, rest=%q, shellword=%q", state, rest, shellword) |
line.errorf("Internal pkglint error: checklineMkShellword state=%s, rest=%q, shellword=%q", state, repl.rest, shellword) |
|
} |
|
} |
|
|
|
// 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 (msline *MkShellLine) unescapeBackticks(shellword string, repl *PrefixReplacer, state ShellwordState) (unescaped string, newState ShellwordState) { |
|
line := msline.line |
|
for repl.rest != "" { |
|
switch { |
|
case repl.startsWith("^`"): |
|
if state == swstBackt { |
|
state = swstPlain |
|
} else { |
|
state = swstDquot |
|
} |
|
return unescaped, state |
|
|
|
case repl.startsWith("^\\\\([\\\\`$])"): |
|
unescaped += repl.m[1] |
|
|
|
case repl.startsWith(`^(\\)`): |
|
line.warnf("Backslashes should be doubled inside backticks.") |
|
unescaped += repl.m[1] |
|
|
|
case state == swstDquotBackt && repl.startsWith(`^"`): |
|
line.warnf("Double quotes inside backticks inside double quotes are error prone.") |
|
line.explain( |
|
"According to the SUSv3, they produce undefined results.", |
|
"", |
|
"See the paragraph starting \"Within the backquoted ...\" in", |
|
"http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html") |
|
|
|
case repl.startsWith("^([^\\\\`]+)"): |
|
unescaped += repl.m[1] |
|
|
|
default: |
|
line.errorf("Internal pkglint error: checklineMkShellword shellword=%q rest=%q", shellword, repl.rest) |
|
} |
} |
} |
|
line.errorf("Unfinished backquotes: rest=%q", repl.rest) |
|
return unescaped, state |
} |
} |
|
|
func (msline *MkShellLine) variableNeedsQuoting(shvarname string) bool { |
func (msline *MkShellLine) variableNeedsQuoting(shvarname string) bool { |
Line 372 func (msline *MkShellLine) checkShelltex |
|
Line 373 func (msline *MkShellLine) checkShelltex |
|
line.notef("You don't need to use \"-\" before %q.", cmd) |
line.notef("You don't need to use \"-\" before %q.", cmd) |
} |
} |
|
|
rest := shelltext |
|
|
|
setE := false |
setE := false |
var m []string |
repl := NewPrefixReplacer(shelltext) |
if replacePrefix(&rest, &m, `^\s*([-@]*)(\$\{_PKG_SILENT\}\$\{_PKG_DEBUG\}|\$\{RUN\}|)`) { |
if repl.startsWith(`^\s*([-@]*)(\$\{_PKG_SILENT\}\$\{_PKG_DEBUG\}|\$\{RUN\}|)`) { |
hidden, macro := m[1], m[2] |
hidden, macro := repl.m[1], repl.m[2] |
msline.checkLineStart(hidden, macro, rest, &setE) |
msline.checkLineStart(hidden, macro, repl.rest, &setE) |
} |
} |
|
|
state := scstStart |
state := scstStart |
for replacePrefix(&rest, &m, reShellword) { |
for repl.startsWith(reShellword) { |
shellword := m[1] |
shellword := repl.m[1] |
|
|
_ = G.opts.DebugShell && line.debugf("checklineMkShelltext state=%v shellword=%q", state, shellword) |
_ = G.opts.DebugShell && line.debugf("checklineMkShelltext state=%v shellword=%q", state, shellword) |
|
|
Line 416 func (msline *MkShellLine) checkShelltex |
|
Line 415 func (msline *MkShellLine) checkShelltex |
|
state = nextState(line, state, shellword) |
state = nextState(line, state, shellword) |
} |
} |
|
|
if !matches(rest, `^\s*$`) { |
repl.startsWith(`^\s+`) |
line.errorf("Internal pkglint error: checklineMkShelltext state=%s rest=%q shellword=%q", state, rest, shelltext) |
if repl.rest != "" { |
|
line.errorf("Internal pkglint error: checklineMkShelltext state=%s rest=%q shellword=%q", state, repl.rest, shelltext) |
} |
} |
|
|
} |
} |
Line 862 func nextState(line *Line, state scState |
|
Line 862 func nextState(line *Line, state scState |
|
func splitIntoShellwords(line *Line, text string) ([]string, string) { |
func splitIntoShellwords(line *Line, text string) ([]string, string) { |
var words []string |
var words []string |
|
|
rest := text |
repl := NewPrefixReplacer(text) |
var m []string |
for repl.startsWith(reShellword) { |
for replacePrefix(&rest, &m, reShellword) { |
words = append(words, repl.m[1]) |
words = append(words, m[1]) |
|
} |
} |
|
repl.startsWith(`^\s+`) |
rest = strings.TrimLeftFunc(rest, unicode.IsSpace) |
return words, repl.rest |
return words, rest |
|
} |
} |