Skip to content

Commit 9041cb8

Browse files
committed
Initial code import
1 parent 4d11671 commit 9041cb8

9 files changed

Lines changed: 312 additions & 18 deletions

File tree

Package.resolved

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
1-
// swift-tools-version:5.5
1+
// swift-tools-version:5.4
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
55

66
let package = Package(
7-
name: "SystemAction",
7+
name: "SwiftShellUtilities",
8+
platforms: [.macOS(.v11)],
89
products: [
910
// Products define the executables and libraries a package produces, and make them visible to other packages.
1011
.library(
11-
name: "SystemAction",
12-
targets: ["SystemAction"]),
12+
name: "SwiftShellUtilities",
13+
targets: ["SwiftShellUtilities"]),
1314
],
1415
dependencies: [
1516
// Dependencies declare other packages that this package depends on.
1617
// .package(url: /* package url */, from: "1.0.0"),
18+
.package(url: "https://github.com/kareman/SwiftShell.git", from: "5.1.0"),
19+
.package(url: "https://github.com/onevcat/Rainbow.git", from: "4.0.0"),
1720
],
1821
targets: [
1922
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2023
// Targets can depend on other targets in this package, and on products in packages this package depends on.
2124
.target(
22-
name: "SystemAction",
23-
dependencies: []),
25+
name: "SwiftShellUtilities",
26+
dependencies: [ "SwiftShell", "Rainbow", ]),
2427
.testTarget(
25-
name: "SystemActionTests",
26-
dependencies: ["SystemAction"]),
28+
name: "SwiftShellUtilitiesTests",
29+
dependencies: ["SwiftShellUtilities"]),
2730
]
2831
)

README.md

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,71 @@
1-
# SystemAction
1+
# SwiftShellUtilities
2+
<p align="center">
3+
![badge-platforms] ![badge-languages]
4+
</p>
25

