From d089eb2922fa22d250d6d187291f8b9e587e3b96 Mon Sep 17 00:00:00 2001 From: NipunaRanasinghe Date: Tue, 24 Feb 2026 15:25:49 +0530 Subject: [PATCH 1/3] Quote executable paths to handle spaces in directory names across various components --- .../ballerina-extension/src/core/extension.ts | 17 ++++++++++------- .../src/features/ai/agent/tools/test-runner.ts | 3 ++- .../src/features/debugger/config-provider.ts | 8 ++++++-- .../src/features/project/cmds/cmd-runner.ts | 6 ++++-- .../src/features/testing/runner.ts | 3 ++- .../src/utils/migrate-integration.ts | 3 ++- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/workspaces/ballerina/ballerina-extension/src/core/extension.ts b/workspaces/ballerina/ballerina-extension/src/core/extension.ts index 7565c7ee696..34d5d1cc07b 100644 --- a/workspaces/ballerina/ballerina-extension/src/core/extension.ts +++ b/workspaces/ballerina/ballerina-extension/src/core/extension.ts @@ -1819,7 +1819,8 @@ export class BallerinaExtension { debug("[VERSION] Non-Windows platform detected, no extension needed"); } - let ballerinaCommand = distPath + 'bal' + exeExtension + ' version'; + // Build the executable path separately so we can quote it for shell execution + let balExecutablePath = distPath + 'bal' + exeExtension; // Handle WSL environment - prefer native Linux installation over Windows .bat files if (isWSL()) { @@ -1829,26 +1830,28 @@ export class BallerinaExtension { // Check if 'bal' command is available in PATH execSync('which bal', { encoding: 'utf8', timeout: 5000 }); // If we get here, 'bal' is available, use it instead of .bat - ballerinaCommand = 'bal version'; + balExecutablePath = 'bal'; debug("[VERSION] WSL detected native 'bal' command, using it instead of .bat file"); } catch (error) { debug("[VERSION] No native 'bal' command found in WSL, will try .bat file"); // If the path contains Windows-style paths, we need to handle them properly - if (ballerinaCommand.includes('\\') || ballerinaCommand.match(/^[A-Za-z]:/)) { + if (balExecutablePath.includes('\\') || balExecutablePath.match(/^[A-Za-z]:/)) { debug("[VERSION] WSL detected with Windows path, attempting to convert to WSL path"); // Try to convert Windows path to WSL path - const wslPath = ballerinaCommand.replace(/^([A-Za-z]):/, '/mnt/$1').replace(/\\/g, '/').toLowerCase(); - debug(`[VERSION] Converted Windows path to WSL path: ${wslPath}`); - ballerinaCommand = wslPath; + balExecutablePath = balExecutablePath.replace(/^([A-Za-z]):/, '/mnt/$1').replace(/\\/g, '/').toLowerCase(); + debug(`[VERSION] Converted Windows path to WSL path: ${balExecutablePath}`); } } } else { // We have a native Linux installation, use it directly - ballerinaCommand = 'bal version'; + balExecutablePath = 'bal'; debug("[VERSION] WSL detected with native Linux installation, using 'bal version'"); } } + // Quote the executable path to handle spaces in directory names + let ballerinaCommand = balExecutablePath.includes(' ') ? `"${balExecutablePath}" version` : `${balExecutablePath} version`; + debug(`[VERSION] Executing command: '${ballerinaCommand}'`); let ballerinaExecutor = ''; diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts index b92930a7272..1601ceb34b0 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts @@ -99,7 +99,8 @@ function parseTestSummary(output: string): string { async function runBallerinaTests(cwd: string): Promise { return new Promise((resolve) => { const balCmd = extension.ballerinaExtInstance.getBallerinaCmd(); - const command = `${balCmd} test`; + const quotedCmd = balCmd.includes(' ') ? `"${balCmd}"` : balCmd; + const command = `${quotedCmd} test`; console.log(`[TestRunner] Running: ${command} in ${cwd}`); diff --git a/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts b/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts index fbe1b8472e6..9232f4ce2c6 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts @@ -603,7 +603,9 @@ class BallerinaDebugAdapterDescriptorFactory implements DebugAdapterDescriptorFa } getScriptPath(args: string[]): string { args.push('start-debugger-adapter'); - return extension.ballerinaExtInstance.getBallerinaCmd(); + const cmd = extension.ballerinaExtInstance.getBallerinaCmd(); + // Quote the path to handle spaces in directory names (used with shell: true) + return cmd.includes(' ') ? `"${cmd}"` : cmd; } getCurrentWorkingDir(): string { return path.join(extension.ballerinaExtInstance.ballerinaHome, "bin"); @@ -662,7 +664,9 @@ class BIRunAdapter extends LoggingDebugSession { task: 'run' }; - let runCommand: string = `${extension.ballerinaExtInstance.getBallerinaCmd()} run`; + const balCmd = extension.ballerinaExtInstance.getBallerinaCmd(); + const quotedBalCmd = balCmd.includes(' ') ? `"${balCmd}"` : balCmd; + let runCommand: string = `${quotedBalCmd} run`; const programArgs = (args as any).programArgs; if (programArgs && programArgs.length > 0) { diff --git a/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts b/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts index ec8018823d2..954df114dc7 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts @@ -127,8 +127,10 @@ export function runCommandWithConf(file: BallerinaProject | string, executor: st }); } let commandText; + // Quote executor path to handle spaces in directory names + const quotedExecutor = /\s/g.test(executor) ? `"${executor}"` : executor; if (cmd === BALLERINA_COMMANDS.OTHER) { - commandText = `${executor} ${argsList}`; + commandText = `${quotedExecutor} ${argsList}`; terminal = window.createTerminal({ name: TERMINAL_NAME }); } else { let env = {}; @@ -164,7 +166,7 @@ export function runCommandWithConf(file: BallerinaProject | string, executor: st } } - commandText = `${executor} ${cmd} ${argsList}`; + commandText = `${quotedExecutor} ${cmd} ${argsList}`; if (confPath !== '') { const configs = env['BAL_CONFIG_FILES'] ? `${env['BAL_CONFIG_FILES']}:${confPath}` : confPath; Object.assign(env, { BAL_CONFIG_FILES: configs }); diff --git a/workspaces/ballerina/ballerina-extension/src/features/testing/runner.ts b/workspaces/ballerina/ballerina-extension/src/features/testing/runner.ts index b136c9560ae..962a0413644 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/testing/runner.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/testing/runner.ts @@ -72,7 +72,8 @@ export function runHandler(request: TestRunRequest, cancellation: CancellationTo try { // execute test const executor = extension.ballerinaExtInstance.getBallerinaCmd(); - const commandText = `${executor} ${BALLERINA_COMMANDS.TEST} ${EXEC_ARG.TESTS} ${testNames} ${EXEC_ARG.COVERAGE}`; + const quotedExecutor = executor.includes(' ') ? `"${executor}"` : executor; + const commandText = `${quotedExecutor} ${BALLERINA_COMMANDS.TEST} ${EXEC_ARG.TESTS} ${testNames} ${EXEC_ARG.COVERAGE}`; await runCommand(commandText, projectRoot); } catch { diff --git a/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts b/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts index 02da6e72759..964fba9ff37 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts @@ -53,7 +53,8 @@ export async function pullMigrationTool(migrationToolName: string, version: stri } const ballerinaCmd = extension.ballerinaExtInstance.getBallerinaCmd(); - const command = `${ballerinaCmd} tool pull ${migrationToolName}:${version}`; + const quotedCmd = ballerinaCmd.includes(' ') ? `"${ballerinaCmd}"` : ballerinaCmd; + const command = `${quotedCmd} tool pull ${migrationToolName}:${version}`; debug(`Executing migration tool pull command: ${command}`); // 2. This function now returns a promise that wraps the exec lifecycle From a3477d85826f6a49acafba4434440b4cb893165d Mon Sep 17 00:00:00 2001 From: NipunaRanasinghe Date: Tue, 24 Feb 2026 19:54:56 +0530 Subject: [PATCH 2/3] Refactor to use quoteShellPath utility for quoting executable paths --- .../ballerina/ballerina-extension/src/core/extension.ts | 3 ++- .../src/features/ai/agent/tools/test-runner.ts | 4 ++-- .../src/features/debugger/config-provider.ts | 8 +++----- .../src/features/project/cmds/cmd-runner.ts | 8 +++----- .../ballerina-extension/src/features/testing/runner.ts | 4 ++-- .../ballerina/ballerina-extension/src/utils/config.ts | 8 ++++++++ .../ballerina-extension/src/utils/migrate-integration.ts | 4 ++-- 7 files changed, 22 insertions(+), 17 deletions(-) diff --git a/workspaces/ballerina/ballerina-extension/src/core/extension.ts b/workspaces/ballerina/ballerina-extension/src/core/extension.ts index 34d5d1cc07b..385a28af002 100644 --- a/workspaces/ballerina/ballerina-extension/src/core/extension.ts +++ b/workspaces/ballerina/ballerina-extension/src/core/extension.ts @@ -40,6 +40,7 @@ import { outputChannel, isWindows, isWSL, + quoteShellPath, isSupportedVersion, VERSION, isSupportedSLVersion, @@ -1850,7 +1851,7 @@ export class BallerinaExtension { } // Quote the executable path to handle spaces in directory names - let ballerinaCommand = balExecutablePath.includes(' ') ? `"${balExecutablePath}" version` : `${balExecutablePath} version`; + let ballerinaCommand = `${quoteShellPath(balExecutablePath)} version`; debug(`[VERSION] Executing command: '${ballerinaCommand}'`); diff --git a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts index 1601ceb34b0..4c9774c9834 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/ai/agent/tools/test-runner.ts @@ -19,6 +19,7 @@ import { z } from 'zod'; import child_process from 'child_process'; import { CopilotEventHandler } from '../../utils/events'; import { extension } from '../../../../BalExtensionContext'; +import { quoteShellPath } from '../../../../utils/config'; import { DIAGNOSTICS_TOOL_NAME } from './diagnostics'; export const TEST_RUNNER_TOOL_NAME = "runTests"; @@ -99,8 +100,7 @@ function parseTestSummary(output: string): string { async function runBallerinaTests(cwd: string): Promise { return new Promise((resolve) => { const balCmd = extension.ballerinaExtInstance.getBallerinaCmd(); - const quotedCmd = balCmd.includes(' ') ? `"${balCmd}"` : balCmd; - const command = `${quotedCmd} test`; + const command = `${quoteShellPath(balCmd)} test`; console.log(`[TestRunner] Running: ${command} in ${cwd}`); diff --git a/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts b/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts index 9232f4ce2c6..03941c2a0d3 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/debugger/config-provider.ts @@ -48,6 +48,7 @@ import { createVersionNumber } from "../../utils"; import { getProjectWorkingDirectory } from "../../utils/file-utils"; +import { quoteShellPath } from "../../utils/config"; import { decimal, ExecutableOptions } from 'vscode-languageclient/node'; import { BAL_NOTEBOOK, getTempFile, NOTEBOOK_CELL_SCHEME } from '../../views/notebook'; import fileUriToPath from 'file-uri-to-path'; @@ -603,9 +604,8 @@ class BallerinaDebugAdapterDescriptorFactory implements DebugAdapterDescriptorFa } getScriptPath(args: string[]): string { args.push('start-debugger-adapter'); - const cmd = extension.ballerinaExtInstance.getBallerinaCmd(); // Quote the path to handle spaces in directory names (used with shell: true) - return cmd.includes(' ') ? `"${cmd}"` : cmd; + return quoteShellPath(extension.ballerinaExtInstance.getBallerinaCmd()); } getCurrentWorkingDir(): string { return path.join(extension.ballerinaExtInstance.ballerinaHome, "bin"); @@ -664,9 +664,7 @@ class BIRunAdapter extends LoggingDebugSession { task: 'run' }; - const balCmd = extension.ballerinaExtInstance.getBallerinaCmd(); - const quotedBalCmd = balCmd.includes(' ') ? `"${balCmd}"` : balCmd; - let runCommand: string = `${quotedBalCmd} run`; + let runCommand: string = `${quoteShellPath(extension.ballerinaExtInstance.getBallerinaCmd())} run`; const programArgs = (args as any).programArgs; if (programArgs && programArgs.length > 0) { diff --git a/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts b/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts index 954df114dc7..52383dae9fe 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/project/cmds/cmd-runner.ts @@ -18,7 +18,7 @@ import { BallerinaProject } from "@wso2/ballerina-core"; import { Terminal, window, workspace } from "vscode"; -import { isSupportedSLVersion, isWindows, createVersionNumber } from "../../../utils"; +import { isSupportedSLVersion, isWindows, createVersionNumber, quoteShellPath } from "../../../utils"; import { extension } from "../../../BalExtensionContext"; import { TracerMachine } from "../../../features/tracing"; @@ -127,10 +127,8 @@ export function runCommandWithConf(file: BallerinaProject | string, executor: st }); } let commandText; - // Quote executor path to handle spaces in directory names - const quotedExecutor = /\s/g.test(executor) ? `"${executor}"` : executor; if (cmd === BALLERINA_COMMANDS.OTHER) { - commandText = `${quotedExecutor} ${argsList}`; + commandText = `${quoteShellPath(executor)} ${argsList}`; terminal = window.createTerminal({ name: TERMINAL_NAME }); } else { let env = {}; @@ -166,7 +164,7 @@ export function runCommandWithConf(file: BallerinaProject | string, executor: st } } - commandText = `${quotedExecutor} ${cmd} ${argsList}`; + commandText = `${quoteShellPath(executor)} ${cmd} ${argsList}`; if (confPath !== '') { const configs = env['BAL_CONFIG_FILES'] ? `${env['BAL_CONFIG_FILES']}:${confPath}` : confPath; Object.assign(env, { BAL_CONFIG_FILES: configs }); diff --git a/workspaces/ballerina/ballerina-extension/src/features/testing/runner.ts b/workspaces/ballerina/ballerina-extension/src/features/testing/runner.ts index 962a0413644..877f05992d2 100644 --- a/workspaces/ballerina/ballerina-extension/src/features/testing/runner.ts +++ b/workspaces/ballerina/ballerina-extension/src/features/testing/runner.ts @@ -34,6 +34,7 @@ import { BALLERINA_COMMANDS } from "../project"; import { discoverTests, gatherTestItems } from "./discover"; import { testController, projectRoot } from "./activator"; import { extension } from "../../BalExtensionContext"; +import { quoteShellPath } from "../../utils/config"; enum EXEC_ARG { TESTS = '--tests', @@ -72,8 +73,7 @@ export function runHandler(request: TestRunRequest, cancellation: CancellationTo try { // execute test const executor = extension.ballerinaExtInstance.getBallerinaCmd(); - const quotedExecutor = executor.includes(' ') ? `"${executor}"` : executor; - const commandText = `${quotedExecutor} ${BALLERINA_COMMANDS.TEST} ${EXEC_ARG.TESTS} ${testNames} ${EXEC_ARG.COVERAGE}`; + const commandText = `${quoteShellPath(executor)} ${BALLERINA_COMMANDS.TEST} ${EXEC_ARG.TESTS} ${testNames} ${EXEC_ARG.COVERAGE}`; await runCommand(commandText, projectRoot); } catch { diff --git a/workspaces/ballerina/ballerina-extension/src/utils/config.ts b/workspaces/ballerina/ballerina-extension/src/utils/config.ts index de73c71053e..6ea5ca9eb18 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/config.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/config.ts @@ -73,6 +73,14 @@ export function isWSL(): boolean { ); } +/** + * Wraps a file path in double quotes if it contains spaces, + * so it can be safely used in shell command strings. + */ +export function quoteShellPath(filePath: string): string { + return filePath.includes(' ') ? `"${filePath}"` : filePath; +} + export function isSupportedVersion(ballerinaExtInstance: BallerinaExtension, supportedRelease: VERSION, supportedVersion: number): boolean { const ballerinaVersion: string = ballerinaExtInstance.ballerinaVersion.toLocaleLowerCase(); diff --git a/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts b/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts index 964fba9ff37..317fcb0f163 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/migrate-integration.ts @@ -22,6 +22,7 @@ import { extension } from "../BalExtensionContext"; import { RPCLayer } from "../RPCLayer"; import { VisualizerWebview } from "../views/visualizer/webview"; import { debug } from "./logger"; +import { quoteShellPath } from "./config"; const PROGRESS_COMPLETE = 100; @@ -53,8 +54,7 @@ export async function pullMigrationTool(migrationToolName: string, version: stri } const ballerinaCmd = extension.ballerinaExtInstance.getBallerinaCmd(); - const quotedCmd = ballerinaCmd.includes(' ') ? `"${ballerinaCmd}"` : ballerinaCmd; - const command = `${quotedCmd} tool pull ${migrationToolName}:${version}`; + const command = `${quoteShellPath(ballerinaCmd)} tool pull ${migrationToolName}:${version}`; debug(`Executing migration tool pull command: ${command}`); // 2. This function now returns a promise that wraps the exec lifecycle From 11d65606916fb63258bf1d4c6f446d3373965a3d Mon Sep 17 00:00:00 2001 From: NipunaRanasinghe Date: Tue, 24 Feb 2026 23:29:11 +0530 Subject: [PATCH 3/3] Address review suggestions --- .../ballerina-extension/src/utils/config.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/workspaces/ballerina/ballerina-extension/src/utils/config.ts b/workspaces/ballerina/ballerina-extension/src/utils/config.ts index 6ea5ca9eb18..c802db2727d 100644 --- a/workspaces/ballerina/ballerina-extension/src/utils/config.ts +++ b/workspaces/ballerina/ballerina-extension/src/utils/config.ts @@ -76,9 +76,22 @@ export function isWSL(): boolean { /** * Wraps a file path in double quotes if it contains spaces, * so it can be safely used in shell command strings. + * Handles already-quoted paths and escapes embedded double quotes. */ export function quoteShellPath(filePath: string): string { - return filePath.includes(' ') ? `"${filePath}"` : filePath; + // Strip existing surrounding quotes to normalize + let normalized = filePath; + if (normalized.length >= 2 && normalized.startsWith('"') && normalized.endsWith('"')) { + normalized = normalized.slice(1, -1); + } + + if (!normalized.includes(' ')) { + return normalized; + } + + // Escape any embedded double quotes + const escaped = normalized.replace(/"/g, '\\"'); + return `"${escaped}"`; } export function isSupportedVersion(ballerinaExtInstance: BallerinaExtension, supportedRelease: VERSION,