checkpoint, testing new compgen. fixed superoffset bug

This commit is contained in:
sawka 2022-11-22 00:26:41 -08:00
parent bb3e12fee7
commit bd3595c954
5 changed files with 291 additions and 107 deletions

View File

@ -1172,7 +1172,7 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto
pos = len(cmdLine)
}
showComps := resolveBool(pk.Kwargs["compshow"], false)
cmdSP := comp.StrWithPos{Str: cmdLine, Pos: pos}
cmdSP := utilfn.StrWithPos{Str: cmdLine, Pos: pos}
compCtx := comp.CompContext{}
if ids.Remote != nil {
rptr := ids.Remote.RemotePtr
@ -1185,39 +1185,19 @@ func CompGenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto
return nil, err
}
if crtn == nil {
return nil, fmt.Errorf("no return value from DoCompGen")
return nil, nil
}
if showComps || newSP == nil {
if showComps {
compStrs := crtn.GetCompDisplayStrs()
return makeInfoFromComps(crtn.CompType, compStrs, crtn.HasMore), nil
}
if newSP == nil || cmdSP == *newSP {
return nil, nil
}
update := sstore.ModelUpdate{
CmdLine: &sstore.CmdLineType{CmdLine: newSP.Str, CursorPos: newSP.Pos},
}
return update, nil
// prefix := cmdLine[:pos]
// parts := strings.Split(prefix, " ")
// compType := "file"
// if len(parts) > 0 && len(parts) < 2 && strings.HasPrefix(parts[0], "/") {
// compType = "metacommand"
// } else if len(parts) == 2 && (parts[0] == "cd" || parts[0] == "/cd") {
// compType = "directory"
// } else if len(parts) <= 1 {
// compType = "command"
// }
// lastPart := ""
// if len(parts) > 0 {
// lastPart = parts[len(parts)-1]
// }
// comps, hasMore, err := doCompGen(ctx, pk, lastPart, compType, showComps)
// if err != nil {
// return nil, err
// }
// if showComps {
// return makeInfoFromComps(compType, comps, hasMore), nil
// }
// return makeInsertUpdateFromComps(int64(pos), lastPart, comps, hasMore), nil
}
func CommentCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {

View File

@ -7,6 +7,7 @@ import (
"strings"
"github.com/scripthaus-dev/mshell/pkg/shexec"
"github.com/scripthaus-dev/mshell/pkg/simpleexpand"
"github.com/scripthaus-dev/sh2-server/pkg/scpacket"
"github.com/scripthaus-dev/sh2-server/pkg/utilfn"
"mvdan.cc/sh/v3/expand"
@ -124,9 +125,9 @@ func setBracketArgs(argMap map[string]string, bracketStr string) error {
strReader := strings.NewReader(bracketStr)
parser := syntax.NewParser(syntax.Variant(syntax.LangBash))
var wordErr error
var ectx shexec.SimpleExpandContext // do not set HomeDir (we don't expand ~ in bracket args)
var ectx simpleexpand.SimpleExpandContext // do not set HomeDir (we don't expand ~ in bracket args)
err := parser.Words(strReader, func(w *syntax.Word) bool {
litStr := shexec.SimpleExpandWord(ectx, w, bracketStr)
litStr, _ := simpleexpand.SimpleExpandWord(ectx, w, bracketStr)
eqIdx := strings.Index(litStr, "=")
var varName, varVal string
if eqIdx == -1 {
@ -285,8 +286,8 @@ func parseAliasStmt(stmt *syntax.Stmt, sourceStr string) (string, string, error)
return "", "", fmt.Errorf("invalid alias cmd word (not 'alias')")
}
secondWord := callExpr.Args[1]
var ectx shexec.SimpleExpandContext // no homedir, do not want ~ expansion
val := shexec.SimpleExpandWord(ectx, secondWord, sourceStr)
var ectx simpleexpand.SimpleExpandContext // no homedir, do not want ~ expansion
val, _ := simpleexpand.SimpleExpandWord(ectx, secondWord, sourceStr)
eqIdx := strings.Index(val, "=")
if eqIdx == -1 {
return "", "", fmt.Errorf("no '=' in alias definition")

View File

@ -9,9 +9,11 @@ import (
"strconv"
"strings"
"unicode"
"unicode/utf8"
"github.com/scripthaus-dev/mshell/pkg/packet"
"github.com/scripthaus-dev/mshell/pkg/shexec"
"github.com/scripthaus-dev/mshell/pkg/simpleexpand"
"github.com/scripthaus-dev/sh2-server/pkg/shparse"
"github.com/scripthaus-dev/sh2-server/pkg/sstore"
"github.com/scripthaus-dev/sh2-server/pkg/utilfn"
"mvdan.cc/sh/v3/syntax"
@ -48,11 +50,6 @@ type CompContext struct {
ForDisplay bool
}
type StrWithPos struct {
Str string
Pos int
}
type ParsedWord struct {
Offset int
Word *syntax.Word
@ -81,6 +78,28 @@ type CompReturn struct {
HasMore bool
}
var noEscChars []bool
var specialEsc []string
func init() {
noEscChars = make([]bool, 256)
for ch := 0; ch < 256; ch++ {
if (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
ch == '-' || ch == '.' || ch == '/' || ch == ':' || ch == '=' {
noEscChars[byte(ch)] = true
}
}
specialEsc = make([]string, 256)
specialEsc[0x7] = "\\a"
specialEsc[0x8] = "\\b"
specialEsc[0x9] = "\\t"
specialEsc[0xa] = "\\n"
specialEsc[0xb] = "\\v"
specialEsc[0xc] = "\\f"
specialEsc[0xd] = "\\r"
specialEsc[0x1b] = "\\E"
}
func compQuoteDQString(s string, close bool) string {
var buf bytes.Buffer
buf.WriteByte('"')
@ -98,15 +117,108 @@ func compQuoteDQString(s string, close bool) string {
return buf.String()
}
func hasGlob(s string) bool {
var lastExtGlob bool
for _, ch := range s {
if ch == '*' || ch == '?' || ch == '[' || ch == '{' {
return true
}
if ch == '+' || ch == '@' || ch == '!' {
lastExtGlob = true
continue
}
if lastExtGlob && ch == '(' {
return true
}
lastExtGlob = false
}
return false
}
func writeUtf8Literal(buf *bytes.Buffer, ch rune) {
var runeArr [utf8.UTFMax]byte
buf.WriteString("$'")
barr := runeArr[:]
byteLen := utf8.EncodeRune(barr, ch)
for i := 0; i < byteLen; i++ {
buf.WriteString("\\x")
buf.WriteByte(utilfn.HexDigits[barr[i]/16])
buf.WriteByte(utilfn.HexDigits[barr[i]%16])
}
buf.WriteByte('\'')
}
func compQuoteLiteralString(s string) string {
var buf bytes.Buffer
for idx, ch := range s {
if ch == 0 {
break
}
if idx == 0 && ch == '~' {
buf.WriteRune(ch)
continue
}
if ch > unicode.MaxASCII {
writeUtf8Literal(&buf, ch)
continue
}
var bch = byte(ch)
if noEscChars[bch] {
buf.WriteRune(ch)
continue
}
if specialEsc[bch] != "" {
buf.WriteString(specialEsc[bch])
continue
}
if !unicode.IsPrint(ch) {
writeUtf8Literal(&buf, ch)
continue
}
buf.WriteByte('\\')
buf.WriteByte(bch)
}
return buf.String()
}
func compQuoteSQString(s string) string {
var buf bytes.Buffer
for _, ch := range s {
if ch == 0 {
break
}
if ch == '\'' {
buf.WriteString("'\\''")
continue
}
var bch byte
if ch <= unicode.MaxASCII {
bch = byte(ch)
}
if ch > unicode.MaxASCII || !unicode.IsPrint(ch) {
buf.WriteByte('\'')
if bch != 0 && specialEsc[bch] != "" {
buf.WriteString(specialEsc[bch])
} else {
writeUtf8Literal(&buf, ch)
}
buf.WriteByte('\'')
continue
}
buf.WriteByte(bch)
}
return buf.String()
}
func compQuoteString(s string, quoteType string, close bool) string {
if quoteType != QuoteTypeANSI {
if quoteType != QuoteTypeANSI && quoteType != QuoteTypeLiteral {
for _, ch := range s {
if ch > unicode.MaxASCII || !unicode.IsPrint(ch) || ch == '!' {
quoteType = QuoteTypeANSI
break
}
if ch == '\'' {
if quoteType == QuoteTypeSQ || quoteType == QuoteTypeLiteral {
if quoteType == QuoteTypeSQ {
quoteType = QuoteTypeANSI
break
}
@ -122,11 +234,7 @@ func compQuoteString(s string, quoteType string, close bool) string {
return rtn
}
if quoteType == QuoteTypeLiteral {
rtn := utilfn.ShellQuote(s, false, MaxCompQuoteLen)
if len(rtn) > 0 && rtn[0] == '\'' && !close {
rtn = rtn[0 : len(rtn)-1]
}
return rtn
return compQuoteLiteralString(s)
}
if quoteType == QuoteTypeSQ {
rtn := utilfn.ShellQuote(s, false, MaxCompQuoteLen)
@ -149,12 +257,12 @@ func (p *CompPoint) wordAsStr(w ParsedWord) string {
return w.PartialWord
}
func (p *CompPoint) simpleExpandWord(w ParsedWord) string {
ectx := shexec.SimpleExpandContext{}
func (p *CompPoint) simpleExpandWord(w ParsedWord) (string, simpleexpand.SimpleExpandInfo) {
ectx := simpleexpand.SimpleExpandContext{}
if w.Word != nil {
return shexec.SimpleExpandWord(ectx, w.Word, p.StmtStr)
return simpleexpand.SimpleExpandWord(ectx, w.Word, p.StmtStr)
}
return shexec.SimpleExpandPartialWord(ectx, w.PartialWord, false)
return simpleexpand.SimpleExpandPartialWord(ectx, w.PartialWord, false)
}
func getQuoteTypePref(str string) string {
@ -170,9 +278,9 @@ func getQuoteTypePref(str string) string {
return QuoteTypeLiteral
}
func (p *CompPoint) getCompPrefix() string {
func (p *CompPoint) getCompPrefix() (string, simpleexpand.SimpleExpandInfo) {
if p.CompWordPos == 0 {
return ""
return "", simpleexpand.SimpleExpandInfo{}
}
pword := p.Words[p.CompWord]
wordStr := p.wordAsStr(pword)
@ -184,10 +292,10 @@ func (p *CompPoint) getCompPrefix() string {
// and a partial on just the current part. this is an uncommon case though
// and has very little upside (even bash does not expand multipart words correctly)
partialWordStr := wordStr[:p.CompWordPos]
return shexec.SimpleExpandPartialWord(shexec.SimpleExpandContext{}, partialWordStr, false)
return simpleexpand.SimpleExpandPartialWord(simpleexpand.SimpleExpandContext{}, partialWordStr, false)
}
func (p *CompPoint) extendWord(newWord string, newWordComplete bool) StrWithPos {
func (p *CompPoint) extendWord(newWord string, newWordComplete bool) utilfn.StrWithPos {
pword := p.Words[p.CompWord]
wordStr := p.wordAsStr(pword)
quotePref := getQuoteTypePref(wordStr)
@ -198,18 +306,31 @@ func (p *CompPoint) extendWord(newWord string, newWordComplete bool) StrWithPos
newQuotedStr = newQuotedStr + " "
}
newPos := len(newQuotedStr)
return StrWithPos{Str: newQuotedStr + wordSuffix, Pos: newPos}
return utilfn.StrWithPos{Str: newQuotedStr + wordSuffix, Pos: newPos}
}
func (p *CompPoint) FullyExtend(crtn *CompReturn) StrWithPos {
// returns (extension, complete)
func computeCompExtension(compPrefix string, crtn *CompReturn) (string, bool) {
if crtn == nil || crtn.HasMore {
return StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()}
return "", false
}
compStrs := crtn.GetCompStrs()
compPrefix := p.getCompPrefix()
lcp := utilfn.LongestPrefix(compPrefix, compStrs)
if lcp == compPrefix || len(lcp) < len(compPrefix) || !strings.HasPrefix(lcp, compPrefix) {
return StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()}
return "", false
}
return lcp[len(compPrefix):], utilfn.ContainsStr(compStrs, lcp)
}
func (p *CompPoint) FullyExtend(crtn *CompReturn) utilfn.StrWithPos {
if crtn == nil || crtn.HasMore {
return utilfn.StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()}
}
compStrs := crtn.GetCompStrs()
compPrefix, _ := p.getCompPrefix()
lcp := utilfn.LongestPrefix(compPrefix, compStrs)
if lcp == compPrefix || len(lcp) < len(compPrefix) || !strings.HasPrefix(lcp, compPrefix) {
return utilfn.StrWithPos{Str: p.getOrigStr(), Pos: p.getOrigPos()}
}
newStr := p.extendWord(lcp, utilfn.ContainsStr(compStrs, lcp))
var buf bytes.Buffer
@ -226,7 +347,7 @@ func (p *CompPoint) FullyExtend(crtn *CompReturn) StrWithPos {
buf.WriteString(p.Suffix)
compWord := p.Words[p.CompWord]
newPos := len(p.Prefix) + compWord.Offset + len(compWord.Prefix) + newStr.Pos
return StrWithPos{Str: buf.String(), Pos: newPos}
return utilfn.StrWithPos{Str: buf.String(), Pos: newPos}
}
func (p *CompPoint) dump() {
@ -240,7 +361,7 @@ func (p *CompPoint) dump() {
fmt.Printf("{%s}", w.Prefix)
}
if idx == p.CompWord {
fmt.Printf("%s\n", strWithCursor(p.wordAsStr(w), p.CompWordPos))
fmt.Printf("%s\n", utilfn.StrWithPos{Str: p.wordAsStr(w), Pos: p.CompWordPos})
} else {
fmt.Printf("%s\n", p.wordAsStr(w))
}
@ -253,24 +374,6 @@ func (p *CompPoint) dump() {
var SimpleCompGenFns map[string]SimpleCompGenFnType
func (sp StrWithPos) String() string {
return strWithCursor(sp.Str, sp.Pos)
}
func strWithCursor(str string, pos int) string {
if pos < 0 {
return "[*]_" + str
}
if pos >= len(str) {
if pos > len(str) {
return str + "_[*]"
}
return str + "[*]"
} else {
return str[:pos] + "[*]" + str[pos:]
}
}
func isWhitespace(str string) bool {
return strings.TrimSpace(str) == ""
}
@ -284,7 +387,7 @@ func splitInitialWhitespace(str string) (string, string) {
return str, ""
}
func ParseCompPoint(cmdStr StrWithPos) *CompPoint {
func ParseCompPoint(cmdStr utilfn.StrWithPos) *CompPoint {
fullCmdStr := cmdStr.Str
pos := cmdStr.Pos
// fmt.Printf("---\n")
@ -388,14 +491,86 @@ func splitCompWord(p *CompPoint) {
p.Words = newWords
}
func DoCompGen(ctx context.Context, sp StrWithPos, compCtx CompContext) (*CompReturn, *StrWithPos, error) {
func getCompType(compPos shparse.CompletionPos) string {
switch compPos.CompType {
case shparse.CompTypeCommandMeta:
return CGTypeCommandMeta
case shparse.CompTypeCommand:
return CGTypeCommand
case shparse.CompTypeVar:
return CGTypeVariable
case shparse.CompTypeArg, shparse.CompTypeBasic, shparse.CompTypeAssignment:
return CGTypeFile
default:
return CGTypeFile
}
}
func fixupVarPrefix(varPrefix string) string {
if strings.HasPrefix(varPrefix, "${") {
varPrefix = varPrefix[2:]
if strings.HasSuffix(varPrefix, "}") {
varPrefix = varPrefix[:len(varPrefix)-1]
}
} else if strings.HasPrefix(varPrefix, "$") {
varPrefix = varPrefix[1:]
}
return varPrefix
}
func DoCompGen(ctx context.Context, cmdStr utilfn.StrWithPos, compCtx CompContext) (*CompReturn, *utilfn.StrWithPos, error) {
words := shparse.Tokenize(cmdStr.Str)
cmds := shparse.ParseCommands(words)
compPos := shparse.FindCompletionPos(cmds, cmdStr.Pos, 0)
fmt.Printf("comppos: %v\n", compPos)
if compPos.CompType == shparse.CompTypeInvalid {
return nil, nil, nil
}
var compPrefix string
if compPos.CompWord != nil {
var info shparse.ExpandInfo
compPrefix, info = shparse.SimpleExpandPrefix(shparse.ExpandContext{}, compPos.CompWord, compPos.CompWordOffset)
if info.HasGlob || info.HasExtGlob || info.HasHistory || info.HasSpecial {
return nil, nil, nil
}
if compPos.CompType != shparse.CompTypeVar && info.HasVar {
return nil, nil, nil
}
if compPos.CompType == shparse.CompTypeVar {
compPrefix = fixupVarPrefix(compPrefix)
}
}
scType := getCompType(compPos)
crtn, err := DoSimpleComp(ctx, scType, compPrefix, compCtx, nil)
if err != nil {
return nil, nil, err
}
if compCtx.ForDisplay {
return crtn, nil, nil
}
extensionStr, extensionComplete := computeCompExtension(compPrefix, crtn)
if extensionStr == "" {
return crtn, nil, nil
}
rtnSP := compPos.Extend(cmdStr, extensionStr, extensionComplete)
return crtn, &rtnSP, nil
}
func DoCompGenOld(ctx context.Context, sp utilfn.StrWithPos, compCtx CompContext) (*CompReturn, *utilfn.StrWithPos, error) {
compPoint := ParseCompPoint(sp)
compType := CGTypeFile
if compPoint.CompWord == 0 {
compType = CGTypeCommandMeta
}
// TODO lookup special types
compPrefix := compPoint.getCompPrefix()
compPrefix, info := compPoint.getCompPrefix()
if info.HasVar || info.HasGlob || info.HasExtGlob || info.HasHistory || info.HasSpecial {
return nil, nil, nil
}
crtn, err := DoSimpleComp(ctx, compType, compPrefix, compCtx, nil)
if err != nil {
return nil, nil, err

View File

@ -1,5 +1,12 @@
package shparse
import (
"fmt"
"strings"
"github.com/scripthaus-dev/sh2-server/pkg/utilfn"
)
const (
CompTypeCommandMeta = "command-meta"
CompTypeCommand = "command"
@ -156,38 +163,38 @@ func findCompletionWordAtPos(words []*WordType, pos int, allowEndMatch bool) *Wo
// recursively descend down the word, parse commands and find a sub completion point if any.
// return nil if there is no sub completion point in this word
func findCompletionPosInWord(word *WordType, pos int, superOffset int) *CompletionPos {
func findCompletionPosInWord(word *WordType, posInWord int, superOffset int) *CompletionPos {
rawPos := word.Offset + posInWord
if word.Type == WordTypeGroup || word.Type == WordTypeDQ || word.Type == WordTypeDDQ {
// need to descend further
if pos <= word.contentStartPos() {
if posInWord <= word.contentStartPos() {
return nil
}
if pos > word.contentEndPos() {
if posInWord > word.contentEndPos() {
return nil
}
subWord := findCompletionWordAtPos(word.Subs, pos-word.contentStartPos(), false)
subWord := findCompletionWordAtPos(word.Subs, posInWord-word.contentStartPos(), false)
if subWord == nil {
return nil
}
fullOffset := subWord.Offset + word.contentStartPos()
return findCompletionPosInWord(subWord, pos-fullOffset, superOffset+fullOffset)
return findCompletionPosInWord(subWord, posInWord-(subWord.Offset+word.contentStartPos()), superOffset+(word.Offset+word.contentStartPos()))
}
if word.Type == WordTypeDP || word.Type == WordTypeBQ {
if pos < word.contentStartPos() {
if posInWord < word.contentStartPos() {
return nil
}
if pos > word.contentEndPos() {
if posInWord > word.contentEndPos() {
return nil
}
subCmds := ParseCommands(word.Subs)
newPos := FindCompletionPos(subCmds, pos-word.contentStartPos(), superOffset+word.contentStartPos())
newPos := FindCompletionPos(subCmds, posInWord-word.contentStartPos(), superOffset+(word.Offset+word.contentStartPos()))
return &newPos
}
if word.Type == WordTypeSimpleVar || word.Type == WordTypeVarBrace {
// special "var" completion
rtn := &CompletionPos{RawPos: pos, SuperOffset: superOffset}
rtn := &CompletionPos{RawPos: rawPos, SuperOffset: superOffset}
rtn.CompType = CompTypeVar
rtn.CompWordOffset = pos
rtn.CompWordOffset = posInWord
rtn.CompWord = word
return rtn
}
@ -250,10 +257,27 @@ func FindCompletionPos(cmds []*CmdType, pos int, superOffset int) CompletionPos
if cpos.CompWord == nil {
return cpos
}
subPos := findCompletionPosInWord(cpos.CompWord, cpos.CompWordOffset, superOffset+cpos.CompWord.Offset)
subPos := findCompletionPosInWord(cpos.CompWord, cpos.CompWordOffset, superOffset)
if subPos == nil {
return cpos
} else {
return *subPos
}
}
func (cpos CompletionPos) Extend(origStr utilfn.StrWithPos, extensionStr string, extensionComplete bool) utilfn.StrWithPos {
compWord := cpos.CompWord
if compWord == nil {
compWord = MakeEmptyWord(WordTypeLit, nil, cpos.RawPos, true)
}
realOffset := compWord.Offset + cpos.SuperOffset
fmt.Printf("cpos-extend: %d[%s] ext[%s] cword[%v] off:%d super:%d real:%d\n", len([]rune(origStr.Str)), origStr, extensionStr, compWord, compWord.Offset, cpos.SuperOffset, realOffset)
if strings.HasSuffix(extensionStr, "/") {
extensionComplete = false
}
rtnSP := Extend(compWord, cpos.CompWordOffset, extensionStr, extensionComplete)
origRunes := []rune(origStr.Str)
rtnSP = rtnSP.Prepend(string(origRunes[0:realOffset]))
rtnSP = rtnSP.Append(string(origRunes[realOffset+len(compWord.Raw):]))
return rtnSP
}

View File

@ -131,7 +131,7 @@ func TestCmd(t *testing.T) {
testParseCommands(t, `x="foo $y" z=10 ls`)
}
func testCompPos(t *testing.T, cmdStr string, compType string, hasCommand bool, cmdWordPos int, hasWord bool) {
func testCompPos(t *testing.T, cmdStr string, compType string, hasCommand bool, cmdWordPos int, hasWord bool, superOffset int) {
cmdSP := utilfn.ParseToSP(cmdStr)
words := Tokenize(cmdSP.Str)
cmds := ParseCommands(words)
@ -161,24 +161,28 @@ func testCompPos(t *testing.T, cmdStr string, compType string, hasCommand bool,
if cpos.CmdWordPos != cmdWordPos {
t.Errorf("testCompPos %q => bad cmd-word-pos got:%d exp:%d", cmdStr, cpos.CmdWordPos, cmdWordPos)
}
if cpos.SuperOffset != superOffset {
t.Errorf("testCompPos %q => bad super-offset got:%d exp:%d", cmdStr, cpos.SuperOffset, superOffset)
}
}
func TestCompPos(t *testing.T) {
testCompPos(t, "ls [*]foo", CompTypeArg, true, 1, false)
testCompPos(t, "ls foo [*];", CompTypeArg, true, 2, false)
testCompPos(t, "ls foo ;[*]", CompTypeCommand, false, 0, false)
testCompPos(t, "ls foo >[*]> ./bar", CompTypeInvalid, true, 2, true)
testCompPos(t, "l[*]s", CompTypeCommand, true, 0, true)
testCompPos(t, "ls[*]", CompTypeCommand, true, 0, true)
testCompPos(t, "x=10 { (ls ./f[*] more); ls }", CompTypeArg, true, 1, true)
testCompPos(t, "for x in 1[*] 2 3; do ", CompTypeBasic, false, 0, true)
testCompPos(t, "for[*] x in 1 2 3;", CompTypeInvalid, false, 0, true)
testCompPos(t, "ls \"abc $(ls -l t[*])\" && foo", CompTypeArg, true, 2, true)
testCompPos(t, "ls ${abc:$(ls -l [*])}", CompTypeVar, false, 0, true) // we don't sub-parse inside of ${} (so this returns "var" right now)
testCompPos(t, `ls abc"$(ls $"echo $(ls ./[*]x) foo)" `, CompTypeArg, true, 1, true)
testCompPos(t, `ls "abc$d[*]"`, CompTypeVar, false, 0, true)
testCompPos(t, `ls "abc$d$'a[*]`, CompTypeArg, true, 1, true)
testCompPos(t, `ls $[*]'foo`, CompTypeInvalid, false, 0, false)
testCompPos(t, "ls [*]foo", CompTypeArg, true, 1, false, 0)
testCompPos(t, "ls foo [*];", CompTypeArg, true, 2, false, 0)
testCompPos(t, "ls foo ;[*]", CompTypeCommand, false, 0, false, 0)
testCompPos(t, "ls foo >[*]> ./bar", CompTypeInvalid, true, 2, true, 0)
testCompPos(t, "l[*]s", CompTypeCommand, true, 0, true, 0)
testCompPos(t, "ls[*]", CompTypeCommand, true, 0, true, 0)
testCompPos(t, "x=10 { (ls ./f[*] more); ls }", CompTypeArg, true, 1, true, 0)
testCompPos(t, "for x in 1[*] 2 3; do ", CompTypeBasic, false, 0, true, 0)
testCompPos(t, "for[*] x in 1 2 3;", CompTypeInvalid, false, 0, true, 0)
testCompPos(t, `ls "abc $(ls -l t[*])" && foo`, CompTypeArg, true, 2, true, 10)
testCompPos(t, "ls ${abc:$(ls -l [*])}", CompTypeVar, false, 0, true, 0) // we don't sub-parse inside of ${} (so this returns "var" right now)
testCompPos(t, `ls abc"$(ls $"echo $(ls ./[*]x) foo)" `, CompTypeArg, true, 1, true, 21)
testCompPos(t, `ls "abc$d[*]"`, CompTypeVar, false, 0, true, 4)
testCompPos(t, `ls "abc$d$'a[*]`, CompTypeArg, true, 1, true, 0)
testCompPos(t, `ls $[*]'foo`, CompTypeArg, true, 1, true, 0)
testCompPos(t, `echo $TE[*]`, CompTypeVar, false, 0, true, 0)
}
func testExpand(t *testing.T, str string, pos int, expStr string, expInfo *ExpandInfo) {