Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions vscode/extension/src/commands/printEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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(
Expand Down
94 changes: 94 additions & 0 deletions vscode/extension/src/utilities/shellCommand.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
73 changes: 73 additions & 0 deletions vscode/extension/src/utilities/shellCommand.ts
Original file line number Diff line number Diff line change
@@ -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<ShellFamily, string> = {
fish: 'set',
powershell: 'Get-ChildItem Env: | Sort-Object Name',
cmd: 'set',
posix: 'env | sort',
}

const SHELL_FAMILIES_BY_NAME: Record<string, ShellFamily> = {
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)]