checkpoint on compgen

This commit is contained in:
sawka 2022-11-10 13:52:51 -08:00
parent 848f7164a3
commit 055dc7c8ac
5 changed files with 315 additions and 112 deletions

View File

@ -20,10 +20,12 @@ import (
"github.com/scripthaus-dev/mshell/pkg/base" "github.com/scripthaus-dev/mshell/pkg/base"
"github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/packet"
"github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/mshell/pkg/shexec"
"github.com/scripthaus-dev/sh2-server/pkg/comp"
"github.com/scripthaus-dev/sh2-server/pkg/remote" "github.com/scripthaus-dev/sh2-server/pkg/remote"
"github.com/scripthaus-dev/sh2-server/pkg/scbase" "github.com/scripthaus-dev/sh2-server/pkg/scbase"
"github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/scpacket"
"github.com/scripthaus-dev/sh2-server/pkg/sstore" "github.com/scripthaus-dev/sh2-server/pkg/sstore"
"github.com/scripthaus-dev/sh2-server/pkg/utilfn"
) )
const ( const (
@ -32,6 +34,11 @@ const (
HistoryTypeGlobal = "global" HistoryTypeGlobal = "global"
) )
func init() {
comp.RegisterSimpleCompFn("meta", simpleCompMeta)
comp.RegisterSimpleCompFn("command+meta", simpleCompCommandMeta)
}
const DefaultUserId = "sawka" const DefaultUserId = "sawka"
const MaxNameLen = 50 const MaxNameLen = 50
const MaxRemoteAliasLen = 50 const MaxRemoteAliasLen = 50
@ -1043,50 +1050,6 @@ func updateHistoryContext(ctx context.Context, line *sstore.LineType, cmd *sstor
} }
} }
func getStrArr(v interface{}, field string) []string {
if v == nil {
return nil
}
m, ok := v.(map[string]interface{})
if !ok {
return nil
}
fieldVal := m[field]
if fieldVal == nil {
return nil
}
iarr, ok := fieldVal.([]interface{})
if !ok {
return nil
}
var sarr []string
for _, iv := range iarr {
if sv, ok := iv.(string); ok {
sarr = append(sarr, sv)
}
}
return sarr
}
func getBool(v interface{}, field string) bool {
if v == nil {
return false
}
m, ok := v.(map[string]interface{})
if !ok {
return false
}
fieldVal := m[field]
if fieldVal == nil {
return false
}
bval, ok := fieldVal.(bool)
if !ok {
return false
}
return bval
}
func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.UpdatePacket { func makeInfoFromComps(compType string, comps []string, hasMore bool) sstore.UpdatePacket {
sort.Slice(comps, func(i int, j int) bool { sort.Slice(comps, func(i int, j int) bool {
c1 := comps[i] c1 := comps[i]
@ -1156,6 +1119,23 @@ func longestPrefix(root string, comps []string) string {
return lcp return lcp
} }
func simpleCompMeta(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) {
compsCmd, _ := comp.DoSimpleComp(ctx, "command", prefix, compCtx, nil)
compsMeta, _ := simpleCompCommandMeta(ctx, prefix, compCtx, nil)
return comp.CombineCompReturn(compsCmd, compsMeta), nil
}
func simpleCompCommandMeta(ctx context.Context, prefix string, compCtx comp.CompContext, args []interface{}) (*comp.CompReturn, error) {
rtn := comp.CompReturn{}
validCommands := getValidCommands()
for _, cmd := range validCommands {
if strings.HasPrefix(cmd, prefix) {
rtn.Entries = append(rtn.Entries, comp.CompEntry{Word: cmd, IsMetaCmd: true})
}
}
return &rtn, nil
}
func doMetaCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix string, forDisplay bool) ([]string, bool, error) { func doMetaCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix string, forDisplay bool) ([]string, bool, error) {
ids, err := resolveUiIds(ctx, pk, 0) // best effort ids, err := resolveUiIds(ctx, pk, 0) // best effort
var comps []string var comps []string
@ -1202,8 +1182,8 @@ func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix str
if err = resp.Err(); err != nil { if err = resp.Err(); err != nil {
return nil, false, err return nil, false, err
} }
comps := getStrArr(resp.Data, "comps") comps := utilfn.GetStrArr(resp.Data, "comps")
hasMore := getBool(resp.Data, "hasmore") hasMore := utilfn.GetBool(resp.Data, "hasmore")
return comps, hasMore, nil return comps, hasMore, nil
} }
@ -1743,7 +1723,7 @@ func displayStateUpdateDiff(buf *bytes.Buffer, oldState packet.ShellState, newSt
if newVal.IsExport() { if newVal.IsExport() {
exportStr = "export " exportStr = "export "
} }
buf.WriteString(fmt.Sprintf("%s%s=%s\n", exportStr, key, ShellQuote(newVal.Value, false, 50))) buf.WriteString(fmt.Sprintf("%s%s=%s\n", exportStr, key, utilfn.ShellQuote(newVal.Value, false, 50)))
} }
} }
for key, _ := range oldEnvMap { for key, _ := range oldEnvMap {

View File

@ -6,9 +6,9 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/alessio/shellescape"
"github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/mshell/pkg/shexec"
"github.com/scripthaus-dev/sh2-server/pkg/scpacket" "github.com/scripthaus-dev/sh2-server/pkg/scpacket"
"github.com/scripthaus-dev/sh2-server/pkg/utilfn"
"mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"
) )
@ -116,31 +116,6 @@ func onlyRawArgs(metaCmd string, metaSubCmd string) bool {
return metaCmd == "run" || metaCmd == "comment" return metaCmd == "run" || metaCmd == "comment"
} }
// minimum maxlen=6
func ShellQuote(val string, forceQuote bool, maxLen int) string {
if maxLen < 6 {
maxLen = 6
}
rtn := shellescape.Quote(val)
if strings.HasPrefix(rtn, "\"") || strings.HasPrefix(rtn, "'") {
if len(rtn) > maxLen {
return rtn[0:maxLen-4] + "..." + rtn[0:1]
}
return rtn
}
if forceQuote {
if len(rtn) > maxLen-2 {
return "\"" + rtn[0:maxLen-5] + "...\""
}
return "\"" + rtn + "\""
} else {
if len(rtn) > maxLen {
return rtn[0:maxLen-3] + "..."
}
return rtn
}
}
func setBracketArgs(argMap map[string]string, bracketStr string) error { func setBracketArgs(argMap map[string]string, bracketStr string) error {
bracketStr = strings.TrimSpace(bracketStr) bracketStr = strings.TrimSpace(bracketStr)
if bracketStr == "" { if bracketStr == "" {
@ -161,7 +136,7 @@ func setBracketArgs(argMap map[string]string, bracketStr string) error {
varVal = litStr[eqIdx+1:] varVal = litStr[eqIdx+1:]
} }
if !shexec.IsValidBashIdentifier(varName) { if !shexec.IsValidBashIdentifier(varName) {
wordErr = fmt.Errorf("invalid identifier %s in bracket args", ShellQuote(varName, true, 20)) wordErr = fmt.Errorf("invalid identifier %s in bracket args", utilfn.ShellQuote(varName, true, 20))
return false return false
} }
if varVal == "" { if varVal == "" {

View File

@ -2,17 +2,23 @@
package comp package comp
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"sort"
"strconv"
"strings" "strings"
"unicode" "unicode"
"github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/packet"
"github.com/scripthaus-dev/mshell/pkg/shexec" "github.com/scripthaus-dev/mshell/pkg/shexec"
"github.com/scripthaus-dev/sh2-server/pkg/sstore" "github.com/scripthaus-dev/sh2-server/pkg/sstore"
"github.com/scripthaus-dev/sh2-server/pkg/utilfn"
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"
) )
const MaxCompQuoteLen = 5000
const ( const (
SimpleCompGenTypeFile = "file" SimpleCompGenTypeFile = "file"
SimpleCompGenTypeDir = "dir" SimpleCompGenTypeDir = "dir"
@ -24,19 +30,31 @@ const (
SimpleCompGenTypeVariable = "variable" SimpleCompGenTypeVariable = "variable"
) )
const (
QuoteTypeLiteral = ""
QuoteTypeDQ = "\""
QuoteTypeANSI = "$'"
QuoteTypeSQ = "'"
)
type CompContext struct { type CompContext struct {
RemotePtr sstore.RemotePtrType RemotePtr sstore.RemotePtrType
State *packet.ShellState State *packet.ShellState
ForDisplay bool ForDisplay bool
} }
type SimpleCompGenFnType = func(ctx context.Context, point SimpleCompPoint, compCtx CompContext, args []interface{}) (*CompReturn, error)
type SimpleCompPoint struct { type SimpleCompPoint struct {
Word string Word string
Pos int Pos int
} }
type fullCompPrefix struct {
RawStr string
RawPos int
CompPrefix string
QuoteTypePref string
}
type ParsedWord struct { type ParsedWord struct {
Offset int Offset int
Word *syntax.Word Word *syntax.Word
@ -53,6 +71,75 @@ type CompPoint struct {
Suffix string Suffix string
} }
// directories will have a trailing "/"
type CompEntry struct {
Word string
IsMetaCmd bool
}
type CompReturn struct {
Entries []CompEntry
HasMore bool
}
func compQuoteDQString(s string, close bool) string {
var buf bytes.Buffer
buf.WriteByte('"')
for _, ch := range s {
if ch == '"' || ch == '\\' || ch == '$' || ch == '`' {
buf.WriteByte('\\')
buf.WriteRune(ch)
continue
}
buf.WriteRune(ch)
}
if close {
buf.WriteByte('"')
}
return buf.String()
}
func compQuoteString(s string, quoteType string, close bool) string {
if quoteType != QuoteTypeANSI {
for _, ch := range s {
if ch > unicode.MaxASCII || !unicode.IsPrint(ch) || ch == '!' {
quoteType = QuoteTypeANSI
break
}
if ch == '\'' {
if quoteType == QuoteTypeSQ || quoteType == QuoteTypeLiteral {
quoteType = QuoteTypeANSI
break
}
}
}
}
if quoteType == QuoteTypeANSI {
rtn := strconv.QuoteToASCII(s)
rtn = "$'" + strings.ReplaceAll(rtn[1:len(rtn)-1], "'", "\\'")
if close {
rtn = rtn + "'"
}
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
}
if quoteType == QuoteTypeSQ {
rtn := utilfn.ShellQuote(s, true, MaxCompQuoteLen)
if !close {
rtn = rtn[0 : len(rtn)-1]
}
return rtn
}
// QuoteTypeDQ
return compQuoteDQString(s, close)
}
func (p *CompPoint) wordAsStr(w ParsedWord) string { func (p *CompPoint) wordAsStr(w ParsedWord) string {
if w.Word != nil { if w.Word != nil {
return p.StmtStr[w.Word.Pos().Offset():w.Word.End().Offset()] return p.StmtStr[w.Word.Pos().Offset():w.Word.End().Offset()]
@ -63,16 +150,29 @@ func (p *CompPoint) wordAsStr(w ParsedWord) string {
func (p *CompPoint) simpleExpandWord(w ParsedWord) string { func (p *CompPoint) simpleExpandWord(w ParsedWord) string {
ectx := shexec.SimpleExpandContext{} ectx := shexec.SimpleExpandContext{}
if w.Word != nil { if w.Word != nil {
return SimpleExpandWord(ectx, w.Word, p.StmtStr) return shexec.SimpleExpandWord(ectx, w.Word, p.StmtStr)
} }
return SimpleExpandPartialWord(ectx, p.PartialWord, false) return shexec.SimpleExpandPartialWord(ectx, w.PartialWord, false)
} }
func (p *CompPoint) compPrefix() string { func getQuoteTypePref(str string) string {
pword := p.Words[p.CompWord] if strings.HasPrefix(str, QuoteTypeANSI) {
return QuoteTypeANSI
}
if strings.HasPrefix(str, QuoteTypeDQ) {
return QuoteTypeDQ
}
if strings.HasPrefix(str, QuoteTypeSQ) {
return QuoteTypeSQ
}
return QuoteTypeLiteral
}
func (p *CompPoint) getCompPrefix() string {
if p.CompWordPos == 0 { if p.CompWordPos == 0 {
return "" return ""
} }
pword := p.Words[p.CompWord]
wordStr := p.wordAsStr(pword) wordStr := p.wordAsStr(pword)
if p.CompWordPos == len(wordStr) { if p.CompWordPos == len(wordStr) {
return p.simpleExpandWord(pword) return p.simpleExpandWord(pword)
@ -82,7 +182,21 @@ func (p *CompPoint) compPrefix() string {
// and a partial on just the current part. this is an uncommon case though // 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) // and has very little upside (even bash does not expand multipart words correctly)
partialWordStr := wordStr[:p.CompWordPos] partialWordStr := wordStr[:p.CompWordPos]
return SimpleExpandPartialWord(shexec.SimpleExpandContext{}, partialWordStr, false) return shexec.SimpleExpandPartialWord(shexec.SimpleExpandContext{}, partialWordStr, false)
}
func (p *CompPoint) extendWord(newWord string, newWordComplete bool) (string, int) {
pword := p.Words[p.CompWord]
wordStr := p.wordAsStr(pword)
quotePref := getQuoteTypePref(wordStr)
needsClose := newWordComplete && (len(wordStr) == p.CompWordPos)
wordSuffix := wordStr[p.CompWordPos:]
newQuotedStr := compQuoteString(newWord, quotePref, needsClose)
if needsClose && wordSuffix == "" {
newQuotedStr = newQuotedStr + " "
}
newPos := len(newQuotedStr)
return newQuotedStr + wordSuffix, newPos
} }
func (p *CompPoint) dump() { func (p *CompPoint) dump() {
@ -107,17 +221,6 @@ func (p *CompPoint) dump() {
fmt.Printf("\n") fmt.Printf("\n")
} }
// directories will have a trailing "/"
type CompEntry struct {
Word string
IsMetaCmd bool
}
type CompReturn struct {
Entries []CompEntry
HasMore bool
}
var SimpleCompGenFns map[string]SimpleCompGenFnType var SimpleCompGenFns map[string]SimpleCompGenFnType
func strWithCursor(str string, pos int) string { func strWithCursor(str string, pos int) string {
@ -229,7 +332,6 @@ func ParseCompPoint(fullCmdStr string, pos int) (*CompPoint, error) {
} }
} }
} }
rtnPoint.dump()
return &rtnPoint, nil return &rtnPoint, nil
} }
@ -253,3 +355,32 @@ func splitCompWord(p *CompPoint) {
func DoCompGen(ctx context.Context, point CompPoint, rptr sstore.RemotePtrType, state *packet.ShellState) (*CompReturn, error) { func DoCompGen(ctx context.Context, point CompPoint, rptr sstore.RemotePtrType, state *packet.ShellState) (*CompReturn, error) {
return nil, nil return nil, nil
} }
func SortCompReturnEntries(c *CompReturn) {
sort.Slice(c.Entries, func(i int, j int) bool {
e1 := c.Entries[i]
e2 := c.Entries[j]
if e1.Word < e2.Word {
return true
}
if e1.Word == e2.Word && e1.IsMetaCmd && !e2.IsMetaCmd {
return true
}
return false
})
}
func CombineCompReturn(c1 *CompReturn, c2 *CompReturn) *CompReturn {
if c1 == nil {
return c2
}
if c2 == nil {
return c1
}
var rtn CompReturn
rtn.HasMore = c1.HasMore || c2.HasMore
rtn.Entries = append([]CompEntry{}, c1.Entries...)
rtn.Entries = append(rtn.Entries, c2.Entries...)
SortCompReturnEntries(&rtn)
return &rtn
}

View File

@ -3,12 +3,47 @@ package comp
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/scripthaus-dev/mshell/pkg/packet" "github.com/scripthaus-dev/mshell/pkg/packet"
"github.com/scripthaus-dev/sh2-server/pkg/remote" "github.com/scripthaus-dev/sh2-server/pkg/remote"
"github.com/scripthaus-dev/sh2-server/pkg/utilfn"
) )
var globalLock = &sync.Mutex{}
var simpleCompMap = map[string]SimpleCompGenFnType{
"file": simpleCompFile,
"directory": simpleCompDir,
"variable": simpleCompVar,
"command": simpleCompCommand,
}
type SimpleCompGenFnType = func(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error)
func RegisterSimpleCompFn(compType string, fn SimpleCompGenFnType) {
globalLock.Lock()
defer globalLock.Unlock()
if _, ok := simpleCompMap[compType]; ok {
panic(fmt.Sprintf("simpleCompFn %q already registered", compType))
}
simpleCompMap[compType] = fn
}
func getSimpleCompFn(compType string) SimpleCompGenFnType {
globalLock.Lock()
defer globalLock.Unlock()
return simpleCompMap[compType]
}
func DoSimpleComp(ctx context.Context, compType string, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) {
compFn := getSimpleCompFn(compType)
if compFn == nil {
return nil, fmt.Errorf("no simple comp fn for %q", compType)
}
return compFn(ctx, prefix, compCtx, args)
}
func compsToCompReturn(comps []string, hasMore bool) *CompReturn { func compsToCompReturn(comps []string, hasMore bool) *CompReturn {
var rtn CompReturn var rtn CompReturn
rtn.HasMore = hasMore rtn.HasMore = hasMore
@ -18,13 +53,13 @@ func compsToCompReturn(comps []string, hasMore bool) *CompReturn {
return &rtn return &rtn
} }
func doCompGen(ctx context.Context, compCtx CompContext, prefix string, compType string, forDisplay bool) (*CompReturn, error) { func doCompGen(ctx context.Context, prefix string, compType string, compCtx CompContext) (*CompReturn, error) {
if !packet.IsValidCompGenType(compType) { if !packet.IsValidCompGenType(compType) {
return nil, false, fmt.Errorf("/compgen invalid type '%s'", compType) return nil, fmt.Errorf("/compgen invalid type '%s'", compType)
} }
msh := remote.GetRemoteById(compCtx.RemotePtr.RemoteId) msh := remote.GetRemoteById(compCtx.RemotePtr.RemoteId)
if msh == nil { if msh == nil {
return nil, false, fmt.Errorf("invalid remote '%s', not found", compCtx.RemotePtr) return nil, fmt.Errorf("invalid remote '%s', not found", compCtx.RemotePtr)
} }
cgPacket := packet.MakeCompGenPacket() cgPacket := packet.MakeCompGenPacket()
cgPacket.ReqId = uuid.New().String() cgPacket.ReqId = uuid.New().String()
@ -32,23 +67,29 @@ func doCompGen(ctx context.Context, compCtx CompContext, prefix string, compType
cgPacket.Prefix = prefix cgPacket.Prefix = prefix
cgPacket.Cwd = compCtx.State.Cwd cgPacket.Cwd = compCtx.State.Cwd
resp, err := msh.PacketRpc(ctx, cgPacket) resp, err := msh.PacketRpc(ctx, cgPacket)
if err != nil {
return nil, false, err
}
if err = resp.Err(); err != nil {
return nil, false, err
}
comps := getStrArr(resp.Data, "comps")
hasMore := getBool(resp.Data, "hasmore")
return compsToCompReturn(conmps, hasMore), nil
}
func SimpleCompFile(ctx context.Context, point SimpleCompPoint, compCtx CompContext, args []interface{}) (*CompReturn, error) {
pword := point.Words[p.CompWord]
prefix := ""
crtn, err := doCompGen(ctx, prefix, "file")
if err != nil { if err != nil {
return nil, err return nil, err
} }
return crtn, nil if err = resp.Err(); err != nil {
return nil, err
}
comps := utilfn.GetStrArr(resp.Data, "comps")
hasMore := utilfn.GetBool(resp.Data, "hasmore")
return compsToCompReturn(comps, hasMore), nil
}
func simpleCompFile(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) {
return doCompGen(ctx, prefix, "file", compCtx)
}
func simpleCompDir(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) {
return doCompGen(ctx, prefix, "directory", compCtx)
}
func simpleCompVar(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) {
return doCompGen(ctx, prefix, "variable", compCtx)
}
func simpleCompCommand(ctx context.Context, prefix string, compCtx CompContext, args []interface{}) (*CompReturn, error) {
return doCompGen(ctx, prefix, "command", compCtx)
} }

76
pkg/utilfn/utilfn.go Normal file
View File

@ -0,0 +1,76 @@
package utilfn
import (
"strings"
"github.com/alessio/shellescape"
)
func GetStrArr(v interface{}, field string) []string {
if v == nil {
return nil
}
m, ok := v.(map[string]interface{})
if !ok {
return nil
}
fieldVal := m[field]
if fieldVal == nil {
return nil
}
iarr, ok := fieldVal.([]interface{})
if !ok {
return nil
}
var sarr []string
for _, iv := range iarr {
if sv, ok := iv.(string); ok {
sarr = append(sarr, sv)
}
}
return sarr
}
func GetBool(v interface{}, field string) bool {
if v == nil {
return false
}
m, ok := v.(map[string]interface{})
if !ok {
return false
}
fieldVal := m[field]
if fieldVal == nil {
return false
}
bval, ok := fieldVal.(bool)
if !ok {
return false
}
return bval
}
// minimum maxlen=6
func ShellQuote(val string, forceQuote bool, maxLen int) string {
if maxLen < 6 {
maxLen = 6
}
rtn := shellescape.Quote(val)
if strings.HasPrefix(rtn, "\"") || strings.HasPrefix(rtn, "'") {
if len(rtn) > maxLen {
return rtn[0:maxLen-4] + "..." + rtn[0:1]
}
return rtn
}
if forceQuote {
if len(rtn) > maxLen-2 {
return "\"" + rtn[0:maxLen-5] + "...\""
}
return "\"" + rtn + "\""
} else {
if len(rtn) > maxLen {
return rtn[0:maxLen-3] + "..."
}
return rtn
}
}