Skip to content

Commit 74d1f1b

Browse files
rohanKanojiapraveenkumar
authored andcommitted
fix (shell) : Improve shell detection on windows (#3767)
- Currently we rely on `SHELL` environment variable for detecting active shell type. This will work when the environment variable is set. As per my observations, this environment variable is not set explicitly by various linux shell environments (`bash`,`zsh`,`fish`). We should detect currently active shell by checking currently active processes instead. - While generating statements for export statements on Windows, we shall make sure that we have converted windows paths to linux paths. Signed-off-by: Rohan Kumar <[email protected]>
1 parent 676e51e commit 74d1f1b

File tree

6 files changed

+475
-25
lines changed

6 files changed

+475
-25
lines changed

pkg/os/shell/shell.go

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@ package shell
22

33
import (
44
"fmt"
5+
"os"
6+
"strconv"
57
"strings"
8+
9+
crcos "github.com/crc-org/crc/v2/pkg/os"
10+
)
11+
12+
var (
13+
CommandRunner = crcos.NewLocalCommandRunner()
14+
WindowsSubsystemLinuxKernelMetadataFile = "/proc/version"
615
)
716

817
type Config struct {
@@ -65,9 +74,9 @@ func GetEnvString(userShell string, envName string, envValue string) string {
6574
case "cmd":
6675
return fmt.Sprintf("SET %s=%s", envName, envValue)
6776
case "fish":
68-
return fmt.Sprintf("contains %s $fish_user_paths; or set -U fish_user_paths %s $fish_user_paths", envValue, envValue)
77+
return fmt.Sprintf("contains %s $fish_user_paths; or set -U fish_user_paths %s $fish_user_paths", convertToLinuxStylePath(userShell, envValue), convertToLinuxStylePath(userShell, envValue))
6978
default:
70-
return fmt.Sprintf("export %s=\"%s\"", envName, envValue)
79+
return fmt.Sprintf("export %s=\"%s\"", envName, convertToLinuxStylePath(userShell, envValue))
7180
}
7281
}
7382

@@ -81,8 +90,140 @@ func GetPathEnvString(userShell string, prependedPath string) string {
8190
case "cmd":
8291
pathStr = fmt.Sprintf("%s;%%PATH%%", prependedPath)
8392
default:
84-
pathStr = fmt.Sprintf("%s:$PATH", prependedPath)
93+
pathStr = fmt.Sprintf("%s:$PATH", convertToLinuxStylePath(userShell, prependedPath))
8594
}
8695

8796
return GetEnvString(userShell, "PATH", pathStr)
8897
}
98+
99+
// convertToLinuxStylePath is a utility method to translate Windows paths to Linux environments (e.g. Git Bash).
100+
//
101+
// It receives two arguments:
102+
// - userShell : currently active shell
103+
// - path : Windows path to be converted
104+
//
105+
// It returns Linux equivalent of the Windows path.
106+
//
107+
// For example, a Windows path like `C:\Users\foo\.crc\bin\oc` is converted into `/C/Users/foo/.crc/bin/oc`.
108+
func convertToLinuxStylePath(userShell string, path string) string {
109+
if IsWindowsSubsystemLinux() {
110+
return convertToWindowsSubsystemLinuxPath(path)
111+
}
112+
if strings.Contains(path, "\\") &&
113+
(userShell == "bash" || userShell == "zsh" || userShell == "fish") {
114+
path = strings.ReplaceAll(path, ":", "")
115+
path = strings.ReplaceAll(path, "\\", "/")
116+
117+
return fmt.Sprintf("/%s", path)
118+
}
119+
return path
120+
}
121+
122+
// convertToWindowsSubsystemLinuxPath is a utility method to translate between Windows and WSL(Windows Subsystem for
123+
// Linux) paths. It relies on `wslpath` command to perform this conversion.
124+
//
125+
// It receives one argument:
126+
// - path : Windows path to be converted to WSL path
127+
//
128+
// It returns translated WSL equivalent of provided windows path.
129+
func convertToWindowsSubsystemLinuxPath(path string) string {
130+
stdOut, _, err := CommandRunner.Run("wsl", "-e", "bash", "-c", fmt.Sprintf("wslpath -a '%s'", path))
131+
if err != nil {
132+
return path
133+
}
134+
return strings.TrimSpace(stdOut)
135+
}
136+
137+
// IsWindowsSubsystemLinux detects whether current system is using Windows Subsystem for Linux or not
138+
//
139+
// It checks for these conditions to make sure that current system has WSL installed:
140+
// - `/proc/version` file is present
141+
// - `/proc/version` file contents contain keywords `Microsoft` and `WSL`
142+
//
143+
// It above conditions are met, then this method returns `true` otherwise `false`.
144+
func IsWindowsSubsystemLinux() bool {
145+
procVersionContent, err := os.ReadFile(WindowsSubsystemLinuxKernelMetadataFile)
146+
if err != nil {
147+
return false
148+
}
149+
if strings.Contains(string(procVersionContent), "Microsoft") ||
150+
strings.Contains(string(procVersionContent), "WSL") {
151+
return true
152+
}
153+
return false
154+
}
155+
156+
// detectShellByInvokingCommand is a utility method that tries to detect current shell in use by invoking `ps` command.
157+
// This method is extracted so that it could be used by unix systems as well as Windows (in case of WSL). It executes
158+
// the command provided in the method arguments and then passes the output to inspectProcessOutputForRecentlyUsedShell
159+
// for evaluation.
160+
//
161+
// It receives two arguments:
162+
// - defaultShell : default shell to revert back to in case it's unable to detect.
163+
// - command: command to be executed
164+
// - args: a string array containing command arguments
165+
//
166+
// It returns a string value representing current shell.
167+
func detectShellByInvokingCommand(defaultShell string, command string, args []string) string {
168+
stdOut, _, err := CommandRunner.Run(command, args...)
169+
if err != nil {
170+
return defaultShell
171+
}
172+
173+
detectedShell := inspectProcessOutputForRecentlyUsedShell(stdOut)
174+
if detectedShell == "" {
175+
return defaultShell
176+
}
177+
return detectedShell
178+
}
179+
180+
// inspectProcessOutputForRecentlyUsedShell inspects output of ps command to detect currently active shell session.
181+
//
182+
// Note : This method assumes that ps command has already sorted the processes by `pid` in reverse order.
183+
// It parses the output into a struct, filters process types by name and returns the first element.
184+
//
185+
// It takes one argument:
186+
//
187+
// - psCommandOutput: output of ps command executed on a particular shell session
188+
//
189+
// It returns:
190+
//
191+
// - a string value (one of `zsh`, `bash` or `fish`) for current shell environment in use. If it's not able to determine
192+
// underlying shell type, it returns and empty string.
193+
//
194+
// This method tries to check all processes open and filters out shell sessions (one of `zsh`, `bash` or `fish)
195+
// It then returns first shell process.
196+
//
197+
// For example, if ps command gives this output:
198+
//
199+
// 2908 ps
200+
// 2889 fish
201+
// 823 bash
202+
//
203+
// Then this method would return `fish` as it's the first shell process.
204+
func inspectProcessOutputForRecentlyUsedShell(psCommandOutput string) string {
205+
type ProcessOutput struct {
206+
processID int
207+
output string
208+
}
209+
var processOutputs []ProcessOutput
210+
lines := strings.Split(psCommandOutput, "\n")
211+
for _, line := range lines {
212+
lineParts := strings.Split(strings.TrimSpace(line), " ")
213+
if len(lineParts) == 2 && (strings.Contains(lineParts[1], "zsh") ||
214+
strings.Contains(lineParts[1], "bash") ||
215+
strings.Contains(lineParts[1], "fish")) {
216+
parsedProcessID, err := strconv.Atoi(lineParts[0])
217+
if err == nil {
218+
processOutputs = append(processOutputs, ProcessOutput{
219+
processID: parsedProcessID,
220+
output: lineParts[1],
221+
})
222+
}
223+
}
224+
}
225+
if len(processOutputs) > 0 {
226+
return processOutputs[0].output
227+
}
228+
return ""
229+
}

0 commit comments

Comments
 (0)