프로그래밍/iOS,macOS
[swift] 커맨드라인 실행 파일 만들기(swift-argument-parser)
chance
2025. 10. 16. 05:16
swift 파일을 입력받아 swift 클래스목록을 파일로 출력하는 간단한 명령줄 실행 파일을 만들어보자.
명령줄 실행 및 인자 파싱을 위해 swift-argument-parser 사용
swift 스크립트 파싱을 위해 swift-syntax 사용
패키지 설정 및 dependencies 추가
타겟 설정
let package = Package(
name: "MyPackage",
products: [
.excutable(name: "MyCommand", targets: ["MyCommand"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
.package(url: "https://github.com/siwftlang/swift-syntax", from: "600.0.1"),
],
targets: [
.executableTarget(
name: "MyCommand",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax"),
]
)
]
)
구현
swift-argument-parser 는 ParsableCommand 프로토콜을 준수해 구현해 줘야 한다.
ParsableCommand 구현하는 객체를 @main 으로 엔트리포인트로 설정한다.
엔트리포인트 객체는 static func main() 메쏘드를 제공해야 하는데,
PasableCommand 에 main 함수가 구현되어 있어, 실행시 run() 메쏘드를 호출해준다.
MyCommand.swift
import Foundation
import ArgumentParser
import SwiftParser
@main
struct MyCommand: ParsableCommand {
// 기본 실행파일명이 케밥 케이스(my-command)로 표시되므로, 실제 파일명으로 정의해 준다.
// abstract: 는 파일 실행 시 OVERVIEW : 옆에 표시할 문자열
static let configuration = CommandConfiguration(
commandName: "MyCommand",
abstract: "show swift classes"
)
// 옵션과 인자목록은 아래 선언 순서에 따라 USAGE에 표시된다.
// 실행파일 사용시 정의된 변수명이 아닌 케밥 케이스로 변경된 이름으로 사용하게 된다.
// 옵션 정의
// .shortAndLong의 형식은 -o, —output-file 두가지 모두 사용
@Option(name: .shortAndLong, help: "Output file")
var outputFile: String
// 기본값이 있는 옵션, 입력하지 않아도 오류가 나지 않고, usage에 [--flag <flag>] 로 표시
@Option(name: .shortAndLong, help: "Flag")
var flag: Bool = false
// 입력받을 인자 목록 정의
@Argument(help: "Source files")
var sourceFiles: [String]
// run 구현
mutating func run() throws {
// swift 파일을 파싱하기 위한 SyntaxVisitor
let visitor = MySwiftVisitor(viewMode: .all, logger: log)
// 입력받은 argument 목록을 돌며 파일 가져오기
log("source files")
for file in sourceFiles {
log("file : \(file)")
let source = try String(contentsOf: URL(fileURLWithPath: file))
// swift-syntax 로 AST 로 변경
let fileSyntax = Parser.parse(source: source)
// visitor의 visit 실행
visitor.walk(fileSyntax)
}
// 출력할 데이터
let count = visitor.classes.count
let names = visitor.classes.map { $0 }.joined(separator: "\n")
let output = "count: \(count)\nclasses:\(names)"
// 출력 파일
let outURL = URL(fileURLWithPath: outputFile)
try output.write(to: outURL, atomically: true, encoding: .utf8)
log("finished")
}
// 로그 출력
func log(_ message: String) {
print(message)
}
}
MySwiftVisitor.swift
swift 파싱하기 위한 visitor 작성
import Foundation
import SwiftSyntax
public class MySwiftVisitor: SyntaxVisitor {
internal init(viewMode: SyntaxTreeViewMode, logger: @escaping ((String) -> Void)) {
self.logger = logger
super.init(viewMode: viewMode)
}
public let logger: ((String) -> Void)
public var classes: [String] = []
public override func visit(_ classDecl: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
let className = classDecl.name.trimmedDescription
logger("class: \(className)")
classes.append(className)
for attribute in classDecl.attributes {
if let attributeSyntax = attribute.as(AttributeSyntax.self) {
let attributeName = attributeSyntax.attributeName.trimmedDescription
logger("attribute: \(attributeName)")
}
}
return .skipChildren
}
}
실행
./MyCommand --output-file out /swift-file-path/swift-file.swift
플러그인
위에 작성한 커맨드라인 실행파일을 타겟 빌드 과정에서 자동으로 실행시키려면 plugin 을 설정하면 된다.
혹시 나중에 쓸일이 있을까 해서 참고삼아 정리.
패키지 추가
products와 targets에 plugin을 추가한다.
let package = Package(
name: "MyPackage",
products: [
.excutable(name: "MyCommand", targets: ["MyCommand"]),
.plugin(name: "RunnerPlugin", targets: ["RunnerPlugin"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
.package(url: "https://github.com/siwftlang/swift-syntax", from: "600.0.1"),
],
targets: [
.executableTarget(
name: "MyCommand",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax"),
]
),
.plugin(
name: "RunnerPlugin",
capability: .buildTool(),
dependencies: ["MyCommand"]
)
]
)
플러그인 코드 작성
플러그인은 /package/Plugins/plugin-name 에 위치해야 한다.
RunnerPlugin.swift
import Foundation
import PackagePlugin
@main
struct RunnerPlugin: BuildToolPlugin {
func createBuildCommands(context: PluginContext,
target: any Target) async throws -> [Command] {
// 타겟에 포함된 소스 파일 목록
guard let sourcesFiles = target.sourceModule?.sourceFiles else { return [] }
// 실행할 커맨드라인 실행 파일
let executableFilePath = try context.tool(named: "MyCommand").url
// 결과 출력 파일
let output = context.pluginWorkDirectoryURL
.appending(path: "Output")
.appending(path: "out.dat")
// 입력 파일 목록
let inputFiles = sourceFile.map(\.url)
// 실행 파일에 전달할 인자 목록
let arguments = ["-o", output.path] + inputFiles.map(\.path)
return [
Command.buildCommand(
displayName: "run MyCommand",
executable: executableFilePath,
arguments: arguments,
inputFiles: inputFiles,
outputFiles: [output]
)
]
}
}
Command.buildCommand() 는 빌드 프로세스의 한 부분으로 동작하며, 입력과 출력을 지정한다.
Command.prebuildCommand() 는 빌드가 진행되기전에 실행된다.