Skip to content

Commit f20f100

Browse files
liamrosenfeldcmyr
authored andcommitted
Swift Argument Parser for the CLI
Allows for a simpler, more maintainable CLI.
1 parent 20c39ce commit f20f100

File tree

8 files changed

+190
-157
lines changed

8 files changed

+190
-157
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
language: rust
2-
osx_image: xcode10.2
2+
osx_image: xcode11.3
33

44
git:
55
submodules: true

README.md

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -120,23 +120,14 @@ XiEditor includes a CLI for opening files directly from the command line.
120120
### Usage
121121

122122
```text
123-
xi <file> [--wait | -w] [--help | -h]
124-
open XiEditor at specified file path
123+
USAGE: xi [<files> ...] [--wait]
125124
126-
file
127-
the path to the file (relative or absolute)
125+
ARGUMENTS:
126+
<files> Relative or absolute path to the file(s) to open. If none, opens empty editor.
128127
129-
--wait, -w
130-
wait for the editor to close
131-
132-
--help, -h
133-
prints this
134-
135-
xi [--help | -h]
136-
open XiEditor
137-
138-
--help, -h
139-
prints this
128+
OPTIONS:
129+
--wait Wait for the editor to close before finishing process.
130+
-h, --help Show help information.
140131
```
141132

142133
### Git Editor

Sources/XiCli/main.swift

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,6 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import Foundation
1615
import XiCliCore
1716

18-
let args = Arguments()
19-
let tool = CommandLineTool(args: args)
20-
21-
do {
22-
try tool.run()
23-
} catch {
24-
print("Whoops! An error occurred: \(error)")
25-
exit(1)
26-
}
17+
Xi.main()
Lines changed: 11 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2018 The xi-editor Authors.
1+
// Copyright 2020 The xi-editor Authors.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -12,44 +12,11 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import Cocoa
15+
import AppKit
16+
import ArgumentParser
1617

