From a821a3fc6665ce10da467d2f1d34e346fe33101d Mon Sep 17 00:00:00 2001 From: Rauhul Varma Date: Sun, 28 Feb 2021 21:17:41 -0800 Subject: [PATCH 1/2] Add experimental manual page generation - Adds a swift package manager command plugin called GenerateManualPlugin. The plugin can be invoked from the command line using `swift package experimental-generate-manual`. The plugin is prefixed for now with "experimental-" to indicate it is not mature and may see breaking changes to its CLI and output in the future. The plugin can be can be used to generate a manual in MDoc syntax for any swift-argument-parser tool that can be executed via `tool --experimental-dump-info`. - The plugin works by converting the `ToolInfoV0` structure from the `ArgumentParserToolInfo` library into MDoc AST nodes using a custom (SwiftUI-esk) result builder DSL. The MDoc AST is then lowered to a string and written to disk. - The MDoc AST included is not general purpose and doesn't represent the true language exactly, so it is private to the underlying `generate-manual` tool. In the future it would be interesting to finish fleshing out this MDoc library and spin it out, however this is not a priority. - Next steps include: - Improving the command line interface for the plugin. - Adding support for "extended discussions" to Commands and exposing this information in manuals. - Further improve the escaping logic to properly escape MDoc macros that might happen to appear in user's help strings. - Ingesting external content a-la swift-docc so the entire tool documentation does not need to be included in the binary itself. - Bug fixes and addressing developer/user feedback. Built with love, @rauhul --- Package.swift | 6 + Package@swift-5.6.swift | 101 ++ .../GenerateManualPlugin.swift | 95 ++ .../GenerateManualPluginError.swift | 50 + .../PackagePlugin+Helpers.swift | 93 + Tools/generate-manual/AuthorArgument.swift | 68 + .../DSL/ArgumentSynopsis.swift | 42 + Tools/generate-manual/DSL/Author.swift | 37 + Tools/generate-manual/DSL/Authors.swift | 30 + .../generate-manual/DSL/Core/Container.swift | 19 + Tools/generate-manual/DSL/Core/Empty.swift | 15 + Tools/generate-manual/DSL/Core/ForEach.swift | 34 + .../DSL/Core/MDocASTNodeWrapper.swift | 16 + .../DSL/Core/MDocBuilder.swift | 21 + .../DSL/Core/MDocComponent.swift | 20 + Tools/generate-manual/DSL/Document.swift | 38 + Tools/generate-manual/DSL/DocumentDate.swift | 37 + Tools/generate-manual/DSL/Exit.swift | 25 + Tools/generate-manual/DSL/List.swift | 26 + .../DSL/MultiPageDescription.swift | 43 + Tools/generate-manual/DSL/Name.swift | 26 + Tools/generate-manual/DSL/Preamble.swift | 27 + Tools/generate-manual/DSL/Section.swift | 27 + Tools/generate-manual/DSL/SeeAlso.swift | 32 + .../DSL/SinglePageDescription.swift | 53 + Tools/generate-manual/DSL/Synopsis.swift | 36 + .../Extensions/ArgumentParser+MDoc.swift | 100 ++ .../Date+ExpressibleByArgument.swift | 47 + .../Extensions/Process+SimpleAPI.swift | 74 + Tools/generate-manual/GenerateManual.swift | 119 ++ Tools/generate-manual/MDoc/MDocASTNode.swift | 42 + Tools/generate-manual/MDoc/MDocMacro.swift | 1510 +++++++++++++++++ .../MDoc/MDocSerializationContext.swift | 17 + .../MDoc/String+Escaping.swift | 97 ++ 34 files changed, 3023 insertions(+) create mode 100644 Package@swift-5.6.swift create mode 100644 Plugins/GenerateManualPlugin/GenerateManualPlugin.swift create mode 100644 Plugins/GenerateManualPlugin/GenerateManualPluginError.swift create mode 100644 Plugins/GenerateManualPlugin/PackagePlugin+Helpers.swift create mode 100644 Tools/generate-manual/AuthorArgument.swift create mode 100644 Tools/generate-manual/DSL/ArgumentSynopsis.swift create mode 100644 Tools/generate-manual/DSL/Author.swift create mode 100644 Tools/generate-manual/DSL/Authors.swift create mode 100644 Tools/generate-manual/DSL/Core/Container.swift create mode 100644 Tools/generate-manual/DSL/Core/Empty.swift create mode 100644 Tools/generate-manual/DSL/Core/ForEach.swift create mode 100644 Tools/generate-manual/DSL/Core/MDocASTNodeWrapper.swift create mode 100644 Tools/generate-manual/DSL/Core/MDocBuilder.swift create mode 100644 Tools/generate-manual/DSL/Core/MDocComponent.swift create mode 100644 Tools/generate-manual/DSL/Document.swift create mode 100644 Tools/generate-manual/DSL/DocumentDate.swift create mode 100644 Tools/generate-manual/DSL/Exit.swift create mode 100644 Tools/generate-manual/DSL/List.swift create mode 100644 Tools/generate-manual/DSL/MultiPageDescription.swift create mode 100644 Tools/generate-manual/DSL/Name.swift create mode 100644 Tools/generate-manual/DSL/Preamble.swift create mode 100644 Tools/generate-manual/DSL/Section.swift create mode 100644 Tools/generate-manual/DSL/SeeAlso.swift create mode 100644 Tools/generate-manual/DSL/SinglePageDescription.swift create mode 100644 Tools/generate-manual/DSL/Synopsis.swift create mode 100644 Tools/generate-manual/Extensions/ArgumentParser+MDoc.swift create mode 100644 Tools/generate-manual/Extensions/Date+ExpressibleByArgument.swift create mode 100644 Tools/generate-manual/Extensions/Process+SimpleAPI.swift create mode 100644 Tools/generate-manual/GenerateManual.swift create mode 100644 Tools/generate-manual/MDoc/MDocASTNode.swift create mode 100644 Tools/generate-manual/MDoc/MDocMacro.swift create mode 100644 Tools/generate-manual/MDoc/MDocSerializationContext.swift create mode 100644 Tools/generate-manual/MDoc/String+Escaping.swift diff --git a/Package.swift b/Package.swift index 29529eb8a..ec8ac2311 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,7 @@ var package = Package( ], dependencies: [], targets: [ + // Core Library .target( name: "ArgumentParser", dependencies: ["ArgumentParserToolInfo"], @@ -34,6 +35,7 @@ var package = Package( dependencies: [], exclude: ["CMakeLists.txt"]), + // Examples .executableTarget( name: "roll", dependencies: ["ArgumentParser"], @@ -47,6 +49,7 @@ var package = Package( dependencies: ["ArgumentParser"], path: "Examples/repeat"), + // Tests .testTarget( name: "ArgumentParserEndToEndTests", dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], @@ -68,10 +71,13 @@ var package = Package( #if swift(>=5.6) && os(macOS) package.targets.append(contentsOf: [ + // Examples .executableTarget( name: "count-lines", dependencies: ["ArgumentParser"], path: "Examples/count-lines"), + + // Tools .executableTarget( name: "changelog-authors", dependencies: ["ArgumentParser"], diff --git a/Package@swift-5.6.swift b/Package@swift-5.6.swift new file mode 100644 index 000000000..dfd7547e7 --- /dev/null +++ b/Package@swift-5.6.swift @@ -0,0 +1,101 @@ +// swift-tools-version:5.6 +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import PackageDescription + +var package = Package( + name: "swift-argument-parser", + products: [ + .library( + name: "ArgumentParser", + targets: ["ArgumentParser"]), + ], + dependencies: [], + targets: [ + // Core Library + .target( + name: "ArgumentParser", + dependencies: ["ArgumentParserToolInfo"], + exclude: ["CMakeLists.txt"]), + .target( + name: "ArgumentParserTestHelpers", + dependencies: ["ArgumentParser", "ArgumentParserToolInfo"], + exclude: ["CMakeLists.txt"]), + .target( + name: "ArgumentParserToolInfo", + dependencies: [ ], + exclude: ["CMakeLists.txt"]), + + // Plugins + .plugin( + name: "GenerateManualPlugin", + capability: .command( + intent: .custom( + verb: "experimental-generate-manual", + description: "Generate a manual entry for a specified target.")), + dependencies: ["generate-manual"]), + + // Examples + .executableTarget( + name: "roll", + dependencies: ["ArgumentParser"], + path: "Examples/roll"), + .executableTarget( + name: "math", + dependencies: ["ArgumentParser"], + path: "Examples/math"), + .executableTarget( + name: "repeat", + dependencies: ["ArgumentParser"], + path: "Examples/repeat"), + + // Tools + .executableTarget( + name: "generate-manual", + dependencies: ["ArgumentParser", "ArgumentParserToolInfo"], + path: "Tools/generate-manual"), + + // Tests + .testTarget( + name: "ArgumentParserEndToEndTests", + dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], + exclude: ["CMakeLists.txt"]), + .testTarget( + name: "ArgumentParserUnitTests", + dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], + exclude: ["CMakeLists.txt"]), + .testTarget( + name: "ArgumentParserPackageManagerTests", + dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], + exclude: ["CMakeLists.txt"]), + .testTarget( + name: "ArgumentParserExampleTests", + dependencies: ["ArgumentParserTestHelpers"], + resources: [.copy("CountLinesTest.txt")]), + ] +) + +#if os(macOS) +package.targets.append(contentsOf: [ + // Examples + .executableTarget( + name: "count-lines", + dependencies: ["ArgumentParser"], + path: "Examples/count-lines"), + + // Tools + .executableTarget( + name: "changelog-authors", + dependencies: ["ArgumentParser"], + path: "Tools/changelog-authors"), + ]) +#endif diff --git a/Plugins/GenerateManualPlugin/GenerateManualPlugin.swift b/Plugins/GenerateManualPlugin/GenerateManualPlugin.swift new file mode 100644 index 000000000..636d756eb --- /dev/null +++ b/Plugins/GenerateManualPlugin/GenerateManualPlugin.swift @@ -0,0 +1,95 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import PackagePlugin +import Foundation + +@main +struct GenerateManualPlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) async throws { + // Locate generation tool. + let generationToolFile = try context.tool(named: "generate-manual").path + + // Create an extractor to extract plugin-only arguments from the `arguments` + // array. + var extractor = ArgumentExtractor(arguments) + + // Run generation tool once if help is requested. + if extractor.helpRequest() { + try generationToolFile.exec(arguments: ["--help"]) + print(""" + ADDITIONAL OPTIONS: + --configuration + Tool build configuration used to generate the + manual. (default: release) + + NOTE: The "GenerateManual" plugin handles passing the "" and + "--output-directory " arguments. Manually supplying + these arguments will result in a runtime failure. + """) + return + } + + // Extract configuration argument before making it to the + // "generate-manual" tool. + let configuration = try extractor.configuration() + + // Build all products first. + print("Building package in \(configuration) mode...") + let buildResult = try packageManager.build( + .all(includingTests: false), + parameters: .init(configuration: configuration)) + + guard buildResult.succeeded else { + throw GenerateManualPluginError.buildFailed(buildResult.logText) + } + print("Built package in \(configuration) mode") + + // Run generate-manual on all executable artifacts. + for builtArtifact in buildResult.builtArtifacts { + // Skip non-executable targets + guard builtArtifact.kind == .executable else { continue } + + // Skip executables without a matching product. + guard let product = builtArtifact.matchingProduct(context: context) + else { continue } + + // Skip products without a dependency on ArgumentParser. + guard product.hasDependency(named: "ArgumentParser") else { continue } + + // Get the artifacts name. + let executableName = builtArtifact.path.lastComponent + print("Generating manual for \(executableName)...") + + // Create output directory. + let outputDirectory = context + .pluginWorkDirectory + .appending(executableName) + try outputDirectory.createOutputDirectory() + + // Create generation tool arguments. + var generationToolArguments = [ + builtArtifact.path.string, + "--output-directory", + outputDirectory.string + ] + generationToolArguments.append( + contentsOf: extractor.unextractedOptionsOrFlags) + + // Spawn generation tool. + try generationToolFile.exec(arguments: generationToolArguments) + print("Generated manual in '\(outputDirectory)'") + } + } +} diff --git a/Plugins/GenerateManualPlugin/GenerateManualPluginError.swift b/Plugins/GenerateManualPlugin/GenerateManualPluginError.swift new file mode 100644 index 000000000..24b79210f --- /dev/null +++ b/Plugins/GenerateManualPlugin/GenerateManualPluginError.swift @@ -0,0 +1,50 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import Foundation +import PackagePlugin + +enum GenerateManualPluginError: Error { + case unknownBuildConfiguration(String) + case buildFailed(String) + case createOutputDirectoryFailed(Error) + case subprocessFailedNonZeroExit(Path, Int32) + case subprocessFailedError(Path, Error) +} + +extension GenerateManualPluginError: CustomStringConvertible { + var description: String { + switch self { + case .unknownBuildConfiguration(let configuration): + return "Build failed: Unknown build configuration '\(configuration)'." + case .buildFailed(let logText): + return "Build failed: \(logText)." + case .createOutputDirectoryFailed(let error): + return """ + Failed to create output directory: '\(error.localizedDescription)' + """ + case .subprocessFailedNonZeroExit(let tool, let exitCode): + return """ + '\(tool.lastComponent)' invocation failed with a nonzero exit code: \ + '\(exitCode)'. + """ + case .subprocessFailedError(let tool, let error): + return """ + '\(tool.lastComponent)' invocation failed: \ + '\(error.localizedDescription)' + """ + } + } +} + +extension GenerateManualPluginError: LocalizedError { + var localizedDescription: String { self.description } +} diff --git a/Plugins/GenerateManualPlugin/PackagePlugin+Helpers.swift b/Plugins/GenerateManualPlugin/PackagePlugin+Helpers.swift new file mode 100644 index 000000000..1aed3a81e --- /dev/null +++ b/Plugins/GenerateManualPlugin/PackagePlugin+Helpers.swift @@ -0,0 +1,93 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import Foundation +import PackagePlugin + +extension ArgumentExtractor { + mutating func helpRequest() -> Bool { + self.extractFlag(named: "help") > 0 + } + + mutating func configuration() throws -> PackageManager.BuildConfiguration { + switch self.extractOption(named: "configuration").first { + case .some(let configurationString): + switch configurationString { + case "debug": + return .debug + case "release": + return .release + default: + throw GenerateManualPluginError + .unknownBuildConfiguration(configurationString) + } + case .none: + return .release + } + } +} + +extension Path { + func createOutputDirectory() throws { + do { + try FileManager.default.createDirectory( + atPath: self.string, + withIntermediateDirectories: true) + } catch { + throw GenerateManualPluginError.createOutputDirectoryFailed(error) + } + } + + func exec(arguments: [String]) throws { + do { + let process = Process() + process.executableURL = URL(fileURLWithPath: self.string) + process.arguments = arguments + try process.run() + process.waitUntilExit() + guard + process.terminationReason == .exit, + process.terminationStatus == 0 + else { + throw GenerateManualPluginError.subprocessFailedNonZeroExit( + self, process.terminationStatus) + } + } catch { + throw GenerateManualPluginError.subprocessFailedError(self, error) + } + } +} + +extension PackageManager.BuildResult.BuiltArtifact { + func matchingProduct(context: PluginContext) -> Product? { + context + .package + .products + .first { $0.name == self.path.lastComponent } + } +} + +extension Product { + func hasDependency(named name: String) -> Bool { + recursiveTargetDependencies + .contains { $0.name == name } + } + + var recursiveTargetDependencies: [Target] { + var dependencies = [Target.ID: Target]() + for target in self.targets { + for dependency in target.recursiveTargetDependencies { + dependencies[dependency.id] = dependency + } + } + return Array(dependencies.values) + } +} diff --git a/Tools/generate-manual/AuthorArgument.swift b/Tools/generate-manual/AuthorArgument.swift new file mode 100644 index 000000000..11092ecf9 --- /dev/null +++ b/Tools/generate-manual/AuthorArgument.swift @@ -0,0 +1,68 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo + +fileprivate extension Character { + static let emailStart: Character = "<" + static let emailEnd: Character = ">" +} + +fileprivate extension Substring { + mutating func collecting(until terminator: (Element) throws -> Bool) rethrows -> String { + let terminatorIndex = try firstIndex(where: terminator) ?? endIndex + let collected = String(self[..` + // - both: `name` + public init?(argument: String) { + var argument = argument[...] + // collect until the email start character is seen. + let name = argument.collecting(until: { $0 == .emailStart }) + // drop the email start character. + argument.next() + // collect until the email end character is seen. + let email = argument.collecting(until: { $0 == .emailEnd }) + // drop the email end character. + argument.next() + // ensure no collected characters remain. + guard argument.isEmpty else { return nil } + + switch (name.isEmpty, email.isEmpty) { + case (true, true): + return nil + case (false, true): + self = .name(name: name) + case (true, false): + self = .email(email: email) + case (false, false): + self = .both(name: name, email: email) + } + } +} diff --git a/Tools/generate-manual/DSL/ArgumentSynopsis.swift b/Tools/generate-manual/DSL/ArgumentSynopsis.swift new file mode 100644 index 000000000..e57eb42b5 --- /dev/null +++ b/Tools/generate-manual/DSL/ArgumentSynopsis.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo + +struct ArgumentSynopsis: MDocComponent { + var argument: ArgumentInfoV0 + + var body: MDocComponent { + if argument.isOptional { + MDocMacro.OptionalCommandLineComponent(arguments: [synopsis]) + } else { + synopsis + } + } + + // ArgumentInfoV0 formatted as MDoc without optional bracket wrapper. + var synopsis: MDocASTNode { + switch argument.kind { + case .positional: + return argument.manualPageDescription + case .option: + // preferredName cannot be nil + let name = argument.preferredName! + return MDocMacro.CommandOption(options: [name.manualPage]) + case .flag: + // preferredName cannot be nil + let name = argument.preferredName! + return MDocMacro.CommandOption(options: [name.manualPage]) + .withUnsafeChildren(nodes: [argument.manualPageValueName]) + } + } +} diff --git a/Tools/generate-manual/DSL/Author.swift b/Tools/generate-manual/DSL/Author.swift new file mode 100644 index 000000000..9ba1f3ef2 --- /dev/null +++ b/Tools/generate-manual/DSL/Author.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo + +struct Author: MDocComponent { + var author: AuthorArgument + var trailing: String + + var body: MDocComponent { + switch author { + case let .name(name): + MDocMacro.Author(split: false) + MDocMacro.Author(name: name) + .withUnsafeChildren(nodes: [trailing]) + case let .email(email): + MDocMacro.MailTo(email: email) + .withUnsafeChildren(nodes: [trailing]) + case let .both(name, email): + MDocMacro.Author(split: false) + MDocMacro.Author(name: name) + MDocMacro.BeginAngleBrackets() + MDocMacro.MailTo(email: email) + MDocMacro.EndAngleBrackets() + .withUnsafeChildren(nodes: [trailing]) + } + } +} diff --git a/Tools/generate-manual/DSL/Authors.swift b/Tools/generate-manual/DSL/Authors.swift new file mode 100644 index 000000000..00b363891 --- /dev/null +++ b/Tools/generate-manual/DSL/Authors.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo + +struct Authors: MDocComponent { + var authors: [AuthorArgument] + + var body: MDocComponent { + Section(title: "authors") { + if !authors.isEmpty { + "The" + MDocMacro.DocumentName() + "reference was written by" + ForEach(authors) { author, last in + Author(author: author, trailing: last ? "." : ",") + } + } + } + } +} diff --git a/Tools/generate-manual/DSL/Core/Container.swift b/Tools/generate-manual/DSL/Core/Container.swift new file mode 100644 index 000000000..a0651d3dc --- /dev/null +++ b/Tools/generate-manual/DSL/Core/Container.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo + +struct Container: MDocComponent { + var ast: [MDocASTNode] { children.flatMap { $0.ast } } + var body: MDocComponent { self } + var children: [MDocComponent] +} diff --git a/Tools/generate-manual/DSL/Core/Empty.swift b/Tools/generate-manual/DSL/Core/Empty.swift new file mode 100644 index 000000000..d681fcca7 --- /dev/null +++ b/Tools/generate-manual/DSL/Core/Empty.swift @@ -0,0 +1,15 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct Empty: MDocComponent { + var ast: [MDocASTNode] { [] } + var body: MDocComponent { self } +} diff --git a/Tools/generate-manual/DSL/Core/ForEach.swift b/Tools/generate-manual/DSL/Core/ForEach.swift new file mode 100644 index 000000000..6593d7c63 --- /dev/null +++ b/Tools/generate-manual/DSL/Core/ForEach.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct ForEach: MDocComponent where C: Collection { + var items: C + var builder: (C.Element, Bool) -> MDocComponent + + init(_ items: C, @MDocBuilder builder: @escaping (C.Element, Bool) -> MDocComponent) { + self.items = items + self.builder = builder + } + + var body: MDocComponent { + guard !items.isEmpty else { return Empty() } + var currentIndex = items.startIndex + var last = false + var components = [MDocComponent]() + repeat { + let item = items[currentIndex] + currentIndex = items.index(after: currentIndex) + last = currentIndex == items.endIndex + components.append(builder(item, last)) + } while !last + return Container(children: components) + } +} diff --git a/Tools/generate-manual/DSL/Core/MDocASTNodeWrapper.swift b/Tools/generate-manual/DSL/Core/MDocASTNodeWrapper.swift new file mode 100644 index 000000000..1f3c2de1c --- /dev/null +++ b/Tools/generate-manual/DSL/Core/MDocASTNodeWrapper.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct MDocASTNodeWrapper: MDocComponent { + var ast: [MDocASTNode] { [node] } + var body: MDocComponent { self } + var node: MDocASTNode +} diff --git a/Tools/generate-manual/DSL/Core/MDocBuilder.swift b/Tools/generate-manual/DSL/Core/MDocBuilder.swift new file mode 100644 index 000000000..2cfafbb4b --- /dev/null +++ b/Tools/generate-manual/DSL/Core/MDocBuilder.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +@resultBuilder +struct MDocBuilder { + static func buildBlock(_ components: MDocComponent...) -> MDocComponent { Container(children: components) } + static func buildArray(_ components: [MDocComponent]) -> MDocComponent { Container(children: components) } + static func buildOptional(_ component: MDocComponent?) -> MDocComponent { component ?? Empty() } + static func buildEither(first component: MDocComponent) -> MDocComponent { component } + static func buildEither(second component: MDocComponent) -> MDocComponent { component } + static func buildExpression(_ expression: MDocComponent) -> MDocComponent { expression } + static func buildExpression(_ expression: MDocASTNode) -> MDocComponent { MDocASTNodeWrapper(node: expression) } +} diff --git a/Tools/generate-manual/DSL/Core/MDocComponent.swift b/Tools/generate-manual/DSL/Core/MDocComponent.swift new file mode 100644 index 000000000..12fc67b2f --- /dev/null +++ b/Tools/generate-manual/DSL/Core/MDocComponent.swift @@ -0,0 +1,20 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +protocol MDocComponent { + var ast: [MDocASTNode] { get } + @MDocBuilder + var body: MDocComponent { get } +} + +extension MDocComponent { + var ast: [MDocASTNode] { body.ast } +} diff --git a/Tools/generate-manual/DSL/Document.swift b/Tools/generate-manual/DSL/Document.swift new file mode 100644 index 000000000..dfb0514f9 --- /dev/null +++ b/Tools/generate-manual/DSL/Document.swift @@ -0,0 +1,38 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo +import Foundation + +struct Document: MDocComponent { + var singlePage: Bool + var date: Date + var section: Int + var authors: [AuthorArgument] + var command: CommandInfoV0 + + var body: MDocComponent { + Preamble(date: date, section: section, command: command) + Name(command: command) + Synopsis(command: command) + if singlePage { + SinglePageDescription(command: command) + } else { + MultiPageDescription(command: command) + } + Exit(section: section) + if !singlePage { + SeeAlso(section: section, command: command) + } + Authors(authors: authors) + } +} diff --git a/Tools/generate-manual/DSL/DocumentDate.swift b/Tools/generate-manual/DSL/DocumentDate.swift new file mode 100644 index 000000000..a014ef50d --- /dev/null +++ b/Tools/generate-manual/DSL/DocumentDate.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo +import Foundation + +struct DocumentDate: MDocComponent { + private var month: String + private var day: Int + private var year: Int + + init(date: Date) { + let calendar = Calendar(identifier: .iso8601) + let timeZone = TimeZone(identifier: "UTC")! + let formatter = DateFormatter() + formatter.calendar = calendar + formatter.timeZone = timeZone + formatter.dateFormat = "MMMM" + self.month = formatter.string(from: date) + let components = calendar.dateComponents(in: timeZone, from: date) + self.day = components.day! + self.year = components.year! + } + + var body: MDocComponent { + MDocMacro.DocumentDate(day: day, month: month, year: year) + } +} diff --git a/Tools/generate-manual/DSL/Exit.swift b/Tools/generate-manual/DSL/Exit.swift new file mode 100644 index 000000000..a67a2f38a --- /dev/null +++ b/Tools/generate-manual/DSL/Exit.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo + +struct Exit: MDocComponent { + var section: Int + + var body: MDocComponent { + Section(title: "exit status") { + if [1, 6, 8].contains(section) { + MDocMacro.ExitStandard() + } + } + } +} diff --git a/Tools/generate-manual/DSL/List.swift b/Tools/generate-manual/DSL/List.swift new file mode 100644 index 000000000..516dc680a --- /dev/null +++ b/Tools/generate-manual/DSL/List.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct List: MDocComponent { + var content: MDocComponent + + init(@MDocBuilder content: () -> MDocComponent) { + self.content = content() + } + + var body: MDocComponent { + if !content.ast.isEmpty { + MDocMacro.BeginList(style: .tag, width: 6) + content + MDocMacro.EndList() + } + } +} diff --git a/Tools/generate-manual/DSL/MultiPageDescription.swift b/Tools/generate-manual/DSL/MultiPageDescription.swift new file mode 100644 index 000000000..77aa97ef9 --- /dev/null +++ b/Tools/generate-manual/DSL/MultiPageDescription.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo + +struct MultiPageDescription: MDocComponent { + var command: CommandInfoV0 + + var body: MDocComponent { + Section(title: "description") { + if let discussion = command.discussion { + discussion + } + + List { + for argument in command.arguments ?? [] { + MDocMacro.ListItem(title: argument.manualPageDescription) + + if let abstract = argument.abstract { + abstract + } + + if argument.abstract != nil, argument.discussion != nil { + MDocMacro.ParagraphBreak() + } + + if let discussion = argument.discussion { + discussion + } + } + } + } + } +} diff --git a/Tools/generate-manual/DSL/Name.swift b/Tools/generate-manual/DSL/Name.swift new file mode 100644 index 000000000..4eacd8207 --- /dev/null +++ b/Tools/generate-manual/DSL/Name.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo + +struct Name: MDocComponent { + var command: CommandInfoV0 + + var body: MDocComponent { + Section(title: "name") { + MDocMacro.DocumentName(name: command.manualPageName) + if let abstract = command.abstract { + MDocMacro.DocumentDescription(description: abstract) + } + } + } +} diff --git a/Tools/generate-manual/DSL/Preamble.swift b/Tools/generate-manual/DSL/Preamble.swift new file mode 100644 index 000000000..a75383c0b --- /dev/null +++ b/Tools/generate-manual/DSL/Preamble.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo +import Foundation + +struct Preamble: MDocComponent { + var date: Date + var section: Int + var command: CommandInfoV0 + + var body: MDocComponent { + MDocMacro.Comment("Generated by swift-argument-parser") + DocumentDate(date: date) + MDocMacro.DocumentTitle(title: command.manualPageDocumentTitle, section: section) + MDocMacro.OperatingSystem() + } +} diff --git a/Tools/generate-manual/DSL/Section.swift b/Tools/generate-manual/DSL/Section.swift new file mode 100644 index 000000000..c741b1993 --- /dev/null +++ b/Tools/generate-manual/DSL/Section.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +struct Section: MDocComponent { + var title: String + var content: MDocComponent + + init(title: String, @MDocBuilder content: () -> MDocComponent) { + self.title = title + self.content = content() + } + + var body: MDocComponent { + if !content.ast.isEmpty { + MDocMacro.SectionHeader(title: title.uppercased()) + content + } + } +} diff --git a/Tools/generate-manual/DSL/SeeAlso.swift b/Tools/generate-manual/DSL/SeeAlso.swift new file mode 100644 index 000000000..ac3590297 --- /dev/null +++ b/Tools/generate-manual/DSL/SeeAlso.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo + +struct SeeAlso: MDocComponent { + var section: Int + var command: CommandInfoV0 + private var references: [String] { + (command.subcommands ?? []) + .map(\.manualPageTitle) + .sorted() + } + + var body: MDocComponent { + Section(title: "see also") { + ForEach(references) { reference, isLast in + MDocMacro.CrossManualReference(title: reference, section: section) + .withUnsafeChildren(nodes: isLast ? [] : [","]) + } + } + } +} diff --git a/Tools/generate-manual/DSL/SinglePageDescription.swift b/Tools/generate-manual/DSL/SinglePageDescription.swift new file mode 100644 index 000000000..dbbe947a2 --- /dev/null +++ b/Tools/generate-manual/DSL/SinglePageDescription.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo + +struct SinglePageDescription: MDocComponent { + var command: CommandInfoV0 + + var body: MDocComponent { + Section(title: "description") { + core + } + } + + @MDocBuilder + var core: MDocComponent { + if let discussion = command.discussion { + discussion + } + + List { + for argument in command.arguments ?? [] { + MDocMacro.ListItem(title: argument.manualPageDescription) + + if let abstract = argument.abstract { + abstract + } + + if argument.abstract != nil, argument.discussion != nil { + MDocMacro.ParagraphBreak() + } + + if let discussion = argument.discussion { + discussion + } + } + + for subcommand in command.subcommands ?? [] { + MDocMacro.ListItem(title: MDocMacro.Emphasis(arguments: [subcommand.commandName])) + SinglePageDescription(command: subcommand).core + } + } + } +} diff --git a/Tools/generate-manual/DSL/Synopsis.swift b/Tools/generate-manual/DSL/Synopsis.swift new file mode 100644 index 000000000..1b978317b --- /dev/null +++ b/Tools/generate-manual/DSL/Synopsis.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo + +struct Synopsis: MDocComponent { + var command: CommandInfoV0 + + var body: MDocComponent { + Section(title: "synopsis") { + MDocMacro.DocumentName() + + if command.subcommands != nil { + if command.defaultSubcommand != nil { + MDocMacro.BeginOptionalCommandLineComponent() + } + MDocMacro.CommandArgument(arguments: ["subcommand"]) + if command.defaultSubcommand != nil { + MDocMacro.EndOptionalCommandLineComponent() + } + } + for argument in command.arguments ?? [] { + ArgumentSynopsis(argument: argument) + } + } + } +} diff --git a/Tools/generate-manual/Extensions/ArgumentParser+MDoc.swift b/Tools/generate-manual/Extensions/ArgumentParser+MDoc.swift new file mode 100644 index 000000000..77dfd4980 --- /dev/null +++ b/Tools/generate-manual/Extensions/ArgumentParser+MDoc.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo + +extension CommandInfoV0 { + func manualPageFileName(section: Int) -> String { + manualPageTitle + ".\(section)" + } + + var manualPageDocumentTitle: String { + let parts = (superCommands ?? []) + [commandName] + return parts.joined(separator: ".").uppercased() + } + + var manualPageTitle: String { + let parts = (superCommands ?? []) + [commandName] + return parts.joined(separator: ".") + } + + var manualPageName: String { + let parts = (superCommands ?? []) + [commandName] + return parts.joined(separator: " ") + } +} + +extension ArgumentInfoV0 { + // ArgumentInfoV0 value name as MDoc with "..." appended if the argument is + // repeating. + var manualPageValueName: MDocASTNode { + var valueName = valueName ?? "" + if isRepeating { + valueName += "..." + } + // FIXME: MDocMacro.Emphasis? + return MDocMacro.CommandArgument(arguments: [valueName]) + } + + // ArgumentDefinition formatted as MDoc for use in a description section. + var manualPageDescription: MDocASTNode { + // names.partitioned.map(\.manualPage).interspersed(with: ",") + var synopses = (names ?? []).partitioned + .flatMap { [$0.manualPage, ","] } + synopses = synopses.dropLast() + + switch kind { + case .positional: + return manualPageValueName + case .option: + return MDocMacro.CommandOption(options: synopses) + .withUnsafeChildren(nodes: [manualPageValueName]) + case .flag: + return MDocMacro.CommandOption(options: synopses) + } + } +} + +extension ArgumentInfoV0.NameInfoV0 { + // Name formatted as MDoc. + var manualPage: MDocASTNode { + switch kind { + case .long: + return "-\(name)" + case .short: + return name + case .longWithSingleDash: + return name + } + } +} + +extension Array where Element == ParsableCommand.Type { + var commandNames: [String] { + var commandNames = [String]() + if let superName = first?.configuration._superCommandName { + commandNames.append(superName) + } + commandNames.append(contentsOf: map { $0._commandName }) + return commandNames + } +} + +extension BidirectionalCollection where Element == ArgumentInfoV0.NameInfoV0 { + var preferredName: Element? { + first { $0.kind != .short } ?? first + } + + var partitioned: [Element] { + filter { $0.kind == .short } + filter { $0.kind != .short } + } +} diff --git a/Tools/generate-manual/Extensions/Date+ExpressibleByArgument.swift b/Tools/generate-manual/Extensions/Date+ExpressibleByArgument.swift new file mode 100644 index 000000000..cf1709783 --- /dev/null +++ b/Tools/generate-manual/Extensions/Date+ExpressibleByArgument.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Foundation + +extension Date: ExpressibleByArgument { + // parsed as `yyyy-mm-dd` + public init?(argument: String) { + // ensure the input argument is composed of exactly 3 components separated + // by dashes ('-') + let components = argument.split(separator: "-") + let empty = components.filter { $0.isEmpty } + guard components.count == 3, empty.count == 0 else { return nil } + + // ensure the year component is exactly 4 characters + let _year = components[0] + guard _year.count == 4, let year = Int(_year) else { return nil } + + // ensure the month component is exactly 2 characters + let _month = components[1] + guard _month.count == 2, let month = Int(_month) else { return nil } + + // ensure the day component is exactly 2 characters + let _day = components[2] + guard _day.count == 2, let day = Int(_day) else { return nil } + + // ensure the combination of year, month, day is valid + let dateComponents = DateComponents( + calendar: Calendar(identifier: .iso8601), + timeZone: TimeZone(identifier: "UTC"), + year: year, + month: month, + day: day) + guard dateComponents.isValidDate else { return nil } + guard let date = dateComponents.date else { return nil } + self = date + } +} diff --git a/Tools/generate-manual/Extensions/Process+SimpleAPI.swift b/Tools/generate-manual/Extensions/Process+SimpleAPI.swift new file mode 100644 index 000000000..5e01fcc2e --- /dev/null +++ b/Tools/generate-manual/Extensions/Process+SimpleAPI.swift @@ -0,0 +1,74 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import Foundation + +enum SubprocessError: Swift.Error, LocalizedError, CustomStringConvertible { + case missingExecutable(url: URL) + case failedToLaunch(error: Swift.Error) + case nonZeroExitCode(code: Int) + + var description: String { + switch self { + case .missingExecutable(let url): + return "No executable at '\(url.standardizedFileURL.path)'." + case .failedToLaunch(let error): + return "Couldn't run command process. \(error.localizedDescription)" + case .nonZeroExitCode(let code): + return "Process returned non-zero exit code '\(code)'." + } + } + + var errorDescription: String? { description } +} + +func executeCommand( + executable: URL, + arguments: [String] +) throws -> String { + guard (try? executable.checkResourceIsReachable()) ?? false else { + throw SubprocessError.missingExecutable(url: executable) + } + + let process = Process() + if #available(macOS 10.13, *) { + process.executableURL = executable + } else { + process.launchPath = executable.path + } + process.arguments = arguments + + let output = Pipe() + process.standardOutput = output + process.standardError = FileHandle.nullDevice + + if #available(macOS 10.13, *) { + do { + try process.run() + } catch { + throw SubprocessError.failedToLaunch(error: error) + } + } else { + process.launch() + } + let outputData = output.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + + guard process.terminationStatus == 0 else { + throw SubprocessError.nonZeroExitCode(code: Int(process.terminationStatus)) + } + + let outputActual = String(data: outputData, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines) + ?? "" + + return outputActual +} diff --git a/Tools/generate-manual/GenerateManual.swift b/Tools/generate-manual/GenerateManual.swift new file mode 100644 index 000000000..528e10800 --- /dev/null +++ b/Tools/generate-manual/GenerateManual.swift @@ -0,0 +1,119 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import ArgumentParserToolInfo +import Foundation + +@main +struct GenerateManual: ParsableCommand { + enum Error: Swift.Error { + case failedToRunSubprocess(error: Swift.Error) + case unableToParseToolOutput(error: Swift.Error) + case unsupportedDumpHelpVersion(expected: Int, found: Int) + case failedToGenerateManualPages(error: Swift.Error) + } + + static let configuration = CommandConfiguration( + commandName: "generate-manual", + abstract: "Generate a manual for the provided tool.") + + @Argument(help: "Tool to generate manual for.") + var tool: String + + @Flag(help: "Generate a single page with information for all subcommands.") + var singlePage = false + + @Option(name: .long, help: "Override the creation date of the manual. Format: 'yyyy-mm-dd'.") + var date: Date = Date() + + @Option(name: .long, help: "Section of the manual.") + var section: Int = 1 + + @Option(name: .long, help: "Names and/or emails of the tool's authors. Format: 'name'.") + var authors: [AuthorArgument] = [] + + @Option(name: .shortAndLong, help: "Directory to save generated manual.") + var outputDirectory: String + + func validate() throws { + // Only man pages 1 through 9 are valid. + if !(1...9).contains(section) { + throw ValidationError("Invalid manual section passed to --section") + } + + // outputDirectory must already exist, `GenerateManual` will not create it. + var objcBool: ObjCBool = true + guard FileManager.default.fileExists(atPath: outputDirectory, isDirectory: &objcBool) else { + throw ValidationError("Output directory \(outputDirectory) does not exist") + } + + guard objcBool.boolValue else { + throw ValidationError("Output directory \(outputDirectory) is not a directory") + } + } + + func run() throws { + let data: Data + do { + let tool = URL(fileURLWithPath: tool) + let output = try executeCommand(executable: tool, arguments: ["--experimental-dump-help"]) + data = output.data(using: .utf8) ?? Data() + } catch { + throw Error.failedToRunSubprocess(error: error) + } + + do { + let toolInfoThin = try JSONDecoder().decode(ToolInfoHeader.self, from: data) + guard toolInfoThin.serializationVersion == 0 else { + throw Error.unsupportedDumpHelpVersion( + expected: 0, + found: toolInfoThin.serializationVersion) + } + } catch { + throw Error.unableToParseToolOutput(error: error) + } + + let toolInfo: ToolInfoV0 + do { + toolInfo = try JSONDecoder().decode(ToolInfoV0.self, from: data) + } catch { + throw Error.unableToParseToolOutput(error: error) + } + + do { + let outputDirectory = URL(fileURLWithPath: outputDirectory) + try generatePages(from: toolInfo.command, savingTo: outputDirectory) + } catch { + throw Error.failedToGenerateManualPages(error: error) + } + } + + func generatePages(from command: CommandInfoV0, savingTo directory: URL) throws { + let document = Document( + singlePage: singlePage, + date: date, + section: section, + authors: authors, + command: command) + let page = document.ast.map { $0.serialized() }.joined(separator: "\n") + + let fileName = command.manualPageFileName(section: section) + let outputPath = directory.appendingPathComponent(fileName) + try page.write(to: outputPath, atomically: false, encoding: .utf8) + + if !singlePage { + for subcommand in command.subcommands ?? [] { + try generatePages(from: subcommand, savingTo: directory) + } + } + } +} diff --git a/Tools/generate-manual/MDoc/MDocASTNode.swift b/Tools/generate-manual/MDoc/MDocASTNode.swift new file mode 100644 index 000000000..177cdaf2c --- /dev/null +++ b/Tools/generate-manual/MDoc/MDocASTNode.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// `MDocASTNode` represents a single abstract syntax tree node in an `mdoc` +/// document. `mdoc` is a semantic markup language for formatting manual pages. +/// +/// See: https://mandoc.bsd.lv/man/mdoc.7.html for more information. +public protocol MDocASTNode { + /// `_serialized` is an implementation detail and should not be used directly. + /// Please use `serialized` instead. + func _serialized(context: MDocSerializationContext) -> String +} + +extension MDocASTNode { + /// `serialized` Serializes an MDocASTNode and children into its string + /// representation for use with other tools. + public func serialized() -> String { + _serialized(context: MDocSerializationContext()) + } +} + +extension Int: MDocASTNode { + public func _serialized(context: MDocSerializationContext) -> String { + "\(self)" + } +} + +extension String: MDocASTNode { + public func _serialized(context: MDocSerializationContext) -> String { + context.macroLine + ? self.escapedMacroArgument() + : self.escapedTextLine() + } +} diff --git a/Tools/generate-manual/MDoc/MDocMacro.swift b/Tools/generate-manual/MDoc/MDocMacro.swift new file mode 100644 index 000000000..82b413639 --- /dev/null +++ b/Tools/generate-manual/MDoc/MDocMacro.swift @@ -0,0 +1,1510 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +//===--------------------------------------------------------*- openbsd -*-===// +// +// This source file contains descriptions of mandoc syntax tree nodes derived +// from their original descriptions in the mandoc source found here: +// https://github.com/openbsd/src/blob/master/share/man/man7/mdoc.7 +// +// $Id: LICENSE,v 1.22 2021/09/19 11:02:09 schwarze Exp $ +// +// With the exceptions noted below, all non-trivial files contained +// in the mandoc toolkit are protected by the Copyright of the following +// developers: +// +// Copyright (c) 2008-2012, 2014 Kristaps Dzonsons +// Copyright (c) 2010-2021 Ingo Schwarze +// Copyright (c) 1999, 2004, 2017 Marc Espie +// Copyright (c) 2009, 2010, 2011, 2012 Joerg Sonnenberger +// Copyright (c) 2013 Franco Fichtner +// Copyright (c) 2014 Baptiste Daroussin +// Copyright (c) 2016 Ed Maste +// Copyright (c) 2017 Michael Stapelberg +// Copyright (c) 2017 Anthony Bentley +// Copyright (c) 1998, 2004, 2010, 2015 Todd C. Miller +// Copyright (c) 2008, 2017 Otto Moerbeek +// Copyright (c) 2004 Ted Unangst +// Copyright (c) 1994 Christos Zoulas +// Copyright (c) 2003, 2007, 2008, 2014 Jason McIntyre +// +// See the individual files for information about who contributed +// to which file during which years. +// +// +// The mandoc distribution as a whole is distributed by its developers +// under the following license: +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// +// +// The following files included from outside sources are protected by +// other people's Copyright and are distributed under various 2-clause +// and 3-clause BSD licenses; see these individual files for details. +// +// soelim.c, soelim.1: +// Copyright (c) 2014 Baptiste Daroussin +// +// compat_err.c, compat_fts.c, compat_fts.h, +// compat_getsubopt.c, compat_strcasestr.c, compat_strsep.c, +// man.1: +// Copyright (c) 1989,1990,1993,1994 The Regents of the University of California +// +// compat_stringlist.c, compat_stringlist.h: +// Copyright (c) 1994 Christos Zoulas +// +// See https://mandoc.bsd.lv/LICENSE for license information +// +//===----------------------------------------------------------------------===// + +fileprivate extension Array { + mutating func append(optional newElement: Element?) { + if let newElement = newElement { + append(newElement) + } + } +} + +/// `MDocMacroProtocol` defines the properties required to serialize a +/// strongly-typed mdoc macro to the raw format. +public protocol MDocMacroProtocol: MDocASTNode { + /// The underlying `mdoc` macro string; used during serialization. + static var kind: String { get } + /// The arguments passed to the underlying `mdoc` macro; used during + /// serialization. + var arguments: [MDocASTNode] { get set } +} + +extension MDocMacroProtocol { + /// Append unchecked arguments to a `MDocMacroProtocol`. + public func withUnsafeChildren(nodes: [MDocASTNode]) -> Self { + var copy = self + copy.arguments.append(contentsOf: nodes) + return copy + } +} + +extension MDocMacroProtocol { + public func _serialized(context: MDocSerializationContext) -> String { + var result = "" + + // Prepend a dot if we aren't already in a macroLine context + if !context.macroLine { + result += "." + } + result += Self.kind + + if !arguments.isEmpty { + var context = context + context.macroLine = true + + result += " " + result += arguments + .map { $0._serialized(context: context) } + .joined(separator: " ") + } + return result + } +} + +/// `MDocMacro` is a namespace for types conforming to ``MDocMacroProtocol``. +public enum MDocMacro { + /// Comment placed inline in the manual page. + /// + /// Comment are not displayed by tools consuming serialized manual pages. + /// + /// __Example Usage__: + /// ```swift + /// Comment("WIP: History section...") + /// ``` + public struct Comment: MDocMacroProtocol { + public static let kind = #"\""# + public var arguments: [MDocASTNode] + /// Creates a new `Comment` macro. + /// + /// - Parameters: + /// - comment: A string to insert as an inline comment. + public init(_ comment: String) { + self.arguments = [comment] + } + } + + // MARK: - Document preamble and NAME section macros + + /// Document date displayed in the manual page footer. + /// + /// This must be the first macro in any `mdoc` document. + /// + /// __Example Usage__: + /// ```swift + /// DocumentDate(day: 9, month: "September", year: 2014) + /// ``` + public struct DocumentDate: MDocMacroProtocol { + public static let kind = "Dd" + public var arguments: [MDocASTNode] + /// Creates a new `DocumentDate` macro. + /// + /// - Parameters: + /// - day: An integer number day of the month the manual was written. + /// - month: The full English month name the manual was written. + /// - year: The four digit year the manual was written. + public init(day: Int, month: String, year: Int) { + arguments = [month, "\(day),", year] + } + } + + /// Document title displayed in the manual page header. + /// + /// This must be the second macro in any `mdoc` document. + /// + /// __Example Usage__: + /// ```swift + /// DocumentTitle(title: "swift", section: 1) + /// DocumentTitle(title: "swift", section: 1, arch: "arm64e") + /// ``` + public struct DocumentTitle: MDocMacroProtocol { + public static let kind = "Dt" + public var arguments: [MDocASTNode] + /// Creates a new `DocumentTitle` macro. + /// + /// - Parameters: + /// - title: The document's title or name. By convention the title should + /// be all caps. + /// - section: The manual section. The section should match the manual + /// page's file extension. Must be one of the following values: + /// 1. General Commands + /// 2. System Calls + /// 3. Library Functions + /// 4. Device Drivers + /// 5. File Formats + /// 6. Games + /// 7. Miscellaneous Information + /// 8. System Manager's Manual + /// 9. Kernel Developer's Manual + /// - arch: The machine architecture the manual page applies to, for + /// example: `alpha`, `i386`, `x86_64` or `arm64e`. + public init(title: String, section: Int, arch: String? = nil) { + precondition((1...9).contains(section)) + self.arguments = [title, section] + self.arguments.append(optional: arch) + } + } + + /// Operating system and version displayed in the manual page footer. + /// + /// This must be the third macro in any `mdoc` document. + /// + /// __Example Usage__: + /// ```swift + /// OperatingSystem() + /// OperatingSystem(name: "macOS") + /// OperatingSystem(name: "macOS", version: "10.13") + /// ``` + public struct OperatingSystem: MDocMacroProtocol { + public static let kind = "Os" + public var arguments: [MDocASTNode] + /// Creates a new `OperatingSystem` macro. + /// + /// - Note: The `version` parameter must not be specified without the `name` + /// parameter. + /// + /// - Parameters: + /// - name: The operating system the manual page contents is valid for. + /// Omitting `name` is recommended and will result in the user's + /// operating system name being used. + /// - version: The version the of the operating system specified by `name` + /// the manual page contents is valid for. Omitting `version` is + /// recommended. + public init(name: String? = nil, version: String? = nil) { + precondition(!(name == nil && version != nil)) + self.arguments = [] + self.arguments.append(optional: name) + self.arguments.append(optional: version) + } + } + + /// The name of the manual page. + /// + /// The first use of ``DocumentName`` is typically in the "NAME" section. The + /// name provided to the created ``DocumentName`` will be remembered and + /// subsequent uses of the ``DocumentName`` can omit the name argument. + /// + /// - Note: Manual pages in sections 1, 6, and 8 may use the name of command + /// or feature documented in the manual page as the name. + /// + /// In sections 2, 3, and 9 use the ``FunctionName`` macro instead of the + /// ``DocumentName`` macro to indicate the name of the document. + /// + /// __Example Usage__: + /// ```swift + /// SectionHeader(title: "SYNOPSIS") + /// DocumentName(name: "swift") + /// OptionalCommandLineComponent(arguments: CommandArgument(arguments: ["h"])) + /// ``` + public struct DocumentName: MDocMacroProtocol { + public static let kind = "Nm" + public var arguments: [MDocASTNode] + /// Creates a new `DocumentName` macro. + /// + /// - Parameters: + /// - name: The name of the manual page. + public init(name: String? = nil) { + self.arguments = [] + self.arguments.append(optional: name) + } + } + + /// Single line description of the manual page. + /// + /// This must be the last macro in the "NAME" section `mdoc` document and + /// should not appear in any other section. + /// + /// __Example Usage__: + /// ```swift + /// DocumentDescription(description: "Safe, fast, and expressive general-purpose programming language") + /// ``` + public struct DocumentDescription: MDocMacroProtocol { + public static let kind = "Nd" + public var arguments: [MDocASTNode] + /// Creates a new `DocumentDescription` macro. + /// + /// - Parameters: + /// - description: The description of the manual page. + public init(description: String) { + self.arguments = [description] + } + } + + // MARK: - Sections and cross references + + /// Start a new manual section. + /// + /// See [Manual Structure](http://mandoc.bsd.lv/man/mdoc.7.html#MANUAL_STRUCTURE) + /// for a list of standard sections. Custom sections should be avoided though + /// can be used. + /// + /// - Note: Section names should be unique so they can be referenced using a + /// ``SectionReference``. + /// + /// __Example Usage__: + /// ```swift + /// SectionHeader(title: "NAME") + /// ``` + public struct SectionHeader: MDocMacroProtocol { + public static let kind = "Sh" + public var arguments: [MDocASTNode] + /// Creates a new `SectionHeader` macro. + /// + /// - Parameters: + /// - title: The title of the section. + public init(title: String) { + self.arguments = [title] + } + } + + /// Start a new manual subsection. + /// + /// There is no standard naming convention of subsections. + /// + /// - Note: Subsection names should be unique so they can be referenced using + /// a ``SectionReference``. + /// + /// __Example Usage__: + /// ```swift + /// SubsectionHeader(title: "DETAILS") + /// ``` + public struct SubsectionHeader: MDocMacroProtocol { + public static let kind = "Ss" + public var arguments: [MDocASTNode] + /// Creates a new `SubsectionHeader` macro. + /// + /// - Parameters: + /// - title: The title of the subsection. + public init(title: String) { + self.arguments = [title] + } + } + + /// Reference a section or subsection in the same manual page. + /// + /// The section or subsection title must exactly match the title passed to + /// ``SectionReference``. + /// + /// __Example Usage__: + /// ```swift + /// SectionReference(title: "NAME") + /// ``` + public struct SectionReference: MDocMacroProtocol { + public static let kind = "Sx" + public var arguments: [MDocASTNode] + /// Creates a new `SectionReference` macro. + /// + /// - Parameters: + /// - title: The title of the section or subsection to reference. + public init(title: String) { + self.arguments = [title] + } + } + + /// Reference another manual page. + /// + /// __Example Usage__: + /// ```swift + /// CrossManualReference(title: "swift", section: 1) + /// ``` + public struct CrossManualReference: MDocMacroProtocol { + public static let kind = "Xr" + public var arguments: [MDocASTNode] + /// Creates a new `CrossManualReference` macro. + /// + /// - Parameters: + /// - title: The title of the section or subsection to reference. + public init(title: String, section: Int) { + precondition((1...9).contains(section)) + self.arguments = [title, section] + } + } + + /// Whitespace break between paragaphs. + /// + /// Breaks should not be inserted immeediately before or after + /// ``SectionHeader``, ``SubsectionHeader``, and ``BeginList`` macros. + public struct ParagraphBreak: MDocMacroProtocol { + public static let kind = "Pp" + public var arguments: [MDocASTNode] + /// Creates a new `ParagraphBreak` macro. + public init() { + self.arguments = [] + } + } + + // MARK: - Displays and lists + + // Display block: -type [-offset width] [-compact]. + // TODO: "Ed" + + // Indented display (one line). + // TODO: "D1" + + // Indented literal display (one line). + // TODO: "Dl" + + // In-line literal display: ‘text’. + // TODO: "Ql" + + // FIXME: Documentation + /// Open a list scope. + /// + /// Closed by an ``EndList`` macro. + /// + /// Lists are made of ``ListItem``s which are displayed in a variety of styles + /// depending on the ``ListStyle`` used to create the list scope. + /// List scopes can be nested in other list scopes, however nesting `.column` + /// and `ListStyle.enum` lists is not recommended as they may display inconsistently + /// between tools. + /// + /// __Example Usage__: + /// ```swift + /// BeginList(style: .tag, width: 6) + /// ListItem(title: "Hello, Swift!") + /// "Welcome to the Swift programming language." + /// ListItem(title: "Goodbye!") + /// EndList() + /// ``` + public struct BeginList: MDocMacroProtocol { + /// Enumeration of styles supported by the ``BeginList`` macro. + public enum ListStyle: String { + /// A bulleted list. + /// + /// Item titles should not be provided, instead item bodies are displayed + /// indented from a preceding bullet point using the specified width. + case bullet + // TODO: case column + // /// A columnated list. + // case column + /// A dashed list. + /// + /// Identical to `.bullet` except dashes precede each item. + case dash + /// An unindented list without newlines following important item titles + /// without macro parsing. + /// + /// Identical to `.inset` except item titles are displayed with importance + /// and are not parsed for macros. `.diag` is typically used in the + /// "DIAGNOSTICS" section with errors as the item titles. + case diag + /// An enumerated list. + /// + /// Identical to `.bullet` except increasing numbers starting at 1 precede + /// each item. + case `enum` + /// An indented list without joined item titles and bodies. + /// + /// Identical to `.tag` except item bodies always on the line after the + /// item title. + case hang + /// Alias for `.dash`. + case hyphen + /// An unindented list without newlines following item titles. + /// + /// Identical to `.ohang` except item titles are not followed by newlines. + case inset + /// An unindented list without item titles. + /// + /// Identical to `.ohang` except item titles should not be provided and + /// are not displayed. + case item + /// An unindented list. + /// + /// Item titles are displayed on a single line, with unindented item + /// bodies on the succeeding lines. + case ohang + /// An indented list. + /// + /// Item titles are displayed on a single line with item bodies indented + /// using the specified width on succeeding lines. If the item title is + /// shorter than the indentation width, item bodies are displayed on the + /// same as the title. + case tag + } + public static let kind = "Bl" + public var arguments: [MDocASTNode] + /// Creates a new `BeginList` macro. + /// + /// - Parameters: + /// - style: Display style. + /// - width: Number of characters to indent item bodies from titles. + /// - offset: Number of characters to indent both the item titles and bodies. + /// - compact: Disable vertical spacing between list items. + public init(style: ListStyle, width: Int? = nil, offset: Int? = nil, compact: Bool = false) { + self.arguments = ["-\(style)"] + switch style { + case .bullet, .dash, .`enum`, .hang, .hyphen, .tag: + if let width = width { + self.arguments.append(contentsOf: ["-width", "\(width)n"]) + } + case /*.column, */.diag, .inset, .item, .ohang: + assert(width == nil, "`width` should be nil for style: \(style)") + } + if let offset = offset { + self.arguments.append(contentsOf: ["-offset", "\(offset)n"]) + } + if compact { + self.arguments.append(contentsOf: ["-compact"]) + } + } + } + + /// A list item. + /// + /// ``ListItem`` begins a list item scope continuing until another + /// ``ListItem`` is encountered or the enclosing list scope is closed by + /// ``EndList``. ``ListItem``s may include a title if the the enclosing list + /// scope was constructed with one of the following styles: + /// - `.bullet` + /// - `.dash` + /// - `.enum` + /// - `.hang` + /// - `.hyphen` + /// - `.tag` + /// + /// __Example Usage__: + /// ```swift + /// BeginList(style: .tag, width: 6) + /// ListItem(title: "Hello, Swift!") + /// "Welcome to the Swift programming language." + /// ListItem(title: "Goodbye!") + /// EndList() + /// ``` + public struct ListItem: MDocMacroProtocol { + public static let kind = "It" + public var arguments: [MDocASTNode] + /// Creates a new `ListItem` macro. + /// + /// - Parameters: + /// - title: List item title, only valid depending on the ``ListStyle``. + public init(title: MDocASTNode? = nil) { + arguments = [] + arguments.append(optional: title) + } + } + + // Table cell separator in Bl -column lists. + // TODO: "Ta" + + /// Close a list scope opened by a ``BeginList`` macro. + public struct EndList: MDocMacroProtocol { + public static let kind = "El" + public var arguments: [MDocASTNode] + /// Creates a new `EndList` macro. + public init() { + self.arguments = [] + } + } + + // Bibliographic block (references). + // TODO: "Re" + + // MARK: Spacing control + + /// Text without a trailing space. + /// + /// __Example Usage__: + /// ```swift + /// WithoutTrailingSpace(text: "swift") + /// ``` + public struct WithoutTrailingSpace: MDocMacroProtocol { + public static let kind = "Pf" + public var arguments: [MDocASTNode] + /// Creates a new `WithoutTrailingSpace` macro. + /// + /// - Parameters: + /// - text: The text to display without a trailing space. + public init(text: String) { + self.arguments = [text] + } + } + + /// Text without a leading space. + /// + /// __Example Usage__: + /// ```swift + /// WithoutLeadingSpace(text: "swift") + /// ``` + public struct WithoutLeadingSpace: MDocMacroProtocol { + public static let kind = "Ns" + public var arguments: [MDocASTNode] + /// Creates a new `WithoutLeadingSpace` macro. + /// + /// - Parameters: + /// - text: The text to display without a trailing space. + public init(text: String) { + self.arguments = [text] + } + } + + /// An apostrophe without leading and trailing spaces. + /// + /// __Example Usage__: + /// ```swift + /// Apostrophe() + /// ``` + public struct Apostrophe: MDocMacroProtocol { + public static let kind = "Ap" + public var arguments: [MDocASTNode] + /// Creates a new `Apostrophe` macro. + public init() { + self.arguments = [] + } + } + + // TODO: HorizontalSpacing + // /// Switch horizontal spacing mode: [on | off]. + // public struct HorizontalSpacing: MDocMacroProtocol { + // public static let kind = "Sm" + // public var arguments: [MDocASTNode] + // public init() { + // self.arguments = [] + // } + // } + + // Keep block: -words. + // TODO: "Ek" + + // MARK: - Semantic markup for command-line utilities + + /// Command-line flags and options. + /// + /// Displays a hyphen (`-`) before each argument. ``CommandOption`` is + /// typically used in the "SYNOPSIS" and "DESCRIPTION" sections when listing + /// and describing options in a manual page. + /// + /// __Example Usage__: + /// ```swift + /// CommandOption(arguments: ["-version"]) + /// .withUnsafeChildren(CommandArgument(arguments: "version")) + /// ``` + public struct CommandOption: MDocMacroProtocol { + public static let kind = "Fl" + public var arguments: [MDocASTNode] + /// Creates a new `CommandOption` macro. + /// + /// - Parameters: + /// - arguments: Command-line flags and options. + public init(options: [MDocASTNode]) { + self.arguments = options + } + } + + /// Command-line modifiers. + /// + /// ``CommandModifier`` is typically used to denote strings exactly passed as + /// arguments, if and only if, ``CommandOption`` is not appropriate. + /// ``CommandModifier`` can also be used to specify configuration options and + /// keys. + /// + /// __Example Usage__: + /// ```swift + /// CommandModifier(modifiers: ["Configuration File"]) + /// .withUnsafeChildren(nodes: [FilePath(path: "$HOME/.swiftpm")]) + /// ``` + public struct CommandModifier: MDocMacroProtocol { + public static let kind = "Cm" + public var arguments: [MDocASTNode] + /// Creates a new `CommandModifier` macro. + /// + /// - Parameters: + /// - modifiers: Command-line modifiers. + public init(modifiers: [MDocASTNode]) { + self.arguments = modifiers + } + } + + /// Command-line placeholders. + /// + /// ``CommandArgument`` displays emphasized placeholders for command-line + /// flags, options and arguments. Flag and option names must use + /// ``CommandOption`` or `CommandModifier` macros. If no arguments are + /// provided to ``CommandArgument``, the string `"file ..."` is used. + /// + /// __Example Usage__: + /// ```swift + /// CommandArgument() + /// CommandArgument(arguments: [arg1, ",", arg2, "."]) + /// CommandOption(arguments: ["-version"]) + /// .withUnsafeChildren(CommandArgument(arguments: "version")) + /// ``` + public struct CommandArgument: MDocMacroProtocol { + public static let kind = "Ar" + public var arguments: [MDocASTNode] + /// Creates a new `CommandArgument` macro. + /// + /// - Parameters: + /// - arguments: Command-line argument placeholders. + public init(arguments: [MDocASTNode]) { + self.arguments = arguments + } + } + + /// Single-line optional command-line components. + /// + /// Displays the arguments in `[squareBrackets]`. + /// ``OptionalCommandLineComponent`` is typically used in the "SYNOPSIS" + /// section. + /// + /// __Example Usage__: + /// ```swift + /// SectionHeader(title: "SYNOPSIS") + /// DocumentName(name: "swift") + /// OptionalCommandLineComponent(arguments: CommandArgument(arguments: ["h"])) + /// ``` + public struct OptionalCommandLineComponent: MDocMacroProtocol { + public static let kind = "Op" + public var arguments: [MDocASTNode] + /// Creates a new `OptionalCommandLineComponent` macro. + /// + /// - Parameters: + /// - arguments: Command-line components to enclose. + public init(arguments: [MDocASTNode]) { + self.arguments = arguments + } + } + + /// Begin a multi-line optional command-line comment scope. + /// + /// Displays the scope contents in `[squareBrackets]`. + /// ``BeginOptionalCommandLineComponent`` is typically used in the "SYNOPSIS" + /// section. + /// + /// Closed by an ``EndOptionalCommandLineComponent`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginOptionalCommandLineComponent() + /// "Hello, Swift!" + /// EndOptionalCommandLineComponent() + /// ``` + public struct BeginOptionalCommandLineComponent: MDocMacroProtocol { + public static let kind = "Oo" + public var arguments: [MDocASTNode] + /// Creates a new `BeginOptionalCommandLineComponent` macro. + public init() { + self.arguments = [] + } + } + + /// Close a ```BeginOptionalCommandLineComponent``` block. + public struct EndOptionalCommandLineComponent: MDocMacroProtocol { + public static let kind = "Oc" + public var arguments: [MDocASTNode] + /// Creates a new `EndOptionalCommandLineComponent` macro. + public init() { + self.arguments = [] + } + } + + /// An interactive command. + /// + /// ``InteractiveCommand`` is similar to ``CommandModifier`` but should be used + /// to describe commands instead of arguments. For example, + /// ``InteractiveCommand`` can be used to describe the commands to editors + /// like `emacs` and `vim` or shells like `bash` or `fish`. + /// + /// __Example Usage__: + /// ```swift + /// InteractiveCommand(name: "print") + /// InteractiveCommand(name: "save") + /// InteractiveCommand(name: "quit") + /// ``` + public struct InteractiveCommand: MDocMacroProtocol { + public static let kind = "Ic" + public var arguments: [MDocASTNode] + /// Creates a new `InteractiveCommand` macro. + /// + /// - Parameters: + /// - name: Name of the interactive command. + public init(name: String) { + self.arguments = [name] + } + } + + /// An environment variable. + /// + /// __Example Usage__: + /// ```swift + /// EnvironmentVariable(variable: "DISPLAY") + /// EnvironmentVariable(variable: "PATH") + /// ``` + public struct EnvironmentVariable: MDocMacroProtocol { + public static let kind = "Ev" + public var arguments: [MDocASTNode] + /// Creates a new `EnvironmentVariable` macro. + /// + /// - Parameters: + /// - name: Name of the environment variable. + public init(name: String) { + self.arguments = [name] + } + } + + /// A file path. + /// + /// __Example Usage__: + /// ```swift + /// FilePath() + /// FilePath(path: "/usr/bin/swift") + /// FilePath(path: "/usr/share/man/man1/swift.1") + /// ``` + public struct FilePath: MDocMacroProtocol { + public static let kind = "Pa" + public var arguments: [MDocASTNode] + /// Creates a new `FilePath` macro. + /// + /// - Parameters: + /// - path: An optional absolute or relative path or a file or directory. + /// Tilde (`~`) will be used, if no path is used. + public init(path: String? = nil) { + self.arguments = [] + self.arguments.append(optional: path) + } + } + + // MARK: - Semantic markup for function libraries + + // Function library (one argument). + // TODO: "Lb" + + // Include file (one argument). + // TODO: "In" + + // Other preprocessor directive (>0 arguments). + // TODO: "Fd" + + // Function type (>0 arguments). + // TODO: "Ft" + + // Function block: funcname. + // TODO: "Fc" + + // Function name: funcname [argument ...]. + // TODO: "Fn" + + // Function argument (>0 arguments). + // TODO: "Fa" + + // Variable type (>0 arguments). + // TODO: "Vt" + + // Variable name (>0 arguments). + // TODO: "Va" + + /// Defined variable or preprocessor constant (>0 arguments). + // TODO: "Dv" + + /// Error constant (>0 arguments). + // TODO: "Er" + + /// Environmental variable (>0 arguments). + // TODO: "Ev" + + // MARK: - Various semantic markup + + /// An author's name. + /// + /// ``Author`` can be used to designate any author. Specifying an author of + /// the manual page itself should only occur in the "AUTHORS" section. + /// + /// ``Author`` also controls the display mode of authors. In the split mode, + /// a new-line will be inserted before each author, otherwise authors will + /// appear inline with other macros and text. Outside of the "AUTHORS" + /// section, the default display mode is unsplit. The display mode is reset at + /// the start of the "AUTHORS" section. In the "AUTHORS" section, the first + /// use of ``Author`` will use the unsplit mode and subsequent uses with use + /// the split mode. This behavior can be overridden by inserting an author + /// display mode macro before the normal author macro. + /// + /// __Example Usage__: + /// ```swift + /// Author(split: false) + /// Author(name: "Rauhul Varma") + /// ``` + public struct Author: MDocMacroProtocol { + public static let kind = "An" + public var arguments: [MDocASTNode] + /// Creates a new `Author` macro. + /// + /// - Parameters: + /// - name: The author name to display. + public init(name: String) { + self.arguments = [name] + } + /// Creates a new `Author` macro. + /// + /// - Parameters: + /// - split: The split display mode to use for subsequent uses of + /// ``Author``. + public init(split: Bool) { + self.arguments = [split ? "-split" : "-nosplit"] + } + } + + /// A website hyperlink. + /// + /// __Example Usage__: + /// ```swift + /// Hyperlink(url: "http://swift.org") + /// Hyperlink(url: "http://swift.org", displayText: "Programming in Swift") + /// ``` + public struct Hyperlink: MDocMacroProtocol { + public static let kind = "Lk" + public var arguments: [MDocASTNode] + /// Creates a new `Hyperlink` macro. + /// + /// - Parameters: + /// - url: The website address to link. + /// - displayText: Optional title text accompanying the url. + public init(url: String, displayText: String? = nil) { + self.arguments = [url] + self.arguments.append(optional: displayText) + } + } + + /// An email hyperlink. + /// + /// __Example Usage__: + /// ```swift + /// MailTo(email: "swift+evolution-discuss@forums.swift.org") + /// ``` + public struct MailTo: MDocMacroProtocol { + public static let kind = "Mt" + public var arguments: [MDocASTNode] + /// Creates a new `MailTo` macro. + /// + /// - Parameters: + /// - email: The email address to link. + public init(email: String) { + self.arguments = [email] + } + } + +// TODO: KernelConfiguration +// /// Kernel configuration declaration (>0 arguments). +// public struct KernelConfiguration: MDocMacroProtocol { +// public static let kind = "Cd" +// public var arguments: [MDocASTNode] +// public init() { +// self.arguments = [] +// } +// } + +// TODO: MemoryAddress +// /// Memory address (>0 arguments). +// public struct MemoryAddress: MDocMacroProtocol { +// public static let kind = "Ad" +// public var arguments: [MDocASTNode] +// public init() { +// self.arguments = [] +// } +// } + +// TODO: MathematicalSymbol +// /// Mathematical symbol (>0 arguments). +// public struct MathematicalSymbol: MDocMacroProtocol { +// public static let kind = "Ms" +// public var arguments: [MDocASTNode] +// public init() { +// self.arguments = [] +// } +// } + + // MARK: - Physical markup + + /// Emphasize single-line text. + /// + /// ``Emphasis`` should only be used when no other semantic macros are + /// appropriate. ``Emphasis`` is used to express "emphasis"; for example: + /// ``Emphasis`` can be used to highlight technical terms and placeholders, + /// except when they appear in syntactic elements. ``Emphasis`` should not be + /// conflated with "importance" which should be expressed using ``Boldface``. + /// + /// - Note: Emphasizes text is usually italicized. If the output program does + /// not support italicizing text, it is underlined instead. + /// + /// __Example Usage__: + /// ```swift + /// Emphasis(arguments: ["Hello", ", "Swift!"]) + /// ``` + public struct Emphasis: MDocMacroProtocol { + public static let kind = "Em" + public var arguments: [MDocASTNode] + /// Creates a new `Emphasis` macro. + /// + /// - Parameters: + /// - arguments: Text to emphasize. + public init(arguments: [MDocASTNode]) { + self.arguments = arguments + } + } + + /// Embolden single-line text. + /// + /// ``Boldface`` should only be used when no other semantic macros are + /// appropriate. ``Boldface`` is used to express "importance"; for example: + /// ``Boldface`` can be used to highlight required arguments and exact text. + /// ``Boldface`` should not be conflated with "emphasis" which + /// should be expressed using ``Emphasis``. + /// + /// __Example Usage__: + /// ```swift + /// Boldface(arguments: ["Hello,", " Swift!"]) + /// ``` + public struct Boldface: MDocMacroProtocol { + public static let kind = "Sy" + public var arguments: [MDocASTNode] + /// Creates a new `Boldface` macro. + /// + /// - Parameters: + /// - arguments: Text to embolden. + public init(arguments: [MDocASTNode]) { + self.arguments = arguments + } + } + + /// Reset the font style, set by a single-line text macro. + /// + /// __Example Usage__: + /// ```swift + /// Boldface(arguments: ["Hello,"]) + /// .withUnsafeChildren(nodes: [NormalText(), " Swift!"]) + /// ``` + public struct NormalText: MDocMacroProtocol { + public static let kind = "No" + public var arguments: [MDocASTNode] + /// Creates a new `NormalText` macro. + public init() { + self.arguments = [] + } + } + + /// Open a font scope with a font style. + /// + /// Closed by a ``EndFont`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginFont(style: .boldface) + /// "Hello, Swift!" + /// EndFont() + /// ``` + public struct BeginFont: MDocMacroProtocol { + /// Enumeration of font styles supported by `mdoc`. + public enum FontStyle { + /// Italic font style. + case emphasis + /// Typewriter font style. + /// + /// `literal` should not be used because it is visually identical to + /// normal text. + case literal + /// Bold font style. + case boldface + } + + public static let kind = "Bf" + public var arguments: [MDocASTNode] + /// Creates a new `BeginFont` macro. + /// + /// - Parameters: + /// - style: The style of font scope the macro opens. + public init(style: FontStyle) { + switch style { + case .emphasis: + self.arguments = ["-emphasis"] + case .literal: + self.arguments = ["-literal"] + case .boldface: + self.arguments = ["-symbolic"] + } + } + } + + /// Close a font scope opened by a ``BeginFont`` macro. + public struct EndFont: MDocMacroProtocol { + public static let kind = "Ef" + public var arguments: [MDocASTNode] + /// Creates a new `EndFont` macro. + public init() { + self.arguments = [] + } + } + + // MARK: - Physical enclosures + + /// Open a scope enclosed by `“typographic”` double-quotes. + /// + /// Closed by a ``EndTypographicDoubleQuotes`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginTypographicDoubleQuotes() + /// "Hello, Swift!" + /// EndTypographicDoubleQuotes() + /// ``` + public struct BeginTypographicDoubleQuotes: MDocMacroProtocol { + public static let kind = "Do" + public var arguments: [MDocASTNode] + /// Creates a new `BeginTypographicDoubleQuotes` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginTypographicDoubleQuotes`` macro. + public struct EndTypographicDoubleQuotes: MDocMacroProtocol { + public static let kind = "Dc" + public var arguments: [MDocASTNode] + /// Creates a new `EndTypographicDoubleQuotes` macro. + public init() { + self.arguments = [] + } + } + + /// Open a scope enclosed by `"typewriter"` double-quotes. + /// + /// Closed by a ``EndTypewriterDoubleQuotes`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginTypewriterDoubleQuotes() + /// "Hello, Swift!" + /// EndTypewriterDoubleQuotes() + /// ``` + public struct BeginTypewriterDoubleQuotes: MDocMacroProtocol { + public static let kind = "Qo" + public var arguments: [MDocASTNode] + /// Creates a new `BeginTypewriterDoubleQuotes` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginTypewriterDoubleQuotes`` macro. + public struct EndTypewriterDoubleQuotes: MDocMacroProtocol { + public static let kind = "Qc" + public var arguments: [MDocASTNode] + /// Creates a new `EndTypewriterDoubleQuotes` macro. + public init() { + self.arguments = [] + } + } + + /// Open a scope enclosed by `'single'` quotes. + /// + /// Closed by a ``EndSingleQuotes`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginSingleQuotes() + /// "Hello, Swift!" + /// EndSingleQuotes() + /// ``` + public struct BeginSingleQuotes: MDocMacroProtocol { + public static let kind = "So" + public var arguments: [MDocASTNode] + /// Creates a new `BeginSingleQuotes` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginSingleQuotes`` macro. + public struct EndSingleQuotes: MDocMacroProtocol { + public static let kind = "Sc" + public var arguments: [MDocASTNode] + /// Creates a new `EndSingleQuotes` macro. + public init() { + self.arguments = [] + } + } + + /// Open a scope enclosed by `(parentheses)`. + /// + /// Closed by a ``EndParentheses`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginParentheses() + /// "Hello, Swift!" + /// EndParentheses() + /// ``` + public struct BeginParentheses: MDocMacroProtocol { + public static let kind = "Po" + public var arguments: [MDocASTNode] + /// Creates a new `BeginParentheses` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginParentheses`` macro. + public struct EndParentheses: MDocMacroProtocol { + public static let kind = "Pc" + public var arguments: [MDocASTNode] + /// Creates a new `EndParentheses` macro. + public init() { + self.arguments = [] + } + } + + /// Open a scope enclosed by `[squareBrackets]`. + /// + /// Closed by a ``EndSquareBrackets`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginSquareBrackets() + /// "Hello, Swift!" + /// EndSquareBrackets() + /// ``` + public struct BeginSquareBrackets: MDocMacroProtocol { + public static let kind = "Bo" + public var arguments: [MDocASTNode] + /// Creates a new `BeginSquareBrackets` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginSquareBrackets`` macro. + public struct EndSquareBrackets: MDocMacroProtocol { + public static let kind = "Bc" + public var arguments: [MDocASTNode] + /// Creates a new `EndSquareBrackets` macro. + public init() { + self.arguments = [] + } + } + + /// Open a scope enclosed by `{curlyBraces}`. + /// + /// Closed by a ``EndCurlyBraces`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginCurlyBraces() + /// "Hello, Swift!" + /// EndCurlyBraces() + /// ``` + public struct BeginCurlyBraces: MDocMacroProtocol { + public static let kind = "Bro" + public var arguments: [MDocASTNode] + /// Creates a new `BeginCurlyBraces` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginCurlyBraces`` macro. + public struct EndCurlyBraces: MDocMacroProtocol { + public static let kind = "Brc" + public var arguments: [MDocASTNode] + /// Creates a new `EndCurlyBraces` macro. + public init() { + self.arguments = [] + } + } + + /// Open a scope enclosed by ``. + /// + /// Closed by a ``EndAngleBrackets`` macro. + /// + /// __Example Usage__: + /// ```swift + /// BeginAngleBrackets() + /// "Hello, Swift!" + /// EndAngleBrackets() + /// ``` + public struct BeginAngleBrackets: MDocMacroProtocol { + public static let kind = "Ao" + public var arguments: [MDocASTNode] + /// Creates a new `BeginAngleBrackets` macro. + public init() { + self.arguments = [] + } + } + + /// Close a scope opened by a ``BeginAngleBrackets`` macro. + public struct EndAngleBrackets: MDocMacroProtocol { + public static let kind = "Ac" + public var arguments: [MDocASTNode] + /// Creates a new `EndAngleBrackets` macro. + public init() { + self.arguments = [] + } + } + + // TODO: GenericEnclosure + // /// Enclose another element generically. + // case genericEnclosure(MDocLowLevelASTNode) + + // MARK: - Text production + + /// Display a standard line about the exit code of specified utilities. + /// + /// This macro indicates the specified utilities exit 0 on success and other + /// values on failure. ``ExitStandard``` should be only included in the + /// "EXIT STATUS" section. + /// + /// ``ExitStandard`` should only be used in sections 1, 6, and 8. + public struct ExitStandard: MDocMacroProtocol { + public static let kind = "Ex" + public var arguments: [MDocASTNode] + /// Creates a new `ExitStandard` macro. + /// + /// - Parameters: + /// - utilities: A list of utilities the exit standard applies to. If no + /// utilities are specified the document's name set by ``DocumentName`` + /// is used. + public init(utilities: [String] = []) { + self.arguments = ["-std"] + utilities + } + } + +// TODO: ReturnStandard +// /// Insert a standard sentence regarding a function call's return value of 0 on success and -1 on error, with the errno libc global variable set on error. +// /// +// /// If function is not specified, the document's name set by ``DocumentName`` is used. Multiple function arguments are treated as separate functions. +// public struct ReturnStandard: MDocMacroProtocol { +// public static let kind = "Rv" +// public var arguments: [MDocASTNode] +// public init() { +// self.arguments = [] +// } +// } + +// TODO: StandardsReference +// /// Reference to a standards document (one argument). +// public struct StandardsReference: MDocMacroProtocol { +// public static let kind = "St" +// public var arguments: [MDocASTNode] +// public init() { +// self.arguments = [] +// } +// } + + /// Display a formatted version of AT&T UNIX. + /// + /// __Example Usage__: + /// ```swift + /// AttUnix() + /// AttUnix(version: "V.1") + /// ``` + public struct AttUnix: MDocMacroProtocol { + public static let kind = "At" + public var arguments: [MDocASTNode] + /// Creates a new `AttUnix` macro. + /// + /// - Parameters: + /// - version: The version of Att Unix to stylize. Omitting + /// `version` will result in an unversioned OS being displayed. + /// `version` should be one of the following values; + /// - `v[1-7] | 32v` - A version of AT&T UNIX. + /// - `III` - AT&T System III UNIX. + /// - `V | V.[1-4]` - A version of AT&T System V UNIX. + public init(version: String? = nil) { + self.arguments = [] + self.arguments.append(optional: version) + } + } + + /// Display a formatted variant and version of BSD. + /// + /// __Example Usage__: + /// ```swift + /// BSD() + /// BSD(name: "Ghost") + /// BSD(name: "Ghost", version: "21.04.27") + /// ``` + public struct BSD: MDocMacroProtocol { + public static let kind = "Bx" + public var arguments: [MDocASTNode] + /// Creates a new `BSD` macro. + /// + /// - Note: The `version` parameter must not be specified without + /// the `name` parameter. + /// + /// - Parameters: + /// - name: The name of the BSD variant to stylize. + /// - version: The version `name` to stylize. Omitting `version` + /// will result in an unversioned OS being displayed. + public init(name: String? = nil, version: String? = nil) { + precondition(!(name == nil && version != nil)) + self.arguments = [] + self.arguments.append(optional: name) + self.arguments.append(optional: version) + } + } + + /// Display a formatted version of BSD/OS. + /// + /// __Example Usage__: + /// ```swift + /// BSDOS() + /// BSDOS(version: "5.1") + /// ``` + public struct BSDOS: MDocMacroProtocol { + public static let kind = "Bsx" + public var arguments: [MDocASTNode] + /// Creates a new `BSDOS` macro. + /// + /// - Parameters: + /// - version: The version of BSD/OS to stylize. Omitting + /// `version` will result in an unversioned OS being displayed. + public init(version: String? = nil) { + self.arguments = [] + self.arguments.append(optional: version) + } + } + + /// Display a formatted version of NetBSD. + /// + /// __Example Usage__: + /// ```swift + /// NetBSD() + /// NetBSD(version: "9.2") + /// ``` + public struct NetBSD: MDocMacroProtocol { + public static let kind = "Nx" + public var arguments: [MDocASTNode] + /// Creates a new `NetBSD` macro. + /// + /// - Parameters: + /// - version: The version of NetBSD to stylize. Omitting + /// `version` will result in an unversioned OS being displayed. + public init(version: String? = nil) { + self.arguments = [] + self.arguments.append(optional: version) + } + } + + /// Display a formatted version of FreeBSD. + /// + /// __Example Usage__: + /// ```swift + /// FreeBSD() + /// FreeBSD(version: "13.0") + /// ``` + public struct FreeBSD: MDocMacroProtocol { + public static let kind = "Fx" + public var arguments: [MDocASTNode] + /// Creates a new `FreeBSD` macro. + /// + /// - Parameters: + /// - version: The version of FreeBSD to stylize. Omitting + /// `version` will result in an unversioned OS being displayed. + public init(version: String? = nil) { + self.arguments = [] + self.arguments.append(optional: version) + } + } + + /// Display a formatted version of OpenBSD. + /// + /// __Example Usage__: + /// ```swift + /// OpenBSD() + /// OpenBSD(version: "6.9") + /// ``` + public struct OpenBSD: MDocMacroProtocol { + public static let kind = "Ox" + public var arguments: [MDocASTNode] + /// Creates a new `OpenBSD` macro. + /// + /// - Parameters: + /// - version: The version of OpenBSD to stylize. Omitting + /// `version` will result in an unversioned OS being displayed. + public init(version: String? = nil) { + self.arguments = [] + self.arguments.append(optional: version) + } + } + + /// Display a formatted version of DragonFly. + /// + /// __Example Usage__: + /// ```swift + /// DragonFly() + /// DragonFly(version: "6.0") + /// ``` + public struct DragonFly: MDocMacroProtocol { + public static let kind = "Dx" + public var arguments: [MDocASTNode] + /// Creates a new `DragonFly` macro. + /// + /// - Parameters: + /// - version: The version of DragonFly to stylize. Omitting + /// `version` will result in an unversioned OS being displayed. + public init(version: String? = nil) { + self.arguments = [] + self.arguments.append(optional: version) + } + } +} diff --git a/Tools/generate-manual/MDoc/MDocSerializationContext.swift b/Tools/generate-manual/MDoc/MDocSerializationContext.swift new file mode 100644 index 000000000..8619adf8a --- /dev/null +++ b/Tools/generate-manual/MDoc/MDocSerializationContext.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// The context needed to serialize an AST Node to its string representation. +public struct MDocSerializationContext { + var macroLine: Bool = false + + public init() { } +} diff --git a/Tools/generate-manual/MDoc/String+Escaping.swift b/Tools/generate-manual/MDoc/String+Escaping.swift new file mode 100644 index 000000000..e87e3bfd8 --- /dev/null +++ b/Tools/generate-manual/MDoc/String+Escaping.swift @@ -0,0 +1,97 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +// Escaping rules +// https://mandoc.bsd.lv/mdoc/intro/escaping.html +extension String { + func escapedMacroArgument() -> String { + var escaped = "" + var containsBlankCharacter = false + + // TODO: maybe drop `where character.isASCII` clause + for character in self where character.isASCII { + switch character { + case " ": + escaped.append(character) + containsBlankCharacter = true + + // backslashes: + // To output a backslash, use the escape sequence `\e`. Never use the escape sequence `\\` in any context. + case #"\"#: + escaped += #"\e"# + + // double quotes in macro arguments: + // If a macro argument needs to contain a double quote character, write it as “\(dq”. No escaping is needed on text input lines. + case "\"": + escaped += #"\(dq"# + + default: + // Custom addition: + // newlines in macro arguments: + // If a macro argument contains a newline character, replace it with a blank character. + if character.isNewline { + escaped.append(" ") + containsBlankCharacter = true + } else { + escaped.append(character) + } + } + } + + // FIXME: + // macro names as macro arguments: + // If the name of another mdoc(7) macro occurs as an argument on an mdoc(7) macro line, the former macro is called, and any remaining arguments are passed to it. To prevent this call and instead render the name of the former macro literally, prepend the name with a zero-width space (‘\&’). See the MACRO SYNTAX section of the mdoc(7) manual for details. + + // blanks in macro arguments + // If a macro argument needs to contain a blank character, enclose the whole argument in double quotes. For example, this often occurs with Fa macros. See the MACRO SYNTAX in the roff(7) manual for details. + if escaped.isEmpty || containsBlankCharacter { + return "\"\(escaped)\"" + } + + return escaped + } + + func escapedTextLine() -> String { + var escaped = "" + var atBeginning = true + + // TODO: maybe drop `where character.isASCII` clause + for character in self where character.isASCII { + switch (character, atBeginning) { + + // backslashes: + // To output a backslash, use the escape sequence `\e`. Never use the escape sequence `\\` in any context. + case (#"\"#, _): + escaped += #"\e"# + atBeginning = false + + // dots and apostrophes at the beginning of text lines: + // If a text input line needs to begin with a dot (`.`) or apostrophe (`'`), prepend a zero-width space (`\&`) to prevent the line from being mistaken for a macro line. Never use the escape sequence `\.` in any context. + case (".", true), ("'", true): + escaped += #"\&"# + escaped.append(character) + atBeginning = false + + // blank characters at the beginning of text lines: + // If a text input line needs to begin with a blank character (` `) and no line break is desired before that line, prepend a zero-width space (`\&`). + case (" ", true): + escaped += #"\&"# + escaped.append(character) + + default: + escaped.append(character) + atBeginning = false + } + } + + return escaped + } +} From a2b4375cc0a0a27b33894dbec37369c84de00f61 Mon Sep 17 00:00:00 2001 From: Rauhul Varma Date: Mon, 25 Apr 2022 23:47:27 -0700 Subject: [PATCH 2/2] Add some integration tests using the built-in example commands --- Package@swift-5.6.swift | 15 +- .../TestHelpers.swift | 48 ++- .../CountLinesGenerateManualTests.swift | 101 +++++ .../MathGenerateManualTests.swift | 389 ++++++++++++++++++ .../RepeatGenerateManualTests.swift | 95 +++++ .../RollDiceGenerateManualTests.swift | 105 +++++ Tools/generate-manual/GenerateManual.swift | 39 +- 7 files changed, 768 insertions(+), 24 deletions(-) create mode 100644 Tests/ArgumentParserGenerateManualTests/CountLinesGenerateManualTests.swift create mode 100644 Tests/ArgumentParserGenerateManualTests/MathGenerateManualTests.swift create mode 100644 Tests/ArgumentParserGenerateManualTests/RepeatGenerateManualTests.swift create mode 100644 Tests/ArgumentParserGenerateManualTests/RollDiceGenerateManualTests.swift diff --git a/Package@swift-5.6.swift b/Package@swift-5.6.swift index dfd7547e7..72c83e425 100644 --- a/Package@swift-5.6.swift +++ b/Package@swift-5.6.swift @@ -70,17 +70,20 @@ var package = Package( dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], exclude: ["CMakeLists.txt"]), .testTarget( - name: "ArgumentParserUnitTests", - dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], - exclude: ["CMakeLists.txt"]), + name: "ArgumentParserExampleTests", + dependencies: ["ArgumentParserTestHelpers"], + resources: [.copy("CountLinesTest.txt")]), + .testTarget( + name: "ArgumentParserGenerateManualTests", + dependencies: ["ArgumentParserTestHelpers"]), .testTarget( name: "ArgumentParserPackageManagerTests", dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], exclude: ["CMakeLists.txt"]), .testTarget( - name: "ArgumentParserExampleTests", - dependencies: ["ArgumentParserTestHelpers"], - resources: [.copy("CountLinesTest.txt")]), + name: "ArgumentParserUnitTests", + dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"], + exclude: ["CMakeLists.txt"]), ] ) diff --git a/Sources/ArgumentParserTestHelpers/TestHelpers.swift b/Sources/ArgumentParserTestHelpers/TestHelpers.swift index c159214e3..d0ccb0589 100644 --- a/Sources/ArgumentParserTestHelpers/TestHelpers.swift +++ b/Sources/ArgumentParserTestHelpers/TestHelpers.swift @@ -212,10 +212,22 @@ extension XCTest { exitCode: ExitCode = .success, file: StaticString = #file, line: UInt = #line) throws { - let splitCommand = command.split(separator: " ") - let arguments = splitCommand.dropFirst().map(String.init) - - let commandName = String(splitCommand.first!) + try AssertExecuteCommand( + command: command.split(separator: " ").map(String.init), + expected: expected, + exitCode: exitCode, + file: file, + line: line) + } + + public func AssertExecuteCommand( + command: [String], + expected: String? = nil, + exitCode: ExitCode = .success, + file: StaticString = #file, line: UInt = #line) throws + { + let arguments = Array(command.dropFirst()) + let commandName = String(command.first!) let commandURL = debugURL.appendingPathComponent(commandName) guard (try? commandURL.checkResourceIsReachable()) ?? false else { XCTFail("No executable at '\(commandURL.standardizedFileURL.path)'.", @@ -311,4 +323,32 @@ extension XCTest { throw XCTSkip("Not supported on this platform") #endif } + + public func AssertGenerateManual( + singlePage: Bool, + command: String, + expected: String, + file: StaticString = #file, + line: UInt = #line + ) throws { + let commandURL = debugURL.appendingPathComponent(command) + var command = [ + "generate-manual", commandURL.path, + "--date", "1996-05-12", + "--section", "9", + "--authors", "Jane Appleseed", + "--authors", "", + "--authors", "The Appleseeds", + "--output-directory", "-", + ] + if singlePage { + command.append("--single-page") + } + try AssertExecuteCommand( + command: command, + expected: expected, + exitCode: .success, + file: file, + line: line) + } } diff --git a/Tests/ArgumentParserGenerateManualTests/CountLinesGenerateManualTests.swift b/Tests/ArgumentParserGenerateManualTests/CountLinesGenerateManualTests.swift new file mode 100644 index 000000000..fd9f824a3 --- /dev/null +++ b/Tests/ArgumentParserGenerateManualTests/CountLinesGenerateManualTests.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if os(macOS) + +import XCTest +import ArgumentParserTestHelpers + +final class CountLinesGenerateManualTests: XCTestCase { + func testCountLines_SinglePageManual() throws { + guard #available(macOS 12, *) else { return } + try AssertGenerateManual(singlePage: true, command: "count-lines", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt COUNT-LINES 9 + .Os + .Sh NAME + .Nm count-lines + .Sh SYNOPSIS + .Nm + .Ar input-file + .Op Fl -prefix + .Op Fl -verbose Ar verbose + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Ar input-file + A file to count lines in. If omitted, counts the lines of stdin. + .It Fl -prefix Ar prefix + Only count lines with this prefix. + .It Fl -verbose + Include extra information in the output. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } + + func testCountLines_MultiPageManual() throws { + guard #available(macOS 12, *) else { return } + try AssertGenerateManual(singlePage: false, command: "count-lines", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt COUNT-LINES 9 + .Os + .Sh NAME + .Nm count-lines + .Sh SYNOPSIS + .Nm + .Ar input-file + .Op Fl -prefix + .Op Fl -verbose Ar verbose + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Ar input-file + A file to count lines in. If omitted, counts the lines of stdin. + .It Fl -prefix Ar prefix + Only count lines with this prefix. + .It Fl -verbose + Include extra information in the output. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } +} + +#endif diff --git a/Tests/ArgumentParserGenerateManualTests/MathGenerateManualTests.swift b/Tests/ArgumentParserGenerateManualTests/MathGenerateManualTests.swift new file mode 100644 index 000000000..34187d8e5 --- /dev/null +++ b/Tests/ArgumentParserGenerateManualTests/MathGenerateManualTests.swift @@ -0,0 +1,389 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParser +import ArgumentParserTestHelpers + +final class MathGenerateManualTests: XCTestCase { + func testMath_SinglePageManual() throws { + try AssertGenerateManual(singlePage: true, command: "math", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH 9 + .Os + .Sh NAME + .Nm math + .Nd "A utility for performing maths." + .Sh SYNOPSIS + .Nm + .Ar subcommand + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .It Em add + .Bl -tag -width 6n + .It Fl x , -hex-output + Use hexadecimal notation for the result. + .It Ar values... + A group of integers to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .It Em multiply + .Bl -tag -width 6n + .It Fl x , -hex-output + Use hexadecimal notation for the result. + .It Ar values... + A group of integers to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .It Em stats + .Bl -tag -width 6n + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .It Em average + .Bl -tag -width 6n + .It Fl -kind Ar kind + The kind of average to provide. + .It Ar values... + A group of floating-point values to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .It Em stdev + .Bl -tag -width 6n + .It Ar values... + A group of floating-point values to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .It Em quantiles + .Bl -tag -width 6n + .It Ar one-of-four + .It Ar custom-arg + .It Ar values... + A group of floating-point values to operate on. + .It Fl -test-success-exit-code + .It Fl -test-failure-exit-code + .It Fl -test-validation-exit-code + .It Fl -test-custom-exit-code Ar test-custom-exit-code + .It Fl -file Ar file + .It Fl -directory Ar directory + .It Fl -shell Ar shell + .It Fl -custom Ar custom + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .El + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } + + func testMath_MultiPageManual() throws { + try AssertGenerateManual(singlePage: false, command: "math", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH 9 + .Os + .Sh NAME + .Nm math + .Nd "A utility for performing maths." + .Sh SYNOPSIS + .Nm + .Ar subcommand + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh "SEE ALSO" + .Xr math.add 9 , + .Xr math.multiply 9 , + .Xr math.stats 9 + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH.ADD 9 + .Os + .Sh NAME + .Nm "math add" + .Nd "Print the sum of the values." + .Sh SYNOPSIS + .Nm + .Op Fl -hex-output Ar hex-output + .Op Ar values... + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl x , -hex-output + Use hexadecimal notation for the result. + .It Ar values... + A group of integers to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH.MULTIPLY 9 + .Os + .Sh NAME + .Nm "math multiply" + .Nd "Print the product of the values." + .Sh SYNOPSIS + .Nm + .Op Fl -hex-output Ar hex-output + .Op Ar values... + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl x , -hex-output + Use hexadecimal notation for the result. + .It Ar values... + A group of integers to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH.STATS 9 + .Os + .Sh NAME + .Nm "math stats" + .Nd "Calculate descriptive statistics." + .Sh SYNOPSIS + .Nm + .Ar subcommand + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh "SEE ALSO" + .Xr math.stats.average 9 , + .Xr math.stats.quantiles 9 , + .Xr math.stats.stdev 9 + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH.STATS.AVERAGE 9 + .Os + .Sh NAME + .Nm "math stats average" + .Nd "Print the average of the values." + .Sh SYNOPSIS + .Nm + .Op Fl -kind + .Op Ar values... + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -kind Ar kind + The kind of average to provide. + .It Ar values... + A group of floating-point values to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH.STATS.STDEV 9 + .Os + .Sh NAME + .Nm "math stats stdev" + .Nd "Print the standard deviation of the values." + .Sh SYNOPSIS + .Nm + .Op Ar values... + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Ar values... + A group of floating-point values to operate on. + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt MATH.STATS.QUANTILES 9 + .Os + .Sh NAME + .Nm "math stats quantiles" + .Nd "Print the quantiles of the values (TBD)." + .Sh SYNOPSIS + .Nm + .Op Ar one-of-four + .Op Ar custom-arg + .Op Ar values... + .Op Fl -test-success-exit-code Ar test-success-exit-code + .Op Fl -test-failure-exit-code Ar test-failure-exit-code + .Op Fl -test-validation-exit-code Ar test-validation-exit-code + .Op Fl -test-custom-exit-code + .Op Fl -file + .Op Fl -directory + .Op Fl -shell + .Op Fl -custom + .Fl -version Ar version + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Ar one-of-four + .It Ar custom-arg + .It Ar values... + A group of floating-point values to operate on. + .It Fl -test-success-exit-code + .It Fl -test-failure-exit-code + .It Fl -test-validation-exit-code + .It Fl -test-custom-exit-code Ar test-custom-exit-code + .It Fl -file Ar file + .It Fl -directory Ar directory + .It Fl -shell Ar shell + .It Fl -custom Ar custom + .It Fl -version + Show the version. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } +} diff --git a/Tests/ArgumentParserGenerateManualTests/RepeatGenerateManualTests.swift b/Tests/ArgumentParserGenerateManualTests/RepeatGenerateManualTests.swift new file mode 100644 index 000000000..c4ba9f728 --- /dev/null +++ b/Tests/ArgumentParserGenerateManualTests/RepeatGenerateManualTests.swift @@ -0,0 +1,95 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers + +final class RepeatGenerateManualTests: XCTestCase { + func testMath_SinglePageManual() throws { + try AssertGenerateManual(singlePage: true, command: "repeat", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt REPEAT 9 + .Os + .Sh NAME + .Nm repeat + .Sh SYNOPSIS + .Nm + .Op Fl -count + .Op Fl -include-counter Ar include-counter + .Ar phrase + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -count Ar count + The number of times to repeat 'phrase'. + .It Fl -include-counter + Include a counter with each repetition. + .It Ar phrase + The phrase to repeat. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } + + func testMath_MultiPageManual() throws { + try AssertGenerateManual(singlePage: false, command: "repeat", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt REPEAT 9 + .Os + .Sh NAME + .Nm repeat + .Sh SYNOPSIS + .Nm + .Op Fl -count + .Op Fl -include-counter Ar include-counter + .Ar phrase + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -count Ar count + The number of times to repeat 'phrase'. + .It Fl -include-counter + Include a counter with each repetition. + .It Ar phrase + The phrase to repeat. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } +} diff --git a/Tests/ArgumentParserGenerateManualTests/RollDiceGenerateManualTests.swift b/Tests/ArgumentParserGenerateManualTests/RollDiceGenerateManualTests.swift new file mode 100644 index 000000000..115e46ddf --- /dev/null +++ b/Tests/ArgumentParserGenerateManualTests/RollDiceGenerateManualTests.swift @@ -0,0 +1,105 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers + +final class RollDiceGenerateManualTests: XCTestCase { + func testRollDice_SinglePageManual() throws { + try AssertGenerateManual(singlePage: true, command: "roll", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt ROLL 9 + .Os + .Sh NAME + .Nm roll + .Sh SYNOPSIS + .Nm + .Op Fl -times + .Op Fl -sides + .Op Fl -seed + .Op Fl -verbose Ar verbose + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -times Ar n + Rolls the dice times. + .It Fl -sides Ar m + Rolls an -sided dice. + .Pp + Use this option to override the default value of a six-sided die. + .It Fl -seed Ar seed + A seed to use for repeatable random generation. + .It Fl v , -verbose + Show all roll results. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } + + func testRollDice_MultiPageManual() throws { + try AssertGenerateManual(singlePage: false, command: "roll", expected: #""" + .\" "Generated by swift-argument-parser" + .Dd May 12, 1996 + .Dt ROLL 9 + .Os + .Sh NAME + .Nm roll + .Sh SYNOPSIS + .Nm + .Op Fl -times + .Op Fl -sides + .Op Fl -seed + .Op Fl -verbose Ar verbose + .Fl -help Ar help + .Sh DESCRIPTION + .Bl -tag -width 6n + .It Fl -times Ar n + Rolls the dice times. + .It Fl -sides Ar m + Rolls an -sided dice. + .Pp + Use this option to override the default value of a six-sided die. + .It Fl -seed Ar seed + A seed to use for repeatable random generation. + .It Fl v , -verbose + Show all roll results. + .It Fl h , -help + Show help information. + .El + .Sh AUTHORS + The + .Nm + reference was written by + .An -nosplit + .An "Jane Appleseed" , + .Mt johnappleseed@apple.com , + .An -nosplit + .An "The Appleseeds" + .Ao + .Mt appleseeds@apple.com + .Ac . + """#) + } +} diff --git a/Tools/generate-manual/GenerateManual.swift b/Tools/generate-manual/GenerateManual.swift index 528e10800..0520c0f44 100644 --- a/Tools/generate-manual/GenerateManual.swift +++ b/Tools/generate-manual/GenerateManual.swift @@ -41,7 +41,7 @@ struct GenerateManual: ParsableCommand { @Option(name: .long, help: "Names and/or emails of the tool's authors. Format: 'name'.") var authors: [AuthorArgument] = [] - @Option(name: .shortAndLong, help: "Directory to save generated manual.") + @Option(name: .shortAndLong, help: "Directory to save generated manual. Use '-' for stdout.") var outputDirectory: String func validate() throws { @@ -50,14 +50,16 @@ struct GenerateManual: ParsableCommand { throw ValidationError("Invalid manual section passed to --section") } - // outputDirectory must already exist, `GenerateManual` will not create it. - var objcBool: ObjCBool = true - guard FileManager.default.fileExists(atPath: outputDirectory, isDirectory: &objcBool) else { - throw ValidationError("Output directory \(outputDirectory) does not exist") - } + if outputDirectory != "-" { + // outputDirectory must already exist, `GenerateManual` will not create it. + var objcBool: ObjCBool = true + guard FileManager.default.fileExists(atPath: outputDirectory, isDirectory: &objcBool) else { + throw ValidationError("Output directory \(outputDirectory) does not exist") + } - guard objcBool.boolValue else { - throw ValidationError("Output directory \(outputDirectory) is not a directory") + guard objcBool.boolValue else { + throw ValidationError("Output directory \(outputDirectory) is not a directory") + } } } @@ -90,14 +92,19 @@ struct GenerateManual: ParsableCommand { } do { - let outputDirectory = URL(fileURLWithPath: outputDirectory) - try generatePages(from: toolInfo.command, savingTo: outputDirectory) + if outputDirectory == "-" { + try generatePages(from: toolInfo.command, savingTo: nil) + } else { + try generatePages( + from: toolInfo.command, + savingTo: URL(fileURLWithPath: outputDirectory)) + } } catch { throw Error.failedToGenerateManualPages(error: error) } } - func generatePages(from command: CommandInfoV0, savingTo directory: URL) throws { + func generatePages(from command: CommandInfoV0, savingTo directory: URL?) throws { let document = Document( singlePage: singlePage, date: date, @@ -106,9 +113,13 @@ struct GenerateManual: ParsableCommand { command: command) let page = document.ast.map { $0.serialized() }.joined(separator: "\n") - let fileName = command.manualPageFileName(section: section) - let outputPath = directory.appendingPathComponent(fileName) - try page.write(to: outputPath, atomically: false, encoding: .utf8) + if let directory = directory { + let fileName = command.manualPageFileName(section: section) + let outputPath = directory.appendingPathComponent(fileName) + try page.write(to: outputPath, atomically: false, encoding: .utf8) + } else { + print(page) + } if !singlePage { for subcommand in command.subcommands ?? [] {