From 9e96e7dcccbc23d1b102d6527311d976e2b8ee8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:07:06 +0000 Subject: [PATCH 1/3] Initial plan From 979c0be46e690df6c9bc137735f883750d4fa8d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:11:19 +0000 Subject: [PATCH 2/3] Add leading space to bash/zsh activation commands to prevent history bloating Co-authored-by: anthonykim1 <62267334+anthonykim1@users.noreply.github.com> --- .../terminal/shells/common/shellUtils.ts | 29 ++++- .../shells/common/shellUtils.unit.test.ts | 108 ++++++++++++++++++ 2 files changed, 133 insertions(+), 4 deletions(-) diff --git a/src/features/terminal/shells/common/shellUtils.ts b/src/features/terminal/shells/common/shellUtils.ts index e662b337..08bb54ea 100644 --- a/src/features/terminal/shells/common/shellUtils.ts +++ b/src/features/terminal/shells/common/shellUtils.ts @@ -4,6 +4,14 @@ import { getConfiguration } from '../../../../common/workspace.apis'; import { ShellConstants } from '../../../common/shellConstants'; import { quoteArgs } from '../../../execution/execUtils'; +/** + * Shells that support a leading space to prevent command from being saved in history. + * - Bash: When HISTCONTROL contains 'ignorespace' or 'ignoreboth' + * - Zsh: When setopt HIST_IGNORE_SPACE is enabled + * - Git Bash: Uses bash under the hood, same behavior as Bash + */ +export const shellsWithLeadingSpaceHistorySupport = [ShellConstants.BASH, ShellConstants.ZSH, ShellConstants.GITBASH]; + function getCommandAsString(command: PythonCommandRunConfiguration[], shell: string, delimiter: string): string { const parts = []; for (const cmd of command) { @@ -20,13 +28,17 @@ function getCommandAsString(command: PythonCommandRunConfiguration[], shell: str } export function getShellCommandAsString(shell: string, command: PythonCommandRunConfiguration[]): string { + let commandStr: string; switch (shell) { case ShellConstants.PWSH: - return getCommandAsString(command, shell, ';'); + commandStr = getCommandAsString(command, shell, ';'); + break; case ShellConstants.NU: - return getCommandAsString(command, shell, ';'); + commandStr = getCommandAsString(command, shell, ';'); + break; case ShellConstants.FISH: - return getCommandAsString(command, shell, '; and'); + commandStr = getCommandAsString(command, shell, '; and'); + break; case ShellConstants.BASH: case ShellConstants.SH: case ShellConstants.ZSH: @@ -34,8 +46,17 @@ export function getShellCommandAsString(shell: string, command: PythonCommandRun case ShellConstants.CMD: case ShellConstants.GITBASH: default: - return getCommandAsString(command, shell, '&&'); + commandStr = getCommandAsString(command, shell, '&&'); + break; + } + + // Add a leading space for shells that support history ignore with leading space. + // This prevents the activation command from being saved in bash/zsh history + // when HISTCONTROL=ignorespace (bash) or setopt HIST_IGNORE_SPACE (zsh) is set. + if (shellsWithLeadingSpaceHistorySupport.includes(shell)) { + return ` ${commandStr}`; } + return commandStr; } export function normalizeShellPath(filePath: string, shellType?: string): string { diff --git a/src/test/features/terminal/shells/common/shellUtils.unit.test.ts b/src/test/features/terminal/shells/common/shellUtils.unit.test.ts index 12eedfdb..cd371f09 100644 --- a/src/test/features/terminal/shells/common/shellUtils.unit.test.ts +++ b/src/test/features/terminal/shells/common/shellUtils.unit.test.ts @@ -1,9 +1,13 @@ import * as assert from 'assert'; import { extractProfilePath, + getShellCommandAsString, PROFILE_TAG_END, PROFILE_TAG_START, + shellsWithLeadingSpaceHistorySupport, } from '../../../../../features/terminal/shells/common/shellUtils'; +import { ShellConstants } from '../../../../../features/common/shellConstants'; +import { PythonCommandRunConfiguration } from '../../../../../api'; suite('Shell Utils', () => { suite('extractProfilePath', () => { @@ -77,4 +81,108 @@ suite('Shell Utils', () => { assert.strictEqual(result, expectedPath); }); }); + + suite('shellsWithLeadingSpaceHistorySupport', () => { + test('should include bash, zsh, and gitbash', () => { + assert.ok(shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.BASH)); + assert.ok(shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.ZSH)); + assert.ok(shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.GITBASH)); + }); + + test('should not include shells without leading space history support', () => { + assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.PWSH)); + assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.CMD)); + assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.FISH)); + assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.SH)); + assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.NU)); + }); + }); + + suite('getShellCommandAsString', () => { + const sampleCommand: PythonCommandRunConfiguration[] = [ + { executable: 'source', args: ['/path/to/activate'] }, + ]; + + suite('leading space for history ignore', () => { + test('should add leading space for bash commands', () => { + const result = getShellCommandAsString(ShellConstants.BASH, sampleCommand); + assert.ok(result.startsWith(' '), 'Bash command should start with a leading space'); + assert.ok(result.includes('source'), 'Command should contain source'); + }); + + test('should add leading space for zsh commands', () => { + const result = getShellCommandAsString(ShellConstants.ZSH, sampleCommand); + assert.ok(result.startsWith(' '), 'Zsh command should start with a leading space'); + assert.ok(result.includes('source'), 'Command should contain source'); + }); + + test('should add leading space for gitbash commands', () => { + const result = getShellCommandAsString(ShellConstants.GITBASH, sampleCommand); + assert.ok(result.startsWith(' '), 'Git Bash command should start with a leading space'); + assert.ok(result.includes('source'), 'Command should contain source'); + }); + + test('should not add leading space for pwsh commands', () => { + const result = getShellCommandAsString(ShellConstants.PWSH, sampleCommand); + assert.ok(!result.startsWith(' '), 'PowerShell command should not start with a leading space'); + }); + + test('should not add leading space for cmd commands', () => { + const result = getShellCommandAsString(ShellConstants.CMD, sampleCommand); + assert.ok(!result.startsWith(' '), 'CMD command should not start with a leading space'); + }); + + test('should not add leading space for fish commands', () => { + const result = getShellCommandAsString(ShellConstants.FISH, sampleCommand); + assert.ok(!result.startsWith(' '), 'Fish command should not start with a leading space'); + }); + + test('should not add leading space for sh commands', () => { + const result = getShellCommandAsString(ShellConstants.SH, sampleCommand); + assert.ok(!result.startsWith(' '), 'SH command should not start with a leading space'); + }); + + test('should not add leading space for nu commands', () => { + const result = getShellCommandAsString(ShellConstants.NU, sampleCommand); + assert.ok(!result.startsWith(' '), 'Nu command should not start with a leading space'); + }); + + test('should not add leading space for unknown shells', () => { + const result = getShellCommandAsString('unknown', sampleCommand); + assert.ok(!result.startsWith(' '), 'Unknown shell command should not start with a leading space'); + }); + }); + + suite('command formatting', () => { + test('should format multiple commands with && for bash', () => { + const multiCommand: PythonCommandRunConfiguration[] = [ + { executable: 'source', args: ['/path/to/init'] }, + { executable: 'conda', args: ['activate', 'myenv'] }, + ]; + const result = getShellCommandAsString(ShellConstants.BASH, multiCommand); + assert.ok(result.includes('&&'), 'Bash should use && to join commands'); + assert.ok(result.startsWith(' '), 'Bash command should start with a leading space'); + }); + + test('should format multiple commands with ; for pwsh', () => { + const multiCommand: PythonCommandRunConfiguration[] = [ + { executable: 'source', args: ['/path/to/init'] }, + { executable: 'conda', args: ['activate', 'myenv'] }, + ]; + const result = getShellCommandAsString(ShellConstants.PWSH, multiCommand); + assert.ok(result.includes(';'), 'PowerShell should use ; to join commands'); + assert.ok(!result.startsWith(' '), 'PowerShell command should not start with a leading space'); + }); + + test('should format multiple commands with "; and" for fish', () => { + const multiCommand: PythonCommandRunConfiguration[] = [ + { executable: 'source', args: ['/path/to/init'] }, + { executable: 'conda', args: ['activate', 'myenv'] }, + ]; + const result = getShellCommandAsString(ShellConstants.FISH, multiCommand); + assert.ok(result.includes('; and'), 'Fish should use "; and" to join commands'); + assert.ok(!result.startsWith(' '), 'Fish command should not start with a leading space'); + }); + }); + }); }); From 00337542001e24d76ef084f0f4bab5276c49648a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 3 Feb 2026 08:20:21 -0800 Subject: [PATCH 3/3] Simplify getShellCommandAsString --- .../terminal/shells/common/shellUtils.ts | 50 +++++++------------ .../shells/common/shellUtils.unit.test.ts | 20 ++++---- 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/src/features/terminal/shells/common/shellUtils.ts b/src/features/terminal/shells/common/shellUtils.ts index 08bb54ea..09867f29 100644 --- a/src/features/terminal/shells/common/shellUtils.ts +++ b/src/features/terminal/shells/common/shellUtils.ts @@ -10,50 +10,36 @@ import { quoteArgs } from '../../../execution/execUtils'; * - Zsh: When setopt HIST_IGNORE_SPACE is enabled * - Git Bash: Uses bash under the hood, same behavior as Bash */ -export const shellsWithLeadingSpaceHistorySupport = [ShellConstants.BASH, ShellConstants.ZSH, ShellConstants.GITBASH]; +export const shellsWithLeadingSpaceHistorySupport = new Set([ + ShellConstants.BASH, + ShellConstants.ZSH, + ShellConstants.GITBASH, +]); -function getCommandAsString(command: PythonCommandRunConfiguration[], shell: string, delimiter: string): string { +const defaultShellDelimiter = '&&'; +const shellDelimiterByShell = new Map([ + [ShellConstants.PWSH, ';'], + [ShellConstants.NU, ';'], + [ShellConstants.FISH, '; and'], +]); + +export function getShellCommandAsString(shell: string, command: PythonCommandRunConfiguration[]): string { + const delimiter = shellDelimiterByShell.get(shell) ?? defaultShellDelimiter; const parts = []; for (const cmd of command) { const args = cmd.args ?? []; parts.push(quoteArgs([normalizeShellPath(cmd.executable, shell), ...args]).join(' ')); } - if (shell === ShellConstants.PWSH) { - if (parts.length === 1) { - return parts[0]; - } - return parts.map((p) => `(${p})`).join(` ${delimiter} `); - } - return parts.join(` ${delimiter} `); -} -export function getShellCommandAsString(shell: string, command: PythonCommandRunConfiguration[]): string { - let commandStr: string; - switch (shell) { - case ShellConstants.PWSH: - commandStr = getCommandAsString(command, shell, ';'); - break; - case ShellConstants.NU: - commandStr = getCommandAsString(command, shell, ';'); - break; - case ShellConstants.FISH: - commandStr = getCommandAsString(command, shell, '; and'); - break; - case ShellConstants.BASH: - case ShellConstants.SH: - case ShellConstants.ZSH: - - case ShellConstants.CMD: - case ShellConstants.GITBASH: - default: - commandStr = getCommandAsString(command, shell, '&&'); - break; + let commandStr = parts.join(` ${delimiter} `); + if (shell === ShellConstants.PWSH && parts.length > 1) { + commandStr = parts.map((p) => `(${p})`).join(` ${delimiter} `); } // Add a leading space for shells that support history ignore with leading space. // This prevents the activation command from being saved in bash/zsh history // when HISTCONTROL=ignorespace (bash) or setopt HIST_IGNORE_SPACE (zsh) is set. - if (shellsWithLeadingSpaceHistorySupport.includes(shell)) { + if (shellsWithLeadingSpaceHistorySupport.has(shell)) { return ` ${commandStr}`; } return commandStr; diff --git a/src/test/features/terminal/shells/common/shellUtils.unit.test.ts b/src/test/features/terminal/shells/common/shellUtils.unit.test.ts index cd371f09..97281b80 100644 --- a/src/test/features/terminal/shells/common/shellUtils.unit.test.ts +++ b/src/test/features/terminal/shells/common/shellUtils.unit.test.ts @@ -1,4 +1,6 @@ import * as assert from 'assert'; +import { PythonCommandRunConfiguration } from '../../../../../api'; +import { ShellConstants } from '../../../../../features/common/shellConstants'; import { extractProfilePath, getShellCommandAsString, @@ -6,8 +8,6 @@ import { PROFILE_TAG_START, shellsWithLeadingSpaceHistorySupport, } from '../../../../../features/terminal/shells/common/shellUtils'; -import { ShellConstants } from '../../../../../features/common/shellConstants'; -import { PythonCommandRunConfiguration } from '../../../../../api'; suite('Shell Utils', () => { suite('extractProfilePath', () => { @@ -84,17 +84,17 @@ suite('Shell Utils', () => { suite('shellsWithLeadingSpaceHistorySupport', () => { test('should include bash, zsh, and gitbash', () => { - assert.ok(shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.BASH)); - assert.ok(shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.ZSH)); - assert.ok(shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.GITBASH)); + assert.ok(shellsWithLeadingSpaceHistorySupport.has(ShellConstants.BASH)); + assert.ok(shellsWithLeadingSpaceHistorySupport.has(ShellConstants.ZSH)); + assert.ok(shellsWithLeadingSpaceHistorySupport.has(ShellConstants.GITBASH)); }); test('should not include shells without leading space history support', () => { - assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.PWSH)); - assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.CMD)); - assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.FISH)); - assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.SH)); - assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.NU)); + assert.ok(!shellsWithLeadingSpaceHistorySupport.has(ShellConstants.PWSH)); + assert.ok(!shellsWithLeadingSpaceHistorySupport.has(ShellConstants.CMD)); + assert.ok(!shellsWithLeadingSpaceHistorySupport.has(ShellConstants.FISH)); + assert.ok(!shellsWithLeadingSpaceHistorySupport.has(ShellConstants.SH)); + assert.ok(!shellsWithLeadingSpaceHistorySupport.has(ShellConstants.NU)); }); });