Path: blob/main/src/vs/platform/agentHost/common/commandLineHelpers.ts
13394 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { extUriBiasedIgnorePathCase } from '../../../base/common/resources.js';6import { URI } from '../../../base/common/uri.js';78/**9* Result of {@link extractCdPrefix}: the directory the `cd` jumps to and the10* remaining command after the chain operator.11*/12export interface IExtractedCdPrefix {13readonly directory: string;14readonly command: string;15}1617/**18* Extracts a `cd <dir> &&` (or PowerShell equivalent) prefix from a command19* line, returning the directory and remaining command. Does not check whether20* the directory matches anything — callers do that comparison themselves.21*22* Recognized forms:23* - bash: `cd <dir> && <suffix>`24* - powershell: `cd <dir> && <suffix>`, `cd <dir>; <suffix>`25* `cd /d <dir> && <suffix>`, `cd /d <dir>; <suffix>`26* `Set-Location <dir> && <suffix>`, `Set-Location <dir>; <suffix>`27* `Set-Location -Path <dir> && <suffix>`, `Set-Location -Path <dir>; <suffix>`28*29* Surrounding double quotes around `<dir>` are stripped.30*/31export function extractCdPrefix(commandLine: string, isPowerShell: boolean): IExtractedCdPrefix | undefined {32const cdPrefixMatch = commandLine.match(33isPowerShell34? /^(?:cd(?: \/d)?|Set-Location(?: -Path)?) (?<dir>"[^"]*"|[^\s]+) ?(?:&&|;)\s+(?<suffix>.+)$/i35: /^cd (?<dir>"[^"]*"|[^\s]+) &&\s+(?<suffix>.+)$/36);37const cdDir = cdPrefixMatch?.groups?.dir;38const cdSuffix = cdPrefixMatch?.groups?.suffix;39if (cdDir && cdSuffix) {40let cdDirPath = cdDir;41if (cdDirPath.startsWith('"') && cdDirPath.endsWith('"')) {42cdDirPath = cdDirPath.slice(1, -1);43}44return { directory: cdDirPath, command: cdSuffix };45}46return undefined;47}4849/**50* If `toolName` is a shell tool (`bash` or `powershell`) and51* `parameters.command` starts with a `cd <workingDirectory> && …` (or52* PowerShell equivalent) prefix, mutate `parameters.command` to drop the53* prefix and return `true`. Returns `false` otherwise.54*55* Path comparison normalizes trailing slashes and is case-insensitive on56* Windows.57*/58export function stripRedundantCdPrefix(59toolName: string,60parameters: Record<string, unknown> | undefined,61workingDirectory: URI | undefined,62): boolean {63if (!workingDirectory || !parameters) {64return false;65}66const isBash = toolName === 'bash';67const isPowerShell = toolName === 'powershell';68if (!isBash && !isPowerShell) {69return false;70}71const command = parameters.command;72if (typeof command !== 'string') {73return false;74}75const extracted = extractCdPrefix(command, isPowerShell);76if (!extracted) {77return false;78}79if (!sameDirectory(extracted.directory, workingDirectory)) {80return false;81}82parameters.command = extracted.command;83return true;84}8586/**87* Compares an extracted `cd <dir>` argument (a raw filesystem path string,88* possibly using either `/` or `\` separators) to a working-directory URI.89* Normalizes separators by routing the extracted string through `URI.file`,90* which converts to the platform-native `fsPath` shape, so that e.g.91* `cd C:/repo` matches a working directory of `C:\repo` on Windows.92*93* Path comparison uses {@link extUriBiasedIgnorePathCase}, which is94* case-insensitive on Windows / macOS.95*/96function sameDirectory(extractedDir: string, workingDirectory: URI): boolean {97if (!extractedDir) {98return false;99}100// Strip trailing path separators (either flavor) so e.g. `/repo/project/`101// matches `/repo/project`. Without this, URI.file would preserve the102// trailing slash and the URIs would not compare equal. We do this for103// both sides because the working directory may also end in a separator.104const trim = (p: string) => p.replace(/[\\/]+$/, '');105const trimmedExtracted = trim(extractedDir);106const trimmedWd = trim(workingDirectory.fsPath);107if (!trimmedExtracted || !trimmedWd) {108return false;109}110let extractedUri: URI;111let wdUri: URI;112try {113extractedUri = URI.file(trimmedExtracted);114wdUri = URI.file(trimmedWd);115} catch {116return false;117}118return extUriBiasedIgnorePathCase.isEqual(extractedUri, wdUri);119}120121122123