mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-29 17:58:54 +01:00
412 lines
9.9 KiB
Go
412 lines
9.9 KiB
Go
package shparse
|
|
|
|
import (
|
|
"fmt"
|
|
"unicode"
|
|
)
|
|
|
|
//
|
|
// cmds := cmd (sep cmd)*
|
|
// sep := ';' | '&' | '&&' | '||' | '|' | '\n'
|
|
// cmd := simple-cmd | compound-command redirect-list?
|
|
// compound-command := brace-group | subshell | for-clause | case-clause | if-clause | while-clause | until-clause
|
|
// brace-group := '{' cmds '}'
|
|
// subshell := '(' cmds ')'
|
|
// simple-command := cmd-prefix cmd-word (io-redirect)*
|
|
// cmd-prefix := (io-redirect | assignment)*
|
|
// cmd-suffix := (io-redirect | word)*
|
|
// cmd-name := word
|
|
// cmd-word := word
|
|
// io-redirect := (io-number? io-file) | (io-number? io-here)
|
|
// io-file := ('<' | '<&' | '>' | '>&' | '>>' | '>|' ) filename
|
|
// io-here := ('<<' | '<<-') here_end
|
|
// here-end := word
|
|
// if-clause := 'if' compound-list 'then' compound-list else-part 'fi'
|
|
// else-part := 'elif' compound-list 'then' compound-list
|
|
// | 'elif' compount-list 'then' compound-list else-part
|
|
// | 'else' compound-list
|
|
// compound-list := linebreak term sep?
|
|
//
|
|
//
|
|
//
|
|
// $var
|
|
// ${var}
|
|
// ${var op word?}
|
|
// op := '-' | '=' | '?' | '+' | ':-' | ':=' | ':?' | ':+' | '%' | '%%' | '#' | '##'
|
|
// ${ '#' var }
|
|
//
|
|
// $(command)
|
|
// `command`
|
|
// $(( arith ))
|
|
//
|
|
// " ... "
|
|
// ' ... '
|
|
// $' ... '
|
|
// $" ... '
|
|
|
|
// " => $, ", `, \
|
|
// ' => '
|
|
// (process quotes)
|
|
// mark as escaped
|
|
// split into commands (use ';' as separator)
|
|
// parse special operators
|
|
// perform expansions (vars, globs, commands)
|
|
// split command into name and arguments
|
|
|
|
// A correctly-formed brace expansion must contain unquoted opening and closing braces, and at least one unquoted comma or a valid sequence expression
|
|
// Any incorrectly formed brace expansion is left unchanged.
|
|
|
|
// word: char *word; flags
|
|
// bash aliases are lexical
|
|
|
|
// [[, ((, $(( <- DQ
|
|
|
|
// $ -> expansion
|
|
// $(...)
|
|
// (...)
|
|
// $((...))
|
|
// ((...))
|
|
// ${...}
|
|
// {...}
|
|
// X=(...)
|
|
|
|
// ambiguity between $((...)) and $((ls); ls)
|
|
// ambiguity between foo=([0]=hell) and foo=([abc)
|
|
|
|
// tokenization https://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html#tag_001_003
|
|
|
|
const (
|
|
WordTypeRaw = "raw"
|
|
WordTypeLit = "lit"
|
|
WordTypeOp = "op" // single: & ; | ( ) < > \n multi(2): && || ;; << >> <& >& <> >| (( multi(3): <<- ('((' requires special processing)
|
|
WordTypeKey = "key" // if then else elif fi do done case esac while until for in { } ! (( [[
|
|
WordTypeSimpleVar = "svar" // simplevar $
|
|
WordTypeGroup = "grp" // contains other words e.g. "hello"foo'bar'$x
|
|
|
|
WordTypeDQ = "dq" // " (quote-context)
|
|
WordTypeDDQ = "ddq" // $" (quote-context)
|
|
WordTypeVarBrace = "varb" // ${ (quote-context)
|
|
WordTypeDP = "dp" // $( (quote-context)
|
|
WordTypeBQ = "bq" // ` (quote-context)
|
|
|
|
WordTypeSQ = "sq" // '
|
|
WordTypeDSQ = "dsq" // $'
|
|
WordTypeDPP = "dpp" // $(( (internals not parsed)
|
|
WordTypePP = "pp" // (( (internals not parsed)
|
|
WordTypeDB = "db" // $[ (internals not parsed)
|
|
)
|
|
|
|
type quoteContext []string
|
|
|
|
func (qc quoteContext) push(q string) quoteContext {
|
|
rtn := make([]string, 0, len(qc)+1)
|
|
rtn = append(rtn, qc...)
|
|
rtn = append(rtn, q)
|
|
return rtn
|
|
}
|
|
|
|
func (qc quoteContext) cur() string {
|
|
if len(qc) == 0 {
|
|
return ""
|
|
}
|
|
return qc[len(qc)-1]
|
|
}
|
|
|
|
type parseContext struct {
|
|
Input []rune
|
|
Pos int
|
|
QC quoteContext
|
|
}
|
|
|
|
type wordType struct {
|
|
Type string
|
|
Offset int
|
|
QC quoteContext
|
|
Raw []rune
|
|
Complete bool
|
|
Val string // only for Op and Key (does *not* store string values of quoted expressions or expansions)
|
|
Prefix []rune
|
|
Subs []*wordType
|
|
}
|
|
|
|
func (c *parseContext) clone(pos int, newQuote string) *parseContext {
|
|
rtn := parseContext{Input: c.Input[pos:], QC: c.QC}
|
|
if newQuote != "" {
|
|
rtn.QC = rtn.QC.push(newQuote)
|
|
}
|
|
return &rtn
|
|
}
|
|
|
|
func (c *parseContext) at(offset int) rune {
|
|
pos := c.Pos + offset
|
|
if pos < 0 || pos >= len(c.Input) {
|
|
return 0
|
|
}
|
|
return c.Input[pos]
|
|
}
|
|
|
|
func (c *parseContext) eof() bool {
|
|
return c.Pos >= len(c.Input)
|
|
}
|
|
|
|
func (c *parseContext) cur() rune {
|
|
return c.at(0)
|
|
}
|
|
|
|
func (c *parseContext) match(ch rune) bool {
|
|
return c.at(0) == ch
|
|
}
|
|
|
|
func (c *parseContext) match2(ch rune, ch2 rune) bool {
|
|
return c.at(0) == ch && c.at(1) == ch2
|
|
}
|
|
|
|
func (c *parseContext) match3(ch rune, ch2 rune, ch3 rune) bool {
|
|
return c.at(0) == ch && c.at(1) == ch2 && c.at(2) == ch3
|
|
}
|
|
|
|
func (c *parseContext) makeWord(t string, length int, complete bool) *wordType {
|
|
rtn := &wordType{Type: t}
|
|
rtn.Offset = c.Pos
|
|
rtn.QC = c.QC
|
|
rtn.Raw = c.Input[c.Pos : c.Pos+length]
|
|
rtn.Complete = complete
|
|
c.Pos += length
|
|
return rtn
|
|
}
|
|
|
|
// returns (found, newOffset)
|
|
// shell_meta_chars "()<>;&|"
|
|
// possible to maybe add ;;& &>> &> |& ;&
|
|
func (c *parseContext) parseOp(offset int) (bool, int) {
|
|
ch := c.at(offset)
|
|
if ch == '(' || ch == ')' || ch == '<' || ch == '>' || ch == ';' || ch == '&' || ch == '|' {
|
|
ch2 := c.at(offset + 1)
|
|
if ch2 == 0 {
|
|
return true, offset + 1
|
|
}
|
|
r2 := string([]rune{ch, ch2})
|
|
if r2 == "<<" {
|
|
ch3 := c.at(offset + 2)
|
|
if ch3 == '-' || ch3 == '<' {
|
|
return true, offset + 3 // "<<-" or "<<<"
|
|
}
|
|
return true, offset + 2 // "<<"
|
|
}
|
|
if r2 == ">>" || r2 == "&&" || r2 == "||" || r2 == ";;" || r2 == "<<" || r2 == "<&" || r2 == ">&" || r2 == "<>" || r2 == ">|" {
|
|
// we don't return '((' here (requires special processing)
|
|
return true, offset + 2
|
|
}
|
|
return true, offset + 1
|
|
}
|
|
return false, 0
|
|
}
|
|
|
|
// returns (new-offset, complete)
|
|
func (c *parseContext) skipToChar(offset int, endCh rune, allowEsc bool) (int, bool) {
|
|
for {
|
|
ch := c.at(offset)
|
|
if ch == 0 {
|
|
return offset, false
|
|
}
|
|
if allowEsc && ch == '\\' {
|
|
if c.at(offset+1) == 0 {
|
|
return offset + 1, false
|
|
}
|
|
offset += 2
|
|
continue
|
|
}
|
|
if ch == endCh {
|
|
return offset + 1, true
|
|
}
|
|
offset++
|
|
}
|
|
}
|
|
|
|
// returns (new-offset, complete)
|
|
func (c *parseContext) skipToChar2(offset int, endCh rune, endCh2 rune, allowEsc bool) (int, bool) {
|
|
for {
|
|
ch := c.at(offset)
|
|
ch2 := c.at(offset + 1)
|
|
if ch == 0 {
|
|
return offset, false
|
|
}
|
|
if ch2 == 0 {
|
|
return offset + 1, false
|
|
}
|
|
if allowEsc && ch == '\\' {
|
|
offset += 2
|
|
continue
|
|
}
|
|
if ch == endCh && ch2 == endCh2 {
|
|
return offset + 2, true
|
|
}
|
|
offset++
|
|
}
|
|
}
|
|
|
|
func (c *parseContext) parseStrSQ() *wordType {
|
|
if !c.match('\'') {
|
|
return nil
|
|
}
|
|
newOffset, complete := c.skipToChar(1, '\'', false)
|
|
w := c.makeWord(WordTypeSQ, newOffset, complete)
|
|
return w
|
|
}
|
|
|
|
func (c *parseContext) parseStrDQ() *wordType {
|
|
if !c.match('"') {
|
|
return nil
|
|
}
|
|
newContext := c.clone(c.Pos+1, WordTypeDQ)
|
|
subWords, eofExit := newContext.tokenizeDQ()
|
|
newOffset := newContext.Pos + 1
|
|
w := c.makeWord(WordTypeDQ, newOffset, !eofExit)
|
|
w.Subs = subWords
|
|
return w
|
|
}
|
|
|
|
func (c *parseContext) parseStrDDQ() *wordType {
|
|
if !c.match2('$', '"') {
|
|
return nil
|
|
}
|
|
newContext := c.clone(c.Pos+2, WordTypeDDQ)
|
|
subWords, eofExit := newContext.tokenizeDQ()
|
|
newOffset := newContext.Pos + 2
|
|
w := c.makeWord(WordTypeDDQ, newOffset, !eofExit)
|
|
w.Subs = subWords
|
|
return w
|
|
}
|
|
|
|
func (c *parseContext) parseStrBQ() *wordType {
|
|
if !c.match('`') {
|
|
return nil
|
|
}
|
|
newContext := c.clone(c.Pos+1, WordTypeBQ)
|
|
subWords, eofExit := newContext.tokenizeRaw()
|
|
newOffset := newContext.Pos + 1
|
|
w := c.makeWord(WordTypeBQ, newOffset, !eofExit)
|
|
w.Subs = subWords
|
|
return w
|
|
}
|
|
|
|
func (c *parseContext) parseStrANSI() *wordType {
|
|
if !c.match2('$', '\'') {
|
|
return nil
|
|
}
|
|
newOffset, complete := c.skipToChar(2, '\'', true)
|
|
w := c.makeWord(WordTypeDSQ, newOffset, complete)
|
|
return w
|
|
}
|
|
|
|
func (c *parseContext) parseArith(mustComplete bool) *wordType {
|
|
if !c.match2('(', '(') {
|
|
return nil
|
|
}
|
|
newOffset, complete := c.skipToChar2(2, ')', ')', false)
|
|
if mustComplete && !complete {
|
|
return nil
|
|
}
|
|
w := c.makeWord(WordTypePP, newOffset, complete)
|
|
return w
|
|
}
|
|
|
|
func (c *parseContext) parseExpansion() *wordType {
|
|
if !c.match('$') {
|
|
return nil
|
|
}
|
|
if c.match3('$', '(', '(') {
|
|
newOffset, complete := c.skipToChar2(3, ')', ')', false)
|
|
w := c.makeWord(WordTypeDPP, newOffset, complete)
|
|
return w
|
|
}
|
|
if c.match2('$', '(') {
|
|
// subshell
|
|
newContext := c.clone(c.Pos+2, WordTypeDP)
|
|
subWords, eofExit := newContext.tokenizeRaw()
|
|
newOffset := newContext.Pos + 2
|
|
w := c.makeWord(WordTypeDP, newOffset, !eofExit)
|
|
w.Subs = subWords
|
|
return w
|
|
}
|
|
if c.match2('$', '[') {
|
|
// deprecated arith expansion
|
|
newOffset, complete := c.skipToChar(2, ']', false)
|
|
w := c.makeWord(WordTypeDB, newOffset, complete)
|
|
return w
|
|
}
|
|
if c.match2('$', '{') {
|
|
// variable expansion
|
|
newContext := c.clone(c.Pos+2, WordTypeVarBrace)
|
|
_, eofExit := newContext.tokenizeVarBrace()
|
|
newOffset := newContext.Pos + 2
|
|
w := c.makeWord(WordTypeVarBrace, newOffset, !eofExit)
|
|
return w
|
|
}
|
|
ch2 := c.at(1)
|
|
if ch2 == 0 || unicode.IsSpace(ch2) {
|
|
// no expansion
|
|
return nil
|
|
}
|
|
newOffset := c.parseSimpleVarName(1)
|
|
if newOffset > 1 {
|
|
// simple variable name
|
|
w := c.makeWord(WordTypeSimpleVar, newOffset, true)
|
|
return w
|
|
}
|
|
if ch2 == '*' || ch2 == '@' || ch2 == '#' || ch2 == '?' || ch2 == '-' || ch2 == '$' || ch2 == '!' || (ch2 >= '0' && ch2 <= '9') {
|
|
// single character variable name, e.g. $@, $_, $1, etc.
|
|
w := c.makeWord(WordTypeSimpleVar, 2, true)
|
|
return w
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// returns newOffset
|
|
func (c *parseContext) parseSimpleVarName(offset int) int {
|
|
first := true
|
|
for {
|
|
ch := c.at(offset)
|
|
if ch == 0 {
|
|
return offset
|
|
}
|
|
if (ch == '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) || (!first && ch >= '0' && ch <= '9') {
|
|
first = false
|
|
offset++
|
|
continue
|
|
}
|
|
return offset
|
|
}
|
|
}
|
|
|
|
func makeSpaceStr(slen int) string {
|
|
if slen == 0 {
|
|
return ""
|
|
}
|
|
if slen == 1 {
|
|
return " "
|
|
}
|
|
rtn := make([]byte, slen)
|
|
for i := 0; i < slen; i++ {
|
|
rtn[i] = ' '
|
|
}
|
|
return string(rtn)
|
|
}
|
|
|
|
func (w *wordType) String() string {
|
|
notCompleteFlag := " "
|
|
if !w.Complete {
|
|
notCompleteFlag = "*"
|
|
}
|
|
return fmt.Sprintf("%4s[%3d]%s %s%q", w.Type, w.Offset, notCompleteFlag, makeSpaceStr(len(w.Prefix)), string(w.FullRawString()))
|
|
}
|
|
|
|
func dumpWords(words []*wordType, indentStr string) {
|
|
for _, word := range words {
|
|
fmt.Printf("%s%s\n", indentStr, word.String())
|
|
if len(word.Subs) > 0 {
|
|
dumpWords(word.Subs, indentStr+" ")
|
|
}
|
|
}
|
|
}
|