mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-03 01:40:59 -07:00
feat(cli): support default values for environment variables (#24469)
This commit is contained in:
@@ -62,11 +62,13 @@ locations for these files:
|
||||
|
||||
**Note on environment variables in settings:** String values within your
|
||||
`settings.json` and `gemini-extension.json` files can reference environment
|
||||
variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will
|
||||
be automatically resolved when the settings are loaded. For example, if you have
|
||||
an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like
|
||||
this: `"apiKey": "$MY_API_TOKEN"`. Additionally, each extension can have its own
|
||||
`.env` file in its directory, which will be loaded automatically.
|
||||
variables using `$VAR_NAME`, `${VAR_NAME}`, or `${VAR_NAME:-DEFAULT_VALUE}`
|
||||
syntax. These variables will be automatically resolved when the settings are
|
||||
loaded. For example, if you have an environment variable `MY_API_TOKEN`, you
|
||||
could use it in `settings.json` like this: `"apiKey": "$MY_API_TOKEN"`. If you
|
||||
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
|
||||
in a corporate environment, please see the
|
||||
|
||||
@@ -11,18 +11,16 @@ import {
|
||||
} from './envVarResolver.js';
|
||||
|
||||
describe('resolveEnvVarsInString', () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
vi.stubEnv('TEST_VAR', '');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
@@ -30,20 +28,26 @@ describe('resolveEnvVarsInString', () => {
|
||||
});
|
||||
|
||||
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}');
|
||||
|
||||
expect(result).toBe('Value is test-value');
|
||||
});
|
||||
|
||||
it('should resolve multiple variables in the same string', () => {
|
||||
process.env['HOST'] = 'localhost';
|
||||
process.env['PORT'] = '3000';
|
||||
it('should resolve multiple variables', () => {
|
||||
vi.stubEnv('HOST', 'localhost');
|
||||
vi.stubEnv('PORT', '8080');
|
||||
|
||||
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', () => {
|
||||
@@ -71,28 +75,49 @@ describe('resolveEnvVarsInString', () => {
|
||||
});
|
||||
|
||||
it('should handle mixed defined and undefined variables', () => {
|
||||
process.env['DEFINED'] = 'value';
|
||||
vi.stubEnv('DEFINED', 'value');
|
||||
|
||||
const result = resolveEnvVarsInString('$DEFINED 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', () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should resolve variables in nested objects', () => {
|
||||
process.env['API_KEY'] = 'secret-123';
|
||||
process.env['DB_URL'] = 'postgresql://localhost/test';
|
||||
vi.stubEnv('API_KEY', 'secret-123');
|
||||
vi.stubEnv('DB_URL', 'postgresql://localhost/test');
|
||||
|
||||
const config = {
|
||||
server: {
|
||||
@@ -118,8 +143,8 @@ describe('resolveEnvVarsInObject', () => {
|
||||
});
|
||||
|
||||
it('should resolve variables in arrays', () => {
|
||||
process.env['ENV'] = 'production';
|
||||
process.env['VERSION'] = '1.0.0';
|
||||
vi.stubEnv('ENV', 'production');
|
||||
vi.stubEnv('VERSION', '1.0.0');
|
||||
|
||||
const config = {
|
||||
tags: ['$ENV', 'app', '${VERSION}'],
|
||||
@@ -153,8 +178,8 @@ describe('resolveEnvVarsInObject', () => {
|
||||
});
|
||||
|
||||
it('should handle MCP server config structure', () => {
|
||||
process.env['API_TOKEN'] = 'token-123';
|
||||
process.env['SERVER_PORT'] = '8080';
|
||||
vi.stubEnv('API_TOKEN', 'token-123');
|
||||
vi.stubEnv('SERVER_PORT', '8080');
|
||||
|
||||
const extensionConfig = {
|
||||
name: 'test-extension',
|
||||
@@ -206,7 +231,7 @@ describe('resolveEnvVarsInObject', () => {
|
||||
});
|
||||
|
||||
it('should handle circular references in objects without infinite recursion', () => {
|
||||
process.env['TEST_VAR'] = 'resolved-value';
|
||||
vi.stubEnv('TEST_VAR', 'resolved-value');
|
||||
|
||||
type ConfigWithCircularRef = {
|
||||
name: string;
|
||||
@@ -233,7 +258,7 @@ describe('resolveEnvVarsInObject', () => {
|
||||
});
|
||||
|
||||
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>;
|
||||
const arr: ArrayWithCircularRef = ['$ARRAY_VAR', 123];
|
||||
@@ -253,7 +278,7 @@ describe('resolveEnvVarsInObject', () => {
|
||||
});
|
||||
|
||||
it('should handle complex nested circular references', () => {
|
||||
process.env['NESTED_VAR'] = 'nested-resolved';
|
||||
vi.stubEnv('NESTED_VAR', 'nested-resolved');
|
||||
|
||||
type ObjWithRef = {
|
||||
name: string;
|
||||
|
||||
@@ -6,33 +6,53 @@
|
||||
|
||||
/**
|
||||
* Resolves environment variables in a string.
|
||||
* Replaces $VAR_NAME and ${VAR_NAME} with their corresponding environment variable values.
|
||||
* If the environment variable is not defined, the original placeholder is preserved.
|
||||
* Replaces $VAR_NAME, ${VAR_NAME}, and ${VAR_NAME:-DEFAULT_VALUE} with their corresponding
|
||||
* 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 customEnv - Optional record of environment variables to use before process.env
|
||||
* @returns The string with environment variables resolved
|
||||
*
|
||||
* @example
|
||||
* resolveEnvVarsInString("Token: $API_KEY") // Returns "Token: secret-123"
|
||||
* 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"
|
||||
*/
|
||||
export function resolveEnvVarsInString(
|
||||
value: string,
|
||||
customEnv?: Record<string, string>,
|
||||
): string {
|
||||
const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; // Find $VAR_NAME or ${VAR_NAME}
|
||||
return value.replace(envVarRegex, (match, varName1, varName2) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const varName = varName1 || varName2;
|
||||
if (customEnv && typeof customEnv[varName] === 'string') {
|
||||
return customEnv[varName];
|
||||
}
|
||||
if (process && process.env && typeof process.env[varName] === 'string') {
|
||||
return process.env[varName];
|
||||
}
|
||||
return match;
|
||||
});
|
||||
// Regex matches $VAR_NAME, ${VAR_NAME}, and ${VAR_NAME:-DEFAULT_VALUE}
|
||||
const envVarRegex = /\$(?:(\w+)|{([^}]+?)(?::-([^}]*))?})/g;
|
||||
|
||||
return value.replace(
|
||||
envVarRegex,
|
||||
(
|
||||
match: string,
|
||||
varName1?: string,
|
||||
varName2?: string,
|
||||
defaultValue?: string,
|
||||
): 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;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user