diff --git a/Sources/Container-Compose/Application.swift b/Sources/Container-Compose/Application.swift index b23c2850..b17bf1d7 100644 --- a/Sources/Container-Compose/Application.swift +++ b/Sources/Container-Compose/Application.swift @@ -33,6 +33,9 @@ public struct Main: AsyncParsableCommand { ComposeBuild.self, Version.self ]) + + @OptionGroup + var composeFileOptions: ComposeFileOptions public init() {} } diff --git a/Sources/Container-Compose/Commands/ComposeBuild.swift b/Sources/Container-Compose/Commands/ComposeBuild.swift index 8a2ee1b2..5abde194 100644 --- a/Sources/Container-Compose/Commands/ComposeBuild.swift +++ b/Sources/Container-Compose/Commands/ComposeBuild.swift @@ -39,8 +39,8 @@ public struct ComposeBuild: AsyncParsableCommand, @unchecked Sendable { @Argument(help: "Services to build (builds all if omitted)") var services: [String] = [] - @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") - var composeFilename: String? + @OptionGroup + var composeFileOptions: ComposeFileOptions @Flag(name: .long, help: "Do not use cache when building") var noCache: Bool = false @@ -63,7 +63,7 @@ public struct ComposeBuild: AsyncParsableCommand, @unchecked Sendable { ] private var composePath: String { - if let composeFilename { + if let composeFilename = composeFileOptions.composeFilename { return resolvedPath(for: composeFilename, relativeTo: cwdURL) } for filename in Self.supportedComposeFilenames { diff --git a/Sources/Container-Compose/Commands/ComposeDown.swift b/Sources/Container-Compose/Commands/ComposeDown.swift index 6770d11d..30240c65 100644 --- a/Sources/Container-Compose/Commands/ComposeDown.swift +++ b/Sources/Container-Compose/Commands/ComposeDown.swift @@ -43,8 +43,8 @@ public struct ComposeDown: AsyncParsableCommand { private var cwd: String { process.cwd ?? FileManager.default.currentDirectoryPath } - @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") - var composeFilename: String? + @OptionGroup + var composeFileOptions: ComposeFileOptions private static let supportedComposeFilenames = [ "compose.yml", @@ -58,7 +58,7 @@ public struct ComposeDown: AsyncParsableCommand { } private var composePath: String { - if let composeFilename { + if let composeFilename = composeFileOptions.composeFilename { return resolvedPath(for: composeFilename, relativeTo: cwdURL) } diff --git a/Sources/Container-Compose/Commands/ComposeFileOptions.swift b/Sources/Container-Compose/Commands/ComposeFileOptions.swift new file mode 100644 index 00000000..e27782af --- /dev/null +++ b/Sources/Container-Compose/Commands/ComposeFileOptions.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Morris Richman and the Container-Compose project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ArgumentParser + +public struct ComposeFileOptions: ParsableArguments, Sendable { + public init() {} + + @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") + public var composeFilename: String? +} diff --git a/Sources/Container-Compose/Commands/ComposeUp.swift b/Sources/Container-Compose/Commands/ComposeUp.swift index ebb6360f..3a874f08 100644 --- a/Sources/Container-Compose/Commands/ComposeUp.swift +++ b/Sources/Container-Compose/Commands/ComposeUp.swift @@ -46,8 +46,8 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { help: "Detaches from container logs. Note: If you do NOT detach, killing this process will NOT kill the container. To kill the container, run container-compose down") var detach: Bool = false - @Option(name: [.customShort("f"), .customLong("file")], help: "The path to your Docker Compose file") - var composeFilename: String? + @OptionGroup + var composeFileOptions: ComposeFileOptions private static let supportedComposeFilenames = [ "compose.yml", @@ -61,7 +61,7 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { } private var composePath: String { - if let composeFilename { + if let composeFilename = composeFileOptions.composeFilename { return resolvedPath(for: composeFilename, relativeTo: cwdURL) } @@ -524,7 +524,7 @@ public struct ComposeUp: AsyncParsableCommand, @unchecked Sendable { runCommandArgs.append(networkToConnect) } print( - "Info: Service '\(serviceName)' is configured to connect to networks: \(serviceNetworks.joined(separator: ", ")) ascertained from networks attribute in \(composeFilename)." + "Info: Service '\(serviceName)' is configured to connect to networks: \(serviceNetworks.joined(separator: ", ")) ascertained from networks attribute in \(composePath)." ) print( "Note: This tool assumes custom networks are defined at the top-level 'networks' key or are pre-existing. This tool does not create implicit networks for services if not explicitly defined at the top-level." diff --git a/Tests/Container-Compose-StaticTests/ComposeBuildParsingTests.swift b/Tests/Container-Compose-StaticTests/ComposeBuildParsingTests.swift index 208d6a9f..1355fde9 100644 --- a/Tests/Container-Compose-StaticTests/ComposeBuildParsingTests.swift +++ b/Tests/Container-Compose-StaticTests/ComposeBuildParsingTests.swift @@ -163,6 +163,6 @@ struct ComposeBuildParsingTests { @Test("ComposeBuild command accepts -f flag for compose file") func composeBuildCommandAcceptsFileFlag() throws { let cmd = try ComposeBuild.parse(["-f", "my-compose.yaml"]) - #expect(cmd.composeFilename == "my-compose.yaml") + #expect(cmd.composeFileOptions.composeFilename == "my-compose.yaml") } } diff --git a/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift b/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift new file mode 100644 index 00000000..121e1b0d --- /dev/null +++ b/Tests/Container-Compose-StaticTests/ComposeCommandParsingTests.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Morris Richman and the Container-Compose project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Testing +@testable import ContainerComposeCore + +@Suite("Compose command parsing") +struct ComposeCommandParsingTests { + @Test("Main+ComposeUp command accepts -f flag for compose file from root") + func composeUpCommandAcceptsFileFlag() throws { + let cmd = try Main.parseAsRoot(["-f", "my-compose.yaml", "up"]) as! ComposeUp + #expect(cmd.composeFileOptions.composeFilename == "my-compose.yaml") + } +}