diff --git a/commands/shell/quotes.go b/commands/shell/quotes.go index c396cb1..e04af48 100644 --- a/commands/shell/quotes.go +++ b/commands/shell/quotes.go @@ -12,6 +12,9 @@ func (p *QuoteParameters) SubParsers() []Parser { &BaseParser{ parameters: &DollarParameters{}, }, + &BaseParser{ + parameters: &BacktickParameters{}, + }, &BaseParser{ parameters: &CharParserParameters{}, }, diff --git a/commands/shell/quotes_test.go b/commands/shell/quotes_test.go index 00bee62..8fff632 100644 --- a/commands/shell/quotes_test.go +++ b/commands/shell/quotes_test.go @@ -27,7 +27,8 @@ func (t *QuotesTest) TestParse() { t.NoError(err) expanded, _ := token.(Expandable).Expand(nil, nil, nil) - t.Equal("word1 word2 word3\n word4", strings.Join(expanded, " ")) + t.Equal(1, len(expanded)) + t.Equal("word1 word2 word3\n word4", expanded[0]) } func (t *QuotesTest) TestExpandVariables() { @@ -72,6 +73,52 @@ func (t *QuotesTest) TestExpandSubshell() { t.Equal("This is a subshell", expanded[0]) } +func (t *QuotesTest) TestExpandBacktick() { + parser := &BaseParser{&QuoteParameters{}} + reader := bufio.NewReader( + strings.NewReader("\"This is `echo a backtick`\""), + ) + + state := NewShellState() + + echo := &builtins.Echo{} + state.SetCommand(echo.Name(), echo) + + iterator, err := NewCharIterator(reader) + t.NoError(err) + + token, err := parser.Parse(iterator, &CharCollection{}) + t.NoError(err) + + expanded, _ := token.(Expandable).Expand(state, nil, nil) + t.Equal(1, len(expanded)) + t.Equal("This is a backtick", expanded[0]) +} + +// The only way backticks can be nested at the moment is by wrapping them in +// quotes. +func (t *QuotesTest) TestExpandNestedBacktick() { + parser := &BaseParser{&QuoteParameters{}} + reader := bufio.NewReader( + strings.NewReader("\"This is `echo a \"`echo backtick`\"`\""), + ) + + state := NewShellState() + + echo := &builtins.Echo{} + state.SetCommand(echo.Name(), echo) + + iterator, err := NewCharIterator(reader) + t.NoError(err) + + token, err := parser.Parse(iterator, &CharCollection{}) + t.NoError(err) + + expanded, _ := token.(Expandable).Expand(state, nil, nil) + t.Equal(1, len(expanded)) + t.Equal("This is a backtick", expanded[0]) +} + func TestQuotesTest(t *testing.T) { suite.Run(t, new(QuotesTest)) } diff --git a/commands/shell/subshell.go b/commands/shell/subshell.go index 99c5246..7058125 100644 --- a/commands/shell/subshell.go +++ b/commands/shell/subshell.go @@ -1,6 +1,7 @@ package shell import ( + "bytes" "io" "log" ) @@ -64,11 +65,17 @@ func (s *Subshell) Evaluate(state State, stdin io.Reader, stdout io.Writer, stde } type Backtick struct { - Subshell + BaseToken } -type BacktickParameters struct { - SubshellParameters +type BacktickParameters struct{} + +func (p *BacktickParameters) MakeToken() Token { + return &Backtick{} +} + +func (p *BacktickParameters) Enter(i *CharIterator) error { + return i.Next() } func (p *BacktickParameters) Supports(charsBefore []rune, r rune) bool { @@ -88,3 +95,53 @@ func (p *BacktickParameters) ShouldLeave(charsBefore []rune, r rune) bool { return false } + +func (b *Backtick) Expand(state State, stdin io.Reader, stderr io.Writer) ([]string, uint8) { + var ret uint8 + stdout := bytes.NewBuffer([]byte{}) + + for _, token := range b.Tokens() { + if eval, ok := token.(Evalable); ok { + ret = state.Eval(eval, stdin, stdout, stderr) + } else { + logstr := "shell: Unexpected token: " + token.String() + log.Println(logstr) + stderr.Write([]byte(logstr)) + } + } + + return []string{stdout.String()}, ret +} + +func (p *BacktickParameters) SubParsers() []Parser { + return []Parser{ + &BaseParser{ + parameters: &StatementInsideBacktickparameters{}, + }, + } +} + +type StatementInsideBacktickparameters struct { + StatementParameters +} + +// Note: this is a bit of a hack. There is no way to know whether the new +// backtick is supposed to open a new backtick statement within the backtick, or +// close the current one. At least not in the current implementation that +// doesn't allow us to look ahead. Which means that backticks inside backticks +// are not supported. +func (p *StatementInsideBacktickparameters) ShouldLeave(charsBefore []rune, r rune) bool { + if r == '\n' || r == ';' || r == ')' || r == '`' { + return countBackslashSuffixes(charsBefore)%2 == 0 + } + + return false +} + +func (p *StatementInsideBacktickparameters) Leave(i *CharIterator) error { + if i.Current() == '`' { + return i.Previous() + } + + return nil +}