checkpoint

This commit is contained in:
sawka
2022-11-14 19:57:29 -08:00
parent 55767e3256
commit 9da092816a
2 changed files with 191 additions and 50 deletions
+182 -46
View File
@@ -3,6 +3,7 @@ package shparse
import ( import (
"fmt" "fmt"
"strings" "strings"
"unicode"
) )
// //
@@ -73,10 +74,12 @@ import (
// ambiguity between $((...)) and $((ls); ls) // ambiguity between $((...)) and $((ls); ls)
// ambiguity between foo=([0]=hell) and foo=([abc) // ambiguity between foo=([0]=hell) and foo=([abc)
// tokenization https://pubs.opengroup.org/onlinepubs/7908799/xcu/chap2.html#tag_001_003
const ( const (
WordTypeRaw = "raw" WordTypeRaw = "raw"
WordTypeLit = "lit" WordTypeLit = "lit"
WordTypeOp = "op" // single: & ; | ( ) < > \n multi(2): && || ;; << >> <& >& <> >| (( multi(3): <<- 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 { } ! (( [[ WordTypeKey = "key" // if then else elif fi do done case esac while until for in { } ! (( [[
WordTypeDQ = "dq" // " WordTypeDQ = "dq" // "
WordTypeSQ = "sq" // ' WordTypeSQ = "sq" // '
@@ -98,9 +101,9 @@ type parseContext struct {
} }
type wordType struct { type wordType struct {
Offset int
End int
Type string Type string
Offset int
Raw []rune
Complete bool Complete bool
Val string // only for Op and Key (does *not* store string values of quoted expressions or expansions) Val string // only for Op and Key (does *not* store string values of quoted expressions or expansions)
Prefix []rune Prefix []rune
@@ -131,37 +134,43 @@ func (c *parseContext) match2(ch rune, ch2 rune) bool {
return c.at(0) == ch && c.at(1) == ch2 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) newOp(length int) *wordType { func (c *parseContext) newOp(length int) *wordType {
rtn := &wordType{Offset: c.Pos} rtn := &wordType{Type: WordTypeOp}
rtn.Type = WordTypeOp rtn.Offset = c.Pos
rtn.Raw = c.Input[c.Pos : c.Pos+length]
rtn.Val = string(rtn.Raw)
rtn.Complete = true rtn.Complete = true
rtn.Val = string(c.Input[c.Pos : c.Pos+length])
c.Pos += length c.Pos += length
rtn.End = c.Pos
return rtn return rtn
} }
func (c *parseContext) parseOp() *wordType { // returns (found, newOffset)
ch := c.cur() func (c *parseContext) parseOp(offset int) (bool, int) {
ch := c.at(offset)
if ch == '&' || ch == ';' || ch == '|' || ch == '\n' || ch == '<' || ch == '>' || ch == '!' || ch == '(' { if ch == '&' || ch == ';' || ch == '|' || ch == '\n' || ch == '<' || ch == '>' || ch == '!' || ch == '(' {
ch2 := c.at(1) ch2 := c.at(offset + 1)
if ch2 == 0 { if ch2 == 0 {
return c.newOp(1) return true, offset + 1
} }
r2 := string([]rune{ch, ch2}) r2 := string([]rune{ch, ch2})
if r2 == "<<" { if r2 == "<<" {
ch3 := c.at(2) ch3 := c.at(offset + 2)
if ch3 == '-' { if ch3 == '-' {
return c.newOp(3) // "<<-" return true, offset + 3 // "<<-"
} }
return c.newOp(2) // "<<" return true, offset + 2 // "<<"
} }
if r2 == "&&" || r2 == "||" || r2 == ";;" || r2 == "<<" || r2 == ">>" || r2 == "<&" || r2 == ">&" || r2 == "<>" || r2 == ">|" { if r2 == "&&" || r2 == "||" || r2 == ";;" || r2 == "<<" || r2 == ">>" || r2 == "<&" || r2 == ">&" || r2 == "<>" || r2 == ">|" {
// we don't return '((' here (requires special processing)
return c.newOp(2) return c.newOp(2)
} }
return c.newOp(1) return c.newOp(1)
} }
return nil return false, 0
} }
// returns (new-offset, complete) // returns (new-offset, complete)
@@ -189,12 +198,14 @@ func (c *parseContext) parseStrSQ() *wordType {
if !c.match('\'') { if !c.match('\'') {
return nil return nil
} }
newOffset, complete := c.skipToChar(1, '\'', false)
w := &wordType{ w := &wordType{
Offset: c.Pos, Type: WordTypeDQ,
Type: WordTypeDQ, Offset: c.Pos,
Raw: c.Input[c.Pos : c.Pos+newOffset],
Complete: complete,
} }
w.End, w.Complete = c.skipToChar(1, '\'', false) c.Pos = c.Pos + newOffset
c.Pos = w.End
return w return w
} }
@@ -202,12 +213,14 @@ func (c *parseContext) parseStrDQ() *wordType {
if !c.match('"') { if !c.match('"') {
return nil return nil
} }
newOffset, complete := c.skipToChar(1, '"', false)
w := &wordType{ w := &wordType{
Offset: c.Pos, Type: WordTypeDQ,
Type: WordTypeDQ, Offset: c.Pos,
Raw: c.Input[c.Pos : c.Pos+newOffset],
Complete: complete,
} }
w.End, w.Complete = c.skipToChar(1, '"', true) c.Pos = c.Pos + newOffset
c.Pos = w.End
return w return w
} }
@@ -215,12 +228,14 @@ func (c *parseContext) parseStrBQ() *wordType {
if c.match('`') { if c.match('`') {
return nil return nil
} }
newOffset, complete := c.skipToChar(1, '`', true)
w := &wordType{ w := &wordType{
Offset: c.Pos, Type: WordTypeBQ,
Type: WordTypeBQ, Offset: c.Pos,
Raw: c.Input[c.Pos : c.Pos+newOffset],
Complete: complete,
} }
w.End, w.Complete = c.skipToChar(1, '`', true) c.Pos = c.Pos + newOffset
c.Pos = w.End
return w return w
} }
@@ -228,12 +243,14 @@ func (c *parseContext) parseStrANSI() *wordType {
if !c.match2('$', '\'') { if !c.match2('$', '\'') {
return nil return nil
} }
newOffset, complete := c.skipToChar(2, '\'', true)
w := &wordType{ w := &wordType{
Offset: c.Pos, Type: WordTypeDSQ,
Type: WordTypeDSQ, Offset: c.Pos,
Raw: c.Input[c.Pos : c.Pos+newOffset],
Complete: complete,
} }
w.End, w.Complete = c.skipToChar(1, '\'', true) c.Pos = c.Pos + newOffset
c.Pos = w.End
return w return w
} }
@@ -241,30 +258,103 @@ func (c *parseContext) parseStrDDQ() *wordType {
if !c.match2('$', '"') { if !c.match2('$', '"') {
return nil return nil
} }
newOffset, complete := c.skipToChar(2, '"', true)
w := &wordType{ w := &wordType{
Offset: c.Pos, Type: WordTypeDDQ,
Type: WordTypeDDQ, Offset: c.Pos,
Raw: c.Input[c.Pos : c.Pos+newOffset],
Complete: complete,
} }
w.End, w.Complete = c.skipToChar(1, '"', true) c.Pos = c.Pos + newOffset
c.Pos = w.End
return w return w
} }
func (c *parseContext) parseVar() *wordType { func (c *parseContext) parseExpansion() *wordType {
if !c.match('$') { if !c.match('$') {
return nil return nil
} }
if c.match3('$', '(', '(') {
// arith expansion
return nil
}
if c.match2('$', '(') {
// subshell
return nil
}
if c.match2('$', '[') {
// deprecated arith expansion
return nil
}
if c.match2('$', '{') {
// variable expansion
return nil
}
ch2 := c.at(1)
if ch2 == 0 || unicode.IsSpace(ch2) {
// no expansion
return nil
}
newOffset := c.parseSimpleVarName(1)
if newOffset > 1 {
// simple variable name
rtn := &wordType{
Type: WordTypeVar,
Offset: c.Pos,
Raw: c.Input[c.Pos : c.Pos+newOffset],
Complete: true,
}
c.Pos = c.Pos + newOffset
return rtn
}
// single character variable name, e.g. $@, $_, $1, etc.
rtn := &wordType{
Type: WordTypeVar,
Offset: c.Pos,
Raw: c.Input[c.Pos : c.Pos+2],
Complete: true,
}
c.Pos += 2
return rtn
}
func (c *parseContext) parseShellTest() *wordType {
if !c.match2('[', '[') {
return nil
}
return nil return nil
} }
func (c *parseContext) parseQuotes() []*wordType { func (c *parseContext) parseProcessSubstitution() *wordType {
if !c.match2('<', '(') && !c.match2('>', '(') {
return nil
}
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 parseInput(inputStr string) []*wordType {
c := &parseContext{Input: []rune(inputStr)}
var rtn []*wordType var rtn []*wordType
var litWord *wordType var litWord *wordType
for { for {
var quoteWord *wordType var quoteWord *wordType
ch := c.cur() ch := c.cur()
fmt.Printf("ch: %d %q\n", c.Pos, string([]rune{ch}))
startPos := c.Pos
if ch == 0 { if ch == 0 {
break break
} }
@@ -280,10 +370,12 @@ func (c *parseContext) parseQuotes() []*wordType {
if quoteWord == nil { if quoteWord == nil {
quoteWord = c.parseStrDDQ() quoteWord = c.parseStrDDQ()
} }
if quoteWord == nil {
quoteWord = c.parseExpansion()
}
} }
if quoteWord != nil { if quoteWord != nil {
if litWord != nil { if litWord != nil {
litWord.End = startPos
rtn = append(rtn, litWord) rtn = append(rtn, litWord)
litWord = nil litWord = nil
} }
@@ -291,29 +383,73 @@ func (c *parseContext) parseQuotes() []*wordType {
continue continue
} }
if litWord == nil { if litWord == nil {
litWord = &wordType{Offset: c.Pos, Type: WordTypeLit, Complete: true} litWord = &wordType{Type: WordTypeLit, Offset: c.Pos, Complete: true}
} }
if ch == '\\' && c.at(1) != 0 { if ch == '\\' && c.at(1) != 0 {
litWord.Raw = append(litWord.Raw, ch, c.at(1))
c.Pos += 2 c.Pos += 2
continue continue
} }
litWord.Raw = append(litWord.Raw, ch)
c.Pos++ c.Pos++
} }
if litWord != nil { if litWord != nil {
litWord.End = c.Pos
rtn = append(rtn, litWord) rtn = append(rtn, litWord)
} }
// now we want to expand ops
rtn := expandAllOps(rtn)
return rtn
}
func expandOps(word *wordType) []*wordType {
if word.Type != WordTypeLit {
return nil
}
var lastBackSlash bool
var rtn []*wordType
for idx, ch := range word.Raw {
if ch == 0 {
break
}
if ch == '\\' {
lastBackSlash = true
continue
}
if lastBackSlash {
lastBackSlash = false
continue
}
}
}
func expandAllOps(words []*wordType) []*wordType {
var rtn []*wordType
for _, word := range words {
exWords := expandOps(word)
if len(exWords) == 0 {
rtn = append(rtn, word)
} else {
rtn = append(rtn, exWords...)
}
}
return rtn return rtn
} }
func (c *parseContext) RawString(w *wordType) string { func (w *wordType) String() string {
return fmt.Sprintf("%s[%q]", w.Type, string(c.Input[w.Offset:w.End])) var notCompleteFlag string
if !w.Complete {
notCompleteFlag = "*"
}
return fmt.Sprintf("%s[%d:%q]%s", w.Type, w.Offset, string(w.Raw), notCompleteFlag)
} }
func (c *parseContext) dumpWords(words []*wordType) { func dumpWords(words []*wordType) {
var strs []string var strs []string
for _, word := range words { for _, word := range words {
strs = append(strs, c.RawString(word)) strs = append(strs, word.String())
} }
output := strings.Join(strs, " ") output := strings.Join(strs, " ")
fmt.Printf("%s\n", output) fmt.Printf("%s\n", output)
+9 -4
View File
@@ -1,24 +1,29 @@
package shparse package shparse
import ( import (
"fmt"
"testing" "testing"
) )
// $(ls f[*]); ./x // $(ls f[*]); ./x
// ls f => raw["ls f"] -> lit["ls f"] -> lit["ls"] lit["f"]p // ls f => raw["ls f"] -> lit["ls f"] -> lit["ls"] lit["f"]
// w; ls foo; => raw["w; ls foo;"] // w; ls foo; => raw["w; ls foo;"]
// ls&"ls" => raw["ls&ls"] => lit["ls&"] dq["ls"] => lit["ls"] key["&"] dq["ls"] // ls&"ls" => raw["ls&ls"] => lit["ls&"] dq["ls"] => lit["ls"] key["&"] dq["ls"]
// ls $x; echo `ls f => raw["ls $x; echo `ls f"] // ls $x; echo `ls f => raw["ls $x; echo `ls f"]
// > echo $foo{x,y} // > echo $foo{x,y}
func testParse(t *testing.T, s string) { func testParse(t *testing.T, s string) {
c := &parseContext{Input: []rune(s)} words := parseInput(s)
words := c.parseQuotes()
c.dumpWords(words) fmt.Printf("%s\n", s)
dumpWords(words)
fmt.Printf("\n")
} }
func Test1(t *testing.T) { func Test1(t *testing.T) {
testParse(t, "ls") testParse(t, "ls")
testParse(t, "ls 'foo'") testParse(t, "ls 'foo'")
testParse(t, `ls "hello" $'\''`) testParse(t, `ls "hello" $'\''`)
testParse(t, `ls "foo`)
testParse(t, `echo $11 $xyz $ `)
} }