Skip to content

Commit 014f333

Browse files
committed
feat: Add environment variables to shell command
Added the ability to set additional environment variables for shellCommand, both for all platforms at once and for each platform separately.
1 parent 8628789 commit 014f333

File tree

9 files changed

+312
-3
lines changed

9 files changed

+312
-3
lines changed

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,65 @@ Yes. you can use the variables placeholder documented at [predefined-variables](
163163
}
164164
```
165165

166+
### Can I add environment variables to the shellCommand?
167+
168+
Yes, you can use shellEnv to set additional environment variables:
169+
170+
```jsonc
171+
{
172+
"extensionName": "ts",
173+
"apps": [
174+
{
175+
"extensionName": "*",
176+
"apps": [
177+
{
178+
"title": "run ts file",
179+
"shellCommand": "ts-node ${file}",
180+
"shellEnv":
181+
{
182+
"TOKEN": "tyekjjbqbptcxeycgmwqfepus"
183+
},
184+
}
185+
]
186+
}
187+
]
188+
}
189+
```
190+
191+
Or you can set separate environment variables for Windows, Linux and macOS:
192+
193+
```jsonc
194+
{
195+
"extensionName": "ts",
196+
"apps": [
197+
{
198+
"extensionName": "*",
199+
"apps": [
200+
{
201+
"title": "run ts file",
202+
"shellCommand": "ts-node ${file}",
203+
"shellEnv":
204+
{
205+
"windows":
206+
{
207+
"PLATFORM": "Windows"
208+
},
209+
"linux":
210+
{
211+
"PLATFORM": "GNU/Linux"
212+
},
213+
"osx":
214+
{
215+
"PLATFORM": "macOS"
216+
},
217+
},
218+
}
219+
]
220+
}
221+
]
222+
}
223+
```
224+
166225
### assign keyboard shortcut for specific config item
167226

168227
`keybindings.json`:

package.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,54 @@
194194
"type": "boolean",
195195
"description": "%cfg.openInExternalApp.openMapper.item.apps.item.isElectronApp%",
196196
"default": false
197+
},
198+
"shellEnv": {
199+
"anyOf": [
200+
{
201+
"type": "object",
202+
"description": "%cfg.openInExternalApp.openMapper.item.apps.item.shellEnv%",
203+
"default": {},
204+
"additionalProperties": {
205+
"type": "string",
206+
"description": "%cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.string%"
207+
}
208+
},
209+
{
210+
"type": "object",
211+
"description": "%cfg.openInExternalApp.openMapper.item.apps.item.shellEnv%",
212+
"default": {},
213+
"additionalProperties": false,
214+
"properties": {
215+
"windows": {
216+
"type": "object",
217+
"description": "%cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.windows%",
218+
"default": {},
219+
"additionalProperties": {
220+
"type": "string",
221+
"description": "%cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.string%"
222+
}
223+
},
224+
"linux": {
225+
"type": "object",
226+
"description": "%cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.linux%",
227+
"default": {},
228+
"additionalProperties": {
229+
"type": "string",
230+
"description": "%cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.string%"
231+
}
232+
},
233+
"osx": {
234+
"type": "object",
235+
"description": "%cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.osx%",
236+
"default": {},
237+
"additionalProperties": {
238+
"type": "string",
239+
"description": "%cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.string%"
240+
}
241+
}
242+
}
243+
}
244+
]
197245
}
198246
}
199247
}

package.nls.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,10 @@
1717
"cfg.openInExternalApp.openMapper.item.apps.item.title": "Title which will be shown in the drop list if there are several apps to open the file",
1818
"cfg.openInExternalApp.openMapper.item.apps.item.shellCommand": "Using shell command with file placeholder to deal with file",
1919
"cfg.openInExternalApp.openMapper.item.apps.item.args": "Arguments passed to openCommand",
20-
"cfg.openInExternalApp.openMapper.item.apps.item.isElectronApp": "Set to true when you config an electron app"
20+
"cfg.openInExternalApp.openMapper.item.apps.item.isElectronApp": "Set to true when you config an electron app",
21+
"cfg.openInExternalApp.openMapper.item.apps.item.shellEnv": "Additional shellCommand environment variables",
22+
"cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.string": "Environment variable",
23+
"cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.windows": "Additional shellCommand environment variables for Windows",
24+
"cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.linux": "Additional shellCommand environment variables for Linux",
25+
"cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.osx": "Additional shellCommand environment variables for macOS"
2126
}

package.nls.ru.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,10 @@
1717
"cfg.openInExternalApp.openMapper.item.apps.item.title": "Заголовок, который отобразиться в выпадающем списке, если для этого типа файлов задано несколько приложений",
1818
"cfg.openInExternalApp.openMapper.item.apps.item.shellCommand": "Используйте команду оболочки с переменной file",
1919
"cfg.openInExternalApp.openMapper.item.apps.item.args": "Параметры командной строки для openCommand",
20-
"cfg.openInExternalApp.openMapper.item.apps.item.isElectronApp": "Установите в true, если это приложение electron"
20+
"cfg.openInExternalApp.openMapper.item.apps.item.isElectronApp": "Установите в true, если это приложение electron",
21+
"cfg.openInExternalApp.openMapper.item.apps.item.shellEnv": "Дополнительные переменные окружения для shellCommand",
22+
"cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.string": "Переменная окружения",
23+
"cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.windows": "Дополнительные переменные окружения для shellCommand, для Windows",
24+
"cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.linux": "Дополнительные переменные окружения для shellCommand, для Linux",
25+
"cfg.openInExternalApp.openMapper.item.apps.item.shellEnv.osx": "Дополнительные переменные окружения для shellCommand, для macOS"
2126
}

src/config.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,32 @@ export function validateConfiguration(configuration: ExtensionConfigItem[]): joi
2121
args: joi.array().items(joi.string().required()),
2222
isElectronApp: joi.boolean(),
2323
shellCommand: joi.string(),
24+
shellEnv: joi
25+
.alternatives()
26+
.try(
27+
joi.object()
28+
.pattern(
29+
joi.string(),
30+
joi.string().required()
31+
),
32+
joi.object({
33+
windows: joi.object()
34+
.pattern(
35+
joi.string(),
36+
joi.string().required()
37+
),
38+
osx: joi.object()
39+
.pattern(
40+
joi.string(),
41+
joi.string().required()
42+
),
43+
linux: joi.object()
44+
.pattern(
45+
joi.string(),
46+
joi.string().required()
47+
)
48+
})
49+
)
2450
}),
2551
),
2652
)

src/typings/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@ interface CommandModule {
55
handler: (...args: any[]) => any;
66
}
77

8+
interface PlatformVariables {
9+
windows?: NodeJS.ProcessEnv;
10+
linux?: NodeJS.ProcessEnv;
11+
osx?: NodeJS.ProcessEnv;
12+
}
13+
814
interface ExternalAppConfig {
915
title: string;
1016
openCommand?: string;
1117
args?: string[];
1218
isElectronApp?: boolean;
1319
shellCommand?: string;
20+
shellEnv?: NodeJS.ProcessEnv | PlatformVariables;
1421
}
1522

1623
interface ExtensionConfigItem {

src/utils/open.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ExecOptions } from 'node:child_process';
12
import { exec as _exec } from 'node:child_process';
23
import { promisify } from 'node:util';
34

@@ -6,6 +7,7 @@ import _open from 'open';
67
import vscode, { Uri } from 'vscode';
78

89
import { logger } from './logger';
10+
import { getShellEnv, mergeEnvironments, isWindows, isMacintosh, isLinux } from './platform';
911
import { parseVariables } from './variable';
1012

1113
export function isObject(value: any) {
@@ -43,7 +45,26 @@ export async function open(filePath: string, appConfig?: string | ExternalAppCon
4345
)[0];
4446
logger.info(`open file by shell command: "${parsedCommand}"`);
4547
try {
46-
await exec(parsedCommand);
48+
if (appConfig.shellEnv) {
49+
const shellEnv = getShellEnv();
50+
51+
let additionalEnv: NodeJS.ProcessEnv
52+
if (isWindows && typeof appConfig.shellEnv.windows === 'object') {
53+
additionalEnv = appConfig.shellEnv.windows;
54+
} else if (isMacintosh && typeof appConfig.shellEnv.osx === 'object') {
55+
additionalEnv = appConfig.shellEnv.osx;
56+
} else if (isLinux && typeof appConfig.shellEnv.linux === 'object') {
57+
additionalEnv = appConfig.shellEnv.linux;
58+
} else {
59+
additionalEnv = appConfig.shellEnv as NodeJS.ProcessEnv;
60+
}
61+
62+
await mergeEnvironments(shellEnv, additionalEnv, Uri.file(filePath))
63+
const options: ExecOptions = { env: shellEnv };
64+
await exec(parsedCommand, options);
65+
} else {
66+
await exec(parsedCommand);
67+
}
4768
} catch (error: any) {
4869
vscode.window.showErrorMessage(
4970
`open file by shell command failed, execute: "${parsedCommand}"`,

src/utils/platform.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { Uri } from 'vscode';
2+
3+
import { parseVariables } from './variable';
4+
5+
let _isWindows = false;
6+
let _isMacintosh = false;
7+
let _isLinux = false;
8+
9+
switch(process.platform) {
10+
case 'win32':
11+
_isWindows = true;
12+
break;
13+
case 'darwin':
14+
_isMacintosh = true;
15+
break;
16+
case 'linux':
17+
_isLinux = true;
18+
break;
19+
}
20+
21+
export const isWindows = _isWindows;
22+
export const isMacintosh = _isMacintosh;
23+
export const isLinux = _isLinux;
24+
25+
export function getShellEnv(): NodeJS.ProcessEnv {
26+
return { ...process.env};
27+
}
28+
29+
export async function mergeEnvironments(parent: NodeJS.ProcessEnv, other: NodeJS.ProcessEnv | undefined, activeFile?: Uri) {
30+
if (!other) {
31+
return;
32+
}
33+
34+
if (isWindows) {
35+
// In Windows, environment variables are not case sensitive, so
36+
// this must be taken into account when overwriting.
37+
Object.entries(other).forEach(
38+
async ([key, value]) => {
39+
if (value !== undefined) {
40+
let otherKey = key;
41+
for (const envKey in parent) {
42+
if (key.toLowerCase() === envKey.toLowerCase()) {
43+
otherKey = envKey;
44+
break;
45+
}
46+
}
47+
48+
await _mergeEnvironmentValue(parent, otherKey, value, activeFile);
49+
}
50+
}
51+
);
52+
} else {
53+
Object.entries(other).forEach(
54+
async ([key, value]) => {
55+
if (value !== undefined) {
56+
await _mergeEnvironmentValue(parent, key, value, activeFile);
57+
}
58+
}
59+
);
60+
}
61+
}
62+
63+
async function _mergeEnvironmentValue(env: NodeJS.ProcessEnv, key: string, value: string | null, activeFile?: Uri) : Promise<void> {
64+
if (typeof value === 'string') {
65+
const parsedValue = (
66+
await parseVariables([value], activeFile)
67+
)[0];
68+
env[key] = parsedValue;
69+
} else {
70+
delete env[key];
71+
}
72+
}

test/config.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,44 @@ describe('#config', () => {
1313
assert.notStrictEqual(validateConfiguration(configuration).error, null);
1414
});
1515

16+
it(`should error as env variable have to be a string`, () => {
17+
const configuration: any = [
18+
{
19+
extensionName: 'dat',
20+
apps: [
21+
{
22+
title: 'invalid env block',
23+
shellCommand: 'abc',
24+
shellEnv: {
25+
badvar: 404,
26+
},
27+
},
28+
],
29+
},
30+
];
31+
assert.notStrictEqual(validateConfiguration(configuration).error, null);
32+
});
33+
34+
it(`should error as 'msdos' is unsupported platform`, () => {
35+
const configuration: any = [
36+
{
37+
extensionName: 'dat',
38+
apps: [
39+
{
40+
title: 'invalid env block',
41+
shellCommand: 'abc',
42+
shellEnv: {
43+
msdos: {
44+
dosvar: 'never',
45+
},
46+
},
47+
},
48+
],
49+
},
50+
];
51+
assert.notStrictEqual(validateConfiguration(configuration).error, null);
52+
});
53+
1654
it(`should pass validation`, () => {
1755
const configuration: any = [
1856
{
@@ -41,6 +79,34 @@ describe('#config', () => {
4179
},
4280
],
4381
},
82+
{
83+
extensionName: 'dat',
84+
apps: [
85+
{
86+
title: 'single env block',
87+
shellCommand: 'abc',
88+
shellEnv: {
89+
var1: "123",
90+
var2: "xyz",
91+
},
92+
},
93+
{
94+
title: 'multiplatform env block',
95+
shellCommand: 'abc',
96+
shellEnv: {
97+
windows: {
98+
winvar: 'Windows Platform',
99+
},
100+
osx: {
101+
macvar: 'macOS Platform',
102+
},
103+
linux: {
104+
linvar: 'Linux Platform',
105+
},
106+
},
107+
},
108+
],
109+
},
44110
];
45111
assert.notStrictEqual(validateConfiguration(configuration).error, null);
46112
});

0 commit comments

Comments
 (0)