3-
A description of this package.
6+
`SwiftShellUtilities` is a set of classes that are typically useful when using Swift as a command-line tool.
7+
8+
## Installation
9+
### [Swift Package Manager](https://swift.org/package-manager/)
10+
11+
```swift
12+
import PackageDescription
13+
14+
let package = Package(
15+
name: "YourAwesomeSoftware",
16+
dependencies: [
17+
.package(url: "https://github.com/dannys42/SwiftShellUtilities.git", from: "0.1.0")
18+
],
19+
targets: [
20+
.target(
21+
name: "MyApp",
22+
dependencies: ["SwiftShellUtilities"]
23+
)
24+
]
25+
)
26+
```
27+
28+
### [swift-sh](https://github.com/mxcl/swift-sh)
29+
30+
```
31+
#!/usr/bin/swift sh
32+
33+
import SwiftShellUtilities // @dannys42
34+
35+
36+
```
37+
38+
## System Action
39+
40+
Currently the only class supported is `SystemAction` which allows command-line tools to more easily support variations of verbose/dry-run. It uses the [Rainbow](https://github.com/onevcat/Rainbow) library for colorized output and [SwiftShell](https://github.com/kareman/SwiftShell) for executing commands.
41+
42+
A typical swift-argument-parser based program may look something like this:
43+
44+
```swift
45+
46+
import ArgumentParser // https://github.com/apple/swift-argument-parser.git
47+
48+
struct BuildCommand: ParsableCommand {
49+
@Flag(name: .shortAndLong, help: "Enable verbose mode")
50+
var verbose: Bool = false
51+
52+
@Flag(name: [.customLong("dry-run"), .customShort("n")], help: "Dry-run (print but do not execute commands)")
53+
var enableDryRun: Bool = false
54+
55+
56+
mutating func run() throws {
57+
let actions: SystemAction
58+
59+
if enableDryRun {
60+
actions = CompositeAction([SystemActionPrint()])
61+
} else if verbose {
62+
actions = CompositeAction([SystemActionPrint(), SystemActionReal()])
63+
} else {
64+
actions = CompositeAction([SystemActionReal()])
65+
}
66+
67+
try actions.runAndPrint(command: "echo", "Hello", "World!")
68+
}
69+
70+
}
71+
```
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import Foundation
2+
3+
public enum SystemActionHeading {
4+
case section
5+
case phase
6+
}
7+
8+
/// `SystemAction` provides some common system-level actions typically needed of command-line utilities. It is a protocol that abstracts the intent of the action from the implementation of the action.
9+
///
10+
/// It is particularly well suited to supporting verbose and dry-run modes for command-line utilities. This can be accomplished with code like this:
11+
///
12+
/// ```
13+
/// let actions: SystemAction
14+
/// if enableDryRun {
15+
/// actions = CompositeAction([SystemActionPrint()])
16+
/// } else if verbose {
17+
/// actions = CompositeAction([SystemActionPrint(), SystemActionReal()])
18+
/// } else {
19+
/// actions = CompositeAction([SystemActionReal()])
20+
/// }
21+
/// ```
22+
///
23+
public protocol SystemAction {
24+
/// Declare a heading for the next set of actions
25+
func heading(_ type: SystemActionHeading, _ string: String)
26+
27+
/// Attempt to create a directory
28+
func createDirectory(url: URL) throws
29+
30+
/// Attempt to create a file containing the given string
31+
func createFile(fileUrl: URL, content: String) throws
32+
33+
/// Execute a program and print the results to stdout
34+
func runAndPrint(path: String?, command: [String]) throws
35+
}
36+
37+
public extension SystemAction {
38+
/// Print the title of a section
39+
/// - Parameter string: title to print
40+
func section(_ string: String) {
41+
self.heading(.section, string)
42+
}
43+
44+
/// Print the title of a phase
45+
/// - Parameter string: title to print
46+
func phase(_ string: String) {
47+
self.heading(.phase, string)
48+
}
49+
50+
/// Create a file at a given path.
51+
///
52+
/// This will overwrite existing files.
53+
/// - Parameters:
54+
/// - file: fileURL to create
55+
/// - contentBuilder: A closure that returns the content to write into the file.
56+
/// - Throws: any problems in creating file.
57+
func createFile(fileUrl: URL, _ contentBuilder: ()->String) throws {
58+
let content = contentBuilder()
59+
try self.createFile(fileUrl: fileUrl, content: content)
60+
}
61+
62+
/// Execute the given command and show the results
63+
/// - Parameters:
64+
/// - path: If not-nil, this will be the current working directory when the command is exectued.
65+
/// - command: Command to execute
66+
/// - Throws: any problems in executing the command or if the command has a non-0 return code
67+
func runAndPrint(path: String?=nil, command: String...) throws {
68+
try self.runAndPrint(path: path, command: command)
69+
}
70+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
3+
/// Allow actions to be composited and performed one after another.
4+
/// Actions will be performed in the order they are specified in the initializer
5+
public class SystemActionComposite: SystemAction {
6+
var actions: [SystemAction]
7+
8+
public init(_ actions: [SystemAction] = []) {
9+
self.actions = actions
10+
}
11+
12+
public func heading(_ type: SystemActionHeading, _ string: String) {
13+
self.actions.forEach {
14+
$0.heading(type, string)
15+
}
16+
}
17+
public func createDirectory(url: URL) throws {
18+
try self.actions.forEach {
19+
try $0.createDirectory(url: url)
20+
}
21+
}
22+
23+
public func createFile(fileUrl: URL, content: String) throws {
24+
try self.actions.forEach {
25+
try $0.createFile(fileUrl: fileUrl, content: content)
26+
}
27+
}
28+
29+
public func runAndPrint(path: String?, command: [String]) throws {
30+
try self.actions.forEach {
31+
try $0.runAndPrint(path: path, command: command)
32+
}
33+
}
34+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Foundation
2+
import Rainbow
3+
4+
/// Only print the actions. This is useful for suporting verbose modes.
5+
///
6+
/// ANSI Colors and Styles can be altered by specifying a `ModeCode` from the `Rainbow` module.
7+
public class SystemActionPrint: SystemAction {
8+
public typealias PrintStyle = [ModeCode]
9+
10+
/// Set to false to disable color/styles
11+
public var enableStyle = true
12+
public var sectionStyle: PrintStyle = [ Color.yellow, Style.bold ]
13+
public var phaseStyle: PrintStyle = [ Color.cyan, Style.bold ]
14+
public var createDirectoryStyle: PrintStyle = [ Style.bold ]
15+
public var createFileStyle: PrintStyle = [ Style.bold ]
16+
public var runAndPrintStyle: PrintStyle = [ Style.bold ]
17+
18+
public func heading(_ type: SystemActionHeading, _ string: String) {
19+
switch type {
20+
case .section:
21+
output(" == Section: \(string)", style: sectionStyle)
22+
case .phase:
23+
output(" -- Phase: \(string)", style: phaseStyle)
24+
}
25+
}
26+
27+
public func createDirectory(url: URL) throws {
28+
output(" > Creating directory at path: \(url.path)", style: createDirectoryStyle)
29+
}
30+
31+
public func createFile(fileUrl: URL, content: String) throws {
32+
output(" > Creating file at path: \(fileUrl.path)", style: createFileStyle)
33+
print(content.split(separator: "\n").map { " " + $0 }.joined(separator: "\n").yellow)
34+
}
35+
36+
public func runAndPrint(path: String?, command: [String]) throws {
37+
output(" > Executing command: \(command.joined(separator: " "))", style: runAndPrintStyle)
38+
if let path = path {
39+
output(" Working Directory: \(path)", style: runAndPrintStyle)
40+
}
41+
}
42+
43+
private func output(_ string: String, style: PrintStyle) {
44+
print(string.style(enabled: self.enableStyle, printStyle: style))
45+
}
46+
}
47+
48+
internal extension String {
49+
func style(enabled: Bool, printStyle: SystemActionPrint.PrintStyle) -> String {
50+
guard enabled else {
51+
return self
52+
}
53+
54+
var newString = self
55+
for style in printStyle {
56+
newString = newString.applyingCodes(style)
57+
}
58+
return newString
59+
}
60+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Foundation
2+
import SwiftShell
3+
4+
/// Actually perform the function
5+
public class SystemActionReal: SystemAction {
6+
public var swiftShellContext: Context = main
7+
8+
public func heading(_ type: SystemActionHeading, _ string: String) {
9+
// do nothing
10+
}
11+
12+
public func createDirectory(url: URL) throws {
13+
let fm = FileManager.default
14+
try fm.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
15+
}
16+
17+
/// Create a file at a given path.
18+
///
19+
/// This will overwrite existing files.
20+
/// - Parameters:
21+
/// - file: fileURL to create
22+
/// - content: Content of file
23+
/// - Throws: any problems in creating file.
24+
public func createFile(fileUrl: URL, content: String) throws {
25+
let fm = FileManager.default
26+
try? fm.removeItem(at: fileUrl)
27+
try content.write(to: fileUrl, atomically: false, encoding: .utf8)
28+
}
29+
30+
public func runAndPrint(path: String?, command: [String]) throws {
31+
var context = CustomContext(self.swiftShellContext)
32+
if let path = path {
33+
context.currentdirectory = path
34+
}
35+
let cmd = command.first!
36+
var args = command
37+
args.removeFirst()
38+
try context.runAndPrint(cmd, args)
39+
}
40+
}

Sources/SystemAction/SystemAction.swift

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import XCTest
2-
@testable import SystemAction
2+
@testable import SwiftShellUtilities
33

44
final class SystemActionTests: XCTestCase {
55
func testExample() throws {
66
// This is an example of a functional test case.
77
// Use XCTAssert and related functions to verify your tests produce the correct
88
// results.
9-
XCTAssertEqual(SystemAction().text, "Hello, World!")
9+
// XCTAssertEqual(SystemAction().text, "Hello, World!")
1010
}
1111
}

0 commit comments

Comments
 (0)