add quoting/shell-parsing for commands

This commit is contained in:
sawka 2022-08-26 13:12:17 -07:00
parent d0806bbd63
commit dc8cba79da
7 changed files with 312 additions and 91 deletions

1
go.mod
View File

@ -21,6 +21,7 @@ require (
github.com/mattn/go-shellwords v1.0.12 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.7.0 // indirect
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
mvdan.cc/sh/v3 v3.5.1 // indirect
) )
replace github.com/scripthaus-dev/mshell v0.0.0 => /Users/mike/work/gopath/src/github.com/scripthaus-dev/mshell/ replace github.com/scripthaus-dev/mshell v0.0.0 => /Users/mike/work/gopath/src/github.com/scripthaus-dev/mshell/

2
go.sum
View File

@ -1798,6 +1798,8 @@ modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4=
mvdan.cc/sh/v3 v3.5.1 h1:hmP3UOw4f+EYexsJjFxvU38+kn+V/s2CclXHanIBkmQ=
mvdan.cc/sh/v3 v3.5.1/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=

View File

@ -45,25 +45,6 @@ type resolvedIds struct {
RState remote.RemoteState RState remote.RemoteState
} }
func SubMetaCmd(cmd string) string {
switch cmd {
case "s":
return "screen"
case "w":
return "window"
case "r":
return "run"
case "c":
return "comment"
case "e":
return "eval"
case "export":
return "setenv"
default:
return cmd
}
}
var ValidCommands = []string{ var ValidCommands = []string{
"/run", "/run",
"/eval", "/eval",
@ -377,12 +358,11 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.
if len(pk.Args) == 0 { if len(pk.Args) == 0 {
return nil, fmt.Errorf("usage: /eval [command], no command passed to eval") return nil, fmt.Errorf("usage: /eval [command], no command passed to eval")
} }
// parse metacmd newPk, err := EvalMetaCommand(ctx, pk)
commandStr := strings.TrimSpace(pk.Args[0]) if err != nil {
if commandStr == "" { return nil, err
return nil, fmt.Errorf("/eval, invalid emtpty command")
} }
update, err := evalCommandInternal(ctx, pk) update, err := HandleCommand(ctx, newPk)
if !resolveBool(pk.Kwargs["nohist"], false) { if !resolveBool(pk.Kwargs["nohist"], false) {
err := addToHistory(ctx, pk, update, (err != nil)) err := addToHistory(ctx, pk, update, (err != nil))
if err != nil { if err != nil {
@ -393,73 +373,6 @@ func EvalCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.
return update, err return update, err
} }
func evalCommandInternal(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
commandStr := strings.TrimSpace(pk.Args[0])
metaCmd := ""
metaSubCmd := ""
if commandStr == "cd" || strings.HasPrefix(commandStr, "cd ") {
metaCmd = "cd"
commandStr = strings.TrimSpace(commandStr[2:])
} else if commandStr == "cr" || strings.HasPrefix(commandStr, "cr ") {
metaCmd = "cr"
commandStr = strings.TrimSpace(commandStr[2:])
} else if commandStr == "export" || strings.HasPrefix(commandStr, "export ") {
metaCmd = "setenv"
commandStr = strings.TrimSpace(commandStr[6:])
} else if commandStr == "setenv" || strings.HasPrefix(commandStr, "setenv ") {
metaCmd = "setenv"
commandStr = strings.TrimSpace(commandStr[6:])
} else if commandStr == "unset" || strings.HasPrefix(commandStr, "unset ") {
metaCmd = "unset"
commandStr = strings.TrimSpace(commandStr[5:])
} else if commandStr[0] == '/' {
spaceIdx := strings.Index(commandStr, " ")
if spaceIdx == -1 {
metaCmd = commandStr[1:]
commandStr = ""
} else {
metaCmd = commandStr[1:spaceIdx]
commandStr = strings.TrimSpace(commandStr[spaceIdx+1:])
}
colonIdx := strings.Index(metaCmd, ":")
if colonIdx != -1 {
metaCmd, metaSubCmd = metaCmd[0:colonIdx], metaCmd[colonIdx+1:]
}
if metaCmd == "" {
return nil, fmt.Errorf("invalid command, got bare '/', with no command")
}
}
if metaCmd == "" {
metaCmd = "run"
}
metaCmd = SubMetaCmd(metaCmd)
newPk := &scpacket.FeCommandPacketType{
MetaCmd: metaCmd,
MetaSubCmd: metaSubCmd,
Kwargs: pk.Kwargs,
}
if strings.HasSuffix(commandStr, " ?") {
newPk.Kwargs["ephemeral"] = "1"
commandStr = commandStr[0 : len(commandStr)-2]
}
if metaCmd == "run" || metaCmd == "comment" {
newPk.Args = []string{commandStr}
} else if (metaCmd == "setenv" || metaCmd == "unset") && metaSubCmd == "" {
newPk.Args = strings.Fields(commandStr)
} else {
allArgs := strings.Fields(commandStr)
for _, arg := range allArgs {
if strings.Index(arg, "=") == -1 {
newPk.Args = append(newPk.Args, arg)
continue
}
fields := strings.SplitN(arg, "=", 2)
newPk.Kwargs[fields[0]] = fields[1]
}
}
return HandleCommand(ctx, newPk)
}
func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) { func ScreenCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
if pk.MetaSubCmd == "close" { if pk.MetaSubCmd == "close" {
ids, err := resolveIds(ctx, pk, R_Session|R_Screen) ids, err := resolveIds(ctx, pk, R_Session|R_Screen)
@ -976,6 +889,27 @@ func SessionCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto
} }
return update, nil return update, nil
} }
if pk.MetaSubCmd == "set" {
ids, err := resolveIds(ctx, pk, R_Session)
if err != nil {
return nil, err
}
bareSession, err := sstore.GetBareSessionById(ctx, ids.SessionId)
if err != nil {
return nil, err
}
if bareSession == nil {
return nil, fmt.Errorf("session '%s' not found", ids.SessionId)
}
update := sstore.ModelUpdate{
Sessions: nil,
Info: &sstore.InfoMsgType{
InfoMsg: fmt.Sprintf("[%s]: update", bareSession.Name),
TimeoutMs: 2000,
},
}
return update, nil
}
if pk.MetaSubCmd != "" { if pk.MetaSubCmd != "" {
return nil, fmt.Errorf("invalid /session subcommand '%s'", pk.MetaSubCmd) return nil, fmt.Errorf("invalid /session subcommand '%s'", pk.MetaSubCmd)
} }

210
pkg/cmdrunner/shparse.go Normal file
View File

@ -0,0 +1,210 @@
package cmdrunner
import (
"context"
"fmt"
"io"
"regexp"
"strings"
"github.com/scripthaus-dev/sh2-server/pkg/scpacket"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/syntax"
)
type parseEnviron struct {
Env map[string]string
}
func (e *parseEnviron) Get(name string) expand.Variable {
val, ok := e.Env[name]
if !ok {
return expand.Variable{}
}
return expand.Variable{
Exported: true,
Kind: expand.String,
Str: val,
}
}
func (e *parseEnviron) Each(fn func(name string, vr expand.Variable) bool) {
for key, _ := range e.Env {
rtn := fn(key, e.Get(key))
if !rtn {
break
}
}
}
func DumpPacket(pk *scpacket.FeCommandPacketType) {
if pk == nil || pk.MetaCmd == "" {
fmt.Printf("[no metacmd]\n")
return
}
if pk.MetaSubCmd == "" {
fmt.Printf("/%s\n", pk.MetaCmd)
} else {
fmt.Printf("/%s:%s\n", pk.MetaCmd, pk.MetaSubCmd)
}
for _, arg := range pk.Args {
fmt.Printf(" %q\n", arg)
}
for key, val := range pk.Kwargs {
fmt.Printf(" [%s]=%q\n", key, val)
}
}
func doCmdSubst(commandStr string, w io.Writer, word *syntax.CmdSubst) error {
return nil
}
func doProcSubst(w *syntax.ProcSubst) (string, error) {
return "", nil
}
func isQuoted(source string, w *syntax.Word) bool {
if w == nil {
return false
}
offset := w.Pos().Offset()
if int(offset) >= len(source) {
return false
}
return source[offset] == '"' || source[offset] == '\''
}
func getSourceStr(source string, w *syntax.Word) string {
if w == nil {
return ""
}
offset := w.Pos().Offset()
end := w.End().Offset()
return source[offset:end]
}
var ValidMetaCmdRe = regexp.MustCompile("^/([a-z][a-z0-9_-]*)(:[a-z][a-z0-9_-]*)?$")
type BareMetaCmdDecl struct {
CmdStr string
MetaCmd string
}
var BareMetaCmds = []BareMetaCmdDecl{
BareMetaCmdDecl{"cd", "cd"},
BareMetaCmdDecl{"cr", "cr"},
BareMetaCmdDecl{"setenv", "setenv"},
BareMetaCmdDecl{"export", "setenv"},
BareMetaCmdDecl{"unset", "unset"},
}
func SubMetaCmd(cmd string) string {
switch cmd {
case "s":
return "screen"
case "w":
return "window"
case "r":
return "run"
case "c":
return "comment"
case "e":
return "eval"
case "export":
return "setenv"
default:
return cmd
}
}
// returns (metaCmd, metaSubCmd, rest)
// if metaCmd is "" then this isn't a valid metacmd string
func parseMetaCmd(origCommandStr string) (string, string, string) {
commandStr := strings.TrimSpace(origCommandStr)
if len(commandStr) < 2 {
return "run", "", origCommandStr
}
fields := strings.SplitN(commandStr, " ", 2)
firstArg := fields[0]
rest := ""
if len(fields) > 1 {
rest = strings.TrimSpace(fields[1])
}
for _, decl := range BareMetaCmds {
if firstArg == decl.CmdStr {
return decl.MetaCmd, "", rest
}
}
m := ValidMetaCmdRe.FindStringSubmatch(firstArg)
if m == nil {
return "run", "", origCommandStr
}
return SubMetaCmd(m[1]), m[2], rest
}
func onlyPositionalArgs(metaCmd string, metaSubCmd string) bool {
return (metaCmd == "setenv" || metaCmd == "unset") && metaSubCmd == ""
}
func onlyRawArgs(metaCmd string, metaSubCmd string) bool {
return metaCmd == "run" || metaCmd == "comment"
}
func EvalMetaCommand(ctx context.Context, origPk *scpacket.FeCommandPacketType) (*scpacket.FeCommandPacketType, error) {
if len(origPk.Args) == 0 {
return nil, fmt.Errorf("empty command (no fields)")
}
metaCmd, metaSubCmd, commandArgs := parseMetaCmd(origPk.Args[0])
rtnPk := scpacket.MakeFeCommandPacket()
rtnPk.MetaCmd = metaCmd
rtnPk.MetaSubCmd = metaSubCmd
rtnPk.Kwargs = make(map[string]string)
for key, val := range origPk.Kwargs {
rtnPk.Kwargs[key] = val
}
if onlyRawArgs(metaCmd, metaSubCmd) {
// don't evaluate arguments for /run or /comment
rtnPk.Args = []string{commandArgs}
return rtnPk, nil
}
commandReader := strings.NewReader(commandArgs)
parser := syntax.NewParser(syntax.Variant(syntax.LangBash))
var words []*syntax.Word
err := parser.Words(commandReader, func(w *syntax.Word) bool {
words = append(words, w)
return true
})
if err != nil {
return nil, fmt.Errorf("parsing metacmd, position %v", err)
}
envMap := make(map[string]string) // later we can add vars like session, window, screen, remote, and user
cfg := &expand.Config{
Env: &parseEnviron{Env: envMap},
GlobStar: false,
NullGlob: false,
NoUnset: false,
CmdSubst: func(w io.Writer, word *syntax.CmdSubst) error { return doCmdSubst(commandArgs, w, word) },
ProcSubst: doProcSubst,
ReadDir: nil,
}
// process arguments
for idx, w := range words {
literalVal, err := expand.Literal(cfg, w)
if err != nil {
return nil, fmt.Errorf("error evaluating metacmd argument %d [%s]: %v", idx+1, getSourceStr(commandArgs, w), err)
}
if isQuoted(commandArgs, w) || onlyPositionalArgs(metaCmd, metaSubCmd) {
rtnPk.Args = append(rtnPk.Args, literalVal)
continue
}
eqIdx := strings.Index(literalVal, "=")
if eqIdx != -1 && eqIdx != 0 {
varName := literalVal[:eqIdx]
varVal := literalVal[eqIdx+1:]
rtnPk.Kwargs[varName] = varVal
continue
}
rtnPk.Args = append(rtnPk.Args, literalVal)
}
return rtnPk, nil
}

View File

@ -250,6 +250,12 @@ func (proc *MShellProc) GetRemoteState() RemoteState {
return state return state
} }
func (msh *MShellProc) NotifyUpdate() {
rstate := msh.GetRemoteState()
update := &sstore.ModelUpdate{Remote: rstate}
sstore.MainBus.SendUpdate("", update)
}
func GetAllRemoteState() []RemoteState { func GetAllRemoteState() []RemoteState {
GlobalStore.Lock.Lock() GlobalStore.Lock.Lock()
defer GlobalStore.Lock.Unlock() defer GlobalStore.Lock.Unlock()

View File

@ -142,6 +142,22 @@ func GetBareSessions(ctx context.Context) ([]*SessionType, error) {
return rtn, nil return rtn, nil
} }
func GetBareSessionById(ctx context.Context, sessionId string) (*SessionType, error) {
var rtn SessionType
txErr := WithTx(ctx, func(tx *TxWrap) error {
query := `SELECT * FROM session WHERE sessionid = ?`
tx.GetWrap(&rtn, query, sessionId)
return nil
})
if txErr != nil {
return nil, txErr
}
if rtn.SessionId == "" {
return nil, nil
}
return &rtn, nil
}
func GetAllSessions(ctx context.Context) ([]*SessionType, error) { func GetAllSessions(ctx context.Context) ([]*SessionType, error) {
var rtn []*SessionType var rtn []*SessionType
err := WithTx(ctx, func(tx *TxWrap) error { err := WithTx(ctx, func(tx *TxWrap) error {
@ -636,3 +652,54 @@ func UpdateCurRemote(ctx context.Context, sessionId string, windowId string, rem
}) })
return txErr return txErr
} }
func reorderStrings(strs []string, toMove string, newIndex int) []string {
if toMove == "" {
return strs
}
var newStrs []string
if newIndex < 0 {
newStrs = append(newStrs, toMove)
}
for _, sval := range strs {
if len(newStrs) == newIndex {
newStrs = append(newStrs, toMove)
}
if sval != toMove {
newStrs = append(newStrs, sval)
}
}
if newIndex >= len(newStrs) {
newStrs = append(newStrs, toMove)
}
return newStrs
}
func ReIndexSessions(ctx context.Context, sessionId string, newIndex int) error {
txErr := WithTx(ctx, func(tx *TxWrap) error {
query := `SELECT sessionid FROM sessions ORDER BY sessionidx, name, sessionid`
ids := tx.SelectStrings(query)
if sessionId != "" {
ids = reorderStrings(ids, sessionId, newIndex)
}
query = `UPDATE sessions SET sessionid = ? WHERE sessionid = ?`
for idx, id := range ids {
tx.ExecWrap(query, id, idx+1)
}
return nil
})
return txErr
}
func SetSessionName(ctx context.Context, sessionId string, name string) error {
txErr := WithTx(ctx, func(tx *TxWrap) error {
query := `SELECT sessionid FROM sessions WHERE sessionid = ?`
if !tx.Exists(query, sessionId) {
return fmt.Errorf("session does not exist")
}
query = `UPDATE sessions SET name = ? WHERE sessionid = ?`
tx.ExecWrap(query, name, sessionId)
return nil
})
return txErr
}

View File

@ -31,6 +31,7 @@ type ModelUpdate struct {
Cmd *CmdType `json:"cmd,omitempty"` Cmd *CmdType `json:"cmd,omitempty"`
CmdLine *CmdLineType `json:"cmdline,omitempty"` CmdLine *CmdLineType `json:"cmdline,omitempty"`
Info *InfoMsgType `json:"info,omitempty"` Info *InfoMsgType `json:"info,omitempty"`
Remote interface{} `json:"remote,omitempty"` // *remote.RemoteState
} }
func (ModelUpdate) UpdateType() string { func (ModelUpdate) UpdateType() string {