-
Notifications
You must be signed in to change notification settings - Fork 13
/
subshell.go
239 lines (199 loc) · 7.17 KB
/
subshell.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
package subshell
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/thoas/go-funk"
"github.com/ActiveState/cli/internal/constants"
"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/fileutils"
"github.com/ActiveState/cli/internal/logging"
"github.com/ActiveState/cli/internal/multilog"
"github.com/ActiveState/cli/internal/osutils"
"github.com/ActiveState/cli/internal/output"
"github.com/ActiveState/cli/internal/rollbar"
"github.com/ActiveState/cli/internal/subshell/bash"
"github.com/ActiveState/cli/internal/subshell/cmd"
"github.com/ActiveState/cli/internal/subshell/fish"
"github.com/ActiveState/cli/internal/subshell/sscommon"
"github.com/ActiveState/cli/internal/subshell/tcsh"
"github.com/ActiveState/cli/internal/subshell/zsh"
"github.com/ActiveState/cli/pkg/project"
)
const ConfigKeyShell = "shell"
// SubShell defines the interface for our virtual environment packages, which should be contained in a sub-directory
// under the same directory as this file
type SubShell interface {
// Activate the given subshell
Activate(proj *project.Project, cfg sscommon.Configurable, out output.Outputer) error
// Errors returns a channel to receive errors
Errors() <-chan error
// Deactivate the given subshell
Deactivate() error
// Run a script string, passing the provided command-line arguments, that assumes this shell and returns the exit code
Run(filename string, args ...string) error
// IsActive returns whether the given subshell is active
IsActive() bool
// Binary returns the configured binary
Binary() string
// SetBinary sets the configured binary, this should only be called by the subshell package
SetBinary(string)
// WriteUserEnv writes the given env map to the users environment
WriteUserEnv(sscommon.Configurable, map[string]string, sscommon.RcIdentification, bool) error
// CleanUserEnv removes the environment setting identified
CleanUserEnv(sscommon.Configurable, sscommon.RcIdentification, bool) error
// RemoveLegacyInstallPath removes the install path added to shell configuration by the legacy install scripts
RemoveLegacyInstallPath(sscommon.Configurable) error
// WriteCompletionScript writes the completions script for the current shell
WriteCompletionScript(string) error
// RcFile return the path of the RC file
RcFile() (string, error)
// EnsureRcFile ensures that the RC file exists
EnsureRcFileExists() error
// SetupShellRcFile writes a script or source-able file that updates the environment variables and sets the prompt
SetupShellRcFile(string, map[string]string, *project.Namespaced, sscommon.Configurable) error
// Shell returns an identifiable string representing the shell, eg. bash, zsh
Shell() string
// SetEnv sets the environment up for the given subshell
SetEnv(env map[string]string) error
// Quote will quote the given string, escaping any characters that need escaping
Quote(value string) string
// IsAvailable returns whether the shell is available on the system
IsAvailable() bool
}
// New returns the subshell relevant to the current process, but does not activate it
func New(cfg sscommon.Configurable) SubShell {
name, path := DetectShell(cfg)
var subs SubShell
switch name {
case bash.Name:
subs = &bash.SubShell{}
case zsh.Name:
subs = &zsh.SubShell{}
case tcsh.Name:
subs = &tcsh.SubShell{}
case fish.Name:
subs = &fish.SubShell{}
case cmd.Name:
subs = &cmd.SubShell{}
default:
rollbar.Error("subshell.DetectShell did not return a known name: %s", name)
switch runtime.GOOS {
case "windows":
subs = &cmd.SubShell{}
case "darwin":
subs = &zsh.SubShell{}
default:
subs = &bash.SubShell{}
}
}
logging.Debug("Using binary: %s", path)
subs.SetBinary(path)
env := funk.FilterString(os.Environ(), func(s string) bool {
return !strings.HasPrefix(s, constants.ProjectEnvVarName)
})
err := subs.SetEnv(osutils.EnvSliceToMap(env))
if err != nil {
// We cannot error here, but this error will resurface when activating a runtime, so we can
// notify the user at that point.
logging.Error("Failed to set subshell environment: %v", err)
}
return subs
}
// resolveBinaryPath tries to find the named binary on PATH
func resolveBinaryPath(name string) string {
binaryPath, err := exec.LookPath(name)
if err == nil {
// if we found it, resolve all symlinks, for many Linux distributions the SHELL is "sh" but symlinked to a different default shell like bash or zsh
resolved, err := fileutils.ResolvePath(binaryPath)
if err == nil {
return resolved
} else {
logging.Debug("Failed to resolve path to shell binary %s: %v", binaryPath, err)
}
}
return name
}
func ConfigureAvailableShells(shell SubShell, cfg sscommon.Configurable, env map[string]string, identifier sscommon.RcIdentification, userScope bool) error {
// Ensure the given, detected, and current shell has an RC file or else it will not be considered "available"
err := shell.EnsureRcFileExists()
if err != nil {
return errs.Wrap(err, "Could not ensure RC file for current shell")
}
for _, s := range supportedShells {
if !s.IsAvailable() {
continue
}
err := s.WriteUserEnv(cfg, env, identifier, userScope)
if err != nil {
logging.Error("Could not update PATH for shell %s: %v", s.Shell(), err)
}
}
return nil
}
// DetectShell detects the shell relevant to the current process and returns its name and path.
func DetectShell(cfg sscommon.Configurable) (string, string) {
configured := cfg.GetString(ConfigKeyShell)
var binary string
defer func() {
// do not re-write shell binary to config, if the value did not change.
if configured == binary {
return
}
// We save and use the detected shell to our config so that we can use it when running code through
// a non-interactive shell
if err := cfg.Set(ConfigKeyShell, binary); err != nil {
multilog.Error("Could not save shell binary: %v", errs.JoinMessage(err))
}
}()
binary = os.Getenv("SHELL")
if binary == "" && runtime.GOOS == "windows" {
binary = os.Getenv("ComSpec")
}
if binary == "" {
binary = configured
}
if binary == "" {
if runtime.GOOS == "windows" {
binary = "cmd.exe"
} else {
binary = "bash"
}
}
path := resolveBinaryPath(binary)
name := filepath.Base(path)
name = strings.TrimSuffix(name, filepath.Ext(name))
logging.Debug("Detected SHELL: %s", name)
if runtime.GOOS == "windows" {
// For some reason Go or MSYS doesn't translate paths with spaces correctly, so we have to strip out the
// invalid escape characters for spaces
path = strings.ReplaceAll(path, `\ `, ` `)
}
isKnownShell := false
for _, ssName := range []string{bash.Name, cmd.Name, fish.Name, tcsh.Name, zsh.Name} {
if name == ssName {
isKnownShell = true
break
}
}
if !isKnownShell {
logging.Debug("Unsupported shell: %s, defaulting to OS default.", name)
if !strings.EqualFold(name, "powershell") && name != "sh" {
rollbar.Error("Unsupported shell: %s", name) // we just want to know what this person is using
}
switch runtime.GOOS {
case "windows":
name = cmd.Name
path = resolveBinaryPath("cmd.exe")
case "darwin":
name = zsh.Name
path = resolveBinaryPath("zsh")
default:
name = bash.Name
path = resolveBinaryPath("bash")
}
}
return name, path
}