diff --git a/vscode/extension/src/commands/printEnvironment.ts b/vscode/extension/src/commands/printEnvironment.ts index 41e74e40d3..39fd171bd1 100644 --- a/vscode/extension/src/commands/printEnvironment.ts +++ b/vscode/extension/src/commands/printEnvironment.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode' import { getSqlmeshEnvironment } from '../utilities/sqlmesh/sqlmesh' import { isErr } from '@bus/result' import { IS_WINDOWS } from '../utilities/isWindows' +import { printEnvironmentCommand } from '../utilities/shellCommand' export function printEnvironment() { return async () => { @@ -23,14 +24,9 @@ export function printEnvironment() { // Show the terminal terminal.show() - // Run the appropriate command to display environment variables - if (IS_WINDOWS) { - // On Windows, use 'set' command - terminal.sendText('set') - } else { - // On Unix-like systems, use 'env' command - terminal.sendText('env | sort') - } + // Run the command the user's shell understands, since the terminal is + // opened with the default shell rather than a shell we pick + terminal.sendText(printEnvironmentCommand(vscode.env.shell, IS_WINDOWS)) // Show a notification vscode.window.showInformationMessage( diff --git a/vscode/extension/src/utilities/shellCommand.test.ts b/vscode/extension/src/utilities/shellCommand.test.ts new file mode 100644 index 0000000000..fa197c38ab --- /dev/null +++ b/vscode/extension/src/utilities/shellCommand.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect } from 'vitest' +import { detectShellFamily, printEnvironmentCommand } from './shellCommand' + +describe('detectShellFamily', () => { + it('should detect fish', () => { + expect(detectShellFamily('/usr/bin/fish', false)).toBe('fish') + expect(detectShellFamily('/opt/homebrew/bin/fish', false)).toBe('fish') + }) + + it('should detect POSIX shells', () => { + expect(detectShellFamily('/bin/bash', false)).toBe('posix') + expect(detectShellFamily('/bin/zsh', false)).toBe('posix') + expect(detectShellFamily('/bin/sh', false)).toBe('posix') + expect(detectShellFamily('/usr/bin/dash', false)).toBe('posix') + expect(detectShellFamily('/usr/bin/ksh', false)).toBe('posix') + }) + + it('should detect Windows shells, ignoring the executable extension', () => { + expect(detectShellFamily('C:\\Windows\\System32\\cmd.exe', true)).toBe( + 'cmd', + ) + expect( + detectShellFamily( + 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', + true, + ), + ).toBe('powershell') + expect( + detectShellFamily('C:\\Program Files\\PowerShell\\7\\pwsh.exe', true), + ).toBe('powershell') + }) + + it('should detect a POSIX shell installed on Windows', () => { + expect( + detectShellFamily('C:\\Program Files\\Git\\bin\\bash.exe', true), + ).toBe('posix') + }) + + it('should accept a bare shell name without a path', () => { + expect(detectShellFamily('fish', false)).toBe('fish') + expect(detectShellFamily('pwsh', false)).toBe('powershell') + }) + + it('should ignore the case of the shell name', () => { + expect(detectShellFamily('/usr/bin/FISH', false)).toBe('fish') + expect(detectShellFamily('C:\\WINDOWS\\SYSTEM32\\CMD.EXE', true)).toBe( + 'cmd', + ) + }) + + it('should fall back on the platform for an unknown shell', () => { + expect(detectShellFamily('/usr/bin/elvish', false)).toBe('posix') + expect(detectShellFamily('C:\\tools\\elvish.exe', true)).toBe('cmd') + }) + + it('should fall back on the platform when no shell is reported', () => { + expect(detectShellFamily('', false)).toBe('posix') + expect(detectShellFamily('', true)).toBe('cmd') + expect(detectShellFamily(undefined, false)).toBe('posix') + expect(detectShellFamily(undefined, true)).toBe('cmd') + }) +}) + +describe('printEnvironmentCommand', () => { + it('should use set in fish', () => { + expect(printEnvironmentCommand('/usr/bin/fish', false)).toBe('set') + }) + + it('should use env | sort in POSIX shells', () => { + expect(printEnvironmentCommand('/bin/bash', false)).toBe('env | sort') + expect(printEnvironmentCommand('/bin/zsh', false)).toBe('env | sort') + expect(printEnvironmentCommand('/bin/sh', false)).toBe('env | sort') + }) + + it('should use set in cmd', () => { + expect( + printEnvironmentCommand('C:\\Windows\\System32\\cmd.exe', true), + ).toBe('set') + }) + + it('should not use set in PowerShell, where it prompts for a variable name', () => { + expect( + printEnvironmentCommand( + 'C:\\Program Files\\PowerShell\\7\\pwsh.exe', + true, + ), + ).toBe('Get-ChildItem Env: | Sort-Object Name') + }) + + it('should keep the previous behaviour when the shell is unknown', () => { + expect(printEnvironmentCommand(undefined, false)).toBe('env | sort') + expect(printEnvironmentCommand(undefined, true)).toBe('set') + }) +}) diff --git a/vscode/extension/src/utilities/shellCommand.ts b/vscode/extension/src/utilities/shellCommand.ts new file mode 100644 index 0000000000..296c2482ed --- /dev/null +++ b/vscode/extension/src/utilities/shellCommand.ts @@ -0,0 +1,73 @@ +/** + * The shell families we know how to print environment variables in. + */ +export type ShellFamily = 'fish' | 'powershell' | 'cmd' | 'posix' + +/** + * Commands that list the environment variables of the current shell, one per + * shell family. + */ +const PRINT_ENVIRONMENT_COMMANDS: Record = { + fish: 'set', + powershell: 'Get-ChildItem Env: | Sort-Object Name', + cmd: 'set', + posix: 'env | sort', +} + +const SHELL_FAMILIES_BY_NAME: Record = { + fish: 'fish', + powershell: 'powershell', + pwsh: 'powershell', + cmd: 'cmd', + bash: 'posix', + zsh: 'posix', + sh: 'posix', + dash: 'posix', + ksh: 'posix', + ash: 'posix', +} + +/** + * Extracts the executable name from a shell path, which may use either + * separator and may carry a Windows executable extension. + */ +const shellName = (shellPath: string): string => { + const base = shellPath.split(/[\\/]/).pop() ?? '' + return base.toLowerCase().replace(/\.exe$/, '') +} + +/** + * Resolves a shell path to the family whose syntax it accepts. + * + * Unrecognized shells fall back on the platform default, which keeps POSIX + * shells we have not listed working. + * + * @param shellPath The path reported by `vscode.env.shell`, which is empty in + * environments without a shell + * @param isWindows Whether the extension host runs on Windows + */ +export const detectShellFamily = ( + shellPath: string | undefined, + isWindows: boolean, +): ShellFamily => { + const family = shellPath + ? SHELL_FAMILIES_BY_NAME[shellName(shellPath)] + : undefined + if (family) { + return family + } + return isWindows ? 'cmd' : 'posix' +} + +/** + * Returns the command that prints all environment variables in the given + * shell. + * + * @param shellPath The path reported by `vscode.env.shell`, which is empty in + * environments without a shell + * @param isWindows Whether the extension host runs on Windows + */ +export const printEnvironmentCommand = ( + shellPath: string | undefined, + isWindows: boolean, +): string => PRINT_ENVIRONMENT_COMMANDS[detectShellFamily(shellPath, isWindows)]