ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

macOS上で動くCLIアプリ作成ガイド

こんにちは、プロダクトエンジニアリング部の能登です。春、いいですね。私などは山菜など春が旬を食する楽しみが専らですが…

端的に言うと:

Swift言語を使ったCLIアプリ作成ガイド。Swift言語の平行処理機能とCLIアプリは相性が良い。

1) CLIについて

iPhone/iPadアプリ開発にしようするSwift言語でもコマンドを作成する機能が提供されています。今回の内容はSwift言語のCLIアプリ開発ガイドとなります。

スマートフォンやノート/デスクトップ上で動作するアプリはマウスや画面タッチによる軽快な操作性と見た目を備えています。今日アプリが広く普及する以前からコンピューターは存在しCLIについて簡潔に説明すると行単位でコマンド(命令)を受け付けるインターフェイスが主流だった頃(1990年前半)がありました。

今日でもクラウドコンピューティングサービス上のインスタンス(サーバー)へアクセスする手段やWeb開発向けのインターフェイスとして親しまれており、 2023年現在ではCLI(command line interface)という名称で呼ばれています。

CLIはコマンド(命令)を受け付けて結果を返します。機能としてはシンプルがものが多いですが複数のコマンドを組み合わせて複雑な機能を用意することができます。スマートフォンアプリの提供したいサービスに対して適切なユーザインタフェイスが1セットとなっているのに対して、CLIはコマンドラインという単一のインターフェイスから複数の機能を組み合わせて利用できるとも言えるでしょう。

2010年代はECMAScriptとパッケージ管理ソフトのNodeJSを基盤とするとOSの違いをあまり気にせず使えるコマンドが提供できることに着目されたことでAWS CLIやFirebase CLIなどのようにクラウドコンピューティング上で稼働するOSと開発に使用するOSが異なっても共通の機能を利用されています。

異なるOS上でも動作するコマンドが受け入れられている一方、クラシックなコマンドは特定のOS上で動作するように設計され、プログラミング言語を用いることも可能です。あえてプログラミング言語を用いるケースとしてはOSが動作するハードウェアのリソース(CPU,メモリー、GPU)を活用したい際の選択肢となるでしょう。

2) AppleのCLIアプリ

軽快な操作性と見た目が特徴的なアプリを作成可能なAppleの開発環境ですがCLI上で動作する機能(コマンド)を作成する機能も持っています。Appleの開発環境であるXcode上でCommand Line Toolアプリという形で作成可能です。

Command Line ToolアプリはAppleがサポートしている言語(Objective-C、Swift)のうち、Swift言語を選択した場合は並行処理機能をサポートしているためmacのCPU処理を効率よく使うことが可能です。

Swift言語を理解していているiOSアプリ開発者であれば並行処理機能機能をmacOSのCLIアプリにも適用可能です(Swift言語を学ぶ際に言語の機能を学ぶ際にも利用することも可能でしょう)。

Note:

Swift言語はWindows、Ubuntuでも利用可能ですが動作環境の整備(各種フレームワーク)が最も揃っているのはmacOSとなります。

3) Command Line Toolアプリを準備する

以下の環境上でCommand Line Toolアプリを作成します。

macOS 12.6.3 Xcode14.2

Xcodeを起動、メニューからNew - Projectをタップ、新規プロジェクト(ショートカット Shiftキー+Commandキー+N)作成画面から macOSタブの選択、ApplicationのCommand Line Tool を選択します。言語としてはSwiftを選択、プロジェクト名を入力してCommand Line Toolアプリのプロジェクトを作成します。

Xcode macOS projects

CommandLineTools - Xcode

構成されるソースコードはmain.swift 1つとiPhone/iPadアプリのようなユーザインタフェースに関係する内容がないため、ソース構成は最小構成となっています。

実行方法はメニューから Product - Build(ショートカット B+Commandキー)で実行します。実行結果はDebug画面に表示されます。新規作成した場合は"Hello, World!"が表示されます。

main.swift:
import Foundation

print("Hello, World!")

内容としてはOSの基本的な機能を利用するためのフレームワークであるFoundation を利用することを定義しprint()で"Hello, World!" をdebug画面に出力するだけでシンプルなソースとなっています。Swift言語に親しんでいる開発者であればこちらのシンプルなソースコードに違和感があるはずです。違和感とはimportに続けてprint() が記述されており関数定義に含まれていない点です。

main.swift:
import Foundation

print("Hello, World!") // ← 本来なら func foo() { } の中に記述される必要あり

シンプルなコードの秘密はSwift言語がファイル名がmain.swft の場合は暗黙的にswiftのCommand Line Toolアプリのエントリーポイント(起動箇所)付のものに変換するためです。

シンプルなコードのまま拡張性は少ないので以下のように変更を加えます。

  1. main.swift のファイル名を[任意].swift に変更
  2. ソースコードを以下に置き換え
@main
struct [任意の構造体名] {
    static func main() throws {
        print("test.")
    }
}

@main が付加された構造体のメソッドmain() 関数が呼び出し元となります。@main はSwift言語の仕様には含めないが利用するフレームワークごとに変数や構造体の機能を拡張するための言語仕様Swift Attributes に基づいた機能で@mainの役割としてはアプリが最初に呼び出す開始地点(entory point)を明示するための役割があります。

Note:

Swift Attributesの役割は言語仕様に含めるには用途が限られている機能や、OSごとに対応状況が異なる機能、特定のフレームワークに機能を付加したい場合に多様される機能です。Swift言語に対してAppleが好き勝手機能を追加しているイメージがありますがSwift Attributes経由で機能を追加するなど言語に加える機能は最低限に抑えている印象があります。

