mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-10 19:58:00 +01:00
parse shellvariables with args
This commit is contained in:
parent
674a6ef11e
commit
245a1995e2
@ -51,6 +51,7 @@ const (
|
|||||||
RawPacketStr = "raw"
|
RawPacketStr = "raw"
|
||||||
SpecialInputPacketStr = "sinput" // command
|
SpecialInputPacketStr = "sinput" // command
|
||||||
CompGenPacketStr = "compgen" // rpc
|
CompGenPacketStr = "compgen" // rpc
|
||||||
|
ReInitPacketStr = "reinit" // rpc
|
||||||
)
|
)
|
||||||
|
|
||||||
const PacketSenderQueueSize = 20
|
const PacketSenderQueueSize = 20
|
||||||
@ -111,10 +112,10 @@ func MakePacket(packetType string) (PacketType, error) {
|
|||||||
type ShellState struct {
|
type ShellState struct {
|
||||||
Version string `json:"version,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
Cwd string `json:"cwd,omitempty"`
|
Cwd string `json:"cwd,omitempty"`
|
||||||
ShellVars string `json:"shellvars,omitempty"`
|
ShellVars []byte `json:"shellvars,omitempty"`
|
||||||
Env0 []byte `json:"env0,omitempty"`
|
|
||||||
Aliases string `json:"aliases,omitempty"`
|
Aliases string `json:"aliases,omitempty"`
|
||||||
Funcs string `json:"funcs,omitempty"`
|
Funcs string `json:"funcs,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CmdDataPacketType struct {
|
type CmdDataPacketType struct {
|
||||||
@ -445,6 +446,7 @@ func FmtMessagePacket(fmtStr string, args ...interface{}) *MessagePacketType {
|
|||||||
|
|
||||||
type InitPacketType struct {
|
type InitPacketType struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
RespId string `json:"respid,omitempty"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
MShellHomeDir string `json:"mshellhomedir,omitempty"`
|
MShellHomeDir string `json:"mshellhomedir,omitempty"`
|
||||||
HomeDir string `json:"homedir,omitempty"`
|
HomeDir string `json:"homedir,omitempty"`
|
||||||
@ -460,6 +462,14 @@ func (*InitPacketType) GetType() string {
|
|||||||
return InitPacketStr
|
return InitPacketStr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pk *InitPacketType) GetResponseId() string {
|
||||||
|
return pk.RespId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *InitPacketType) GetResponseDone() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func MakeInitPacket() *InitPacketType {
|
func MakeInitPacket() *InitPacketType {
|
||||||
return &InitPacketType{Type: InitPacketStr}
|
return &InitPacketType{Type: InitPacketStr}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,10 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alessio/shellescape"
|
||||||
"github.com/scripthaus-dev/mshell/pkg/packet"
|
"github.com/scripthaus-dev/mshell/pkg/packet"
|
||||||
"mvdan.cc/sh/v3/expand"
|
"mvdan.cc/sh/v3/expand"
|
||||||
"mvdan.cc/sh/v3/syntax"
|
"mvdan.cc/sh/v3/syntax"
|
||||||
@ -78,8 +80,6 @@ var NoStoreVarNames = map[string]bool{
|
|||||||
"BASH_REMATCH": true,
|
"BASH_REMATCH": true,
|
||||||
"BASH_SOURCE": true,
|
"BASH_SOURCE": true,
|
||||||
"BASH_SUBSHELL": true,
|
"BASH_SUBSHELL": true,
|
||||||
"BASH_VERSINFO": true,
|
|
||||||
"BASH_VERSION": true,
|
|
||||||
"COPROC": true,
|
"COPROC": true,
|
||||||
"DIRSTACK": true,
|
"DIRSTACK": true,
|
||||||
"EPOCHREALTIME": true,
|
"EPOCHREALTIME": true,
|
||||||
@ -100,61 +100,200 @@ var NoStoreVarNames = map[string]bool{
|
|||||||
"HISTSIZE": true,
|
"HISTSIZE": true,
|
||||||
"HISTTIMEFORMAT": true,
|
"HISTTIMEFORMAT": true,
|
||||||
"SRANDOM": true,
|
"SRANDOM": true,
|
||||||
|
|
||||||
|
// we want these in our remote state object
|
||||||
|
// "EUID": true,
|
||||||
|
// "SHELLOPTS": true,
|
||||||
|
// "UID": true,
|
||||||
|
// "BASH_VERSINFO": true,
|
||||||
|
// "BASH_VERSION": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDeclareStmt(envBuffer *bytes.Buffer, varsBuffer *bytes.Buffer, stmt *syntax.Stmt, src []byte) error {
|
type DeclareDeclType struct {
|
||||||
|
Args string
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
var declareDeclArgsRe = regexp.MustCompile("^[aAxrifx]*$")
|
||||||
|
var bashValidIdentifierRe = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$")
|
||||||
|
|
||||||
|
func (d *DeclareDeclType) Validate() error {
|
||||||
|
if len(d.Name) == 0 || !IsValidBashIdentifier(d.Name) {
|
||||||
|
return fmt.Errorf("invalid shell variable name (invalid bash identifier)")
|
||||||
|
}
|
||||||
|
if strings.Index(d.Value, "\x00") >= 0 {
|
||||||
|
return fmt.Errorf("invalid shell variable value (cannot contain 0 byte)")
|
||||||
|
}
|
||||||
|
if !declareDeclArgsRe.MatchString(d.Args) {
|
||||||
|
return fmt.Errorf("invalid shell variable type %s", shellescape.Quote(d.Args))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DeclareDeclType) Serialize() string {
|
||||||
|
return fmt.Sprintf("%s|%s=%s\x00", d.Args, d.Name, d.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DeclareDeclType) EnvString() string {
|
||||||
|
return d.Name + "=" + d.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DeclareDeclType) DeclareStmt() string {
|
||||||
|
var argsStr string
|
||||||
|
if d.Args == "" {
|
||||||
|
argsStr = "--"
|
||||||
|
} else {
|
||||||
|
argsStr = "-" + d.Args
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("declare %s %s=%s", argsStr, d.Name, shellescape.Quote(d.Value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// envline should be valid
|
||||||
|
func ParseDeclLine(envLine string) *DeclareDeclType {
|
||||||
|
eqIdx := strings.Index(envLine, "=")
|
||||||
|
if eqIdx == -1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
namePart := envLine[0:eqIdx]
|
||||||
|
valPart := envLine[eqIdx+1:]
|
||||||
|
pipeIdx := strings.Index(namePart, "|")
|
||||||
|
if pipeIdx == -1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &DeclareDeclType{
|
||||||
|
Args: namePart[0:pipeIdx],
|
||||||
|
Name: namePart[pipeIdx+1:],
|
||||||
|
Value: valPart,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeclMapFromState(state *packet.ShellState) map[string]*DeclareDeclType {
|
||||||
|
if state == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rtn := make(map[string]*DeclareDeclType)
|
||||||
|
vars := bytes.Split(state.ShellVars, []byte{0})
|
||||||
|
for _, varLine := range vars {
|
||||||
|
decl := ParseDeclLine(string(varLine))
|
||||||
|
if decl != nil {
|
||||||
|
rtn[decl.Name] = decl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func SerializeDeclMap(declMap map[string]*DeclareDeclType) []byte {
|
||||||
|
var rtn bytes.Buffer
|
||||||
|
for _, decl := range declMap {
|
||||||
|
rtn.WriteString(decl.Serialize())
|
||||||
|
}
|
||||||
|
return rtn.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnvMapFromState(state *packet.ShellState) map[string]string {
|
||||||
|
if state == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rtn := make(map[string]string)
|
||||||
|
vars := bytes.Split(state.ShellVars, []byte{0})
|
||||||
|
for _, varLine := range vars {
|
||||||
|
decl := ParseDeclLine(string(varLine))
|
||||||
|
if decl != nil && decl.IsExport() {
|
||||||
|
rtn[decl.Name] = decl.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShellVarMapFromState(state *packet.ShellState) map[string]string {
|
||||||
|
if state == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rtn := make(map[string]string)
|
||||||
|
vars := bytes.Split(state.ShellVars, []byte{0})
|
||||||
|
for _, varLine := range vars {
|
||||||
|
decl := ParseDeclLine(string(varLine))
|
||||||
|
if decl != nil {
|
||||||
|
rtn[decl.Name] = decl.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func VarDeclsFromState(state *packet.ShellState) []*DeclareDeclType {
|
||||||
|
if state == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var rtn []*DeclareDeclType
|
||||||
|
vars := bytes.Split(state.ShellVars, []byte{0})
|
||||||
|
for _, varLine := range vars {
|
||||||
|
decl := ParseDeclLine(string(varLine))
|
||||||
|
if decl != nil {
|
||||||
|
rtn = append(rtn, decl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidBashIdentifier(s string) bool {
|
||||||
|
return bashValidIdentifierRe.MatchString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DeclareDeclType) IsExport() bool {
|
||||||
|
return strings.Index(d.Args, "x") >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DeclareDeclType) IsReadOnly() bool {
|
||||||
|
return strings.Index(d.Args, "r") >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDeclareStmt(stmt *syntax.Stmt, src []byte) (*DeclareDeclType, error) {
|
||||||
cmd := stmt.Cmd
|
cmd := stmt.Cmd
|
||||||
decl, ok := cmd.(*syntax.DeclClause)
|
decl, ok := cmd.(*syntax.DeclClause)
|
||||||
if !ok || decl.Variant.Value != "declare" || len(decl.Args) != 2 {
|
if !ok || decl.Variant.Value != "declare" || len(decl.Args) != 2 {
|
||||||
return fmt.Errorf("invalid declare variant")
|
return nil, fmt.Errorf("invalid declare variant")
|
||||||
}
|
}
|
||||||
|
rtn := &DeclareDeclType{}
|
||||||
declArgs := decl.Args[0]
|
declArgs := decl.Args[0]
|
||||||
if !declArgs.Naked || len(declArgs.Value.Parts) != 1 {
|
if !declArgs.Naked || len(declArgs.Value.Parts) != 1 {
|
||||||
return fmt.Errorf("wrong number of declare args parts")
|
return nil, fmt.Errorf("wrong number of declare args parts")
|
||||||
}
|
}
|
||||||
declArgLit, ok := declArgs.Value.Parts[0].(*syntax.Lit)
|
declArgsLit, ok := declArgs.Value.Parts[0].(*syntax.Lit)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("declare args is not a literal")
|
return nil, fmt.Errorf("declare args is not a literal")
|
||||||
}
|
}
|
||||||
declArgStr := declArgLit.Value
|
if !strings.HasPrefix(declArgsLit.Value, "-") {
|
||||||
if !strings.HasPrefix(declArgStr, "-") {
|
return nil, fmt.Errorf("declare args not an argument (does not start with '-')")
|
||||||
return fmt.Errorf("declare args not an argument (does not start with '-')")
|
}
|
||||||
|
if declArgsLit.Value == "--" {
|
||||||
|
rtn.Args = ""
|
||||||
|
} else {
|
||||||
|
rtn.Args = declArgsLit.Value[1:]
|
||||||
}
|
}
|
||||||
declAssign := decl.Args[1]
|
declAssign := decl.Args[1]
|
||||||
if declAssign.Name == nil {
|
if declAssign.Name == nil {
|
||||||
return fmt.Errorf("declare does not have a valid name")
|
return nil, fmt.Errorf("declare does not have a valid name")
|
||||||
}
|
}
|
||||||
varName := declAssign.Name.Value
|
rtn.Name = declAssign.Name.Value
|
||||||
if NoStoreVarNames[varName] {
|
if declAssign.Naked || declAssign.Index != nil || declAssign.Append {
|
||||||
return nil
|
return nil, fmt.Errorf("invalid decl format")
|
||||||
}
|
}
|
||||||
if strings.Index(varName, "=") != -1 || strings.Index(varName, "\x00") != -1 {
|
if declAssign.Value != nil {
|
||||||
return fmt.Errorf("invalid varname (cannot contain '=' or 0 byte)")
|
varValueStr, err := QuotedLitToStr(declAssign.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing declare value: %w", err)
|
||||||
|
}
|
||||||
|
rtn.Value = varValueStr
|
||||||
|
} else if declAssign.Array != nil {
|
||||||
|
rtn.Value = string(src[declAssign.Array.Pos().Offset():declAssign.Array.End().Offset()])
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("invalid decl, not plain value or array")
|
||||||
}
|
}
|
||||||
fullDeclBytes := src[decl.Pos().Offset():decl.End().Offset()]
|
if err := rtn.Validate(); err != nil {
|
||||||
if strings.Index(declArgStr, "x") == -1 {
|
return nil, err
|
||||||
// non-exported vars get written to vars as decl statements
|
|
||||||
varsBuffer.Write(fullDeclBytes)
|
|
||||||
varsBuffer.WriteRune('\n')
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
if declArgStr != "-x" {
|
return rtn, nil
|
||||||
return fmt.Errorf("can only export plain bash variables (no arrays)")
|
|
||||||
}
|
|
||||||
// exported vars are parsed into Env0 format
|
|
||||||
if declAssign.Naked || declAssign.Array != nil || declAssign.Index != nil || declAssign.Append || declAssign.Value == nil {
|
|
||||||
return fmt.Errorf("invalid variable to export")
|
|
||||||
}
|
|
||||||
varValue := declAssign.Value
|
|
||||||
varValueStr, err := QuotedLitToStr(varValue)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing declare value: %w", err)
|
|
||||||
}
|
|
||||||
if strings.Index(varValueStr, "\x00") != -1 {
|
|
||||||
return fmt.Errorf("invalid export var value (cannot contain 0 byte)")
|
|
||||||
}
|
|
||||||
envBuffer.WriteString(fmt.Sprintf("%s=%s\x00", varName, varValueStr))
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDeclareOutput(state *packet.ShellState, declareBytes []byte) error {
|
func parseDeclareOutput(state *packet.ShellState, declareBytes []byte) error {
|
||||||
@ -164,16 +303,23 @@ func parseDeclareOutput(state *packet.ShellState, declareBytes []byte) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var envBuffer, varsBuffer bytes.Buffer
|
var varsBuffer bytes.Buffer
|
||||||
|
var firstParseErr error
|
||||||
for _, stmt := range file.Stmts {
|
for _, stmt := range file.Stmts {
|
||||||
err = parseDeclareStmt(&envBuffer, &varsBuffer, stmt, declareBytes)
|
decl, err := parseDeclareStmt(stmt, declareBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO where to put parse errors?
|
if firstParseErr == nil {
|
||||||
continue
|
firstParseErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if decl != nil && !NoStoreVarNames[decl.Name] {
|
||||||
|
varsBuffer.WriteString(decl.Serialize())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.Env0 = envBuffer.Bytes()
|
state.ShellVars = varsBuffer.Bytes()
|
||||||
state.ShellVars = varsBuffer.String()
|
if firstParseErr != nil {
|
||||||
|
state.Error = firstParseErr.Error()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +339,10 @@ func ParseShellStateOutput(outputBytes []byte) (*packet.ShellState, error) {
|
|||||||
cwdStr = cwdStr[0 : len(cwdStr)-2]
|
cwdStr = cwdStr[0 : len(cwdStr)-2]
|
||||||
}
|
}
|
||||||
rtn.Cwd = string(cwdStr)
|
rtn.Cwd = string(cwdStr)
|
||||||
parseDeclareOutput(rtn, fields[2])
|
err := parseDeclareOutput(rtn, fields[2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
rtn.Aliases = strings.ReplaceAll(string(fields[3]), "\r\n", "\n")
|
rtn.Aliases = strings.ReplaceAll(string(fields[3]), "\r\n", "\n")
|
||||||
rtn.Funcs = strings.ReplaceAll(string(fields[4]), "\r\n", "\n")
|
rtn.Funcs = strings.ReplaceAll(string(fields[4]), "\r\n", "\n")
|
||||||
return rtn, nil
|
return rtn, nil
|
||||||
|
@ -282,7 +282,7 @@ func MakeDetachedExecCmd(pk *packet.RunPacketType, cmdTty *os.File) (*exec.Cmd,
|
|||||||
if !pk.StateComplete {
|
if !pk.StateComplete {
|
||||||
ecmd.Env = os.Environ()
|
ecmd.Env = os.Environ()
|
||||||
}
|
}
|
||||||
UpdateCmdEnv(ecmd, ParseEnv0(state.Env0))
|
UpdateCmdEnv(ecmd, EnvMapFromState(state))
|
||||||
UpdateCmdEnv(ecmd, map[string]string{"TERM": getTermType(pk)})
|
UpdateCmdEnv(ecmd, map[string]string{"TERM": getTermType(pk)})
|
||||||
if state.Cwd != "" {
|
if state.Cwd != "" {
|
||||||
ecmd.Dir = base.ExpandHomeDir(state.Cwd)
|
ecmd.Dir = base.ExpandHomeDir(state.Cwd)
|
||||||
@ -964,26 +964,38 @@ func getTermType(pk *packet.RunPacketType) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeRcFileStr(pk *packet.RunPacketType) string {
|
func makeRcFileStr(pk *packet.RunPacketType) string {
|
||||||
rcFileStr := `
|
var rcBuf bytes.Buffer
|
||||||
|
rcBuf.WriteString(`
|
||||||
set +m
|
set +m
|
||||||
set +H
|
set +H
|
||||||
shopt -s extglob
|
shopt -s extglob
|
||||||
`
|
`)
|
||||||
|
|
||||||
|
varDecls := VarDeclsFromState(pk.State)
|
||||||
|
for _, varDecl := range varDecls {
|
||||||
|
if varDecl.IsExport() || varDecl.IsReadOnly() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rcBuf.WriteString(varDecl.DeclareStmt())
|
||||||
|
rcBuf.WriteString("\n")
|
||||||
|
}
|
||||||
if pk.State != nil && pk.State.Funcs != "" {
|
if pk.State != nil && pk.State.Funcs != "" {
|
||||||
rcFileStr += pk.State.Funcs + "\n"
|
rcBuf.WriteString(pk.State.Funcs)
|
||||||
|
rcBuf.WriteString("\n")
|
||||||
}
|
}
|
||||||
if pk.State != nil && pk.State.Aliases != "" {
|
if pk.State != nil && pk.State.Aliases != "" {
|
||||||
rcFileStr += pk.State.Aliases + "\n"
|
rcBuf.WriteString(pk.State.Aliases)
|
||||||
|
rcBuf.WriteString("\n")
|
||||||
}
|
}
|
||||||
if pk.ReturnState {
|
if pk.ReturnState {
|
||||||
rcFileStr += `
|
rcBuf.WriteString(`
|
||||||
_scripthaus_exittrap () {
|
_scripthaus_exittrap () {
|
||||||
` + GetShellStateCmd + `
|
` + GetShellStateCmd + `
|
||||||
}
|
}
|
||||||
trap _scripthaus_exittrap EXIT
|
trap _scripthaus_exittrap EXIT
|
||||||
`
|
`)
|
||||||
}
|
}
|
||||||
return rcFileStr
|
return rcBuf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeExitTrap(fdNum int) string {
|
func makeExitTrap(fdNum int) string {
|
||||||
@ -1042,7 +1054,7 @@ func RunCommandSimple(pk *packet.RunPacketType, sender *packet.PacketSender, fro
|
|||||||
if !pk.StateComplete {
|
if !pk.StateComplete {
|
||||||
cmd.Cmd.Env = os.Environ()
|
cmd.Cmd.Env = os.Environ()
|
||||||
}
|
}
|
||||||
UpdateCmdEnv(cmd.Cmd, ParseEnv0(state.Env0))
|
UpdateCmdEnv(cmd.Cmd, EnvMapFromState(state))
|
||||||
if state.Cwd != "" {
|
if state.Cwd != "" {
|
||||||
cmd.Cmd.Dir = base.ExpandHomeDir(state.Cwd)
|
cmd.Cmd.Dir = base.ExpandHomeDir(state.Cwd)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user