pkgsrc-ng/pkgtools/pkglint/files/mkshparser_test.go
2016-11-18 22:39:22 +01:00

636 lines
19 KiB
Go

package main
import (
"encoding/json"
"gopkg.in/check.v1"
"strconv"
)
type ShSuite struct {
c *check.C
}
var _ = check.Suite(&ShSuite{})
func (s *ShSuite) Test_ShellParser_program(c *check.C) {
b := s.init(c)
s.test("",
b.List())
s.test("echo ;",
b.List().AddCommand(b.SimpleCommand("echo")).AddSemicolon())
s.test("echo",
b.List().AddCommand(b.SimpleCommand("echo")))
s.test(""+
"cd ${WRKSRC} && ${FIND} ${${_list_}} -type f ! -name '*.orig' 2> /dev/null "+
"| pax -rw -pm ${DESTDIR}${PREFIX}/${${_dir_}}",
b.List().AddAndOr(b.AndOr(
b.Pipeline(false, b.SimpleCommand("cd", "${WRKSRC}"))).Add("&&",
b.Pipeline(false,
b.SimpleCommand("${FIND}", "${${_list_}}", "-type", "f", "!", "-name", "'*.orig'", "2>/dev/null"),
b.SimpleCommand("pax", "-rw", "-pm", "${DESTDIR}${PREFIX}/${${_dir_}}")))))
s.test("${CAT} ${PKGDIR}/PLIST | while read entry ; do : ; done",
b.List().AddAndOr(b.AndOr(b.Pipeline(false,
b.SimpleCommand("${CAT}", "${PKGDIR}/PLIST"),
b.While(
b.List().AddCommand(b.SimpleCommand("read", "entry")).AddSemicolon(),
b.List().AddCommand(b.SimpleCommand(":")).AddSemicolon())))))
s.test("while read entry ; do case \"$$entry\" in include/c-client/* ) ${INSTALL_DATA} $$src $$dest ; esac ; done",
b.List().AddCommand(b.While(
b.List().AddCommand(b.SimpleCommand("read", "entry")).AddSemicolon(),
b.List().AddCommand(b.Case(
b.Token("\"$$entry\""),
b.CaseItem(
b.Words("include/c-client/*"),
b.List().AddCommand(b.SimpleCommand("${INSTALL_DATA}", "$$src", "$$dest")),
sepSemicolon))).AddSemicolon())))
s.test("command | while condition ; do case selector in pattern ) : ; esac ; done",
b.List().AddAndOr(b.AndOr(b.Pipeline(false,
b.SimpleCommand("command"),
b.While(
b.List().AddCommand(b.SimpleCommand("condition")).AddSemicolon(),
b.List().AddCommand(b.Case(
b.Token("selector"),
b.CaseItem(
b.Words("pattern"),
b.List().AddCommand(b.SimpleCommand(":")),
sepSemicolon))).AddSemicolon())))))
s.test("command1 \n command2 \n command3",
b.List().
AddCommand(b.SimpleCommand("command1")).
AddNewline().
AddCommand(b.SimpleCommand("command2")).
AddNewline().
AddCommand(b.SimpleCommand("command3")))
s.test("if condition; then action; else case selector in pattern) case-item-action ;; esac; fi",
b.List().AddCommand(b.If(
b.List().AddCommand(b.SimpleCommand("condition")).AddSemicolon(),
b.List().AddCommand(b.SimpleCommand("action")).AddSemicolon(),
b.List().AddCommand(b.Case(
b.Token("selector"),
b.CaseItem(
b.Words("pattern"),
b.List().AddCommand(b.SimpleCommand("case-item-action")), sepNone))).AddSemicolon())))
}
func (s *ShSuite) Test_ShellParser_list(c *check.C) {
b := s.init(c)
s.test("echo1 && echo2",
b.List().AddAndOr(
b.AndOr(b.Pipeline(false, b.SimpleCommand("echo1"))).
Add("&&", b.Pipeline(false, b.SimpleCommand("echo2")))))
s.test("echo1 ; echo2",
b.List().
AddCommand(b.SimpleCommand("echo1")).
AddSemicolon().
AddCommand(b.SimpleCommand("echo2")))
s.test("echo1 ; echo2 &",
b.List().
AddCommand(b.SimpleCommand("echo1")).
AddSemicolon().
AddCommand(b.SimpleCommand("echo2")).
AddBackground())
}
func (s *ShSuite) Test_ShellParser_and_or(c *check.C) {
b := s.init(c)
s.test("echo1 | echo2",
b.List().AddAndOr(b.AndOr(b.Pipeline(false,
b.SimpleCommand("echo1"),
b.SimpleCommand("echo2")))))
s.test("echo1 | echo2 && echo3 | echo4",
b.List().AddAndOr(b.AndOr(
b.Pipeline(false,
b.SimpleCommand("echo1"),
b.SimpleCommand("echo2")),
).Add("&&",
b.Pipeline(false,
b.SimpleCommand("echo3"),
b.SimpleCommand("echo4")))))
s.test("echo1 | echo2 || ! echo3 | echo4",
b.List().AddAndOr(b.AndOr(
b.Pipeline(false,
b.SimpleCommand("echo1"),
b.SimpleCommand("echo2")),
).Add("||",
b.Pipeline(true,
b.SimpleCommand("echo3"),
b.SimpleCommand("echo4")))))
}
func (s *ShSuite) Test_ShellParser_pipeline(c *check.C) {
b := s.init(c)
s.test("command1 | command2",
b.List().AddAndOr(b.AndOr(b.Pipeline(false,
b.SimpleCommand("command1"),
b.SimpleCommand("command2")))))
s.test("! command1 | command2",
b.List().AddAndOr(b.AndOr(b.Pipeline(true,
b.SimpleCommand("command1"),
b.SimpleCommand("command2")))))
}
func (s *ShSuite) Test_ShellParser_pipe_sequence(c *check.C) {
b := s.init(c)
s.test("command1 | if true ; then : ; fi",
b.List().AddAndOr(b.AndOr(b.Pipeline(false,
b.SimpleCommand("command1"),
b.If(
b.List().AddCommand(b.SimpleCommand("true")).AddSemicolon(),
b.List().AddCommand(b.SimpleCommand(":")).AddSemicolon())))))
}
func (s *ShSuite) Test_ShellParser_command(c *check.C) {
b := s.init(c)
s.test("simple_command",
b.List().AddAndOr(b.AndOr(b.Pipeline(false, b.SimpleCommand("simple_command")))))
s.test("while 1; do 2; done",
b.List().AddAndOr(b.AndOr(b.Pipeline(false,
b.While(
b.List().AddCommand(b.SimpleCommand("1")).AddSemicolon(),
b.List().AddCommand(b.SimpleCommand("2")).AddSemicolon())))))
s.test("while 1; do 2; done 1>&2",
b.List().AddAndOr(b.AndOr(b.Pipeline(false,
b.While(
b.List().AddCommand(b.SimpleCommand("1")).AddSemicolon(),
b.List().AddCommand(b.SimpleCommand("2")).AddSemicolon(),
b.Redirection(1, ">&", "2"))))))
s.test("func(){ echo hello;} 2>&1",
b.List().AddCommand(b.Function(
"func",
b.Brace(b.List().AddCommand(b.SimpleCommand("echo", "hello")).AddSemicolon()).Compound,
b.Redirection(2, ">&", "1"))))
}
func (s *ShSuite) Test_ShellParser_compound_command(c *check.C) {
b := s.init(c)
s.test("{ brace ; }",
b.List().AddCommand(b.Brace(
b.List().AddCommand(b.SimpleCommand("brace")).AddSemicolon())))
s.test("( subshell )",
b.List().AddCommand(b.Subshell(
b.List().AddCommand(b.SimpleCommand("subshell")))))
s.test("for i in * ; do echo $i ; done",
b.List().AddCommand(b.For(
"i",
b.Words("*"),
b.List().AddCommand(b.SimpleCommand("echo", "$i")).AddSemicolon())))
s.test("case $i in esac",
b.List().AddCommand(b.Case(
b.Token("$i"))))
}
func (s *ShSuite) Test_ShellParser_subshell(c *check.C) {
b := s.init(c)
sub3 := b.Subshell(b.List().AddCommand(b.SimpleCommand("sub3")))
sub2 := b.Subshell(b.List().AddCommand(sub3).AddSemicolon().AddCommand(b.SimpleCommand("sub2")))
sub1 := b.Subshell(b.List().AddCommand(sub2).AddSemicolon().AddCommand(b.SimpleCommand("sub1")))
s.test("( ( ( sub3 ) ; sub2 ) ; sub1 )", b.List().AddCommand(sub1))
}
func (s *ShSuite) Test_ShellParser_compound_list(c *check.C) {
b := s.init(c)
s.test("( \n echo )",
b.List().AddCommand(b.Subshell(
b.List().AddCommand(b.SimpleCommand("echo")))))
}
func (s *ShSuite) Test_ShellParser_term(c *check.C) {
b := s.init(c)
_ = b
}
func (s *ShSuite) Test_ShellParser_for_clause(c *check.C) {
b := s.init(c)
s.test("for var do echo $var ; done",
b.List().AddCommand(b.For(
"var",
b.Words("\"$$@\""),
b.List().AddCommand(b.SimpleCommand("echo", "$var")).AddSemicolon())))
// Only linebreak is allowed, but not semicolon.
s.test("for var \n do echo $var ; done",
b.List().AddCommand(b.For(
"var",
b.Words("\"$$@\""),
b.List().AddCommand(b.SimpleCommand("echo", "$var")).AddSemicolon())))
s.test("for var in a b c ; do echo $var ; done",
b.List().AddCommand(b.For(
"var",
b.Words("a", "b", "c"),
b.List().AddCommand(b.SimpleCommand("echo", "$var")).AddSemicolon())))
s.test("for var \n \n \n in a b c ; do echo $var ; done",
b.List().AddCommand(b.For(
"var",
b.Words("a", "b", "c"),
b.List().AddCommand(b.SimpleCommand("echo", "$var")).AddSemicolon())))
s.test("for var in in esac ; do echo $var ; done",
b.List().AddCommand(b.For(
"var",
b.Words("in", "esac"),
b.List().AddCommand(b.SimpleCommand("echo", "$var")).AddSemicolon())))
// No semicolon necessary between the two “done”.
s.test("for i in 1; do for j in 1; do echo $$i$$j; done done",
b.List().AddCommand(b.For(
"i",
b.Words("1"),
b.List().AddCommand(b.For(
"j",
b.Words("1"),
b.List().AddCommand(b.SimpleCommand("echo", "$$i$$j")).AddSemicolon())))))
}
func (s *ShSuite) Test_ShellParser_case_clause(c *check.C) {
b := s.init(c)
s.test("case $var in esac",
b.List().AddCommand(b.Case(b.Token("$var"))))
s.test("case selector in pattern) ;; pattern) esac",
b.List().AddCommand(b.Case(
b.Token("selector"),
b.CaseItem(b.Words("pattern"), b.List(), sepNone),
b.CaseItem(b.Words("pattern"), b.List(), sepNone))))
s.test("case $$i in *.c | *.h ) echo C ;; * ) echo Other ; esac",
b.List().AddCommand(b.Case(
b.Token("$$i"),
b.CaseItem(b.Words("*.c", "*.h"), b.List().AddCommand(b.SimpleCommand("echo", "C")), sepNone),
b.CaseItem(b.Words("*"), b.List().AddCommand(b.SimpleCommand("echo", "Other")), sepSemicolon))))
s.test("case $$i in *.c ) echo ; esac",
b.List().AddCommand(b.Case(
b.Token("$$i"),
b.CaseItem(b.Words("*.c"), b.List().AddCommand(b.SimpleCommand("echo")), sepSemicolon))))
s.test("case selector in pattern) case-item-action ; esac",
b.List().AddCommand(b.Case(
b.Token("selector"),
b.CaseItem(
b.Words("pattern"),
b.List().AddCommand(b.SimpleCommand("case-item-action")), sepSemicolon))))
s.test("case selector in pattern) case-item-action ;; esac",
b.List().AddCommand(b.Case(
b.Token("selector"),
b.CaseItem(
b.Words("pattern"),
b.List().AddCommand(b.SimpleCommand("case-item-action")), sepNone))))
}
func (s *ShSuite) Test_ShellParser_if_clause(c *check.C) {
b := s.init(c)
s.test(
"if true ; then echo yes ; else echo no ; fi",
b.List().AddCommand(b.If(
b.List().AddCommand(b.SimpleCommand("true")).AddSemicolon(),
b.List().AddCommand(b.SimpleCommand("echo", "yes")).AddSemicolon(),
b.List().AddCommand(b.SimpleCommand("echo", "no")).AddSemicolon())))
// No semicolon necessary between the two “fi”.
s.test("if cond1; then if cond2; then action; fi fi",
b.List().AddCommand(b.If(
b.List().AddCommand(b.SimpleCommand("cond1")).AddSemicolon(),
b.List().AddCommand(b.If(
b.List().AddCommand(b.SimpleCommand("cond2")).AddSemicolon(),
b.List().AddCommand(b.SimpleCommand("action")).AddSemicolon())))))
}
func (s *ShSuite) Test_ShellParser_while_clause(c *check.C) {
b := s.init(c)
s.test("while condition ; do action ; done",
b.List().AddCommand(b.While(
b.List().AddCommand(b.SimpleCommand("condition")).AddSemicolon(),
b.List().AddCommand(b.SimpleCommand("action")).AddSemicolon())))
}
func (s *ShSuite) Test_ShellParser_until_clause(c *check.C) {
b := s.init(c)
s.test("until condition ; do action ; done",
b.List().AddCommand(b.Until(
b.List().AddCommand(b.SimpleCommand("condition")).AddSemicolon(),
b.List().AddCommand(b.SimpleCommand("action")).AddSemicolon())))
}
func (s *ShSuite) Test_ShellParser_function_definition(c *check.C) {
b := s.init(c)
_ = b
}
func (s *ShSuite) Test_ShellParser_brace_group(c *check.C) {
b := s.init(c)
// No semicolon necessary after the closing brace.
s.test("if true; then { echo yes; } fi",
b.List().AddCommand(b.If(
b.List().AddCommand(b.SimpleCommand("true")).AddSemicolon(),
b.List().AddCommand(b.Brace(
b.List().AddCommand(b.SimpleCommand("echo", "yes")).AddSemicolon())))))
}
func (s *ShSuite) Test_ShellParser_simple_command(c *check.C) {
b := s.init(c)
s.test(
"echo hello, world",
b.List().AddCommand(b.SimpleCommand("echo", "hello,", "world")))
s.test("echo ${PKGNAME:Q}",
b.List().AddCommand(b.SimpleCommand("echo", "${PKGNAME:Q}")))
s.test("${ECHO} \"Double-quoted\" 'Single-quoted'",
b.List().AddCommand(b.SimpleCommand("${ECHO}", "\"Double-quoted\"", "'Single-quoted'")))
s.test("`cat plain` \"`cat double`\" '`cat single`'",
b.List().AddCommand(b.SimpleCommand("`cat plain`", "\"`cat double`\"", "'`cat single`'")))
s.test("`\"one word\"`",
b.List().AddCommand(b.SimpleCommand("`\"one word\"`")))
s.test("PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\"",
b.List().AddCommand(b.SimpleCommand("PAGES=\"`ls -1 | ${SED} -e 's,3qt$$,3,'`\"")))
s.test("var=Plain var=\"Dquot\" var='Squot' var=Plain\"Dquot\"'Squot'",
b.List().AddCommand(b.SimpleCommand("var=Plain", "var=\"Dquot\"", "var='Squot'", "var=Plain\"Dquot\"'Squot'")))
// RUN is a special Make variable since it ends with a semicolon;
// therefore it needs to be split off before passing the rest of
// the command to the shell command parser.
s.test("${RUN} subdir=\"`unzip -c \"$$e\" install.rdf | awk '/re/ { print \"hello\" }'`\"",
b.List().AddCommand(b.SimpleCommand("${RUN}", "subdir=\"`unzip -c \"$$e\" install.rdf | awk '/re/ { print \"hello\" }'`\"")))
s.test("PATH=/nonexistent env PATH=${PATH:Q} true",
b.List().AddCommand(b.SimpleCommand("PATH=/nonexistent", "env", "PATH=${PATH:Q}", "true")))
s.test("{OpenGrok args",
b.List().AddCommand(b.SimpleCommand("{OpenGrok", "args")))
}
func (s *ShSuite) Test_ShellParser_io_redirect(c *check.C) {
b := s.init(c)
s.test("echo >> ${PLIST_SRC}",
b.List().AddCommand(b.SimpleCommand("echo", ">>${PLIST_SRC}")))
s.test("echo >> ${PLIST_SRC}",
b.List().AddCommand(b.SimpleCommand("echo", ">>${PLIST_SRC}")))
s.test("echo 1>output 2>>append 3>|clobber 4>&5 6<input >>append",
b.List().AddCommand(&MkShCommand{Simple: &MkShSimpleCommand{
Assignments: nil,
Name: b.Token("echo"),
Args: nil,
Redirections: []*MkShRedirection{
{1, ">", b.Token("output")},
{2, ">>", b.Token("append")},
{3, ">|", b.Token("clobber")},
{4, ">&", b.Token("5")},
{6, "<", b.Token("input")},
{-1, ">>", b.Token("append")}}}}))
s.test("echo 1> output 2>> append 3>| clobber 4>& 5 6< input >> append",
b.List().AddCommand(&MkShCommand{Simple: &MkShSimpleCommand{
Assignments: nil,
Name: b.Token("echo"),
Args: nil,
Redirections: []*MkShRedirection{
{1, ">", b.Token("output")},
{2, ">>", b.Token("append")},
{3, ">|", b.Token("clobber")},
{4, ">&", b.Token("5")},
{6, "<", b.Token("input")},
{-1, ">>", b.Token("append")}}}}))
s.test("${MAKE} print-summary-data 2>&1 > /dev/stderr",
b.List().AddCommand(&MkShCommand{Simple: &MkShSimpleCommand{
Assignments: nil,
Name: b.Token("${MAKE}"),
Args: []*ShToken{b.Token("print-summary-data")},
Redirections: []*MkShRedirection{
{2, ">&", b.Token("1")},
{-1, ">", b.Token("/dev/stderr")}}}}))
}
func (s *ShSuite) Test_ShellParser_io_here(c *check.C) {
b := s.init(c)
_ = b
}
func (s *ShSuite) init(c *check.C) *MkShBuilder {
s.c = c
return NewMkShBuilder()
}
func (s *ShSuite) test(program string, expected *MkShList) {
tokens, rest := splitIntoShellTokens(dummyLine, program)
s.c.Check(rest, equals, "")
lexer := &ShellLexer{
current: "",
remaining: tokens,
atCommandStart: true,
error: ""}
parser := &shyyParserImpl{}
succeeded := parser.Parse(lexer)
c := s.c
if ok1, ok2 := c.Check(succeeded, equals, 0), c.Check(lexer.error, equals, ""); ok1 && ok2 {
if !c.Check(lexer.result, deepEquals, expected) {
actualJSON, actualErr := json.MarshalIndent(lexer.result, "", " ")
expectedJSON, expectedErr := json.MarshalIndent(expected, "", " ")
if c.Check(actualErr, check.IsNil) && c.Check(expectedErr, check.IsNil) {
c.Check(string(actualJSON), deepEquals, string(expectedJSON))
}
}
} else {
c.Check(lexer.remaining, deepEquals, []string{})
}
}
func (s *ShSuite) Test_ShellLexer_Lex__redirects(c *check.C) {
tokens, rest := splitIntoShellTokens(dummyLine, "${MAKE} print-summary-data 2>&1 > /dev/stderr")
c.Check(tokens, deepEquals, []string{"${MAKE}", "print-summary-data", "2>&", "1", ">", "/dev/stderr"})
c.Check(rest, equals, "")
lexer := NewShellLexer(tokens, rest)
var llval shyySymType
c.Check(lexer.Lex(&llval), equals, tkWORD)
c.Check(llval.Word.MkText, equals, "${MAKE}")
c.Check(lexer.Lex(&llval), equals, tkWORD)
c.Check(llval.Word.MkText, equals, "print-summary-data")
c.Check(lexer.Lex(&llval), equals, tkIO_NUMBER)
c.Check(llval.IONum, equals, 2)
c.Check(lexer.Lex(&llval), equals, tkGTAND)
c.Check(lexer.Lex(&llval), equals, tkWORD)
c.Check(llval.Word.MkText, equals, "1")
c.Check(lexer.Lex(&llval), equals, tkGT)
c.Check(lexer.Lex(&llval), equals, tkWORD)
c.Check(llval.Word.MkText, equals, "/dev/stderr")
c.Check(lexer.Lex(&llval), equals, 0)
}
type MkShBuilder struct {
}
func NewMkShBuilder() *MkShBuilder {
return &MkShBuilder{}
}
func (b *MkShBuilder) List() *MkShList {
return NewMkShList()
}
func (b *MkShBuilder) AndOr(pipeline *MkShPipeline) *MkShAndOr {
return NewMkShAndOr(pipeline)
}
func (b *MkShBuilder) Pipeline(negated bool, cmds ...*MkShCommand) *MkShPipeline {
return NewMkShPipeline(negated, cmds...)
}
func (b *MkShBuilder) SimpleCommand(words ...string) *MkShCommand {
cmd := &MkShSimpleCommand{}
assignments := true
for _, word := range words {
if assignments && matches(word, `^\w+=`) {
cmd.Assignments = append(cmd.Assignments, b.Token(word))
} else if m, fdstr, op, rest := match3(word, `^(\d*)(<<-|<<|<&|>>|>&|>\||<|>)(.*)$`); m {
fd, err := strconv.Atoi(fdstr)
if err != nil {
fd = -1
}
cmd.Redirections = append(cmd.Redirections, b.Redirection(fd, op, rest))
} else {
assignments = false
if cmd.Name == nil {
cmd.Name = b.Token(word)
} else {
cmd.Args = append(cmd.Args, b.Token(word))
}
}
}
return &MkShCommand{Simple: cmd}
}
func (b *MkShBuilder) If(condActionElse ...*MkShList) *MkShCommand {
ifclause := &MkShIfClause{}
for i, part := range condActionElse {
if i%2 == 0 && i != len(condActionElse)-1 {
ifclause.Conds = append(ifclause.Conds, part)
} else if i%2 == 1 {
ifclause.Actions = append(ifclause.Actions, part)
} else {
ifclause.Else = part
}
}
return &MkShCommand{Compound: &MkShCompoundCommand{If: ifclause}}
}
func (b *MkShBuilder) For(varname string, items []*ShToken, action *MkShList) *MkShCommand {
return &MkShCommand{Compound: &MkShCompoundCommand{For: &MkShForClause{varname, items, action}}}
}
func (b *MkShBuilder) Case(selector *ShToken, items ...*MkShCaseItem) *MkShCommand {
return &MkShCommand{Compound: &MkShCompoundCommand{Case: &MkShCaseClause{selector, items}}}
}
func (b *MkShBuilder) CaseItem(patterns []*ShToken, action *MkShList, separator MkShSeparator) *MkShCaseItem {
return &MkShCaseItem{patterns, action, separator}
}
func (b *MkShBuilder) While(cond, action *MkShList, redirects ...*MkShRedirection) *MkShCommand {
return &MkShCommand{
Compound: &MkShCompoundCommand{
Loop: &MkShLoopClause{cond, action, false}},
Redirects: redirects}
}
func (b *MkShBuilder) Until(cond, action *MkShList, redirects ...*MkShRedirection) *MkShCommand {
return &MkShCommand{
Compound: &MkShCompoundCommand{
Loop: &MkShLoopClause{cond, action, true}},
Redirects: redirects}
}
func (b *MkShBuilder) Function(name string, body *MkShCompoundCommand, redirects ...*MkShRedirection) *MkShCommand {
return &MkShCommand{
FuncDef: &MkShFunctionDefinition{name, body},
Redirects: redirects}
}
func (b *MkShBuilder) Brace(list *MkShList) *MkShCommand {
return &MkShCommand{Compound: &MkShCompoundCommand{Brace: list}}
}
func (b *MkShBuilder) Subshell(list *MkShList) *MkShCommand {
return &MkShCommand{Compound: &MkShCompoundCommand{Subshell: list}}
}
func (b *MkShBuilder) Token(mktext string) *ShToken {
tokenizer := NewShTokenizer(dummyLine, mktext, false)
token := tokenizer.ShToken()
return token
}
func (b *MkShBuilder) Words(words ...string) []*ShToken {
tokens := make([]*ShToken, len(words))
for i, word := range words {
tokens[i] = b.Token(word)
}
return tokens
}
func (b *MkShBuilder) Redirection(fd int, op string, target string) *MkShRedirection {
return &MkShRedirection{fd, op, b.Token(target)}
}