4) Command Line Toolアプリを並行処理(concurrency)対応とする

2021年のApple開発者イベントにて公開された並行処理をCommand Line Toolアプリに対応させることもできます。Swiftの並行処理はSwift Concurrency と呼ばれ、考え方としてはasync/await/actorといった少ない予約語を用いて並行処理を簡潔に記述することができます。Swiftの並行処理はで注意しなければならないのは並行処理を簡潔に書くことに伴うソースコード量の減少と厳密に記述できるも並行処理で起きうるロック状態などを全て排除しているわけではない点があります。

3のサンプルコードのmain()にasyncを付加することで並行処理に対応したコードとなります。

@main
struct [任意の構造体名] {
    static func main() async throws {
        print("test.")
    }
}

以下に狐画像をランダムで表示するWebAPIから画像を取得するサンプルを示します。

randomfox.ca

import Foundation
struct RandomFox: Equatable, Codable { let image: String; let link: String }

@main
struct CommandLineTest {
    static let serviceUrl = URL(string: "https://randomfox.ca/floof/")!
    
    static func fox() async throws -> RandomFox {
        let request = URLRequest( url: serviceUrl, cachePolicy: .reloadIgnoringLocalCacheData)
        let (jsonData, _) = try await URLSession.shared.data(for: request)
        let fox = try JSONDecoder().decode(RandomFox.self, from: jsonData)
        return fox
    }
    
    static func imageData(link: String) async throws -> Data {
        let request = URLRequest( url: URL(string: link)! )
        let (imageData, _) = try await URLSession.shared.data(for: request)
        return imageData
    }
    
    static func foxImage() async throws -> Data {
        let fox = try await fox()
        let image = try await imageData(link: fox.image)
        return image
    }
    
    static func main() async throws {
        let foxImage = try await foxImage()
        let destinationUrl = URL(fileURLWithPath: FileManager.default.currentDirectoryPath + "/fox.jpg")
        try foxImage.write(to: destinationUrl)
    }
}

コードとしてはWebAPIでa.ランダムな狐情報のJSON情報を取得、b.JSON情報に含まれるURLから画像をダウンロードしfox.jpgの名前で保存するだけです。ただし並行処理を行うことが保障されているので複数の狐画像を取得する形にも改造できる拡張性を保つことができるようになりました。

5) ArgumentParserフレームワーク

ここまででCommand Line Toolアプリに興味を持たれたならばよりCLIアプリを作ってみたいと考えた方がいるかもしれません。

CLIアプリらしい機能とはコマンドは様々なパラメーターやフラグを解釈です。コマンド側がは様々なパラメーターやフラグを解釈できるようになることでCLIらしい挙動(パラメーターやフラグを指定できる)を提供することができます。

AppleのCommand Line Tool アプリではパラメーターやフラグ機能を実現するためにAppleはArgumentParserフレームワークを用意しています。ArgumentParserはCommand Line Toolアプリに渡すパラメーターやフラグをプログラム中に使用する変数としてマッピングする、ヘルプ表示に対応するなどの機能が提供されます。

ArgumentParserフレームワークはApple謹製のパッケージ管理ツールSwift Package経由で提供されており必要に応じて機能追加可能となっています。

ArgumentParser: https://apple.github.io/swift-argument-parser/documentation/argumentparser

ArgumentParserについてAppleは以下のサンプルコードを例に出してフレームワークを説明しています。

import ArgumentParser

@main
struct Repeat: ParsableCommand { // ParsableCommandを指定
    @Argument(help: "The phrase to repeat.")
    var phrase: String

    @Option(help: "The number of times to repeat 'phrase'.")
    var count: Int? = nil

    mutating func run() throws {
        let repeatCount = count ?? 2
        for _ in 0..<repeatCount {
            print(phrase)
        }
    }
}

Apple - ArgumentParser framework

ArgumentParserを使う際のソースコードの変更点はa.構造体のprotocolとしてParsableCommandを指定する。b.アプリの開始がmain()からrun()に変更となるのがArgumentParserを使った際の修正となります。パラメータやフラグを加えたい際は@Argument、@Option といったArgumentParserフレームワークが拡張した属性を使って受け取ることができます。

ArgumentParserの並行処理対応版としてはAynsArgumentParserが用意されています。

@main
struct CountLines: AsyncParsableCommand {
    @Argument(transform: URL.init(fileURLWithPath:))
    var inputFile: URL

    mutating func run() async throws {
        let fileHandle = try FileHandle(forReadingFrom: inputFile)
        let lineCount = try await fileHandle.bytes.lines.reduce(into: 0) 
            { count, _ in count += 1 }
        print(lineCount)
    }
}

ParsableCommandとrun()関数の定義にasync が追加された形となります。

まとめ

iPhone/iPadアプリケーション開発が注目されがちなAppleの開発環境ですがmacOS向けのCLIアプリ(Command Line Toolアプリ)を作成することができます。iPhone/iPadアプリで馴染みある並行処理機能をmacOS向けCLIアプリに組み込むための環境が整備されています。またAppleはCLIアプリらしい機能(パラメーターやフラグ対応)をサポートするためAppleはArgumentParserフレームワークを提供しています。

参考

UniFaでは共に働く仲間を募集しております募集している対象は都度かわりますのでお手数ですが採用情報からご確認頂ければさいわいです。

unifa-e.com