2023-10-17 06:31:13 +02:00
|
|
|
// Copyright 2023, Command Line Inc.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-21 21:55:53 +01:00
|
|
|
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) {
|
2022-11-21 21:55:53 +01:00
|
|
|
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
|
|
|
|
|
2022-11-21 21:55:53 +01:00
|
|
|
// not expanded
|
2022-11-19 23:05:38 +01:00
|
|
|
case WordTypeSimpleVar:
|
2022-11-21 21:55:53 +01:00
|
|
|
info.HasVar = true
|
|
|
|
buf.WriteString(string(word.Raw[:pos]))
|
2022-11-19 23:05:38 +01:00
|
|
|
return
|
|
|
|
|
2022-11-21 21:55:53 +01:00
|
|
|
// not expanded
|
2022-11-19 23:05:38 +01:00
|
|
|
case WordTypeVarBrace:
|
2022-11-21 21:55:53 +01:00
|
|
|
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))
|
|
|
|
}
|
2022-11-21 21:55:53 +01:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|