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/packet"
"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/scbase"
"github.com/scripthaus-dev/sh2-server/pkg/scpacket"
"github.com/scripthaus-dev/sh2-server/pkg/sstore"
"github.com/scripthaus-dev/sh2-server/pkg/utilfn"
)
const (
@ -32,6 +34,11 @@ const (
HistoryTypeGlobal = "global"
)
func init() {
comp.RegisterSimpleCompFn("meta", simpleCompMeta)
comp.RegisterSimpleCompFn("command+meta", simpleCompCommandMeta)
}
const DefaultUserId = "sawka"
const MaxNameLen = 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 {
sort.Slice(comps, func(i int, j int) bool {
c1 := comps[i]
@ -1156,6 +1119,23 @@ func longestPrefix(root string, comps []string) string {
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) {
ids, err := resolveUiIds(ctx, pk, 0) // best effort
var comps []string
@ -1202,8 +1182,8 @@ func doCompGen(ctx context.Context, pk *scpacket.FeCommandPacketType, prefix str
if err = resp.Err(); err != nil {
return nil, false, err
}
comps := getStrArr(resp.Data, "comps")
hasMore := getBool(resp.Data, "hasmore")
comps := utilfn.GetStrArr(resp.Data, "comps")
hasMore := utilfn.GetBool(resp.Data, "hasmore")
return comps, hasMore, nil
}
@ -1743,7 +1723,7 @@ func displayStateUpdateDiff(buf *bytes.Buffer, oldState packet.ShellState, newSt
if newVal.IsExport() {
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 {

View File

@ -6,9 +6,9 @@ import (
"regexp"
"strings"
"github.com/alessio/shellescape"
"github.com/scripthaus-dev/mshell/pkg/shexec"
"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/syntax"
)
@ -116,31 +116,6 @@ func onlyRawArgs(metaCmd string, metaSubCmd string) bool {
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 {
bracketStr = strings.TrimSpace(bracketStr)
if bracketStr == "" {
@ -161,7 +136,7 @@ func setBracketArgs(argMap map[string]string, bracketStr string) error {
varVal = litStr[eqIdx+1:]
}
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
}
if varVal == "" {

View File

@ -2,17 +2,23 @@
package comp
import (
"bytes"
"context"
"fmt"
"sort"
"strconv"
"strings"
"unicode"
"github.com/scripthaus-dev/mshell/pkg/packet"
"github.com/scripthaus-dev/mshell/pkg/shexec"
"github.com/scripthaus-dev/sh2-server/pkg/sstore"
"github.com/scripthaus-dev/sh2-server/pkg/utilfn"
"mvdan.cc/sh/v3/syntax"
)
const MaxCompQuoteLen = 5000
const (
SimpleCompGenTypeFile = "file"
SimpleCompGenTypeDir = "dir"
@ -24,19 +30,31 @@ const (
SimpleCompGenTypeVariable = "variable"
)
const (
QuoteTypeLiteral = ""
QuoteTypeDQ = "\""
QuoteTypeANSI = "$'"
QuoteTypeSQ = "'"
)
type CompContext struct {
RemotePtr sstore.RemotePtrType
State *packet.ShellState
ForDisplay bool
}
type SimpleCompGenFnType = func(ctx context.Context, point SimpleCompPoint, compCtx CompContext, args []interface{}) (*CompReturn, error)
type SimpleCompPoint struct {
Word string
Pos int
}
type fullCompPrefix struct {
RawStr string
RawPos int
CompPrefix string
QuoteTypePref string
}
type ParsedWord struct {
Offset int
Word *syntax.Word
@ -53,6 +71,75 @@ type CompPoint struct {
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 {
if w.Word != nil {
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 {
ectx := shexec.SimpleExpandContext{}
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 {
pword := p.Words[p.CompWord]
func getQuoteTypePref(str string) string {
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 {
return ""
}
pword := p.Words[p.CompWord]
wordStr := p.wordAsStr(pword)
if p.CompWordPos == len(wordStr) {
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 has very little upside (even bash does not expand multipart words correctly)
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() {
@ -107,17 +221,6 @@ func (p *CompPoint) dump() {
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
func strWithCursor(str string, pos int) string {
@ -229,7 +332,6 @@ func ParseCompPoint(fullCmdStr string, pos int) (*CompPoint, error) {
}
}
}
rtnPoint.dump()
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) {
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 (
"context"
"fmt"
"sync"
"github.com/google/uuid"
"github.com/scripthaus-dev/mshell/pkg/packet"
"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 {
var rtn CompReturn
rtn.HasMore = hasMore
@ -18,13 +53,13 @@ func compsToCompReturn(comps []string, hasMore bool) *CompReturn {
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) {
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)
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.ReqId = uuid.New().String()
@ -32,23 +67,29 @@ func doCompGen(ctx context.Context, compCtx CompContext, prefix string, compType
cgPacket.Prefix = prefix
cgPacket.Cwd = compCtx.State.Cwd
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 {
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
}
}