From a8ca48f61477475b4d5dd9fe65debbb0f74181b1 Mon Sep 17 00:00:00 2001 From: James Lau Date: Tue, 16 Jun 2026 10:49:04 +0800 Subject: [PATCH] feat(compose): add --env-file and --workdir flags Add --env-file and --workdir/-w as top-level options on ComposeFileOptions, matching Docker Compose CLI semantics. These flags were previously inherited from Flags.Process (the container runtime dependency) which conflated compose-level concerns with container runtime flags. - Add --env-file to ComposeFileOptions for specifying alternate .env files - Add --workdir/-w to ComposeFileOptions for alternate working directory - Remove Flags.Process from ComposeUp, ComposeBuild, ComposeDown - ComposeDown no longer needs import ContainerCommands - Add 7 parsing tests covering all subcommands and defaults Co-authored-by: Cursor --- .../Commands/ComposeBuild.swift | 7 +--- .../Commands/ComposeDown.swift | 8 +--- .../Commands/ComposeFileOptions.swift | 12 ++++++ .../Commands/ComposeUp.swift | 7 +--- .../ComposeCommandParsingTests.swift | 42 +++++++++++++++++++ 5 files changed, 60 insertions(+), 16 deletions(-) diff --git a/Sources/Container-Compose/Commands/ComposeBuild.swift b/Sources/Container-Compose/Commands/ComposeBuild.swift index 5abde194..90cb8ace 100644 --- a/Sources/Container-Compose/Commands/ComposeBuild.swift +++ b/Sources/Container-Compose/Commands/ComposeBuild.swift @@ -45,13 +45,10 @@ public struct ComposeBuild: AsyncParsableCommand, @unchecked Sendable { @Flag(name: .long, help: "Do not use cache when building") var noCache: Bool = false - @OptionGroup - var process: Flags.Process - @OptionGroup var logging: Flags.Logging - private var cwd: String { process.cwd ?? FileManager.default.currentDirectoryPath } + private var cwd: String { composeFileOptions.workdir ?? FileManager.default.currentDirectoryPath } private var cwdURL: URL { URL(fileURLWithPath: cwd) } @@ -80,7 +77,7 @@ public struct ComposeBuild: AsyncParsableCommand, @unchecked Sendable { } private var envFilePath: String { - let envFile = process.envFile.first ?? ".env" + let envFile = composeFileOptions.envFile ?? ".env" return resolvedPath(for: envFile, relativeTo: cwdURL) } diff --git a/Sources/Container-Compose/Commands/ComposeDown.swift b/Sources/Container-Compose/Commands/ComposeDown.swift index 30240c65..8904023f 100644 --- a/Sources/Container-Compose/Commands/ComposeDown.swift +++ b/Sources/Container-Compose/Commands/ComposeDown.swift @@ -22,7 +22,6 @@ // import ArgumentParser -import ContainerCommands import ContainerAPIClient import Foundation import Yams @@ -38,14 +37,11 @@ public struct ComposeDown: AsyncParsableCommand { @Argument(help: "Specify the services to stop") var services: [String] = [] - @OptionGroup - var process: Flags.Process - - private var cwd: String { process.cwd ?? FileManager.default.currentDirectoryPath } - @OptionGroup var composeFileOptions: ComposeFileOptions + private var cwd: String { composeFileOptions.workdir ?? FileManager.default.currentDirectoryPath } + private static let supportedComposeFilenames = [ "compose.yml", "compose.yaml", diff --git a/Sources/Container-Compose/Commands/ComposeFileOptions.swift b/Sources/Container-Compose/Commands/ComposeFileOptions.swift index e27782af..7fb82fcd 100644 --- a/Sources/Container-Compose/Commands/ComposeFileOptions.swift +++ b/Sources/Container-Compose/Commands/ComposeFileOptions.swift @@ -18,7 +18,19 @@ import ArgumentParser public struct ComposeFileOptions: ParsableArguments, Sendable { public init() {} + + public init(composeFilename: String? = nil, envFile: String? = nil, workdir: String? = nil) { + self.composeFilename = composeFilename + self.envFile = envFile + self.workdir = workdir + } @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") public var composeFilename: String? + + @Option(name: .long, help: "Specify an alternate environment file") + public var envFile: String? + + @Option(name: [.customShort("w"), .customLong("workdir")], help: "Specify an alternate working directory (default: the current directory)") + public var workdir: String? } diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index 97a16f96..87ee2b7a 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -75,7 +75,7 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { } private var envFilePath: String { - let envFile = process.envFile.first ?? ".env" + let envFile = composeFileOptions.envFile ?? ".env" return resolvedPath(for: envFile, relativeTo: cwdURL) } @@ -89,13 +89,10 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { @Flag(name: .long, help: "Do not use cache") var noCache: Bool = false - @OptionGroup - var process: Flags.Process - @OptionGroup var logging: Flags.Logging - private var cwd: String { process.cwd ?? FileManager.default.currentDirectoryPath } + private var cwd: String { composeFileOptions.workdir ?? FileManager.default.currentDirectoryPath } private var fileManager: FileManager { FileManager.default } private var projectName: String? diff --git a/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift b/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift index 121e1b0d..97fdb0ab 100644 --- a/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift +++ b/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift @@ -24,4 +24,46 @@ struct ComposeCommandParsingTests { let cmd = try Main.parseAsRoot(["-f", "my-compose.yaml", "up"]) as! ComposeUp #expect(cmd.composeFileOptions.composeFilename == "my-compose.yaml") } + + @Test("Main+ComposeUp command accepts --env-file flag from root") + func composeUpCommandAcceptsEnvFileFlag() throws { + let cmd = try Main.parseAsRoot(["--env-file", "custom.env", "up"]) as! ComposeUp + #expect(cmd.composeFileOptions.envFile == "custom.env") + } + + @Test("Main+ComposeUp command accepts --env-file flag on subcommand") + func composeUpCommandAcceptsEnvFileFlagOnSubcommand() throws { + let cmd = try Main.parseAsRoot(["up", "--env-file", "custom.env"]) as! ComposeUp + #expect(cmd.composeFileOptions.envFile == "custom.env") + } + + @Test("Main+ComposeUp command accepts -w flag for workdir") + func composeUpCommandAcceptsWorkdirFlag() throws { + let cmd = try Main.parseAsRoot(["-w", "/some/path", "up"]) as! ComposeUp + #expect(cmd.composeFileOptions.workdir == "/some/path") + } + + @Test("Main+ComposeDown command accepts --env-file flag") + func composeDownCommandAcceptsEnvFileFlag() throws { + let cmd = try Main.parseAsRoot(["--env-file", "prod.env", "down"]) as! ComposeDown + #expect(cmd.composeFileOptions.envFile == "prod.env") + } + + @Test("Main+ComposeBuild command accepts --env-file flag") + func composeBuildCommandAcceptsEnvFileFlag() throws { + let cmd = try Main.parseAsRoot(["--env-file", "build.env", "build"]) as! ComposeBuild + #expect(cmd.composeFileOptions.envFile == "build.env") + } + + @Test("envFile is nil when not specified") + func envFileDefaultsToNil() throws { + let cmd = try Main.parseAsRoot(["up"]) as! ComposeUp + #expect(cmd.composeFileOptions.envFile == nil) + } + + @Test("workdir is nil when not specified") + func workdirDefaultsToNil() throws { + let cmd = try Main.parseAsRoot(["up"]) as! ComposeUp + #expect(cmd.composeFileOptions.workdir == nil) + } }