diff --git a/interp/interp_test.go b/interp/interp_test.go index fab2b0e1c..36851f172 100644 --- a/interp/interp_test.go +++ b/interp/interp_test.go @@ -3117,6 +3117,20 @@ hello, world hello, world `, }, + { + // globbing wildcard as function name + `@() { echo "$@"; }; @ lala; function +() { echo "$@"; }; + foo`, + "lala\nfoo\n", + }, + { + ` @() { echo "$@"; }; @ lala;`, + "lala\n", + }, + { + // globbing wildcard as function name but with space after the name + `+ () { echo "$@"; }; + foo; @ () { echo "$@"; }; @ lala; ? () { echo "$@"; }; ? bar`, + "foo\nlala\nbar\n", + }, } var runTestsWindows = []runTest{ diff --git a/syntax/lexer.go b/syntax/lexer.go index 3e98b7110..08663318a 100644 --- a/syntax/lexer.go +++ b/syntax/lexer.go @@ -290,7 +290,7 @@ skipSpace: p.advanceLitNone(r) } case '?', '*', '+', '@', '!': - if p.peekByte('(') { + if p.tokenizeGlob() { switch r { case '?': p.tok = globQuest @@ -346,6 +346,28 @@ skipSpace: } } +// tokenizeGlob determines whether the expression should be tokenized as a glob literal +func (p *Parser) tokenizeGlob() bool { + if p.val == "function" { + return false + } + // NOTE: empty pattern list is a valid globbing syntax, eg @() + // but we'll operate on the "likelihood" that it is a function; + // only tokenize if its a non-empty pattern list + if p.peekBytes("()") { + return false + } + return p.peekByte('(') +} + +func (p *Parser) peekBytes(s string) bool { + for p.bsp+(len(p.bs)-1) >= len(p.bs) { + p.fill() + } + bw := p.bsp + len(s) + return bw < len(p.bs) && bytes.HasPrefix(p.bs[p.bsp:bw], []byte(s)) +} + func (p *Parser) peekByte(b byte) bool { if p.bsp == len(p.bs) { p.fill() @@ -882,7 +904,7 @@ loop: tok = _Lit break loop case '?', '*', '+', '@', '!': - if p.peekByte('(') { + if p.tokenizeGlob() { tok = _Lit break loop }