feat(cli): support default values for environment variables (#24469)

This commit is contained in:
ruomeng
2026-04-02 10:38:45 -04:00
committed by GitHub
parent 44c8b43328
commit 7b6ab50138
3 changed files with 93 additions and 46 deletions
+7 -5
View File
@@ -62,11 +62,13 @@ locations for these files:
**Note on environment variables in settings:** String values within your **Note on environment variables in settings:** String values within your
`settings.json` and `gemini-extension.json` files can reference environment `settings.json` and `gemini-extension.json` files can reference environment
variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will variables using `$VAR_NAME`, `${VAR_NAME}`, or `${VAR_NAME:-DEFAULT_VALUE}`
be automatically resolved when the settings are loaded. For example, if you have syntax. These variables will be automatically resolved when the settings are
an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like loaded. For example, if you have an environment variable `MY_API_TOKEN`, you
this: `"apiKey": "$MY_API_TOKEN"`. Additionally, each extension can have its own could use it in `settings.json` like this: `"apiKey": "$MY_API_TOKEN"`. If you
`.env` file in its directory, which will be loaded automatically. want to provide a fallback value, use `${MY_API_TOKEN:-default-token}`.
Additionally, each extension can have its own `.env` file in its directory,
which will be loaded automatically.
**Note for Enterprise Users:** For guidance on deploying and managing Gemini CLI **Note for Enterprise Users:** For guidance on deploying and managing Gemini CLI
in a corporate environment, please see the in a corporate environment, please see the
+52 -27
View File
@@ -11,18 +11,16 @@ import {
} from './envVarResolver.js'; } from './envVarResolver.js';
describe('resolveEnvVarsInString', () => { describe('resolveEnvVarsInString', () => {
let originalEnv: NodeJS.ProcessEnv;
beforeEach(() => { beforeEach(() => {
originalEnv = { ...process.env }; vi.stubEnv('TEST_VAR', '');
}); });
afterEach(() => { afterEach(() => {
process.env = originalEnv; vi.unstubAllEnvs();
}); });
it('should resolve $VAR_NAME format', () => { it('should resolve $VAR_NAME format', () => {
process.env['TEST_VAR'] = 'test-value'; vi.stubEnv('TEST_VAR', 'test-value');
const result = resolveEnvVarsInString('Value is $TEST_VAR'); const result = resolveEnvVarsInString('Value is $TEST_VAR');
@@ -30,20 +28,26 @@ describe('resolveEnvVarsInString', () => {
}); });
it('should resolve ${VAR_NAME} format', () => { it('should resolve ${VAR_NAME} format', () => {
process.env['TEST_VAR'] = 'test-value'; vi.stubEnv('TEST_VAR', 'test-value');
const result = resolveEnvVarsInString('Value is ${TEST_VAR}'); const result = resolveEnvVarsInString('Value is ${TEST_VAR}');
expect(result).toBe('Value is test-value'); expect(result).toBe('Value is test-value');
}); });
it('should resolve multiple variables in the same string', () => { it('should resolve multiple variables', () => {
process.env['HOST'] = 'localhost'; vi.stubEnv('HOST', 'localhost');
process.env['PORT'] = '3000'; vi.stubEnv('PORT', '8080');
const result = resolveEnvVarsInString('URL: http://$HOST:${PORT}/api'); const result = resolveEnvVarsInString('URL: http://$HOST:${PORT}/api');
expect(result).toBe('URL: http://localhost:3000/api'); expect(result).toBe('URL: http://localhost:8080/api');
});
it('should support environment variables with dots', () => {
vi.stubEnv('FOO.BAR', 'baz');
const result = resolveEnvVarsInString('Value: ${FOO.BAR}');
expect(result).toBe('Value: baz');
}); });
it('should leave undefined variables unchanged', () => { it('should leave undefined variables unchanged', () => {
@@ -71,28 +75,49 @@ describe('resolveEnvVarsInString', () => {
}); });
it('should handle mixed defined and undefined variables', () => { it('should handle mixed defined and undefined variables', () => {
process.env['DEFINED'] = 'value'; vi.stubEnv('DEFINED', 'value');
const result = resolveEnvVarsInString('$DEFINED and $UNDEFINED mixed'); const result = resolveEnvVarsInString('$DEFINED and $UNDEFINED mixed');
expect(result).toBe('value and $UNDEFINED mixed'); expect(result).toBe('value and $UNDEFINED mixed');
}); });
it('should use default value when environment variable is missing', () => {
const result = resolveEnvVarsInString(
'URL: ${MISSING_VAR:-https://default.example.com}/api',
);
expect(result).toBe('URL: https://default.example.com/api');
});
it('should ignore default value when environment variable is present', () => {
vi.stubEnv('PRESENT_VAR', 'https://actual.example.com');
const result = resolveEnvVarsInString(
'URL: ${PRESENT_VAR:-https://default.example.com}/api',
);
expect(result).toBe('URL: https://actual.example.com/api');
});
it('should support empty default value', () => {
const result = resolveEnvVarsInString('Value: ${MISSING_VAR:-}');
expect(result).toBe('Value: ');
});
it('should correctly handle default values that contain colons or dashes', () => {
const result = resolveEnvVarsInString(
'Value: ${MISSING_VAR:-val:-123-abc}',
);
expect(result).toBe('Value: val:-123-abc');
});
}); });
describe('resolveEnvVarsInObject', () => { describe('resolveEnvVarsInObject', () => {
let originalEnv: NodeJS.ProcessEnv;
beforeEach(() => {
originalEnv = { ...process.env };
});
afterEach(() => { afterEach(() => {
process.env = originalEnv; vi.unstubAllEnvs();
}); });
it('should resolve variables in nested objects', () => { it('should resolve variables in nested objects', () => {
process.env['API_KEY'] = 'secret-123'; vi.stubEnv('API_KEY', 'secret-123');
process.env['DB_URL'] = 'postgresql://localhost/test'; vi.stubEnv('DB_URL', 'postgresql://localhost/test');
const config = { const config = {
server: { server: {
@@ -118,8 +143,8 @@ describe('resolveEnvVarsInObject', () => {
}); });
it('should resolve variables in arrays', () => { it('should resolve variables in arrays', () => {
process.env['ENV'] = 'production'; vi.stubEnv('ENV', 'production');
process.env['VERSION'] = '1.0.0'; vi.stubEnv('VERSION', '1.0.0');
const config = { const config = {
tags: ['$ENV', 'app', '${VERSION}'], tags: ['$ENV', 'app', '${VERSION}'],
@@ -153,8 +178,8 @@ describe('resolveEnvVarsInObject', () => {
}); });
it('should handle MCP server config structure', () => { it('should handle MCP server config structure', () => {
process.env['API_TOKEN'] = 'token-123'; vi.stubEnv('API_TOKEN', 'token-123');
process.env['SERVER_PORT'] = '8080'; vi.stubEnv('SERVER_PORT', '8080');
const extensionConfig = { const extensionConfig = {
name: 'test-extension', name: 'test-extension',
@@ -206,7 +231,7 @@ describe('resolveEnvVarsInObject', () => {
}); });
it('should handle circular references in objects without infinite recursion', () => { it('should handle circular references in objects without infinite recursion', () => {
process.env['TEST_VAR'] = 'resolved-value'; vi.stubEnv('TEST_VAR', 'resolved-value');
type ConfigWithCircularRef = { type ConfigWithCircularRef = {
name: string; name: string;
@@ -233,7 +258,7 @@ describe('resolveEnvVarsInObject', () => {
}); });
it('should handle circular references in arrays without infinite recursion', () => { it('should handle circular references in arrays without infinite recursion', () => {
process.env['ARRAY_VAR'] = 'array-value'; vi.stubEnv('ARRAY_VAR', 'array-value');
type ArrayWithCircularRef = Array<string | number | ArrayWithCircularRef>; type ArrayWithCircularRef = Array<string | number | ArrayWithCircularRef>;
const arr: ArrayWithCircularRef = ['$ARRAY_VAR', 123]; const arr: ArrayWithCircularRef = ['$ARRAY_VAR', 123];
@@ -253,7 +278,7 @@ describe('resolveEnvVarsInObject', () => {
}); });
it('should handle complex nested circular references', () => { it('should handle complex nested circular references', () => {
process.env['NESTED_VAR'] = 'nested-resolved'; vi.stubEnv('NESTED_VAR', 'nested-resolved');
type ObjWithRef = { type ObjWithRef = {
name: string; name: string;
+34 -14
View File
@@ -6,33 +6,53 @@
/** /**
* Resolves environment variables in a string. * Resolves environment variables in a string.
* Replaces $VAR_NAME and ${VAR_NAME} with their corresponding environment variable values. * Replaces $VAR_NAME, ${VAR_NAME}, and ${VAR_NAME:-DEFAULT_VALUE} with their corresponding
* If the environment variable is not defined, the original placeholder is preserved. * environment variable values. If the environment variable is not defined and no default
* value is provided, the original placeholder is preserved.
* *
* @param value - The string that may contain environment variable placeholders * @param value - The string that may contain environment variable placeholders
* @param customEnv - Optional record of environment variables to use before process.env
* @returns The string with environment variables resolved * @returns The string with environment variables resolved
* *
* @example * @example
* resolveEnvVarsInString("Token: $API_KEY") // Returns "Token: secret-123" * resolveEnvVarsInString("Token: $API_KEY") // Returns "Token: secret-123"
* resolveEnvVarsInString("URL: ${BASE_URL}/api") // Returns "URL: https://api.example.com/api" * resolveEnvVarsInString("URL: ${BASE_URL}/api") // Returns "URL: https://api.example.com/api"
* resolveEnvVarsInString("URL: ${MISSING_VAR:-https://default.com}") // Returns "URL: https://default.com"
* resolveEnvVarsInString("Missing: $UNDEFINED_VAR") // Returns "Missing: $UNDEFINED_VAR" * resolveEnvVarsInString("Missing: $UNDEFINED_VAR") // Returns "Missing: $UNDEFINED_VAR"
*/ */
export function resolveEnvVarsInString( export function resolveEnvVarsInString(
value: string, value: string,
customEnv?: Record<string, string>, customEnv?: Record<string, string>,
): string { ): string {
const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; // Find $VAR_NAME or ${VAR_NAME} // Regex matches $VAR_NAME, ${VAR_NAME}, and ${VAR_NAME:-DEFAULT_VALUE}
return value.replace(envVarRegex, (match, varName1, varName2) => { const envVarRegex = /\$(?:(\w+)|{([^}]+?)(?::-([^}]*))?})/g;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const varName = varName1 || varName2; return value.replace(
if (customEnv && typeof customEnv[varName] === 'string') { envVarRegex,
return customEnv[varName]; (
} match: string,
if (process && process.env && typeof process.env[varName] === 'string') { varName1?: string,
return process.env[varName]; varName2?: string,
} defaultValue?: string,
return match; ): string => {
}); const varName: string = varName1 || varName2 || '';
if (!varName) {
return match;
}
if (customEnv && typeof customEnv[varName] === 'string') {
return customEnv[varName];
}
if (process && process.env && typeof process.env[varName] === 'string') {
return process.env[varName];
}
if (defaultValue !== undefined) {
return defaultValue;
}
return match;
},
);
} }
/** /**