17-
public final class CommandLineTool {
18-
private let args: Arguments
19-
20-
public init(args: Arguments) {
21-
self.args = args
22-
}
23-
24-
public func run() throws {
25-
if args.help == true {
26-
help()
27-
return
28-
}
29-
30-
guard !args.fileInputs.isEmpty else {
31-
NSWorkspace.shared.launchApplication("XiEditor")
32-
return
33-
}
34-
35-
let filePaths = try args.fileInputs.map {
36-
try resolvePath(from: $0)
37-
}
38-
39-
for filePath in filePaths {
40-
try openFile(at: filePath)
41-
}
42-
43-
if args.wait, !filePaths.isEmpty {
44-
print("waiting for editor to close...")
45-
let group = DispatchGroup()
46-
group.enter()
47-
setObserver(group: group, filePaths: filePaths)
48-
group.wait()
49-
}
50-
}
51-
52-
func resolvePath(from input: String) throws -> String {
18+
struct CliHelper {
19+
static func resolvePath(from input: String) throws -> String {
5320
let fileManager = FileManager.default
5421
var filePath: URL!
5522

@@ -66,28 +33,28 @@ public final class CommandLineTool {
6633
let pathString = canonicalPath(input)
6734

6835
guard pathIsNotDirectory(pathString) else {
69-
throw CliError.pathIsDirectory
36+
throw ValidationError("The path entered is to a directory")
7037
}
7138

7239
if !fileManager.fileExists(atPath: pathString) {
7340
let createSuccess = fileManager.createFile(atPath: pathString, contents: nil, attributes: nil)
7441

7542
guard createSuccess else {
76-
throw CliError.couldNotCreateFile
43+
throw RuntimeError("Could not create a file")
7744
}
7845
}
7946

8047
return pathString
8148
}
8249

83-
func openFile(at path: String) throws {
50+
static func openFile(at path: String) throws {
8451
let openSuccess = NSWorkspace.shared.openFile(path, withApplication: "XiEditor")
8552
guard openSuccess else {
86-
throw CliError.failedToOpenEditor
53+
throw RuntimeError("Xi editor could not be opened")
8754
}
8855
}
8956

90-
func setObserver(group: DispatchGroup, filePaths: [String]) {
57+
static func setObserver(group: DispatchGroup, filePaths: [String]) {
9158
let notificationQueue: OperationQueue = {
9259
let queue = OperationQueue()
9360
queue.name = "Notification queue"
@@ -110,33 +77,9 @@ public final class CommandLineTool {
11077
}
11178
}
11279
}
113-
114-
func help() {
115-
let message = """
116-
The Xi CLI Help:
117-
xi <file>... [--wait | -w] [--help | -h]
118-
119-
file
120-
the path to the file (relative or absolute)
121-
122-
--wait, -w
123-
wait for the editor to close
124-
125-
--help, -h
126-
prints this
127-
"""
128-
print(message)
129-
}
13080

131-
func canonicalPath(_ path: String) -> String {
81+
static func canonicalPath(_ path: String) -> String {
13282
return URL(fileURLWithPath: path).standardizedFileURL.resolvingSymlinksInPath().path
13383
}
13484
}
13585

136-
public extension CommandLineTool {
137-
enum CliError: Swift.Error {
138-
case couldNotCreateFile
139-
case failedToOpenEditor
140-
case pathIsDirectory
141-
}
142-
}
Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2018 The xi-editor Authors.
1+
// Copyright 2020 The xi-editor Authors.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -12,22 +12,12 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
public struct Arguments {
16-
var fileInputs: [String] = []
17-
var wait: Bool = false
18-
var help: Bool = false
15+
import Foundation
16+
17+
struct RuntimeError: Error, CustomStringConvertible {
18+
var description: String
1919

20-
public init(arguments: [String] = CommandLine.arguments) {
21-
let actualArgs = Array(arguments.dropFirst())
22-
for arg in actualArgs {
23-
if arg == "--wait" || arg == "-w" {
24-
self.wait = true
25-
} else if arg == "--help" || arg == "-h" {
26-
self.help = true
27-
} else {
28-
self.fileInputs.append(arg)
29-
}
30-
}
20+
init(_ description: String) {
21+
self.description = description
3122
}
3223
}
33-

Sources/XiCliCore/Xi.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2020 The xi-editor Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import ArgumentParser
16+
import AppKit
17+
18+
public struct Xi: ParsableCommand {
19+
@Argument(help: "Relative or absolute path to the file(s) to open. If none, opens empty editor.")
20+
var files: [String]
21+
22+
@Flag(help: "Wait for the editor to close before finishing process.")
23+
var wait: Bool
24+
25+
public init() {}
26+
27+
public func run() throws {
28+
29+
guard !files.isEmpty else {
30+
NSWorkspace.shared.launchApplication("XiEditor")
31+
return
32+
}
33+
34+
let filePaths = try files.map {
35+
try CliHelper.resolvePath(from: $0)
36+
}
37+
38+
for filePath in filePaths {
39+
try CliHelper.openFile(at: filePath)
40+
}
41+
42+
if wait, !filePaths.isEmpty {
43+
print("waiting for editor to close...")
44+
let group = DispatchGroup()
45+
group.enter()
46+
CliHelper.setObserver(group: group, filePaths: filePaths)
47+
group.wait()
48+
}
49+
}
50+
}
51+
52+

Tests/XiCliCoreTests/XiCliCoreTests.swift renamed to Tests/XiCliCoreTests/CliHelperTests.swift

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2018 The xi-editor Authors.
1+
// Copyright 2020 The xi-editor Authors.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -15,14 +15,9 @@
1515
import XCTest
1616
@testable import XiCliCore
1717

18-
class XiCliCoreTests: XCTestCase {
19-
20-
var commandLineTool: CommandLineTool!
21-
18+
class CliHelperTests: XCTestCase {
2219
override func setUp() {
2320
super.setUp()
24-
let testArguments = Arguments(arguments: ["test.txt", "--wait"])
25-
commandLineTool = CommandLineTool(args: testArguments)
2621
let fileManager = FileManager.default
2722
fileManager.createFile(atPath: "test.txt", contents: "This is a tester file".data(using: .utf8), attributes: nil)
2823
try? fileManager.createSymbolicLink(atPath: "test_link.txt", withDestinationPath: "test.txt")
@@ -33,23 +28,14 @@ class XiCliCoreTests: XCTestCase {
3328
try? FileManager.default.removeItem(atPath: "test_link.txt")
3429
try? FileManager.default.removeItem(atPath: "test.txt")
3530
}
36-
37-
func testArgParse() {
38-
let arguments = ["xi", "--wait", "test1.txt", "test2.txt"]
39-
let retrievedArgs = Arguments(arguments: arguments)
40-
XCTAssertNotNil(retrievedArgs)
41-
XCTAssert(retrievedArgs.fileInputs == ["test1.txt", "test2.txt"])
42-
XCTAssert(retrievedArgs.wait == true)
43-
XCTAssert(retrievedArgs.help == false)
44-
}
45-
31+
4632
func testResolveAbsolutePath() {
4733
let fromPath = fileInTempDir("testResolvePath")
4834
FileManager
4935
.default
5036
.createFile(atPath: fromPath, contents: "This is a tester file".data(using: .utf8), attributes: nil)
5137
do {
52-
let path = try commandLineTool.resolvePath(from: fromPath)
38+
let path = try CliHelper.resolvePath(from: fromPath)
5339
XCTAssert(path == fromPath)
5440
} catch {
5541
XCTFail("temp file in temp dir not found")
@@ -58,7 +44,7 @@ class XiCliCoreTests: XCTestCase {
5844

5945
func testResolveRelativePath() {
6046
do {
61-
let path = try commandLineTool.resolvePath(from: "test.txt")
47+
let path = try CliHelper.resolvePath(from: "test.txt")
6248
let expectedPath = fileInCurrentDir("test.txt")
6349
XCTAssert(path == expectedPath, "path does not match expected")
6450
} catch {
@@ -68,7 +54,7 @@ class XiCliCoreTests: XCTestCase {
6854

6955
func testResolveSymlink() {
7056
do {
71-
let path = try commandLineTool.resolvePath(from: "test_link.txt")
57+
let path = try CliHelper.resolvePath(from: "test_link.txt")
7258
let expectedPath = fileInCurrentDir("test.txt")
7359
XCTAssert(path == expectedPath, "path does not match expected")
7460
} catch {
@@ -77,15 +63,15 @@ class XiCliCoreTests: XCTestCase {
7763
}
7864

7965
func testFileOpen() {
80-
XCTAssertNoThrow(try commandLineTool.openFile(at: "test.txt"))
66+
XCTAssertNoThrow(try CliHelper.openFile(at: "test.txt"))
8167
}
8268

8369
func testObserver() {
8470
let group = DispatchGroup()
8571
group.enter()
8672
let observedPaths = ["filePath1", "test_link.txt"]
8773
let expectedPaths = [fileInCurrentDir("filePath1"), fileInCurrentDir("test.txt")]
88-
commandLineTool.setObserver(group: group, filePaths: expectedPaths)
74+
CliHelper.setObserver(group: group, filePaths: expectedPaths)
8975
DistributedNotificationCenter.default().post(name: Notification.Name("io.xi-editor.XiEditor.FileClosed"), object: nil, userInfo: ["path": observedPaths.first!])
9076
DistributedNotificationCenter.default().post(name: Notification.Name("io.xi-editor.XiEditor.FileClosed"), object: nil, userInfo: ["path": observedPaths.last!])
9177
let expectation = XCTestExpectation(description: "Notification Recieved")

0 commit comments

Comments
 (0)