Skip to content

Commit 979c0be

Browse files
Copilotanthonykim1
andcommitted
Add leading space to bash/zsh activation commands to prevent history bloating
Co-authored-by: anthonykim1 <62267334+anthonykim1@users.noreply.github.com>
1 parent 9e96e7d commit 979c0be

2 files changed

Lines changed: 133 additions & 4 deletions

File tree

src/features/terminal/shells/common/shellUtils.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import { getConfiguration } from '../../../../common/workspace.apis';
44
import { ShellConstants } from '../../../common/shellConstants';
55
import { quoteArgs } from '../../../execution/execUtils';
66

7+
/**
8+
* Shells that support a leading space to prevent command from being saved in history.
9+
* - Bash: When HISTCONTROL contains 'ignorespace' or 'ignoreboth'
10+
* - Zsh: When setopt HIST_IGNORE_SPACE is enabled
11+
* - Git Bash: Uses bash under the hood, same behavior as Bash
12+
*/
13+
export const shellsWithLeadingSpaceHistorySupport = [ShellConstants.BASH, ShellConstants.ZSH, ShellConstants.GITBASH];
14+
715
function getCommandAsString(command: PythonCommandRunConfiguration[], shell: string, delimiter: string): string {
816
const parts = [];
917
for (const cmd of command) {
@@ -20,22 +28,35 @@ function getCommandAsString(command: PythonCommandRunConfiguration[], shell: str
2028
}
2129

2230
export function getShellCommandAsString(shell: string, command: PythonCommandRunConfiguration[]): string {
31+
let commandStr: string;
2332
switch (shell) {
2433
case ShellConstants.PWSH:
25-
return getCommandAsString(command, shell, ';');
34+
commandStr = getCommandAsString(command, shell, ';');
35+
break;
2636
case ShellConstants.NU:
27-
return getCommandAsString(command, shell, ';');
37+
commandStr = getCommandAsString(command, shell, ';');
38+
break;
2839
case ShellConstants.FISH:
29-
return getCommandAsString(command, shell, '; and');
40+
commandStr = getCommandAsString(command, shell, '; and');
41+
break;
3042
case ShellConstants.BASH:
3143
case ShellConstants.SH:
3244
case ShellConstants.ZSH:
3345

3446
case ShellConstants.CMD:
3547
case ShellConstants.GITBASH:
3648
default:
37-
return getCommandAsString(command, shell, '&&');
49+
commandStr = getCommandAsString(command, shell, '&&');
50+
break;
51+
}
52+
53+
// Add a leading space for shells that support history ignore with leading space.
54+
// This prevents the activation command from being saved in bash/zsh history
55+
// when HISTCONTROL=ignorespace (bash) or setopt HIST_IGNORE_SPACE (zsh) is set.
56+
if (shellsWithLeadingSpaceHistorySupport.includes(shell)) {
57+
return ` ${commandStr}`;
3858
}
59+
return commandStr;
3960
}
4061

4162
export function normalizeShellPath(filePath: string, shellType?: string): string {

src/test/features/terminal/shells/common/shellUtils.unit.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import * as assert from 'assert';
22
import {
33
extractProfilePath,
4+
getShellCommandAsString,
45
PROFILE_TAG_END,
56
PROFILE_TAG_START,
7+
shellsWithLeadingSpaceHistorySupport,
68
} from '../../../../../features/terminal/shells/common/shellUtils';
9+
import { ShellConstants } from '../../../../../features/common/shellConstants';
10+
import { PythonCommandRunConfiguration } from '../../../../../api';
711

812
suite('Shell Utils', () => {
913
suite('extractProfilePath', () => {
@@ -77,4 +81,108 @@ suite('Shell Utils', () => {
7781
assert.strictEqual(result, expectedPath);
7882
});
7983
});
84+
85+
suite('shellsWithLeadingSpaceHistorySupport', () => {
86+
test('should include bash, zsh, and gitbash', () => {
87+
assert.ok(shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.BASH));
88+
assert.ok(shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.ZSH));
89+
assert.ok(shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.GITBASH));
90+
});
91+
92+
test('should not include shells without leading space history support', () => {
93+
assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.PWSH));
94+
assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.CMD));
95+
assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.FISH));
96+
assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.SH));
97+
assert.ok(!shellsWithLeadingSpaceHistorySupport.includes(ShellConstants.NU));
98+
});
99+
});
100+
101+
suite('getShellCommandAsString', () => {
102+
const sampleCommand: PythonCommandRunConfiguration[] = [
103+
{ executable: 'source', args: ['/path/to/activate'] },
104+
];
105+
106+
suite('leading space for history ignore', () => {
107+
test('should add leading space for bash commands', () => {
108+
const result = getShellCommandAsString(ShellConstants.BASH, sampleCommand);
109+
assert.ok(result.startsWith(' '), 'Bash command should start with a leading space');
110+
assert.ok(result.includes('source'), 'Command should contain source');
111+
});
112+
113+
test('should add leading space for zsh commands', () => {
114+
const result = getShellCommandAsString(ShellConstants.ZSH, sampleCommand);
115+
assert.ok(result.startsWith(' '), 'Zsh command should start with a leading space');
116+
assert.ok(result.includes('source'), 'Command should contain source');
117+
});
118+
119+
test('should add leading space for gitbash commands', () => {
120+
const result = getShellCommandAsString(ShellConstants.GITBASH, sampleCommand);
121+
assert.ok(result.startsWith(' '), 'Git Bash command should start with a leading space');
122+
assert.ok(result.includes('source'), 'Command should contain source');
123+
});
124+
125+
test('should not add leading space for pwsh commands', () => {
126+
const result = getShellCommandAsString(ShellConstants.PWSH, sampleCommand);
127+
assert.ok(!result.startsWith(' '), 'PowerShell command should not start with a leading space');
128+
});
129+
130+
test('should not add leading space for cmd commands', () => {
131+
const result = getShellCommandAsString(ShellConstants.CMD, sampleCommand);
132+
assert.ok(!result.startsWith(' '), 'CMD command should not start with a leading space');
133+
});
134+
135+
test('should not add leading space for fish commands', () => {
136+
const result = getShellCommandAsString(ShellConstants.FISH, sampleCommand);
137+
assert.ok(!result.startsWith(' '), 'Fish command should not start with a leading space');
138+
});
139+
140+
test('should not add leading space for sh commands', () => {
141+
const result = getShellCommandAsString(ShellConstants.SH, sampleCommand);
142+
assert.ok(!result.startsWith(' '), 'SH command should not start with a leading space');
143+
});
144+
145+
test('should not add leading space for nu commands', () => {
146+
const result = getShellCommandAsString(ShellConstants.NU, sampleCommand);
147+
assert.ok(!result.startsWith(' '), 'Nu command should not start with a leading space');
148+
});
149+
150+
test('should not add leading space for unknown shells', () => {
151+
const result = getShellCommandAsString('unknown', sampleCommand);
152+
assert.ok(!result.startsWith(' '), 'Unknown shell command should not start with a leading space');
153+
});
154+
});
155+
156+
suite('command formatting', () => {
157+
test('should format multiple commands with && for bash', () => {
158+
const multiCommand: PythonCommandRunConfiguration[] = [
159+
{ executable: 'source', args: ['/path/to/init'] },
160+
{ executable: 'conda', args: ['activate', 'myenv'] },
161+
];
162+
const result = getShellCommandAsString(ShellConstants.BASH, multiCommand);
163+
assert.ok(result.includes('&&'), 'Bash should use && to join commands');
164+
assert.ok(result.startsWith(' '), 'Bash command should start with a leading space');
165+
});
166+
167+
test('should format multiple commands with ; for pwsh', () => {
168+
const multiCommand: PythonCommandRunConfiguration[] = [
169+
{ executable: 'source', args: ['/path/to/init'] },
170+
{ executable: 'conda', args: ['activate', 'myenv'] },
171+
];
172+
const result = getShellCommandAsString(ShellConstants.PWSH, multiCommand);
173+
assert.ok(result.includes(';'), 'PowerShell should use ; to join commands');
174+
assert.ok(!result.startsWith(' '), 'PowerShell command should not start with a leading space');
175+
});
176+
177+
test('should format multiple commands with "; and" for fish', () => {
178+
const multiCommand: PythonCommandRunConfiguration[] = [
179+
{ executable: 'source', args: ['/path/to/init'] },
180+
{ executable: 'conda', args: ['activate', 'myenv'] },
181+
];
182+
const result = getShellCommandAsString(ShellConstants.FISH, multiCommand);
183+
assert.ok(result.includes('; and'), 'Fish should use "; and" to join commands');
184+
assert.ok(!result.startsWith(' '), 'Fish command should not start with a leading space');
185+
});
186+
});
187+
});
80188
});

0 commit comments

Comments
 (0)