waveterm/waveshell/pkg/shellenv/shellenv.go
Evan Simkowitz ccc63937b6
Support running ephemeral commands (#543)
* initial

* save work, starting to add backend types

* save work

* save work

* Add EphemeralWriteCloser

* Command pipes thru, triggers infinite loop

* save debugging

* fix bad merge

* save debug statements

* fixing spaghetti

* clean up code

* got cwd override working

* Add separate paths for stdout and stderr writers

* fix stdout/stderr

* env vars are now working

* revert waveshell changes

* Making EphemeralWriteCloser into a more generic BufferedPipe

* formatting

* comment

* delete unused package

* more naming changes

* add package comment

* add UsePty to EphemeralRunOpts

* document UsePty

* ensure only one downstream writer can read from the buffer

* store pointer to syncs

* remove inshellisense stuff for now

* remove debugs

* revert yarn

* remove unnecessary debugs in main-server

* more debugging removed

* revert tsconfig
2024-04-02 15:46:32 -07:00

400 lines
8.7 KiB
Go

// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package shellenv
import (
"bytes"
"fmt"
"strings"
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
"github.com/wavetermdev/waveterm/waveshell/pkg/simpleexpand"
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
)
const (
DeclTypeArray = "array"
DeclTypeAssocArray = "assoc"
DeclTypeInt = "int"
DeclTypeNormal = "normal"
)
type DeclareDeclType struct {
IsZshDecl bool
IsExtVar bool // set for "special" wave internal variables
Args string
Name string
// this holds the raw quoted value suitable for bash. this is *not* the real expanded variable value
Value string
// special fields for zsh "-T" output.
// for bound scalars, "Value" hold everything after the "=" (including the separator character)
ZshBoundScalar string // the name of the "scalar" env variable
ZshEnvValue string // unlike Value this *is* the expanded value of scalar env variable
}
func (d *DeclareDeclType) IsExport() bool {
return strings.Contains(d.Args, "x")
}
func (d *DeclareDeclType) IsReadOnly() bool {
return strings.Contains(d.Args, "r")
}
func (d *DeclareDeclType) IsZshScalarBound() bool {
return strings.Contains(d.Args, "T")
}
func (d *DeclareDeclType) IsArray() bool {
return strings.Contains(d.Args, "a")
}
func (d *DeclareDeclType) IsAssocArray() bool {
return strings.Contains(d.Args, "A")
}
func (d *DeclareDeclType) IsUniqueArray() bool {
return d.IsArray() && strings.Contains(d.Args, "U")
}
func (d *DeclareDeclType) AddFlag(flag string) {
if strings.Contains(d.Args, flag) {
return
}
d.Args += flag
}
func (d *DeclareDeclType) SortZshFlags() {
// x is always first (or g)
// T is always last
// the 'i' flags are tricky (they shouldn't be sorted, because the order matters, e.g. i10)
var hasX, hasG, hasT bool
var newArgs []rune
for _, r := range d.Args {
if r == 'x' {
hasX = true
continue
}
if r == 'g' {
hasG = true
continue
}
if r == 'T' {
hasT = true
continue
}
newArgs = append(newArgs, r)
}
newArgsStr := string(newArgs)
if hasG {
newArgsStr = "g" + newArgsStr
}
if hasX {
newArgsStr = "x" + newArgsStr
}
if hasT {
newArgsStr += "T"
}
d.Args = newArgsStr
}
func (d *DeclareDeclType) DataType() string {
if strings.Contains(d.Args, "a") {
return DeclTypeArray
}
if strings.Contains(d.Args, "A") {
return DeclTypeAssocArray
}
if strings.Contains(d.Args, "i") {
return DeclTypeInt
}
return DeclTypeNormal
}
func FindVarDecl(decls []*DeclareDeclType, name string) *DeclareDeclType {
for _, decl := range decls {
if decl.Name == name {
return decl
}
}
return nil
}
// NOTE Serialize no longer writes the final null byte
func (d *DeclareDeclType) Serialize() []byte {
if d.IsExtVar {
parts := []string{
"e1",
d.Args,
d.Name,
d.Value,
}
return utilfn.EncodeStringArray(parts)
} else if d.IsZshDecl {
d.SortZshFlags()
parts := []string{
"z1",
d.Args,
d.Name,
d.Value,
d.ZshBoundScalar,
d.ZshEnvValue,
}
return utilfn.EncodeStringArray(parts)
} else {
parts := []string{
"b1",
d.Args,
d.Name,
d.Value,
}
return utilfn.EncodeStringArray(parts)
}
// this is the v0 encoding (keeping here for reference since we still need to decode this)
// rtn := fmt.Sprintf("%s|%s=%s\x00", d.Args, d.Name, d.Value)
// return []byte(rtn)
}
func (d *DeclareDeclType) UnescapedValue() string {
if d.IsExtVar {
return d.Value
}
ectx := simpleexpand.SimpleExpandContext{}
rtn, _ := simpleexpand.SimpleExpandPartialWord(ectx, d.Value, false)
return rtn
}
func DeclsEqual(compareName bool, d1 *DeclareDeclType, d2 *DeclareDeclType) bool {
if d1.IsExport() != d2.IsExport() {
return false
}
if d1.DataType() != d2.DataType() {
return false
}
if compareName && d1.Name != d2.Name {
return false
}
return d1.Value == d2.Value // this works even for assoc arrays because we normalize them when parsing
}
// envline should be valid
func parseDeclLine(envLineBytes []byte) *DeclareDeclType {
esFirstVal := utilfn.EncodedStringArrayGetFirstVal(envLineBytes)
if esFirstVal == "z1" {
parts, err := utilfn.DecodeStringArray(envLineBytes)
if err != nil {
return nil
}
if len(parts) != 6 {
return nil
}
return &DeclareDeclType{
IsZshDecl: true,
Args: parts[1],
Name: parts[2],
Value: parts[3],
ZshBoundScalar: parts[4],
ZshEnvValue: parts[5],
}
} else if esFirstVal == "b1" {
parts, err := utilfn.DecodeStringArray(envLineBytes)
if err != nil {
return nil
}
if len(parts) != 4 {
return nil
}
return &DeclareDeclType{
Args: parts[1],
Name: parts[2],
Value: parts[3],
}
} else if esFirstVal == "e1" {
parts, err := utilfn.DecodeStringArray(envLineBytes)
if err != nil {
return nil
}
if len(parts) != 4 {
return nil
}
return &DeclareDeclType{
IsExtVar: true,
Args: parts[1],
Name: parts[2],
Value: parts[3],
}
} else if esFirstVal == "p1" {
// deprecated
return nil
}
// legacy decoding (v0) (not an encoded string array)
envLine := string(envLineBytes)
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,
}
}
// returns name => full-line
func parseDeclLineToKV(envLine []byte) (string, []byte) {
decl := parseDeclLine(envLine)
if decl == nil {
return "", nil
}
return decl.Name, envLine
}
func ShellStateVarsToMap(shellVars []byte) map[string][]byte {
if len(shellVars) == 0 {
return nil
}
rtn := make(map[string][]byte)
vars := bytes.Split(shellVars, []byte{0})
for _, varLine := range vars {
name, val := parseDeclLineToKV(varLine)
if name == "" {
continue
}
rtn[name] = val
}
return rtn
}
func StrMapToShellStateVars(varMap map[string][]byte) []byte {
var buf bytes.Buffer
orderedKeys := utilfn.GetOrderedMapKeys(varMap)
for _, key := range orderedKeys {
val := varMap[key]
buf.Write(val)
buf.WriteByte(0)
}
return buf.Bytes()
}
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(varLine)
if decl != nil {
rtn[decl.Name] = decl
}
}
return rtn
}
func SerializeDeclMap(declMap map[string]*DeclareDeclType) []byte {
var rtn bytes.Buffer
orderedKeys := utilfn.GetOrderedMapKeys(declMap)
for _, key := range orderedKeys {
decl := declMap[key]
rtn.Write(decl.Serialize())
rtn.WriteByte(0)
}
return rtn.Bytes()
}
func EnvMapFromState(state *packet.ShellState) map[string]string {
if state == nil {
return nil
}
rtn := make(map[string]string)
ectx := simpleexpand.SimpleExpandContext{}
vars := bytes.Split(state.ShellVars, []byte{0})
for _, varLine := range vars {
decl := parseDeclLine(varLine)
if decl != nil && decl.IsExport() {
rtn[decl.Name], _ = simpleexpand.SimpleExpandPartialWord(ectx, decl.Value, false)
}
}
return rtn
}
func ShellVarMapFromState(state *packet.ShellState) map[string]string {
if state == nil {
return nil
}
rtn := make(map[string]string)
ectx := simpleexpand.SimpleExpandContext{}
vars := bytes.Split(state.ShellVars, []byte{0})
for _, varLine := range vars {
decl := parseDeclLine(varLine)
if decl != nil {
rtn[decl.Name], _ = simpleexpand.SimpleExpandPartialWord(ectx, decl.Value, false)
}
}
return rtn
}
func DumpVarMapFromState(state *packet.ShellState) {
wlog.Logf("DUMP-STATE-VARS:\n")
if state == nil {
wlog.Logf(" nil\n")
return
}
decls := VarDeclsFromState(state)
for _, decl := range decls {
wlog.Logf(" %s %#v\n", decl.Name, decl)
}
envMap := EnvMapFromState(state)
wlog.Logf("DUMP-STATE-ENV:\n")
for k, v := range envMap {
wlog.Logf(" %s=%s\n", k, v)
}
wlog.Logf("\n\n")
}
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(varLine)
if decl != nil {
rtn = append(rtn, decl)
}
}
return rtn
}
func RemoveFunc(funcs string, toRemove string) string {
lines := strings.Split(funcs, "\n")
var newLines []string
removeLine := fmt.Sprintf("%s ()", toRemove)
doingRemove := false
for _, line := range lines {
if line == removeLine {
doingRemove = true
continue
}
if doingRemove {
if line == "}" {
doingRemove = false
}
continue
}
newLines = append(newLines, line)
}
return strings.Join(newLines, "\n")
}