Skip to content

fix: Shell Type API updated in core #221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 20, 2025
Merged
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
14 changes: 14 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@
"label": "tasks: watch-tests",
"dependsOn": ["npm: watch", "npm: watch-tests"],
"problemMatcher": []
},
{
"type": "npm",
"script": "unittest",
"dependsOn": ["tasks: watch-tests"],
"problemMatcher": "$tsc",
"presentation": {
"reveal": "never",
"group": "test"
},
"group": {
"kind": "test",
"isDefault": false
}
}
]
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
"publisher": "ms-python",
"preview": true,
"engines": {
"vscode": "^1.98.0-20250221"
"vscode": "^1.99.0-20250317"
},
"categories": [
"Other"
],
"enabledApiProposals": [
"terminalShellType"
"terminalShellType",
"terminalShellEnv"
],
"capabilities": {
"untrustedWorkspaces": {
Expand Down
33 changes: 8 additions & 25 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,6 @@ export interface PythonCommandRunConfiguration {
args?: string[];
}

export enum TerminalShellType {
powershell = 'powershell',
powershellCore = 'powershellCore',
commandPrompt = 'commandPrompt',
gitbash = 'gitbash',
bash = 'bash',
zsh = 'zsh',
ksh = 'ksh',
fish = 'fish',
cshell = 'cshell',
tcshell = 'tcshell',
nushell = 'nushell',
wsl = 'wsl',
xonsh = 'xonsh',
unknown = 'unknown',
}

/**
* Contains details on how to use a particular python environment
*
Expand All @@ -73,7 +56,7 @@ export enum TerminalShellType {
* 2. If {@link PythonEnvironmentExecutionInfo.activatedRun} is not provided, then:
* - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used.
* - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then:
* - {@link TerminalShellType.unknown} will be used if provided.
* - 'unknown' will be used if provided.
* - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise.
* - If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then {@link PythonEnvironmentExecutionInfo.activation} will be used.
* - If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used.
Expand All @@ -82,7 +65,7 @@ export enum TerminalShellType {
* 1. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used.
* 2. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then {@link PythonEnvironmentExecutionInfo.activation} will be used.
* 3. If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then:
* - {@link TerminalShellType.unknown} will be used if provided.
* - 'unknown' will be used if provided.
* - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise.
* 4. If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used.
*
Expand All @@ -107,11 +90,11 @@ export interface PythonEnvironmentExecutionInfo {
/**
* Details on how to activate an environment using a shell specific command.
* If set this will override the {@link PythonEnvironmentExecutionInfo.activation}.
* {@link TerminalShellType.unknown} is used if shell type is not known.
* If {@link TerminalShellType.unknown} is not provided and shell type is not known then
* 'unknown' is used if shell type is not known.
* If 'unknown' is not provided and shell type is not known then
* {@link PythonEnvironmentExecutionInfo.activation} if set.
*/
shellActivation?: Map<TerminalShellType, PythonCommandRunConfiguration[]>;
shellActivation?: Map<string, PythonCommandRunConfiguration[]>;

/**
* Details on how to deactivate an environment.
Expand All @@ -121,11 +104,11 @@ export interface PythonEnvironmentExecutionInfo {
/**
* Details on how to deactivate an environment using a shell specific command.
* If set this will override the {@link PythonEnvironmentExecutionInfo.deactivation} property.
* {@link TerminalShellType.unknown} is used if shell type is not known.
* If {@link TerminalShellType.unknown} is not provided and shell type is not known then
* 'unknown' is used if shell type is not known.
* If 'unknown' is not provided and shell type is not known then
* {@link PythonEnvironmentExecutionInfo.deactivation} if set.
*/
shellDeactivation?: Map<TerminalShellType, PythonCommandRunConfiguration[]>;
shellDeactivation?: Map<string, PythonCommandRunConfiguration[]>;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/features/common/activation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Terminal } from 'vscode';
import { PythonCommandRunConfiguration, PythonEnvironment, TerminalShellType } from '../../api';
import { PythonCommandRunConfiguration, PythonEnvironment } from '../../api';
import { identifyTerminalShell } from './shellDetector';

export function isActivatableEnvironment(environment: PythonEnvironment): boolean {
Expand All @@ -20,7 +20,7 @@ export function getActivationCommand(
if (environment.execInfo?.shellActivation) {
activation = environment.execInfo.shellActivation.get(shell);
if (!activation) {
activation = environment.execInfo.shellActivation.get(TerminalShellType.unknown);
activation = environment.execInfo.shellActivation.get('unknown');
}
}

Expand All @@ -41,7 +41,7 @@ export function getDeactivationCommand(
if (environment.execInfo?.shellDeactivation) {
deactivation = environment.execInfo.shellDeactivation.get(shell);
if (!deactivation) {
deactivation = environment.execInfo.shellDeactivation.get(TerminalShellType.unknown);
deactivation = environment.execInfo.shellDeactivation.get('unknown');
}
}

Expand Down
120 changes: 52 additions & 68 deletions src/features/common/shellDetector.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as os from 'os';
import { Terminal, TerminalShellType as TerminalShellTypeVscode } from 'vscode';
import { Terminal } from 'vscode';
import { isWindows } from '../../managers/common/utils';
import { vscodeShell } from '../../common/vscodeEnv.apis';
import { getConfiguration } from '../../common/workspace.apis';
import { TerminalShellType } from '../../api';

/*
When identifying the shell use the following algorithm:
Expand All @@ -22,67 +21,56 @@ const IS_WSL = /(wsl$)/i;
const IS_ZSH = /(zsh$)/i;
const IS_KSH = /(ksh$)/i;
const IS_COMMAND = /(cmd$)/i;
const IS_POWERSHELL = /(powershell$)/i;
const IS_POWERSHELL_CORE = /(pwsh$)/i;
const IS_POWERSHELL = /(powershell$|pwsh$)/i;
const IS_FISH = /(fish$)/i;
const IS_CSHELL = /(csh$)/i;
const IS_TCSHELL = /(tcsh$)/i;
const IS_NUSHELL = /(nu$)/i;
const IS_XONSH = /(xonsh$)/i;

/** Converts an object from a trusted source (i.e. without unknown entries) to a typed array */
function _entries<T extends { [key: string]: object }, K extends keyof T>(o: T): [keyof T, T[K]][] {
return Object.entries(o) as [keyof T, T[K]][];
}

type KnownShellType = Exclude<TerminalShellType, TerminalShellType.unknown>;
const detectableShells = new Map<KnownShellType, RegExp>(
_entries({
[TerminalShellType.powershell]: IS_POWERSHELL,
[TerminalShellType.gitbash]: IS_GITBASH,
[TerminalShellType.bash]: IS_BASH,
[TerminalShellType.wsl]: IS_WSL,
[TerminalShellType.zsh]: IS_ZSH,
[TerminalShellType.ksh]: IS_KSH,
[TerminalShellType.commandPrompt]: IS_COMMAND,
[TerminalShellType.fish]: IS_FISH,
[TerminalShellType.tcshell]: IS_TCSHELL,
[TerminalShellType.cshell]: IS_CSHELL,
[TerminalShellType.nushell]: IS_NUSHELL,
[TerminalShellType.powershellCore]: IS_POWERSHELL_CORE,
[TerminalShellType.xonsh]: IS_XONSH,
// This `satisfies` makes sure all shells are covered
} satisfies Record<KnownShellType, RegExp>),
);

function identifyShellFromShellPath(shellPath: string): TerminalShellType {
const detectableShells = new Map<string, RegExp>([
['pwsh', IS_POWERSHELL],
['gitbash', IS_GITBASH],
['bash', IS_BASH],
['wsl', IS_WSL],
['zsh', IS_ZSH],
['ksh', IS_KSH],
['cmd', IS_COMMAND],
['fish', IS_FISH],
['tcsh', IS_TCSHELL],
['csh', IS_CSHELL],
['nu', IS_NUSHELL],
['xonsh', IS_XONSH],
]);

function identifyShellFromShellPath(shellPath: string): string {
// Remove .exe extension so shells can be more consistently detected
// on Windows (including Cygwin).
const basePath = shellPath.replace(/\.exe$/i, '');

const shell = Array.from(detectableShells.keys()).reduce((matchedShell, shellToDetect) => {
if (matchedShell === TerminalShellType.unknown) {
if (matchedShell === 'unknown') {
const pat = detectableShells.get(shellToDetect);
if (pat && pat.test(basePath)) {
return shellToDetect;
}
}
return matchedShell;
}, TerminalShellType.unknown);
}, 'unknown');

return shell;
}

function identifyShellFromTerminalName(terminal: Terminal): TerminalShellType {
function identifyShellFromTerminalName(terminal: Terminal): string {
if (terminal.name === 'sh') {
// Specifically checking this because other shells have `sh` at the end of their name
// We can match and return bash for this case
return TerminalShellType.bash;
return 'bash';
}
return identifyShellFromShellPath(terminal.name);
}

function identifyPlatformDefaultShell(): TerminalShellType {
function identifyPlatformDefaultShell(): string {
if (isWindows()) {
return identifyShellFromShellPath(getTerminalDefaultShellWindows());
}
Expand All @@ -99,16 +87,16 @@ function getTerminalDefaultShellWindows(): string {
return isAtLeastWindows10 ? powerShellPath : process.env.comspec || 'cmd.exe';
}

function identifyShellFromVSC(terminal: Terminal): TerminalShellType {
function identifyShellFromVSC(terminal: Terminal): string {
const shellPath =
terminal?.creationOptions && 'shellPath' in terminal.creationOptions && terminal.creationOptions.shellPath
? terminal.creationOptions.shellPath
: vscodeShell();

return shellPath ? identifyShellFromShellPath(shellPath) : TerminalShellType.unknown;
return shellPath ? identifyShellFromShellPath(shellPath) : 'unknown';
}

function identifyShellFromSettings(): TerminalShellType {
function identifyShellFromSettings(): string {
const shellConfig = getConfiguration('terminal.integrated.shell');
let shellPath: string | undefined;
switch (process.platform) {
Expand All @@ -130,56 +118,52 @@ function identifyShellFromSettings(): TerminalShellType {
shellPath = undefined;
}
}
return shellPath ? identifyShellFromShellPath(shellPath) : TerminalShellType.unknown;
return shellPath ? identifyShellFromShellPath(shellPath) : 'unknown';
}

function fromShellTypeApi(terminal: Terminal): TerminalShellType {
function fromShellTypeApi(terminal: Terminal): string {
try {
switch (terminal.state.shellType) {
case TerminalShellTypeVscode.Sh:
case TerminalShellTypeVscode.Bash:
return TerminalShellType.bash;
case TerminalShellTypeVscode.Fish:
return TerminalShellType.fish;
case TerminalShellTypeVscode.Csh:
return TerminalShellType.cshell;
case TerminalShellTypeVscode.Ksh:
return TerminalShellType.ksh;
case TerminalShellTypeVscode.Zsh:
return TerminalShellType.zsh;
case TerminalShellTypeVscode.CommandPrompt:
return TerminalShellType.commandPrompt;
case TerminalShellTypeVscode.GitBash:
return TerminalShellType.gitbash;
case TerminalShellTypeVscode.PowerShell:
return TerminalShellType.powershellCore;
case TerminalShellTypeVscode.NuShell:
return TerminalShellType.nushell;
default:
return TerminalShellType.unknown;
const known = [
'bash',
'cmd',
'csh',
'fish',
'gitbash',
'julia',
'ksh',
'node',
'nu',
'pwsh',
'python',
'sh',
'wsl',
'zsh',
];
if (terminal.state.shell && known.includes(terminal.state.shell)) {
return terminal.state.shell;
}
} catch {
// If the API is not available, return unknown
return TerminalShellType.unknown;
}
return 'unknown';
}

export function identifyTerminalShell(terminal: Terminal): TerminalShellType {
export function identifyTerminalShell(terminal: Terminal): string {
let shellType = fromShellTypeApi(terminal);

if (shellType === TerminalShellType.unknown) {
if (shellType === 'unknown') {
shellType = identifyShellFromVSC(terminal);
}

if (shellType === TerminalShellType.unknown) {
if (shellType === 'unknown') {
shellType = identifyShellFromTerminalName(terminal);
}

if (shellType === TerminalShellType.unknown) {
if (shellType === 'unknown') {
shellType = identifyShellFromSettings();
}

if (shellType === TerminalShellType.unknown) {
if (shellType === 'unknown') {
shellType = identifyPlatformDefaultShell();
}

Expand Down
Loading