diff --git a/Sources/Subprocess/Configuration.swift b/Sources/Subprocess/Configuration.swift index fe3be73..2031e59 100644 --- a/Sources/Subprocess/Configuration.swift +++ b/Sources/Subprocess/Configuration.swift @@ -401,7 +401,7 @@ extension Arguments: CustomStringConvertible, CustomDebugStringConvertible { /// A set of environment variables to use when executing the subprocess. public struct Environment: Sendable, Hashable { internal enum Configuration: Sendable, Hashable { - case inherit([Key: String]) + case inherit([Key: String?]) case custom([Key: String]) #if !os(Windows) case rawBytes([[UInt8]]) @@ -418,8 +418,10 @@ public struct Environment: Sendable, Hashable { public static var inherit: Self { return .init(config: .inherit([:])) } - /// Override the provided `newValue` in the existing `Environment` - public func updating(_ newValue: [Key: String]) -> Self { + /// Override the provided `newValue` in the existing `Environment`. + /// Keys with `nil` values in `newValue` will be removed from existing + /// `Environment` before passing to child process. + public func updating(_ newValue: [Key: String?]) -> Self { return .init(config: .inherit(newValue)) } /// Use custom environment variables diff --git a/Sources/Subprocess/Platforms/Subprocess+Unix.swift b/Sources/Subprocess/Platforms/Subprocess+Unix.swift index 98eec1d..e66cd07 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Unix.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Unix.swift @@ -203,9 +203,13 @@ extension Environment { var current = Self.currentEnvironmentValues() for (key, value) in updates { // Remove the value from current to override it + // If the `value` is nil, we effectively "unset" + // this value from current current.removeValue(forKey: key) - let fullString = "\(key)=\(value)" - env.append(strdup(fullString)) + if let value { + let fullString = "\(key)=\(value)" + env.append(strdup(fullString)) + } } // Add the rest of `current` to env for (key, value) in current { diff --git a/Sources/Subprocess/Platforms/Subprocess+Windows.swift b/Sources/Subprocess/Platforms/Subprocess+Windows.swift index 371ad20..e010e5f 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Windows.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Windows.swift @@ -1015,7 +1015,13 @@ extension Configuration { // Combine current environment env = Environment.currentEnvironmentValues() for (key, value) in updateValues { - env.updateValue(value, forKey: key) + if let value { + // Update env with ew value + env.updateValue(value, forKey: key) + } else { + // If `value` is `nil`, unset this value from env + env.removeValue(forKey: key) + } } } // On Windows, the PATH is required in order to locate dlls needed by diff --git a/Tests/SubprocessTests/IntegrationTests.swift b/Tests/SubprocessTests/IntegrationTests.swift index a3da958..c11016c 100644 --- a/Tests/SubprocessTests/IntegrationTests.swift +++ b/Tests/SubprocessTests/IntegrationTests.swift @@ -310,6 +310,82 @@ extension SubprocessIntegrationTests { ) } + @Test func testEnvironmentInheritOverrideUnsetValue() async throws { + #if os(Windows) + // First insert our dummy environment variable + "SubprocessTest".withCString(encodedAs: UTF16.self) { keyW in + "value".withCString(encodedAs: UTF16.self) { valueW in + SetEnvironmentVariableW(keyW, valueW) + } + } + defer { + "SubprocessTest".withCString(encodedAs: UTF16.self) { keyW in + SetEnvironmentVariableW(keyW, nil) + } + } + + var setup = TestSetup( + executable: .name("cmd.exe"), + arguments: ["/c", "echo %SubprocessTest%"], + environment: .inherit + ) + #else + // First insert our dummy environment variable + setenv("SubprocessTest", "value", 1); + defer { + unsetenv("SubprocessTest") + } + + var setup = TestSetup( + executable: .path("/bin/sh"), + arguments: ["-c", "printenv SubprocessTest"], + environment: .inherit + ) + #endif + // First make sure child process actually inherited SubprocessTest + let result = try await _run( + setup, + input: .none, + output: .string(limit: 32), + error: .discarded + ) + #expect(result.terminationStatus.isSuccess) + // rdar://138670128 + // https://github.com/swiftlang/swift/issues/77235 + let output = result.standardOutput? + .trimmingNewLineAndQuotes() + #expect(output == "value") + + // Now make sure we can remove `SubprocessTest` + #if os(Windows) + setup = TestSetup( + executable: .name("cmd.exe"), + arguments: ["/c", "echo %SubprocessTest%"], + environment: .inherit + .updating(["SubprocessTest": nil]) + ) + #else + setup = TestSetup( + executable: .path("/bin/sh"), + arguments: ["-c", "printenv SubprocessTest"], + environment: .inherit + .updating(["SubprocessTest": nil]) + ) + #endif + let result2 = try await _run( + setup, + input: .none, + output: .string(limit: 32), + error: .discarded + ) + // The new echo/printenv should not find `SubprocessTest` + #if os(Windows) + #expect(result2.standardOutput?.trimmingNewLineAndQuotes() == "%SubprocessTest%") + #else + #expect(result2.terminationStatus == .exited(1)) + #endif + } + @Test( // Make sure we don't accidentally have this dummy value .enabled(if: ProcessInfo.processInfo.environment["SystemRoot"] != nil)