2024-01-16 16:11:04 -08:00
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package shellapi
import (
"bytes"
"context"
2024-03-06 11:38:27 -08:00
"errors"
2024-01-16 16:11:04 -08:00
"fmt"
"os/exec"
"strings"
"sync"
2024-01-20 13:17:43 -08:00
"unicode"
2024-01-16 16:11:04 -08:00
"github.com/alessio/shellescape"
"github.com/wavetermdev/waveterm/waveshell/pkg/base"
"github.com/wavetermdev/waveterm/waveshell/pkg/binpack"
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
"github.com/wavetermdev/waveterm/waveshell/pkg/shellenv"
"github.com/wavetermdev/waveterm/waveshell/pkg/statediff"
"github.com/wavetermdev/waveterm/waveshell/pkg/utilfn"
2024-03-06 11:38:27 -08:00
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
2024-01-16 16:11:04 -08:00
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
)
const BaseZshOpts = ``
const ZshShellVersionCmdStr = `echo zsh v$ZSH_VERSION`
2024-03-18 22:51:16 -07:00
const (
ZshSection_Version = iota
ZshSection_Cwd
ZshSection_Env
ZshSection_Mods
ZshSection_Vars
ZshSection_Aliases
ZshSection_Fpath
ZshSection_Funcs
ZshSection_PVars
ZshSection_Prompt
2024-03-19 16:38:38 -07:00
ZshSection_EndBytes
2024-03-18 22:51:16 -07:00
ZshSection_NumFieldsExpected // must be last
)
2024-01-16 16:11:04 -08:00
// TODO these need updating
const RunZshSudoCommandFmt = `sudo -n -C %d zsh /dev/fd/%d`
const RunZshSudoPasswordCommandFmt = `cat /dev/fd/%d | sudo -k -S -C %d zsh -c "echo '[from-mshell]'; exec %d>&-; zsh /dev/fd/%d < /dev/fd/%d"`
var ZshIgnoreVars = map [ string ] bool {
"_" : true ,
"0" : true ,
"terminfo" : true ,
"RANDOM" : true ,
"COLUMNS" : true ,
"LINES" : true ,
"argv" : true ,
"SECONDS" : true ,
"PWD" : true ,
"HISTCHARS" : true ,
"HISTFILE" : true ,
"HISTSIZE" : true ,
"SAVEHIST" : true ,
"ZSH_EXECUTION_STRING" : true ,
"EPOCHSECONDS" : true ,
"EPOCHREALTIME" : true ,
"SHLVL" : true ,
"TTY" : true ,
2024-02-14 18:21:23 -08:00
"ZDOTDIR" : true ,
2024-03-18 22:51:16 -07:00
"PPID" : true ,
2024-01-16 16:11:04 -08:00
"epochtime" : true ,
"langinfo" : true ,
2024-01-18 10:50:26 -08:00
"keymaps" : true ,
"widgets" : true ,
2024-03-06 11:38:27 -08:00
"options" : true ,
2024-01-16 16:11:04 -08:00
"aliases" : true ,
"dis_aliases" : true ,
"saliases" : true ,
"dis_saliases" : true ,
"galiases" : true ,
"dis_galiases" : true ,
"builtins" : true ,
"dis_builtins" : true ,
"modules" : true ,
"history" : true ,
"historywords" : true ,
"jobdirs" : true ,
"jobstates" : true ,
"jobtexts" : true ,
"funcfiletrace" : true ,
"funcsourcetrace" : true ,
"funcstack" : true ,
"functrace" : true ,
2024-03-18 22:51:16 -07:00
"nameddirs" : true ,
"userdirs" : true ,
2024-01-16 16:11:04 -08:00
"parameters" : true ,
"commands" : true ,
"functions" : true ,
"dis_functions" : true ,
"functions_source" : true ,
"dis_functions_source" : true ,
"_comps" : true ,
"_patcomps" : true ,
"_postpatcomps" : true ,
2024-03-18 22:51:16 -07:00
// zsh/system
"errnos" : true ,
"sysparams" : true ,
// zsh/curses
"ZCURSES_COLORS" : true ,
"ZCURSES_COLOR_PAIRS" : true ,
"zcurses_attrs" : true ,
"zcurses_colors" : true ,
"zcurses_keycodes" : true ,
"zcurses_windows" : true ,
// not listed, but we also exclude all ZFTP_* variables
2024-03-19 16:38:38 -07:00
// powerlevel10k
"_GITSTATUS_CLIENT_PID_POWERLEVEL9K" : true ,
"GITSTATUS_DAEMON_PID_POWERLEVEL9K" : true ,
"_GITSTATUS_FILE_PREFIX_POWERLEVEL9K" : true ,
2024-03-18 22:51:16 -07:00
}
var ZshIgnoreFuncs = map [ string ] bool {
"zftp_chpwd" : true ,
"zftp_progress" : true ,
2024-01-16 16:11:04 -08:00
}
2024-03-06 11:38:27 -08:00
// only options we restore (other than ZshForceOptions)
var ZshIgnoreOptions = map [ string ] bool {
"globalrcs" : true , // must stay off (otherwise /etc/zprofile runs)
"ksharrays" : true ,
"kshtypeset" : true ,
"kshautoload" : true ,
"kshzerosubscript" : true ,
"interactive" : true ,
"login" : true ,
"zle" : true ,
"shinstdin" : true ,
"privileged" : true ,
"restricted" : true ,
"singlecommand" : true ,
}
// force these options on/off at beginning of rcfile
var ZshForceOptions = map [ string ] bool {
"globalrcs" : false ,
"ksharrays" : false ,
"kshtypeset" : false ,
"kshautoload" : false ,
"kshzerosubscript" : false ,
"xtrace" : false , // not in ZshIgnoreOptions
"verbose" : false , // not in ZshIgnoreOptions
"debugbeforecmd" : false , // not in ZshIgnoreOptions
}
2024-01-16 16:11:04 -08:00
var ZshUniqueArrayVars = map [ string ] bool {
"path" : true ,
"fpath" : true ,
}
var ZshSpecialDecls = map [ string ] bool {
"precmd_functions" : true ,
"preexec_functions" : true ,
}
var ZshUnsetVars = [] string {
"HISTFILE" ,
"ZSH_EXECUTION_STRING" ,
}
2024-03-18 22:51:16 -07:00
var ZshForceLoadMods = map [ string ] bool {
"zsh/parameter" : true ,
"zsh/langinfo" : true ,
2024-03-06 11:38:27 -08:00
}
2024-03-18 22:51:16 -07:00
const ZModsVarName = "WAVESTATE_ZMODS"
2024-01-16 16:11:04 -08:00
// do not use these directly, call GetLocalMajorVersion()
var localZshMajorVersionOnce = & sync . Once {}
var localZshMajorVersion = ""
// sentinel value for functions that should be autoloaded
const ZshFnAutoLoad = "autoload"
2024-01-20 13:17:43 -08:00
const ZshAutoloadFnBody = "builtin autoload -XU"
2024-01-16 16:11:04 -08:00
type ZshParamKey struct {
// paramtype cannot contain spaces
// "aliases", "dis_aliases", "saliases", "dis_saliases", "galiases", "dis_galiases"
// "functions", "dis_functions", "functions_source", "dis_functions_source"
ParamType string
ParamName string
}
func ( k ZshParamKey ) String () string {
return k . ParamType + " " + k . ParamName
}
func ZshParamKeyFromString ( s string ) ( ZshParamKey , error ) {
parts := strings . SplitN ( s , " " , 2 )
if len ( parts ) != 2 {
return ZshParamKey {}, fmt . Errorf ( "invalid zsh param key" )
}
return ZshParamKey { ParamType : parts [ 0 ], ParamName : parts [ 1 ]}, nil
}
type ZshMap = map [ ZshParamKey ] string
type zshShellApi struct {}
2024-04-09 11:33:23 -07:00
func ( zshShellApi ) GetShellType () string {
2024-01-16 16:11:04 -08:00
return packet . ShellType_zsh
}
2024-04-09 11:33:23 -07:00
func ( zshShellApi ) MakeExitTrap ( fdNum int ) ( string , [] byte ) {
2024-01-16 16:11:04 -08:00
return MakeZshExitTrap ( fdNum )
}
2024-04-09 11:33:23 -07:00
func ( zshShellApi ) GetLocalMajorVersion () string {
2024-01-16 16:11:04 -08:00
return GetLocalZshMajorVersion ()
}
2024-04-09 11:33:23 -07:00
func ( zshShellApi ) GetLocalShellPath () string {
2024-01-16 16:11:04 -08:00
return "/bin/zsh"
}
2024-04-09 11:33:23 -07:00
func ( zshShellApi ) GetRemoteShellPath () string {
2024-01-16 16:11:04 -08:00
return "zsh"
}
2024-04-09 11:33:23 -07:00
func ( zshShellApi ) ValidateCommandSyntax ( cmdStr string ) error {
ctx , cancelFn := context . WithTimeout ( context . Background (), ValidateTimeout )
defer cancelFn ()
cmd := exec . CommandContext ( ctx , GetLocalZshPath (), "-n" , "-c" , cmdStr )
output , err := cmd . CombinedOutput ()
if err == nil {
return nil
}
if len ( output ) == 0 {
return errors . New ( "zsh syntax error" )
}
return errors . New ( utilfn . GetFirstLine ( string ( output )))
}
func ( zshShellApi ) MakeRunCommand ( cmdStr string , opts RunCommandOpts ) string {
2024-01-16 16:11:04 -08:00
if ! opts . Sudo {
return cmdStr
}
if opts . SudoWithPass {
return fmt . Sprintf ( RunZshSudoPasswordCommandFmt , opts . PwFdNum , opts . MaxFdNum + 1 , opts . PwFdNum , opts . CommandFdNum , opts . CommandStdinFdNum )
} else {
return fmt . Sprintf ( RunZshSudoCommandFmt , opts . MaxFdNum + 1 , opts . CommandFdNum )
}
}
2024-04-09 11:33:23 -07:00
func ( zshShellApi ) MakeShExecCommand ( cmdStr string , rcFileName string , usePty bool ) * exec . Cmd {
2024-01-16 16:11:04 -08:00
return exec . Command ( GetLocalZshPath (), "-l" , "-i" , "-c" , cmdStr )
}
2024-03-27 00:22:57 -07:00
func ( z zshShellApi ) GetShellState ( ctx context . Context , outCh chan ShellStateOutput , stdinDataCh chan [] byte ) {
2024-03-19 16:38:38 -07:00
defer close ( outCh )
stateCmd , endBytes := GetZshShellStateCmd ( StateOutputFdNum )
cmdStr := BaseZshOpts + "; " + stateCmd
2024-01-16 16:11:04 -08:00
ecmd := exec . CommandContext ( ctx , GetLocalZshPath (), "-l" , "-i" , "-c" , cmdStr )
2024-03-19 16:38:38 -07:00
outputCh := make ( chan [] byte , 10 )
var outputWg sync . WaitGroup
outputWg . Add ( 1 )
go func () {
defer outputWg . Done ()
for outputBytes := range outputCh {
outCh <- ShellStateOutput { Output : outputBytes }
}
}()
2024-03-20 23:38:05 -07:00
outputBytes , err := StreamCommandWithExtraFd ( ctx , ecmd , outputCh , StateOutputFdNum , endBytes , stdinDataCh )
2024-03-19 16:38:38 -07:00
outputWg . Wait ()
2024-01-16 16:11:04 -08:00
if err != nil {
2024-03-19 16:38:38 -07:00
outCh <- ShellStateOutput { Error : err . Error ()}
return
2024-01-16 16:11:04 -08:00
}
2024-03-19 16:38:38 -07:00
rtn , stats , err := z . ParseShellStateOutput ( outputBytes )
2024-01-16 16:11:04 -08:00
if err != nil {
2024-03-19 16:38:38 -07:00
outCh <- ShellStateOutput { Error : err . Error ()}
return
2024-01-16 16:11:04 -08:00
}
2024-03-19 16:38:38 -07:00
outCh <- ShellStateOutput { ShellState : rtn , Stats : stats }
2024-01-16 16:11:04 -08:00
}
2024-04-09 11:33:23 -07:00
func ( zshShellApi ) GetBaseShellOpts () string {
2024-01-16 16:11:04 -08:00
return BaseZshOpts
}
func makeZshTypesetStmt ( varDecl * shellenv . DeclareDeclType ) string {
if ! varDecl . IsZshDecl {
// not sure what to do here?
return ""
}
var argsStr string
if varDecl . Args == "" {
argsStr = "--"
} else {
argsStr = "-" + varDecl . Args
}
if varDecl . IsZshScalarBound () {
// varDecl.Value contains the extra "separator" field (if present in the original typeset def)
return fmt . Sprintf ( "typeset %s %s %s=%s" , argsStr , varDecl . ZshBoundScalar , varDecl . Name , varDecl . Value )
} else {
return fmt . Sprintf ( "typeset %s %s=%s" , argsStr , varDecl . Name , varDecl . Value )
}
}
2024-01-20 13:17:43 -08:00
func isZshSafeNameStr ( name string ) bool {
for _ , ch := range name {
if ch == '_' {
continue
}
if unicode . IsLetter ( ch ) || unicode . IsDigit ( ch ) {
continue
}
return false
}
return true
}
2024-01-16 16:11:04 -08:00
func ( z zshShellApi ) MakeRcFileStr ( pk * packet . RunPacketType ) string {
var rcBuf bytes . Buffer
rcBuf . WriteString ( z . GetBaseShellOpts () + "\n" )
2024-03-06 11:38:27 -08:00
// rcBuf.WriteString("echo 'running generated rcfile' $0 $ZSH_ARGZERO '|' $ZDOTDIR\n")
2024-01-16 16:11:04 -08:00
varDecls := shellenv . VarDeclsFromState ( pk . State )
2024-03-06 11:38:27 -08:00
// force options come at the beginning of the file (other options come at the end)
for optName , optVal := range ZshForceOptions {
if optVal {
rcBuf . WriteString ( fmt . Sprintf ( "setopt %s\n" , optName ))
} else {
rcBuf . WriteString ( fmt . Sprintf ( "unsetopt %s\n" , optName ))
}
}
2024-03-18 22:51:16 -07:00
for modName := range ZshForceLoadMods {
2024-03-06 11:38:27 -08:00
rcBuf . WriteString ( fmt . Sprintf ( "zmodload %s\n" , modName ))
}
2024-03-18 22:51:16 -07:00
modDecl := getDeclByName ( varDecls , ZModsVarName )
if modDecl != nil {
modsArr := utilfn . QuickParseJson [[] string ]( modDecl . Value )
for _ , modName := range modsArr {
if ! ZshForceLoadMods [ modName ] {
rcBuf . WriteString ( fmt . Sprintf ( "zmodload %s\n" , modName ))
}
}
}
2024-01-16 16:11:04 -08:00
var postDecls [] * shellenv . DeclareDeclType
for _ , varDecl := range varDecls {
if ZshIgnoreVars [ varDecl . Name ] {
continue
}
2024-03-18 22:51:16 -07:00
if strings . HasPrefix ( varDecl . Name , "ZFTP_" ) {
continue
}
2024-04-09 11:33:23 -07:00
if strings . HasPrefix ( varDecl . Name , "_wavetemp_" ) {
continue
}
2024-03-18 22:51:16 -07:00
if varDecl . IsExtVar {
continue
}
2024-01-16 16:11:04 -08:00
if ZshUniqueArrayVars [ varDecl . Name ] && ! varDecl . IsUniqueArray () {
varDecl . AddFlag ( "U" )
}
if ZshSpecialDecls [ varDecl . Name ] {
postDecls = append ( postDecls , varDecl )
continue
}
stmt := makeZshTypesetStmt ( varDecl )
if stmt == "" {
continue
}
2024-01-20 13:17:43 -08:00
if varDecl . IsReadOnly () {
// we can't reset read-only variables
// so we check if it is a "safe" name, and then we can write a conditional
// that only sets it if it hasn't already been set.
if ! isZshSafeNameStr ( varDecl . Name ) {
continue
}
rcBuf . WriteString ( fmt . Sprintf ( "if (( ! ${+%s} )); then\n" , varDecl . Name ))
rcBuf . WriteString ( makeZshTypesetStmt ( varDecl ))
rcBuf . WriteString ( "\nfi" )
} else {
rcBuf . WriteString ( makeZshTypesetStmt ( varDecl ))
}
2024-01-16 16:11:04 -08:00
rcBuf . WriteString ( "\n" )
}
2024-02-09 20:05:00 -03:00
// do NOT unset ZDOTDIR, otherwise initialization will start to read initialization files from ~/ again
2024-01-16 16:11:04 -08:00
for _ , varName := range ZshUnsetVars {
rcBuf . WriteString ( "unset " + shellescape . Quote ( varName ) + "\n" )
}
// aliases
aliasMap , err := DecodeZshMap ([] byte ( pk . State . Aliases ))
if err != nil {
base . Logf ( "error decoding zsh aliases: %v\n" , err )
rcBuf . WriteString ( "# error decoding zsh aliases\n" )
} else {
for aliasKey , aliasValue := range aliasMap {
// tricky here, don't quote AliasName (it gets implicit quotes, and quoting doesn't work as expected)
aliasStr := fmt . Sprintf ( "%s[%s]=%s\n" , aliasKey . ParamType , aliasKey . ParamName , shellescape . Quote ( aliasValue ))
rcBuf . WriteString ( aliasStr )
}
}
// functions
fnMap , err := DecodeZshMap ([] byte ( pk . State . Funcs ))
if err != nil {
base . Logf ( "error decoding zsh functions: %v\n" , err )
rcBuf . WriteString ( "# error decoding zsh functions\n" )
} else {
for fnKey , fnValue := range fnMap {
2024-03-18 22:51:16 -07:00
if ZshIgnoreFuncs [ fnKey . ParamName ] {
continue
}
2024-01-16 16:11:04 -08:00
if fnValue == ZshFnAutoLoad {
rcBuf . WriteString ( fmt . Sprintf ( "autoload %s\n" , shellescape . Quote ( fnKey . ParamName )))
} else {
// careful, no whitespace (except newlines)
rcBuf . WriteString ( fmt . Sprintf ( "function %s () {\n%s\n}\n" , shellescape . Quote ( fnKey . ParamName ), fnValue ))
if fnKey . ParamType == "dis_functions" {
rcBuf . WriteString ( fmt . Sprintf ( "disable -f %s\n" , shellescape . Quote ( fnKey . ParamName )))
}
}
}
}
// write postdecls
for _ , varDecl := range postDecls {
rcBuf . WriteString ( makeZshTypesetStmt ( varDecl ))
rcBuf . WriteString ( "\n" )
}
2024-03-06 11:38:27 -08:00
writeZshOptions ( & rcBuf , varDecls )
2024-01-16 16:11:04 -08:00
return rcBuf . String ()
}
2024-03-06 11:38:27 -08:00
func writeZshOptions ( rcBuf * bytes . Buffer , declArr [] * shellenv . DeclareDeclType ) {
optionDecl := getDeclByName ( declArr , "options" )
var optionsMap map [ string ] string
if optionDecl != nil {
var err error
optionsMap , err = parseSimpleZshOptions ( optionDecl . Value )
if err != nil {
wlog . Logf ( "error decoding zsh options: %v\n" , err )
}
}
for optName := range optionsMap {
if ZshIgnoreOptions [ optName ] {
continue
}
if optionsMap [ optName ] == "on" {
rcBuf . WriteString ( fmt . Sprintf ( "setopt %s\n" , optName ))
} else {
rcBuf . WriteString ( fmt . Sprintf ( "unsetopt %s\n" , optName ))
}
}
}
2024-01-16 16:11:04 -08:00
func writeZshId ( buf * bytes . Buffer , idStr string ) {
buf . WriteString ( shellescape . Quote ( idStr ))
}
const numRandomBytes = 4
2024-03-19 16:38:38 -07:00
// returns (cmd-string, endbytes)
func GetZshShellStateCmd ( fdNum int ) ( string , [] byte ) {
2024-01-16 16:11:04 -08:00
var sectionSeparator [] byte
// adding this extra "\n" helps with debuging and readability of output
sectionSeparator = append ( sectionSeparator , byte ( '\n' ))
2024-03-19 16:38:38 -07:00
sectionSeparator = utilfn . AppendNonZeroRandomBytes ( sectionSeparator , numRandomBytes )
2024-01-16 16:11:04 -08:00
sectionSeparator = append ( sectionSeparator , 0 , 0 )
2024-03-19 16:38:38 -07:00
endBytes := utilfn . AppendNonZeroRandomBytes ( nil , NumRandomEndBytes )
endBytes = append ( endBytes , byte ( '\n' ))
2024-01-16 16:11:04 -08:00
// we have to use these crazy separators because zsh allows basically anything in
// variable names and values (including nulls).
// note that we don't need crazy separators for "env" or "typeset".
// environment variables *cannot* contain nulls by definition, and "typeset" already escapes nulls.
// the raw aliases and functions though need to be handled more carefully
// output redirection is necessary to prevent cooked tty options from screwing up the output (funcs especially)
// note we do not need the "extra" separator that bashapi uses because we are reading from OUTPUTFD (which already excludes any spurious stdout/stderr data)
cmd := `
exec > [%OUTPUTFD%]
unsetopt SH_WORD_SPLIT;
zmodload zsh/parameter;
2024-03-06 11:38:27 -08:00
zmodload zsh/langinfo;
2024-01-16 16:11:04 -08:00
[%ZSHVERSION%];
printf "\x00[%SECTIONSEP%]";
pwd;
printf "[%SECTIONSEP%]";
env -0;
printf "[%SECTIONSEP%]";
2024-03-18 22:51:16 -07:00
zmodload -L
printf "[%SECTIONSEP%]";
2024-01-16 16:11:04 -08:00
typeset -p +H -m '*';
printf "[%SECTIONSEP%]";
for var in "${(@k)aliases}"; do
printf "aliases %s[%PARTSEP%]%s[%PARTSEP%]" $var ${aliases[$var]}
done
for var in "${(@k)dis_aliases}"; do
printf "dis_aliases %s[%PARTSEP%]%s[%PARTSEP%]" $var ${dis_aliases[$var]}
done
for var in "${(@k)saliases}"; do
printf "saliases %s[%PARTSEP%]%s[%PARTSEP%]" $var ${saliases[$var]}
done
for var in "${(@k)dis_saliases}"; do
printf "dis_saliases %s[%PARTSEP%]%s[%PARTSEP%]" $var ${dis_saliases[$var]}
done
for var in "${(@k)galiases}"; do
printf "galiases %s[%PARTSEP%]%s[%PARTSEP%]" $var ${galiases[$var]}
done
for var in "${(@k)dis_galiases}"; do
printf "dis_galiases %s[%PARTSEP%]%s[%PARTSEP%]" $var ${dis_galiases[$var]}
done
printf "[%SECTIONSEP%]";
echo $FPATH;
printf "[%SECTIONSEP%]";
for var in "${(@k)functions}"; do
printf "functions %s[%PARTSEP%]%s[%PARTSEP%]" $var ${functions[$var]}
done
for var in "${(@k)dis_functions}"; do
printf "dis_functions %s[%PARTSEP%]%s[%PARTSEP%]" $var ${dis_functions[$var]}
done
for var in "${(@k)functions_source}"; do
printf "functions_source %s[%PARTSEP%]%s[%PARTSEP%]" $var ${functions_source[$var]}
done
for var in "${(@k)dis_functions_source}"; do
printf "dis_functions_source %s[%PARTSEP%]%s[%PARTSEP%]" $var ${dis_functions_source[$var]}
done
printf "[%SECTIONSEP%]";
[%GITBRANCH%]
2024-03-18 22:51:16 -07:00
printf "[%SECTIONSEP%]";
print -P "$PS1"
2024-03-19 16:38:38 -07:00
printf "[%SECTIONSEP%]";
printf "[%ENDBYTES%]"
2024-01-16 16:11:04 -08:00
`
cmd = strings . TrimSpace ( cmd )
cmd = strings . ReplaceAll ( cmd , "[%ZSHVERSION%]" , ZshShellVersionCmdStr )
cmd = strings . ReplaceAll ( cmd , "[%GITBRANCH%]" , GetGitBranchCmdStr )
2024-03-18 22:51:16 -07:00
cmd = strings . ReplaceAll ( cmd , "[%K8SCONTEXT%]" , GetK8sContextCmdStr )
cmd = strings . ReplaceAll ( cmd , "[%K8SNAMESPACE%]" , GetK8sNamespaceCmdStr )
2024-01-16 16:11:04 -08:00
cmd = strings . ReplaceAll ( cmd , "[%PARTSEP%]" , utilfn . ShellHexEscape ( string ( sectionSeparator [ 0 : len ( sectionSeparator ) - 1 ])))
cmd = strings . ReplaceAll ( cmd , "[%SECTIONSEP%]" , utilfn . ShellHexEscape ( string ( sectionSeparator )))
cmd = strings . ReplaceAll ( cmd , "[%OUTPUTFD%]" , fmt . Sprintf ( "/dev/fd/%d" , fdNum ))
2024-03-19 16:38:38 -07:00
cmd = strings . ReplaceAll ( cmd , "[%OUTPUTFDNUM%]" , fmt . Sprintf ( "%d" , fdNum ))
cmd = strings . ReplaceAll ( cmd , "[%ENDBYTES%]" , utilfn . ShellHexEscape ( string ( endBytes )))
return cmd , endBytes
2024-01-16 16:11:04 -08:00
}
2024-03-19 16:38:38 -07:00
func MakeZshExitTrap ( fdNum int ) ( string , [] byte ) {
stateCmd , endBytes := GetZshShellStateCmd ( fdNum )
2024-01-16 16:11:04 -08:00
fmtStr := `
zshexit () {
%s
}
`
2024-03-19 16:38:38 -07:00
return fmt . Sprintf ( fmtStr , stateCmd ), endBytes
2024-01-16 16:11:04 -08:00
}
func execGetLocalZshShellVersion () string {
2024-03-27 00:22:57 -07:00
ctx , cancelFn := context . WithTimeout ( context . Background (), GetVersionTimeout )
2024-01-16 16:11:04 -08:00
defer cancelFn ()
ecmd := exec . CommandContext ( ctx , "zsh" , "-c" , ZshShellVersionCmdStr )
out , err := ecmd . Output ()
if err != nil {
return ""
}
versionStr := strings . TrimSpace ( string ( out ))
if strings . Index ( versionStr , "zsh " ) == - 1 {
return ""
}
return versionStr
}
func GetLocalZshMajorVersion () string {
localZshMajorVersionOnce . Do ( func () {
fullVersion := execGetLocalZshShellVersion ()
localZshMajorVersion = packet . GetMajorVersion ( fullVersion )
})
return localZshMajorVersion
}
func EncodeZshMap ( m ZshMap ) [] byte {
var buf bytes . Buffer
binpack . PackUInt ( & buf , uint64 ( len ( m )))
orderedKeys := utilfn . GetOrderedStringerMapKeys ( m )
for _ , key := range orderedKeys {
value := m [ key ]
binpack . PackValue ( & buf , [] byte ( key . String ()))
binpack . PackValue ( & buf , [] byte ( value ))
}
return buf . Bytes ()
}
func EncodeZshMapForApply ( m map [ string ][] byte ) string {
var buf bytes . Buffer
binpack . PackUInt ( & buf , uint64 ( len ( m )))
orderedKeys := utilfn . GetOrderedMapKeys ( m )
for _ , key := range orderedKeys {
value := m [ key ]
binpack . PackValue ( & buf , [] byte ( key ))
binpack . PackValue ( & buf , value )
}
return buf . String ()
}
func DecodeZshMapForDiff ( barr [] byte ) ( map [ string ][] byte , error ) {
rtn := make ( map [ string ][] byte )
buf := bytes . NewBuffer ( barr )
u := binpack . MakeUnpacker ( buf )
numEntries := u . UnpackUInt ( "numEntries" )
for idx := 0 ; idx < numEntries ; idx ++ {
key := string ( u . UnpackValue ( "key" ))
value := u . UnpackValue ( "value" )
rtn [ key ] = value
}
if u . Error () != nil {
return nil , u . Error ()
}
return rtn , nil
}
func DecodeZshMap ( barr [] byte ) ( ZshMap , error ) {
rtn := make ( ZshMap )
buf := bytes . NewBuffer ( barr )
u := binpack . MakeUnpacker ( buf )
numEntries := u . UnpackUInt ( "numEntries" )
for idx := 0 ; idx < numEntries ; idx ++ {
key := string ( u . UnpackValue ( "key" ))
value := string ( u . UnpackValue ( "value" ))
zshKey , err := ZshParamKeyFromString ( key )
if err != nil {
return nil , err
}
rtn [ zshKey ] = value
}
if u . Error () != nil {
return nil , u . Error ()
}
return rtn , nil
}
func parseZshAliasStateOutput ( aliasBytes [] byte , partSeparator [] byte ) map [ ZshParamKey ] string {
aliasParts := bytes . Split ( aliasBytes , partSeparator )
rtn := make ( map [ ZshParamKey ] string )
for aliasPartIdx := 0 ; aliasPartIdx < len ( aliasParts ) - 1 ; aliasPartIdx += 2 {
aliasNameAndType := string ( aliasParts [ aliasPartIdx ])
aliasNameAndTypeParts := strings . SplitN ( aliasNameAndType , " " , 2 )
if len ( aliasNameAndTypeParts ) != 2 {
continue
}
aliasKey := ZshParamKey { ParamType : aliasNameAndTypeParts [ 0 ], ParamName : aliasNameAndTypeParts [ 1 ]}
aliasValue := string ( aliasParts [ aliasPartIdx + 1 ])
rtn [ aliasKey ] = aliasValue
}
return rtn
}
func isSourceFileInFpath ( fpathArr [] string , sourceFile string ) bool {
for _ , fpath := range fpathArr {
if fpath == "" || fpath == "." {
continue
}
firstChar := fpath [ 0 ]
if firstChar != '/' && firstChar != '~' {
continue
}
if strings . HasPrefix ( sourceFile , fpath ) {
return true
}
}
return false
}
func ParseZshFunctions ( fpathArr [] string , fnBytes [] byte , partSeparator [] byte ) map [ ZshParamKey ] string {
fnBody := make ( map [ ZshParamKey ] string )
fnSource := make ( map [ string ] string )
fnParts := bytes . Split ( fnBytes , partSeparator )
for fnPartIdx := 0 ; fnPartIdx < len ( fnParts ) - 1 ; fnPartIdx += 2 {
fnTypeAndName := string ( fnParts [ fnPartIdx ])
fnValue := string ( fnParts [ fnPartIdx + 1 ])
fnTypeAndNameParts := strings . SplitN ( fnTypeAndName , " " , 2 )
if len ( fnTypeAndNameParts ) != 2 {
continue
}
fnType := fnTypeAndNameParts [ 0 ]
fnName := fnTypeAndNameParts [ 1 ]
if fnName == "zshexit" {
continue
}
2024-03-18 22:51:16 -07:00
if ZshIgnoreFuncs [ fnName ] {
continue
}
2024-01-16 16:11:04 -08:00
if fnType == "functions" || fnType == "dis_functions" {
fnBody [ ZshParamKey { ParamType : fnType , ParamName : fnName }] = fnValue
}
if fnType == "functions_source" || fnType == "dis_functions_source" {
fnSource [ fnName ] = fnValue
}
}
// ok, so the trick here is that we want to only include functions that are *not* autoloaded
// the ones that are pending autoloading or come from a source file in fpath, can just be set to autoload
for fnKey := range fnBody {
2024-03-18 22:51:16 -07:00
var inFpath bool
2024-01-16 16:11:04 -08:00
source := fnSource [ fnKey . ParamName ]
2024-03-18 22:51:16 -07:00
if source != "" {
inFpath = isSourceFileInFpath ( fpathArr , source )
}
isAutoloadFnBody := strings . TrimSpace ( fnBody [ fnKey ]) == ZshAutoloadFnBody
if inFpath || isAutoloadFnBody {
2024-01-20 13:17:43 -08:00
fnBody [ fnKey ] = ZshFnAutoLoad
2024-01-16 16:11:04 -08:00
}
}
return fnBody
}
func makeZshFuncsStrForShellState ( fnMap map [ ZshParamKey ] string ) string {
var buf bytes . Buffer
for fnKey , fnValue := range fnMap {
buf . WriteString ( fmt . Sprintf ( "%s %s %s\x00" , fnKey . ParamType , fnKey . ParamName , fnValue ))
}
return buf . String ()
}
2024-04-09 11:33:23 -07:00
func ( zshShellApi ) ParseShellStateOutput ( outputBytes [] byte ) ( * packet . ShellState , * packet . ShellStateStats , error ) {
2024-01-16 16:11:04 -08:00
if scbase . IsDevMode () && DebugState {
writeStateToFile ( packet . ShellType_zsh , outputBytes )
}
firstZeroIdx := bytes . Index ( outputBytes , [] byte { 0 })
firstDZeroIdx := bytes . Index ( outputBytes , [] byte { 0 , 0 })
if firstZeroIdx == - 1 || firstDZeroIdx == - 1 {
2024-03-19 16:38:38 -07:00
return nil , nil , fmt . Errorf ( "invalid zsh shell state output, could not parse separator bytes" )
2024-01-16 16:11:04 -08:00
}
versionStr := string ( outputBytes [ 0 : firstZeroIdx ])
sectionSeparator := outputBytes [ firstZeroIdx + 1 : firstDZeroIdx + 2 ]
partSeparator := sectionSeparator [ 0 : len ( sectionSeparator ) - 1 ]
2024-03-18 22:51:16 -07:00
// sections: see ZshSection_* consts
sections := bytes . Split ( outputBytes , sectionSeparator )
if len ( sections ) != ZshSection_NumFieldsExpected {
2024-03-19 16:38:38 -07:00
return nil , nil , fmt . Errorf ( "invalid zsh shell state output, wrong number of sections, section=%d" , len ( sections ))
2024-01-16 16:11:04 -08:00
}
rtn := & packet . ShellState {}
rtn . Version = strings . TrimSpace ( versionStr )
if rtn . GetShellType () != packet . ShellType_zsh {
2024-03-19 16:38:38 -07:00
return nil , nil , fmt . Errorf ( "invalid zsh shell state output, wrong shell type" )
2024-01-16 16:11:04 -08:00
}
if _ , _ , err := packet . ParseShellStateVersion ( rtn . Version ); err != nil {
2024-03-19 16:38:38 -07:00
return nil , nil , fmt . Errorf ( "invalid zsh shell state output, invalid version: %v" , err )
2024-01-16 16:11:04 -08:00
}
2024-03-18 22:51:16 -07:00
cwdStr := stripNewLineChars ( string ( sections [ ZshSection_Cwd ]))
2024-01-16 16:11:04 -08:00
rtn . Cwd = cwdStr
2024-03-18 22:51:16 -07:00
zshEnv := parseZshEnv ( sections [ ZshSection_Env ])
zshDecls , err := parseZshDecls ( sections [ ZshSection_Vars ])
2024-01-16 16:11:04 -08:00
if err != nil {
base . Logf ( "invalid - parsedecls %v\n" , err )
2024-03-19 16:38:38 -07:00
return nil , nil , err
2024-01-16 16:11:04 -08:00
}
for _ , decl := range zshDecls {
if decl . IsZshScalarBound () {
decl . ZshEnvValue = zshEnv [ decl . ZshBoundScalar ]
}
}
2024-03-18 22:51:16 -07:00
aliasMap := parseZshAliasStateOutput ( sections [ ZshSection_Aliases ], partSeparator )
2024-01-16 16:11:04 -08:00
rtn . Aliases = string ( EncodeZshMap ( aliasMap ))
2024-03-18 22:51:16 -07:00
fpathStr := stripNewLineChars ( string ( string ( sections [ ZshSection_Fpath ])))
2024-01-16 16:11:04 -08:00
fpathArr := strings . Split ( fpathStr , ":" )
2024-03-18 22:51:16 -07:00
zshFuncs := ParseZshFunctions ( fpathArr , sections [ ZshSection_Funcs ], partSeparator )
2024-01-16 16:11:04 -08:00
rtn . Funcs = string ( EncodeZshMap ( zshFuncs ))
2024-03-18 22:51:16 -07:00
pvarMap := parseExtVarOutput ( sections [ ZshSection_PVars ], string ( sections [ ZshSection_Prompt ]), string ( sections [ ZshSection_Mods ]))
2024-01-16 16:11:04 -08:00
utilfn . CombineMaps ( zshDecls , pvarMap )
rtn . ShellVars = shellenv . SerializeDeclMap ( zshDecls )
2024-03-19 16:38:38 -07:00
stats := & packet . ShellStateStats {
Version : rtn . Version ,
AliasCount : int ( len ( aliasMap )),
FuncCount : int ( len ( zshFuncs )),
VarCount : int ( len ( zshDecls )),
EnvCount : int ( len ( zshEnv )),
HashVal : rtn . GetHashVal ( false ),
OutputSize : int64 ( len ( outputBytes )),
StateSize : rtn . ApproximateSize (),
}
return rtn , stats , nil
2024-01-16 16:11:04 -08:00
}
func parseZshEnv ( output [] byte ) map [ string ] string {
outputStr := string ( output )
lines := strings . Split ( outputStr , "\x00" )
rtn := make ( map [ string ] string )
for _ , line := range lines {
if line == "" {
continue
}
eqIdx := strings . Index ( line , "=" )
if eqIdx == - 1 {
continue
}
name := line [ 0 : eqIdx ]
if ZshIgnoreVars [ name ] {
continue
}
val := line [ eqIdx + 1 :]
rtn [ name ] = val
}
return rtn
}
func parseZshScalarBoundAssignment ( declStr string , decl * DeclareDeclType ) error {
declStr = strings . TrimLeft ( declStr , " " )
spaceIdx := strings . Index ( declStr , " " )
if spaceIdx == - 1 {
return fmt . Errorf ( "invalid zsh decl (scalar bound): %q" , declStr )
}
decl . ZshBoundScalar = declStr [ 0 : spaceIdx ]
standardDecl := declStr [ spaceIdx + 1 :]
return parseStandardZshAssignment ( standardDecl , decl )
}
func parseStandardZshAssignment ( declStr string , decl * DeclareDeclType ) error {
declStr = strings . TrimLeft ( declStr , " " )
eqIdx := strings . Index ( declStr , "=" )
if eqIdx == - 1 {
return fmt . Errorf ( "invalid zsh decl: %q" , declStr )
}
decl . Name = declStr [ 0 : eqIdx ]
decl . Value = declStr [ eqIdx + 1 :]
return nil
}
func parseZshDeclAssignment ( declStr string , decl * DeclareDeclType ) error {
if decl . IsZshScalarBound () {
return parseZshScalarBoundAssignment ( declStr , decl )
}
return parseStandardZshAssignment ( declStr , decl )
}
// returns (newDeclStr, argsStr, err)
func parseZshDeclArgs ( declStr string , isExport bool ) ( string , string , error ) {
origDeclStr := declStr
var argsStr string
if isExport {
argsStr = "x"
}
declStr = strings . TrimLeft ( declStr , " " )
for strings . HasPrefix ( declStr , "-" ) {
spaceIdx := strings . Index ( declStr , " " )
if spaceIdx == - 1 {
return "" , "" , fmt . Errorf ( "invalid zsh export line: %q" , origDeclStr )
}
newArgsStr := strings . TrimSpace ( declStr [ 1 : spaceIdx ])
argsStr = argsStr + newArgsStr
declStr = declStr [ spaceIdx + 1 :]
declStr = strings . TrimLeft ( declStr , " " )
}
return declStr , argsStr , nil
}
func stripNewLineChars ( s string ) string {
for {
if len ( s ) == 0 {
return s
}
lastChar := s [ len ( s ) - 1 ]
if lastChar == '\n' || lastChar == '\r' {
s = s [ 0 : len ( s ) - 1 ]
} else {
return s
}
}
}
func parseZshDeclLine ( line string ) ( * DeclareDeclType , error ) {
line = stripNewLineChars ( line )
if strings . HasPrefix ( line , "export " ) {
exportLine := line [ 7 :]
assignLine , exportArgs , err := parseZshDeclArgs ( exportLine , true )
rtn := & DeclareDeclType { IsZshDecl : true , Args : exportArgs }
err = parseZshDeclAssignment ( assignLine , rtn )
if err != nil {
return nil , err
}
return rtn , nil
} else if strings . HasPrefix ( line , "typeset " ) {
typesetLine := line [ 8 :]
assignLine , typesetArgs , err := parseZshDeclArgs ( typesetLine , false )
rtn := & DeclareDeclType { IsZshDecl : true , Args : typesetArgs }
err = parseZshDeclAssignment ( assignLine , rtn )
if err != nil {
return nil , err
}
return rtn , nil
} else {
return nil , fmt . Errorf ( "invalid zsh decl line: %q" , line )
}
}
// combine decl2 INTO decl1
func combineTiedZshDecls ( decl1 * DeclareDeclType , decl2 * DeclareDeclType ) {
if decl2 . IsExport () {
decl1 . AddFlag ( "x" )
}
if decl2 . IsArray () {
decl1 . AddFlag ( "a" )
}
}
func parseZshDecls ( output [] byte ) ( map [ string ] * DeclareDeclType , error ) {
// NOTES:
// - we get extra \r characters in the output (trimmed in parseZshDeclLine) (we get \r\n)
// - tied variables (-T) are printed twice! this is especially confusing for exported vars:
// (1) `export -T PATH path=( ... )`
// (2) `typeset -aT PATH path=( ... )`
// we have to "combine" these two lines into one decl.
outputStr := string ( output )
lines := strings . Split ( outputStr , "\n" )
rtn := make ( map [ string ] * DeclareDeclType )
for _ , line := range lines {
if line == "" {
continue
}
decl , err := parseZshDeclLine ( line )
if err != nil {
base . Logf ( "error parsing zsh decl line: %v" , err )
continue
}
if decl == nil {
continue
}
if ZshIgnoreVars [ decl . Name ] {
continue
}
if rtn [ decl . Name ] != nil && decl . IsZshScalarBound () {
combineTiedZshDecls ( rtn [ decl . Name ], decl )
continue
}
rtn [ decl . Name ] = decl
}
return rtn , nil
}
func makeZshMapDiff ( oldMap string , newMap string ) ([] byte , error ) {
oldMapMap , err := DecodeZshMapForDiff ([] byte ( oldMap ))
if err != nil {
return nil , fmt . Errorf ( "error zshMapDiff decoding old-zsh map: %v" , err )
}
newMapMap , err := DecodeZshMapForDiff ([] byte ( newMap ))
if err != nil {
return nil , fmt . Errorf ( "error zshMapDiff decoding new-zsh map: %v" , err )
}
return statediff . MakeMapDiff ( oldMapMap , newMapMap ), nil
}
func applyZshMapDiff ( oldMap string , diff [] byte ) ( string , error ) {
oldMapMap , err := DecodeZshMapForDiff ([] byte ( oldMap ))
if err != nil {
return "" , fmt . Errorf ( "error zshMapDiff decoding old-zsh map: %v" , err )
}
newMapMap , err := statediff . ApplyMapDiff ( oldMapMap , diff )
if err != nil {
return "" , fmt . Errorf ( "error zshMapDiff applying diff: %v" , err )
}
return EncodeZshMapForApply ( newMapMap ), nil
}
func ( zshShellApi ) MakeShellStateDiff ( oldState * packet . ShellState , oldStateHash string , newState * packet . ShellState ) ( * packet . ShellStateDiff , error ) {
if oldState == nil {
return nil , fmt . Errorf ( "cannot diff, oldState is nil" )
}
if newState == nil {
return nil , fmt . Errorf ( "cannot diff, newState is nil" )
}
if oldState . Version != newState . Version {
return nil , fmt . Errorf ( "cannot diff, states have different versions" )
}
rtn := & packet . ShellStateDiff {}
rtn . BaseHash = oldStateHash
rtn . Version = newState . Version // always set version
if oldState . Cwd != newState . Cwd {
rtn . Cwd = newState . Cwd
}
rtn . Error = newState . Error
oldVars := shellenv . ShellStateVarsToMap ( oldState . ShellVars )
newVars := shellenv . ShellStateVarsToMap ( newState . ShellVars )
rtn . VarsDiff = statediff . MakeMapDiff ( oldVars , newVars )
var err error
rtn . AliasesDiff , err = makeZshMapDiff ( oldState . Aliases , newState . Aliases )
if err != nil {
return nil , err
}
rtn . FuncsDiff , err = makeZshMapDiff ( oldState . Funcs , newState . Funcs )
if err != nil {
return nil , err
}
return rtn , nil
}
func ( zshShellApi ) ApplyShellStateDiff ( oldState * packet . ShellState , diff * packet . ShellStateDiff ) ( * packet . ShellState , error ) {
if oldState == nil {
return nil , fmt . Errorf ( "cannot apply diff, oldState is nil" )
}
if diff == nil {
return oldState , nil
}
rtnState := & packet . ShellState {}
var err error
rtnState . Version = oldState . Version
if diff . Version != rtnState . Version {
rtnState . Version = diff . Version
}
rtnState . Cwd = oldState . Cwd
if diff . Cwd != "" {
rtnState . Cwd = diff . Cwd
}
rtnState . Error = diff . Error
oldVars := shellenv . ShellStateVarsToMap ( oldState . ShellVars )
newVars , err := statediff . ApplyMapDiff ( oldVars , diff . VarsDiff )
if err != nil {
return nil , fmt . Errorf ( "applying mapdiff 'vars': %v" , err )
}
rtnState . ShellVars = shellenv . StrMapToShellStateVars ( newVars )
rtnState . Aliases , err = applyZshMapDiff ( oldState . Aliases , diff . AliasesDiff )
if err != nil {
return nil , fmt . Errorf ( "applying diff 'aliases': %v" , err )
}
rtnState . Funcs , err = applyZshMapDiff ( oldState . Funcs , diff . FuncsDiff )
if err != nil {
return nil , fmt . Errorf ( "applying diff 'funcs': %v" , err )
}
return rtnState , nil
}
2024-03-06 11:38:27 -08:00
// this will *not* parse general zsh assoc arrays, used to parse zsh options (no spaces)
// ( [posixargzero]=off [autolist]=on )
func parseSimpleZshOptions ( decl string ) ( map [ string ] string , error ) {
decl = strings . TrimSpace ( decl )
if ! strings . HasPrefix ( decl , "(" ) || ! strings . HasSuffix ( decl , ")" ) {
return nil , errors . New ( "invalid assoc array decl, must start and end with parens" )
}
decl = decl [ 1 : len ( decl ) - 1 ]
parts := strings . Split ( decl , " " )
rtn := make ( map [ string ] string )
for _ , part := range parts {
part = strings . TrimSpace ( part )
if part == "" {
continue
}
eqIdx := strings . Index ( part , "=" )
if eqIdx == - 1 {
return nil , fmt . Errorf ( "invalid assoc array decl part: %q" , part )
}
bracketedKey := part [ 0 : eqIdx ]
val := part [ eqIdx + 1 :]
if ! strings . HasPrefix ( bracketedKey , "[" ) || ! strings . HasSuffix ( bracketedKey , "]" ) {
return nil , fmt . Errorf ( "invalid assoc array decl part: %q" , part )
}
key := bracketedKey [ 1 : len ( bracketedKey ) - 1 ]
rtn [ key ] = val
}
return rtn , nil
}
func getDeclByName ( decls [] * shellenv . DeclareDeclType , name string ) * shellenv . DeclareDeclType {
for _ , decl := range decls {
if decl . Name == name {
return decl
}
}
return nil
}