Add experimental manual page generation (#332)

- 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
This commit is contained in:
Rauhul Varma
2022-06-06 11:09:34 -05:00
committed by GitHub
parent 78213f3192
commit 48a799e04a
39 changed files with 3771 additions and 4 deletions
+6
View File
@@ -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"],
+104
View File
@@ -0,0 +1,104 @@
// 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: "ArgumentParserExampleTests",
dependencies: ["ArgumentParserTestHelpers"],
resources: [.copy("CountLinesTest.txt")]),
.testTarget(
name: "ArgumentParserGenerateManualTests",
dependencies: ["ArgumentParserTestHelpers"]),
.testTarget(
name: "ArgumentParserPackageManagerTests",
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
exclude: ["CMakeLists.txt"]),
.testTarget(
name: "ArgumentParserUnitTests",
dependencies: ["ArgumentParser", "ArgumentParserTestHelpers"],
exclude: ["CMakeLists.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
@@ -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 <configuration>
Tool build configuration used to generate the
manual. (default: release)
NOTE: The "GenerateManual" plugin handles passing the "<tool>" and
"--output-directory <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)'")
}
}
}
@@ -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 }
}
@@ -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)
}
}
@@ -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", "<johnappleseed@apple.com>",
"--authors", "The Appleseeds<appleseeds@apple.com>",
"--output-directory", "-",
]
if singlePage {
command.append("--single-page")
}
try AssertExecuteCommand(
command: command,
expected: expected,
exitCode: .success,
file: file,
line: line)
}
}
@@ -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
@@ -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 .
"""#)
}
}
@@ -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 .
"""#)
}
}
@@ -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 <n> times.
.It Fl -sides Ar m
Rolls an <m>-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 <n> times.
.It Fl -sides Ar m
Rolls an <m>-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 .
"""#)
}
}
@@ -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[..<terminatorIndex])
self = self[terminatorIndex...]
return collected
}
mutating func next() {
if !isEmpty { removeFirst() }
}
}
enum AuthorArgument {
case name(name: String)
case email(email: String)
case both(name: String, email: String)
}
extension AuthorArgument: ExpressibleByArgument {
// parsed as:
// - name: `name`
// - email: `<email>`
// - both: `name<email>`
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)
}
}
}
@@ -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])
}
}
}
+37
View File
@@ -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])
}
}
}
+30
View File
@@ -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 ? "." : ",")
}
}
}
}
}
@@ -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]
}
@@ -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 }
}
@@ -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<C>: 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)
}
}
@@ -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
}
@@ -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) }
}
@@ -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 }
}
+38
View File
@@ -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)
}
}
@@ -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)
}
}
+25
View File
@@ -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()
}
}
}
}
+26
View File
@@ -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()
}
}
}
@@ -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
}
}
}
}
}
}
+26
View File
@@ -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)
}
}
}
}
+27
View File
@@ -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()
}
}
+27
View File
@@ -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
}
}
}
+32
View File
@@ -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 ? [] : [","])
}
}
}
}
@@ -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
}
}
}
}
+36
View File
@@ -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)
}
}
}
}
@@ -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 }
}
}
@@ -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
}
}
@@ -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
}
+130
View File
@@ -0,0 +1,130 @@
//===----------------------------------------------------------*- 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<email>'.")
var authors: [AuthorArgument] = []
@Option(name: .shortAndLong, help: "Directory to save generated manual. Use '-' for stdout.")
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")
}
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")
}
}
}
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 {
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 {
let document = Document(
singlePage: singlePage,
date: date,
section: section,
authors: authors,
command: command)
let page = document.ast.map { $0.serialized() }.joined(separator: "\n")
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 ?? [] {
try generatePages(from: subcommand, savingTo: directory)
}
}
}
}
@@ -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()
}
}
File diff suppressed because it is too large Load Diff
@@ -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() { }
}
@@ -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
}
}