Skip to content
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

Add unit tests for preparePathForShell #170250

Merged
merged 3 commits into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
80 changes: 3 additions & 77 deletions src/vs/workbench/contrib/terminal/browser/terminalInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { Schemas } from 'vs/base/common/network';
import * as path from 'vs/base/common/path';
import { isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/platform';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { isString, withNullAsUndefined } from 'vs/base/common/types';
import { withNullAsUndefined } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { TabFocus } from 'vs/editor/browser/config/tabFocus';
import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState';
Expand All @@ -48,7 +48,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IMarkProperties, ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { TerminalCapabilityStoreMultiplexer } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore';
import { IProcessDataEvent, IProcessPropertyMap, IReconnectionProperties, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, PosixShellType, ProcessPropertyType, ShellIntegrationStatus, TerminalExitReason, TerminalIcon, TerminalLocation, TerminalSettingId, TerminalShellType, TitleEventSource, WindowsShellType } from 'vs/platform/terminal/common/terminal';
import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment';
import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings';
import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
Expand Down Expand Up @@ -79,7 +78,7 @@ import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xterm
import { IEnvironmentVariableCollection, IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { deserializeEnvironmentVariableCollections } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
import { getCommandHistory, getDirectoryHistory } from 'vs/workbench/contrib/terminal/common/history';
import { DEFAULT_COMMANDS_TO_SKIP_SHELL, INavigationMode, ITerminalBackend, ITerminalProcessManager, ITerminalProfileResolverService, ProcessState, TerminalCommandId, TERMINAL_CREATION_COMMANDS, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal';
import { DEFAULT_COMMANDS_TO_SKIP_SHELL, INavigationMode, ITerminalProcessManager, ITerminalProfileResolverService, ProcessState, TerminalCommandId, TERMINAL_CREATION_COMMANDS, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
Expand All @@ -91,6 +90,7 @@ import type { IMarker, ITerminalAddon, Terminal as XTermTerminal } from 'xterm';
import { IAudioCueService, AudioCue } from 'vs/platform/audioCues/browser/audioCueService';
import { ITerminalQuickFixOptions } from 'vs/platform/terminal/common/xterm/terminalQuickFix';
import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/common/files';
import { preparePathForShell } from 'vs/workbench/contrib/terminal/common/terminalEnvironment';

const enum Constants {
/**
Expand Down Expand Up @@ -2621,77 +2621,3 @@ export function parseExitResult(

return { code, message };
}

/**
* Takes a path and returns the properly escaped path to send to a given shell. On Windows, this
* included trying to prepare the path for WSL if needed.
*
* @param originalPath The path to be escaped and formatted.
* @param executable The executable off the shellLaunchConfig.
* @param title The terminal's title.
* @param shellType The type of shell the path is being sent to.
* @param backend The backend for the terminal.
* @returns An escaped version of the path to be execuded in the terminal.
*/
async function preparePathForShell(resource: string | URI, executable: string | undefined, title: string, shellType: TerminalShellType, backend: ITerminalBackend | undefined, os: OperatingSystem | undefined): Promise<string> {
let originalPath: string;
if (isString(resource)) {
originalPath = resource;
} else {
originalPath = resource.fsPath;
// Apply backend OS-specific formatting to the path since URI.fsPath uses the frontend's OS
if (isWindows && os !== OperatingSystem.Windows) {
originalPath = originalPath.replace(/\\/g, '\/');
} else if (!isWindows && os === OperatingSystem.Windows) {
originalPath = originalPath.replace(/\//g, '\\');
}
}

if (!executable) {
return originalPath;
}

const hasSpace = originalPath.includes(' ');
const hasParens = originalPath.includes('(') || originalPath.includes(')');

const pathBasename = path.basename(executable, '.exe');
const isPowerShell = pathBasename === 'pwsh' ||
title === 'pwsh' ||
pathBasename === 'powershell' ||
title === 'powershell';


if (isPowerShell && (hasSpace || originalPath.includes('\''))) {
return `& '${originalPath.replace(/'/g, '\'\'')}'`;
}

if (hasParens && isPowerShell) {
return `& '${originalPath}'`;
}

if (os === OperatingSystem.Windows) {
// 17063 is the build number where wsl path was introduced.
// Update Windows uriPath to be executed in WSL.
if (shellType !== undefined) {
if (shellType === WindowsShellType.GitBash) {
return originalPath.replace(/\\/g, '/');
}
else if (shellType === WindowsShellType.Wsl) {
return backend?.getWslPath(originalPath, 'win-to-unix') || originalPath;
}
else if (hasSpace) {
return `"${originalPath}"`;
}
return originalPath;
}
const lowerExecutable = executable.toLowerCase();
if (lowerExecutable.includes('wsl') || (lowerExecutable.includes('bash.exe') && !lowerExecutable.toLowerCase().includes('git'))) {
return backend?.getWslPath(originalPath, 'win-to-unix') || originalPath;
} else if (hasSpace) {
return `"${originalPath}"`;
}
return originalPath;
}

return escapeNonWindowsPath(originalPath);
}
88 changes: 83 additions & 5 deletions src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@
*/

import * as path from 'vs/base/common/path';
import { URI as Uri } from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { sanitizeProcessEnvironment } from 'vs/base/common/processes';
import { ILogService } from 'vs/platform/log/common/log';
import { IShellLaunchConfig, ITerminalEnvironment, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal';
import { IProcessEnvironment, isWindows, locale, platform, Platform } from 'vs/base/common/platform';
import { sanitizeCwd } from 'vs/platform/terminal/common/terminalEnvironment';
import { IShellLaunchConfig, ITerminalEnvironment, TerminalSettingId, TerminalSettingPrefix, TerminalShellType, WindowsShellType } from 'vs/platform/terminal/common/terminal';
import { IProcessEnvironment, isWindows, locale, OperatingSystem, platform, Platform } from 'vs/base/common/platform';
import { escapeNonWindowsPath, sanitizeCwd } from 'vs/platform/terminal/common/terminalEnvironment';
import { isString } from 'vs/base/common/types';
import { ITerminalBackend } from 'vs/workbench/contrib/terminal/common/terminal';

export function mergeEnvironments(parent: IProcessEnvironment, other: ITerminalEnvironment | undefined): void {
if (!other) {
Expand Down Expand Up @@ -184,7 +186,7 @@ export async function getCwd(
shell: IShellLaunchConfig,
userHome: string | undefined,
variableResolver: VariableResolver | undefined,
root: Uri | undefined,
root: URI | undefined,
customCwd: string | undefined,
logService?: ILogService
): Promise<string> {
Expand Down Expand Up @@ -392,3 +394,79 @@ export async function createTerminalEnvironment(
}
return env;
}

/**
* Takes a path and returns the properly escaped path to send to a given shell. On Windows, this
* included trying to prepare the path for WSL if needed.
*
* @param originalPath The path to be escaped and formatted.
* @param executable The executable off the shellLaunchConfig.
* @param title The terminal's title.
* @param shellType The type of shell the path is being sent to.
* @param backend The backend for the terminal.
* @param isWindowsFrontend Whether the frontend is Windows, this is only exposed for injection via
* tests.
* @returns An escaped version of the path to be execuded in the terminal.
*/
export async function preparePathForShell(resource: string | URI, executable: string | undefined, title: string, shellType: TerminalShellType, backend: Pick<ITerminalBackend, 'getWslPath'> | undefined, os: OperatingSystem | undefined, isWindowsFrontend: boolean = isWindows): Promise<string> {
let originalPath: string;
if (isString(resource)) {
originalPath = resource;
} else {
originalPath = resource.fsPath;
// Apply backend OS-specific formatting to the path since URI.fsPath uses the frontend's OS
if (isWindowsFrontend && os !== OperatingSystem.Windows) {
originalPath = originalPath.replace(/\\/g, '\/');
} else if (!isWindowsFrontend && os === OperatingSystem.Windows) {
originalPath = originalPath.replace(/\//g, '\\');
}
}

if (!executable) {
return originalPath;
}

const hasSpace = originalPath.includes(' ');
const hasParens = originalPath.includes('(') || originalPath.includes(')');

const pathBasename = path.basename(executable, '.exe');
const isPowerShell = pathBasename === 'pwsh' ||
title === 'pwsh' ||
pathBasename === 'powershell' ||
title === 'powershell';


if (isPowerShell && (hasSpace || originalPath.includes('\''))) {
return `& '${originalPath.replace(/'/g, '\'\'')}'`;
}

if (hasParens && isPowerShell) {
return `& '${originalPath}'`;
}

if (os === OperatingSystem.Windows) {
// 17063 is the build number where wsl path was introduced.
// Update Windows uriPath to be executed in WSL.
if (shellType !== undefined) {
if (shellType === WindowsShellType.GitBash) {
return escapeNonWindowsPath(originalPath.replace(/\\/g, '/'));
}
else if (shellType === WindowsShellType.Wsl) {
return backend?.getWslPath(originalPath, 'win-to-unix') || originalPath;
}
else if (hasSpace) {
return `"${originalPath}"`;
}
return originalPath;
}
const lowerExecutable = executable.toLowerCase();
if (lowerExecutable.includes('wsl') || (lowerExecutable.includes('bash.exe') && !lowerExecutable.toLowerCase().includes('git'))) {
return backend?.getWslPath(originalPath, 'win-to-unix') || originalPath;
} else if (hasSpace) {
return `"${originalPath}"`;
}
return originalPath;
}

return escapeNonWindowsPath(originalPath);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@

import { deepStrictEqual, strictEqual } from 'assert';
import { IStringDictionary } from 'vs/base/common/collections';
import { isWindows, Platform } from 'vs/base/common/platform';
import { isWindows, OperatingSystem, Platform } from 'vs/base/common/platform';
import { URI as Uri } from 'vs/base/common/uri';
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
import { addTerminalEnvironmentKeys, getCwd, getDefaultShell, getLangEnvVariable, mergeEnvironments, shouldSetLangEnvVariable } from 'vs/workbench/contrib/terminal/common/terminalEnvironment';
import { addTerminalEnvironmentKeys, getCwd, getDefaultShell, getLangEnvVariable, mergeEnvironments, preparePathForShell, shouldSetLangEnvVariable } from 'vs/workbench/contrib/terminal/common/terminalEnvironment';
import { PosixShellType, WindowsShellType } from 'vs/platform/terminal/common/terminal';

suite('Workbench - TerminalEnvironment', () => {
suite('addTerminalEnvironmentKeys', () => {
Expand Down Expand Up @@ -248,4 +249,76 @@ suite('Workbench - TerminalEnvironment', () => {
strictEqual(shell3, 'automationShell', 'automationShell was true and specified in settings');
});
});
suite('preparePathForShell', () => {
const wslPathBackend = {
getWslPath: async (original: string, direction: 'unix-to-win' | 'win-to-unix') => {
if (direction === 'unix-to-win') {
const match = original.match(/^\/mnt\/(?<drive>[a-zA-Z])\/(?<path>.+)$/);
const groups = match?.groups;
if (!groups) {
return original;
}
return `${groups.drive}:\\${groups.path.replace(/\//g, '\\')}`;
}
const match = original.match(/(?<drive>[a-zA-Z]):\\(?<path>.+)/);
const groups = match?.groups;
if (!groups) {
return original;
}
return `/mnt/${groups.drive.toLowerCase()}/${groups.path.replace(/\\/g, '/')}`;
}
};
suite('Windows frontend, Windows backend', () => {
test('Command Prompt', async () => {
strictEqual(await preparePathForShell('c:\\foo\\bar', 'cmd', 'cmd', WindowsShellType.CommandPrompt, wslPathBackend, OperatingSystem.Windows, true), `c:\\foo\\bar`);
strictEqual(await preparePathForShell('c:\\foo\\bar\'baz', 'cmd', 'cmd', WindowsShellType.CommandPrompt, wslPathBackend, OperatingSystem.Windows, true), `c:\\foo\\bar'baz`);
strictEqual(await preparePathForShell('c:\\foo\\bar$(echo evil)baz', 'cmd', 'cmd', WindowsShellType.CommandPrompt, wslPathBackend, OperatingSystem.Windows, true), `"c:\\foo\\bar$(echo evil)baz"`);
});
test('PowerShell', async () => {
strictEqual(await preparePathForShell('c:\\foo\\bar', 'pwsh', 'pwsh', WindowsShellType.PowerShell, wslPathBackend, OperatingSystem.Windows, true), `c:\\foo\\bar`);
strictEqual(await preparePathForShell('c:\\foo\\bar\'baz', 'pwsh', 'pwsh', WindowsShellType.PowerShell, wslPathBackend, OperatingSystem.Windows, true), `& 'c:\\foo\\bar''baz'`);
strictEqual(await preparePathForShell('c:\\foo\\bar$(echo evil)baz', 'pwsh', 'pwsh', WindowsShellType.PowerShell, wslPathBackend, OperatingSystem.Windows, true), `& 'c:\\foo\\bar$(echo evil)baz'`);
});
test('Git Bash', async () => {
strictEqual(await preparePathForShell('c:\\foo\\bar', 'bash', 'bash', WindowsShellType.GitBash, wslPathBackend, OperatingSystem.Windows, true), `'c:/foo/bar'`);
strictEqual(await preparePathForShell('c:\\foo\\bar$(echo evil)baz', 'bash', 'bash', WindowsShellType.GitBash, wslPathBackend, OperatingSystem.Windows, true), `'c:/foo/bar(echo evil)baz'`);
});
test('WSL', async () => {
strictEqual(await preparePathForShell('c:\\foo\\bar', 'bash', 'bash', WindowsShellType.Wsl, wslPathBackend, OperatingSystem.Windows, true), '/mnt/c/foo/bar');
});
});
suite('Windows frontend, Linux backend', () => {
test('Bash', async () => {
strictEqual(await preparePathForShell('/foo/bar', 'bash', 'bash', PosixShellType.Bash, wslPathBackend, OperatingSystem.Linux, true), `'/foo/bar'`);
strictEqual(await preparePathForShell('/foo/bar\'baz', 'bash', 'bash', PosixShellType.Bash, wslPathBackend, OperatingSystem.Linux, true), `'/foo/barbaz'`);
strictEqual(await preparePathForShell('/foo/bar$(echo evil)baz', 'bash', 'bash', PosixShellType.Bash, wslPathBackend, OperatingSystem.Linux, true), `'/foo/bar(echo evil)baz'`);
});
});
suite('Linux frontend, Windows backend', () => {
test('Command Prompt', async () => {
strictEqual(await preparePathForShell('c:\\foo\\bar', 'cmd', 'cmd', WindowsShellType.CommandPrompt, wslPathBackend, OperatingSystem.Windows, false), `c:\\foo\\bar`);
strictEqual(await preparePathForShell('c:\\foo\\bar\'baz', 'cmd', 'cmd', WindowsShellType.CommandPrompt, wslPathBackend, OperatingSystem.Windows, false), `c:\\foo\\bar'baz`);
strictEqual(await preparePathForShell('c:\\foo\\bar$(echo evil)baz', 'cmd', 'cmd', WindowsShellType.CommandPrompt, wslPathBackend, OperatingSystem.Windows, false), `"c:\\foo\\bar$(echo evil)baz"`);
});
test('PowerShell', async () => {
strictEqual(await preparePathForShell('c:\\foo\\bar', 'pwsh', 'pwsh', WindowsShellType.PowerShell, wslPathBackend, OperatingSystem.Windows, false), `c:\\foo\\bar`);
strictEqual(await preparePathForShell('c:\\foo\\bar\'baz', 'pwsh', 'pwsh', WindowsShellType.PowerShell, wslPathBackend, OperatingSystem.Windows, false), `& 'c:\\foo\\bar''baz'`);
strictEqual(await preparePathForShell('c:\\foo\\bar$(echo evil)baz', 'pwsh', 'pwsh', WindowsShellType.PowerShell, wslPathBackend, OperatingSystem.Windows, false), `& 'c:\\foo\\bar$(echo evil)baz'`);
});
test('Git Bash', async () => {
strictEqual(await preparePathForShell('c:\\foo\\bar', 'bash', 'bash', WindowsShellType.GitBash, wslPathBackend, OperatingSystem.Windows, false), `'c:/foo/bar'`);
strictEqual(await preparePathForShell('c:\\foo\\bar$(echo evil)baz', 'bash', 'bash', WindowsShellType.GitBash, wslPathBackend, OperatingSystem.Windows, false), `'c:/foo/bar(echo evil)baz'`);
});
test('WSL', async () => {
strictEqual(await preparePathForShell('c:\\foo\\bar', 'bash', 'bash', WindowsShellType.Wsl, wslPathBackend, OperatingSystem.Windows, false), '/mnt/c/foo/bar');
});
});
suite('Linux frontend, Linux backend', () => {
test('Bash', async () => {
strictEqual(await preparePathForShell('/foo/bar', 'bash', 'bash', PosixShellType.Bash, wslPathBackend, OperatingSystem.Linux, false), `'/foo/bar'`);
strictEqual(await preparePathForShell('/foo/bar\'baz', 'bash', 'bash', PosixShellType.Bash, wslPathBackend, OperatingSystem.Linux, false), `'/foo/barbaz'`);
strictEqual(await preparePathForShell('/foo/bar$(echo evil)baz', 'bash', 'bash', PosixShellType.Bash, wslPathBackend, OperatingSystem.Linux, false), `'/foo/bar(echo evil)baz'`);
});
});
});
});