diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 17edd0b4..c4133713 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -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) { diff --git a/pkg/cmdrunner/shparse.go b/pkg/cmdrunner/shparse.go index 941c9e9d..3ad497eb 100644 --- a/pkg/cmdrunner/shparse.go +++ b/pkg/cmdrunner/shparse.go @@ -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") diff --git a/pkg/comp/comp.go b/pkg/comp/comp.go index b446450d..445ee53d 100644 --- a/pkg/comp/comp.go +++ b/pkg/comp/comp.go @@ -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 diff --git a/pkg/shparse/comp.go b/pkg/shparse/comp.go index 136bf365..c0efe82c 100644 --- a/pkg/shparse/comp.go +++ b/pkg/shparse/comp.go @@ -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 +} diff --git a/pkg/shparse/shparse_test.go b/pkg/shparse/shparse_test.go index 13d48c72..e98064c1 100644 --- a/pkg/shparse/shparse_test.go +++ b/pkg/shparse/shparse_test.go @@ -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) {