fix(patch): cherry-pick 58df1c6 to release/v0.30.0-pr-20374 [CONFLICTS] (#20567)

Co-authored-by: christine betts <chrstn@uw.edu>
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
This commit is contained in:
gemini-cli-robot
2026-02-27 12:55:17 -05:00
committed by GitHub
parent 176b2baeef
commit 0fc15382ae
7 changed files with 458 additions and 15 deletions

View File

@@ -0,0 +1,117 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { expandEnvVars } from './envExpansion.js';
describe('expandEnvVars', () => {
const defaultEnv = {
USER: 'morty',
HOME: '/home/morty',
TEMP: 'C:\\Temp',
EMPTY: '',
};
describe('POSIX behavior (non-Windows)', () => {
beforeEach(() => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin');
});
afterEach(() => {
vi.restoreAllMocks();
});
it.each([
['$VAR (POSIX)', 'Hello $USER', defaultEnv, 'Hello morty'],
[
'${VAR} (POSIX)',
'Welcome to ${HOME}',
defaultEnv,
'Welcome to /home/morty',
],
[
'should NOT expand %VAR% on non-Windows',
'Data in %TEMP%',
defaultEnv,
'Data in %TEMP%',
],
[
'mixed formats (only POSIX expanded)',
'$USER lives in ${HOME} on %TEMP%',
defaultEnv,
'morty lives in /home/morty on %TEMP%',
],
[
'missing variables (POSIX only)',
'Missing $UNDEFINED and ${NONE} and %MISSING%',
defaultEnv,
'Missing and and %MISSING%',
],
[
'empty or undefined values',
'Value is "$EMPTY"',
defaultEnv,
'Value is ""',
],
[
'original string if no variables',
'No vars here',
defaultEnv,
'No vars here',
],
['literal values like "1234"', '1234', defaultEnv, '1234'],
['empty input string', '', defaultEnv, ''],
[
'complex paths',
'${HOME}/bin:$PATH',
{ ...defaultEnv, PATH: '/usr/bin' },
'/home/morty/bin:/usr/bin',
],
])('should handle %s', (_, input, env, expected) => {
expect(expandEnvVars(input, env)).toBe(expected);
});
});
describe('Windows behavior', () => {
beforeEach(() => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
});
afterEach(() => {
vi.restoreAllMocks();
});
it.each([
['$VAR (POSIX)', 'Hello $USER', defaultEnv, 'Hello morty'],
[
'${VAR} (POSIX)',
'Welcome to ${HOME}',
defaultEnv,
'Welcome to /home/morty',
],
[
'should expand %VAR% on Windows',
'Data in %TEMP%',
defaultEnv,
'Data in C:\\Temp',
],
[
'mixed formats (both expanded)',
'$USER lives in ${HOME} on %TEMP%',
defaultEnv,
'morty lives in /home/morty on C:\\Temp',
],
[
'missing variables (all expanded to empty)',
'Missing $UNDEFINED and ${NONE} and %MISSING%',
defaultEnv,
'Missing and and ',
],
])('should handle %s', (_, input, env, expected) => {
expect(expandEnvVars(input, env)).toBe(expected);
});
});
});

View File

@@ -0,0 +1,54 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { expand } from 'dotenv-expand';
/**
* Expands environment variables in a string using the provided environment record.
* Uses the standard `dotenv-expand` library to handle expansion consistently with
* other tools.
*
* Supports POSIX/Bash syntax ($VAR, ${VAR}).
* Note: Windows syntax (%VAR%) is not natively supported by dotenv-expand.
*
* @param str - The string containing environment variable placeholders.
* @param env - A record of environment variable names and their values.
* @returns The string with environment variables expanded. Missing variables resolve to an empty string.
*/
export function expandEnvVars(
str: string,
env: Record<string, string | undefined>,
): string {
if (!str) return str;
// 1. Pre-process Windows-style variables (%VAR%) since dotenv-expand only handles POSIX ($VAR).
// We only do this on Windows to limit the blast radius and avoid conflicts with other
// systems where % might be a literal character (e.g. in URLs or shell commands).
const isWindows = process.platform === 'win32';
const processedStr = isWindows
? str.replace(/%(\w+)%/g, (_, name) => env[name] ?? '')
: str;
// 2. Use dotenv-expand for POSIX/Bash syntax ($VAR, ${VAR}).
// dotenv-expand is designed to process an object of key-value pairs (like a .env file).
// To expand a single string, we wrap it in an object with a temporary key.
const dummyKey = '__GCLI_EXPAND_TARGET__';
// Filter out undefined values to satisfy the Record<string, string> requirement safely
const processEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(env)) {
if (value !== undefined) {
processEnv[key] = value;
}
}
const result = expand({
parsed: { [dummyKey]: processedStr },
processEnv,
});
return result.parsed?.[dummyKey] ?? '';
}