waveterm/wavesrv/pkg/shparse/expand.go

259 lines
5.8 KiB
Go
Raw Normal View History

2022-11-19 23:05:38 +01:00
package shparse
import (
"bytes"
"fmt"
"mvdan.cc/sh/v3/expand"
)
const MaxExpandLen = 64 * 1024
type ExpandInfo struct {
HasTilde bool // only ~ as the first character when SimpleExpandContext.HomeDir is set
HasVar bool // $x, $$, ${...}
HasGlob bool // *, ?, [, {
HasExtGlob bool // ?(...) ... ?*+@!
HasHistory bool // ! (anywhere)
HasSpecial bool // subshell, arith
}
type ExpandContext struct {
HomeDir string
}
func expandSQ(buf *bytes.Buffer, rawLit []rune) {
// no info specials
buf.WriteString(string(rawLit))
}
// TODO implement our own ANSI single quote formatter
func expandANSISQ(buf *bytes.Buffer, rawLit []rune) {
// no info specials
str, _, _ := expand.Format(nil, string(rawLit), nil)
buf.WriteString(str)
}
func expandLiteral(buf *bytes.Buffer, info *ExpandInfo, rawLit []rune) {
var lastBackSlash bool
var lastExtGlob bool
var lastDollar bool
for _, ch := range rawLit {
if ch == 0 {
break
}
if lastBackSlash {
lastBackSlash = false
if ch == '\n' {
// special case, backslash *and* newline are ignored
continue
}
buf.WriteRune(ch)
continue
}
if ch == '\\' {
lastBackSlash = true
lastExtGlob = false
lastDollar = false
continue
}
if ch == '*' || ch == '?' || ch == '[' || ch == '{' {
info.HasGlob = true
}
if ch == '`' {
info.HasSpecial = true
}
if ch == '!' {
info.HasHistory = true
}
if lastExtGlob && ch == '(' {
info.HasExtGlob = true
}
if lastDollar && (ch != ' ' && ch != '"' && ch != '\'' && ch != '(' || ch != '[') {
info.HasVar = true
}
if lastDollar && (ch == '(' || ch == '[') {
info.HasSpecial = true
}
lastExtGlob = (ch == '?' || ch == '*' || ch == '+' || ch == '@' || ch == '!')
lastDollar = (ch == '$')
buf.WriteRune(ch)
}
if lastBackSlash {
buf.WriteByte('\\')
}
}
// will also work for partial double quoted strings
func expandDQLiteral(buf *bytes.Buffer, info *ExpandInfo, rawVal []rune) {
var lastBackSlash bool
var lastDollar bool
for _, ch := range rawVal {
if ch == 0 {
break
}
if lastBackSlash {
lastBackSlash = false
if ch == '"' || ch == '\\' || ch == '$' || ch == '`' {
buf.WriteRune(ch)
continue
}
buf.WriteRune('\\')
buf.WriteRune(ch)
continue
}
if ch == '\\' {
lastBackSlash = true
lastDollar = false
continue
}
// similar to expandLiteral, but no globbing
if ch == '`' {
info.HasSpecial = true
}
if ch == '!' {
info.HasHistory = true
}
if lastDollar && (ch != ' ' && ch != '"' && ch != '\'' && ch != '(' || ch != '[') {
info.HasVar = true
}
if lastDollar && (ch == '(' || ch == '[') {
info.HasSpecial = true
}
lastDollar = (ch == '$')
buf.WriteRune(ch)
}
// in a valid parsed DQ string, you cannot have a trailing backslash (because \" would not end the string)
// still putting the case here though in case we ever deal with incomplete strings (e.g. completion)
if lastBackSlash {
buf.WriteByte('\\')
}
}
func simpleExpandSubs(buf *bytes.Buffer, info *ExpandInfo, ectx ExpandContext, word *WordType, pos int) {
fmt.Printf("expand subs: %v\n", word)
parts := word.Subs
startPos := word.contentStartPos()
for _, part := range parts {
remainingLen := pos - startPos
if remainingLen <= 0 {
break
}
simpleExpandWord(buf, info, ectx, part, remainingLen)
startPos += len(part.Raw)
}
}
func canExpand(ectx ExpandContext, wtype string) bool {
return wtype == WordTypeLit || wtype == WordTypeSQ || wtype == WordTypeDSQ ||
wtype == WordTypeDQ || wtype == WordTypeDDQ || wtype == WordTypeGroup
}
2022-11-19 23:05:38 +01:00
func simpleExpandWord(buf *bytes.Buffer, info *ExpandInfo, ectx ExpandContext, word *WordType, pos int) {
if canExpand(ectx, word.Type) {
if pos >= word.contentEndPos() {
pos = word.contentEndPos()
}
if pos <= word.contentStartPos() {
return
}
} else {
if pos >= len(word.Raw) {
pos = len(word.Raw)
}
if pos <= 0 {
return
}
2022-11-19 23:05:38 +01:00
}
switch word.Type {
case WordTypeLit:
if word.QC.cur() == WordTypeDQ {
expandDQLiteral(buf, info, word.Raw[:pos])
return
}
expandLiteral(buf, info, word.Raw[:pos])
case WordTypeSQ:
expandSQ(buf, word.Raw[word.contentStartPos():pos])
return
case WordTypeDSQ:
expandANSISQ(buf, word.Raw[word.contentStartPos():pos])
return
case WordTypeDQ, WordTypeDDQ:
simpleExpandSubs(buf, info, ectx, word, pos)
return
case WordTypeGroup:
simpleExpandSubs(buf, info, ectx, word, pos)
return
// not expanded
2022-11-19 23:05:38 +01:00
case WordTypeSimpleVar:
info.HasVar = true
buf.WriteString(string(word.Raw[:pos]))
2022-11-19 23:05:38 +01:00
return
// not expanded
2022-11-19 23:05:38 +01:00
case WordTypeVarBrace:
info.HasVar = true
buf.WriteString(string(word.Raw[:pos]))
2022-11-19 23:05:38 +01:00
return
default:
info.HasSpecial = true
buf.WriteString(string(word.Raw[:pos]))
return
}
}
func SimpleExpandPrefix(ectx ExpandContext, word *WordType, pos int) (string, ExpandInfo) {
var buf bytes.Buffer
var info ExpandInfo
simpleExpandWord(&buf, &info, ectx, word, pos)
return buf.String(), info
}
func SimpleExpand(ectx ExpandContext, word *WordType) (string, ExpandInfo) {
return SimpleExpandPrefix(ectx, word, len(word.Raw))
}
// returns varname (no '$') and ok (whether this is a valid varname expansion)
func SimpleVarNamePrefix(ectx ExpandContext, word *WordType, pos int) (string, bool) {
if word.Type != WordTypeSimpleVar && word.Type != WordTypeVarBrace {
return "", false
}
if word.Type == WordTypeSimpleVar {
if pos == 0 {
return "", false
}
if pos == 1 {
return "", true
}
if pos > len(word.Raw) {
pos = len(word.Raw)
}
return string(word.Raw[1:pos]), true
}
// word.Type == WordTypeVarBrace
// knock '${' off the front, then see if the rest is a valid var name.
if pos == 0 || pos == 1 {
return "", false
}
if pos == 2 {
return "", true
}
if pos > word.contentEndPos() {
pos = word.contentEndPos()
}
rawVarName := word.Raw[2:pos]
if isSimpleVarName(rawVarName) {
return string(rawVarName), true
}
return "", false
}