Skip to content

Instantly share code, notes, and snippets.

@sahara-ooga
Last active October 27, 2020 09:06
Show Gist options
  • Save sahara-ooga/bf1a006c5705d9e38c6310dd6a4580d4 to your computer and use it in GitHub Desktop.
Save sahara-ooga/bf1a006c5705d9e38c6310dd6a4580d4 to your computer and use it in GitHub Desktop.
Swift コマンドラインツール
import Foundation
SearchQiita.main()
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Qiita",
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.3.0")),
.package(url: "https://github.com/YusukeHosonuma/SwiftPrettyPrint.git", .upToNextMajor(from: "1.0.0"))
],
targets: [
.target(
name: "Qiita",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"SwiftPrettyPrint",
"QiitaCore"
]),
.target(
name: "QiitaCore",
dependencies: []
),
.testTarget(
name: "QiitaTests",
dependencies: ["QiitaCore"]),
]
)
import Foundation
public struct Qiita {
private(set) var keyword: String
private let session: URLSession
public init(keyword: String, session: URLSession = .shared) {
self.keyword = keyword
self.session = session
}
// keywordをもとに記事を検索する
public func search(completion: @escaping (String?) -> Void) {
let url = URL(string: "https://qiita.com/api/v2/items?page=1&per_page=20&query=\(keyword)")!
var req = URLRequest(url: url)
req.httpMethod = "GET"
session.dataTask(with: req) { (data, _, _) in
if let data = data, let result = String(data: data, encoding: .utf8) {
// 実際にはここでごにょごにょ整形する
completion(result)
} else {
completion(nil)
}
}.resume()
}
}
import XCTest
import class Foundation.Bundle
import QiitaCore
final class QiitaTests: XCTestCase {
func xtestExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
// Some of the APIs that we use below are available in macOS 10.13 and above.
guard #available(macOS 10.13, *) else {
return
}
let fooBinary = productsDirectory.appendingPathComponent("Qiita")
let process = Process()
process.executableURL = fooBinary
let pipe = Pipe()
process.standardOutput = pipe
try process.run()
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
XCTAssertEqual(output, "Hello, world!\n")
}
func testSearch() throws {
let session = URLSessionMock()
let qiita = Qiita(keyword: "swift", session: session)
qiita.search { _ in }
XCTAssertEqual(session.url, "https://qiita.com/api/v2/items?page=1&per_page=20&query=swift")
}
/// Returns path to the built products directory.
var productsDirectory: URL {
#if os(macOS)
for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
return bundle.bundleURL.deletingLastPathComponent()
}
fatalError("couldn't find the products directory")
#else
return Bundle.main.bundleURL
#endif
}
static var allTests = [
//("testExample", testExample),
("testSearch", testSearch),
]
}
final class URLSessionDataTaskMock: URLSessionDataTask {
override func resume() {
// Do nothing
}
}
final class URLSessionMock: URLSession {
typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
var url: String = ""
override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
self.url = request.url?.absoluteString ?? ""
return URLSessionDataTaskMock()
}
}
import ArgumentParser
import QiitaCore
import Dispatch
struct SearchQiita: ParsableCommand {
//コマンドラインツールのメタ情報を設定
static var configuration = CommandConfiguration(
commandName: "qiita",
abstract: "Search articles in Qiita.",
discussion: """
TODO: 複数のキーワード検索
""",
version: "1.0.0",
shouldDisplay: true,
//subcommands: <#T##[ParsableCommand.Type]#>, // non use
//defaultSubcommand: <#T##ParsableCommand.Type?#>, // non use
helpNames: [.long, .short]
)
@Argument(help: "search keywords")
var keywords: [String]
func run() throws {
//TODO: 複数のキーワード検索
if keywords.isEmpty {
throw(RuntimeError("no keywords"))
}
let keyword = keywords[0]
print("\(keyword)でQiitaの記事を検索します")
let q = Qiita(keyword: "Swift")
q.search { result in
print("result: \(result ?? "")")
Dispatch.exit(0)
}
dispatchMain()
}
}
extension SearchQiita {
struct RuntimeError: Error, CustomStringConvertible {
var description: String
init(_ description: String) {
self.description = description
}
}
}
//: [【Swift Argument Parser入門】Swiftでコマンドラインツールを作る](https://blog.personal-factory.com/2020/06/06/how-to-start-swift-argument-parser/)
import ArgumentParser
struct Sounder: ParsableCommand {
//コマンドラインツールのメタ情報を設定
static var configuration = CommandConfiguration(
commandName: "sounds",
abstract: "Output some animal sounds",
discussion: """
Demonstrationg how the Swift Argument Parser works.
""",
version: "1.0.0",
shouldDisplay: true,
//subcommands: <#T##[ParsableCommand.Type]#>, // non use
//defaultSubcommand: <#T##ParsableCommand.Type?#>, // non use
helpNames: [.long, .short]
)
/// debugモードを表すときに使える
/// `% example --verbose`
@Flag(help: "show detail logs")//ヘルプ出力した際の説明を指定
var verbose: Bool = false//--verboseがついた場合はtrueになる
enum AnimalKind: EnumerableFlag {
case cat
case dog
case mouse
static func name(for value: Sounder.AnimalKind) -> NameSpecification {
switch value {
case .cat:
return [.customShort("c"), .long]//-cと--catどちらでも指定できる
case .dog:
return [.customShort("d"), .long]
case .mouse:
return [.customShort("m"), .long]
}
}
}
@Flag(help: "specify the kind of animal")
var animalKind: AnimalKind = .cat
//option
@Option(help: "the number of sounds")
var counter: Int = 2
@Argument(help: "the name of sounds animal")
var names: [String]
func run() throws {
// 引数の検証
if counter < 0 {
throw(RuntimeError("Counter must be positive."))
}
if counter > 10 {
throw(RuntimeError("Too many counter. Max 10."))
}
let sounds: String
switch animalKind {
case .cat:
sounds = "Meow"
case .dog:
sounds = "bow-wow"
case .mouse:
sounds = "squeak"
}
if verbose {
print("start sounds")
}
var outputs = ""
for _ in 0 ..< counter {
outputs += sounds + " "
}
for name in names {
print("\(name): \(outputs)")
}
if verbose {
print("end sounds")
}
}
}
extension Sounder {
struct RuntimeError: Error, CustomStringConvertible {
var description: String
init(_ description: String) {
self.description = description
}
}
}
// if in use:
// Sounder.main()

VScodeでの開発

Swift Development with Visual Studio Code - NSHipster

を参考に、VScodeでlanguage server protocolを利用できるようにした。これで、Swiftの言語機能に関してはXcodeのように補完が効くようになった(UIKitなどAppleのフレームワークに関しては補完が効かない)。

Argument Parser

Swiftでコマンドラインツールを使う際に、引数のパースなどを処理してくれるApple謹製のライブラリapple/swift-argument-parserを利用した。

佐藤さんの記事を見て、Argument Parserのチュートリアルをやってみる。

次に、Qiitaのキーワード検索を行うコマンドラインツールをSwiftでコマンドラインツール作成の誘い - Qiitaを見ながらやってみる。この記事では引数のパースに別のライブラリを使用しているので、Argument Parserを利用した形に書き換えてみた。

関連するコマンド

# プロジェクトルートにて
swift package init --type executable
swift build
swift run
swift run Qiita swift #引数をつけて実行
swift test
# コマンドを登録する
swift buid -c release
cd .build/release
cp -f Qiita /usr/local/bin/
# 以降、コマンドを直接呼ぶことが出来る
Qiita swift
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment