概要
本記事は,OSS Advent Calendar 2023の12月14日の記事です.
本記事では,自作OSSとして,YAMLからCLIを自動生成するツール「cyamli」を試作しましたので,これについて紹介します.
※本記事はバージョンv1.0.0より前のcyamliについて記述しており,バージョンv1.0.0以降のcyamliと互換性のないコードもありますが,ツールのコンセプトは同じです.(追記 2024-06-17)
背景
最近,Goでプログラムを書いています.その中で,作業の自動化を目的としてコンソールアプリケーションを作成することがよくあります. コンソールアプリケーションは,CLI(Command Line Interface)を備えたプログラムで,サブコマンド,オプション引数,位置引数を含むコマンドライン引数を取り扱うことが必要となります.
コマンドライン引数を取り扱うためには,コマンドライン引数を定義・解釈する仕組みとしてが必要となりますが,Goの既存のライブラリの中で以下の条件を満たすAPI(本記事では,API(Application Programming Interface)といえばWeb APIではなく,ライブラリ等のプログラミング言語のためのインターフェースのことを指します.)を見つけることができませんでした.
例えば,Goの標準ライブラリにあるos.Args
は,最も原始的なAPIで,コマンドライン引数をstring
を要素とするスライスとして表現しますが,1も2も満たしません.
pkg.go.dev
Goの標準ライブラリにあるflag
パッケージは,型付けされたオプション引数を扱うことができますが,サブコマンドや位置引数を定義する機能は持ちません.また,オプション引数の定義は,var helpFlag = flag.Bool("help", true, "show help")
のように行うのですが,変数名helpFlag
とオプション引数名"help"
のように情報が重複してしまうことがあり,3を完全に満たしているとは言えません.
pkg.go.dev
外部ライブラリであるcobraは,サブコマンド,オプション引数,位置引数を定義・解釈することができますが,位置引数の型を指定することができないため,2を完全に満たしているとは言えません.また,flag
パッケージと同様にソースコード内で情報が重複してしまうことがあり,3を完全に満たしているとは言えません.
github.com
別の外部ライブラリであるdocoptは,独自のフォーマットに沿って記述されるヘルプテキストによって,サブコマンド,オプション引数,位置引数を含む複雑なコマンドライン引数を定義することができ,これを元にコマンドライン引数を解釈することができますが,型を指定することができません. github.com
YAMLからCLIを自動生成するツール「cyamli」
上で示した条件1は,自分がコマンドライン引数を扱うコンソールアプリケーションを開発する上で,必要だと感じた最低限の機能です. 条件2は,エディタや統合開発環境の支援を享受したり,コンソールアプリケーションのビルド時に網羅的な型チェックを実施するために必要ものです. 条件3は,プログラムを管理する上で,一貫性を保ちやすくするために必要なものです.
コマンドライン引数を定義・解釈する仕組みとして,これらの条件をみたすものを実現するために「cyamli」というツールを開発しました. GitHub上で公開しています. github.com
「cyamli」の仕組みと特徴
「cyamli」は,以下の図に示すような仕組みで動作します.
「cyamli」は以下の特徴を持ちます.
- サブコマンド,オプション引数,位置引数をサポートします.
- CLI定義の情報源が1つのYAMLに集約されるため,SSOTが実現できます.
- コンソールアプリケーションをビルドする前にCLIの型が決定されるため,型付けされたAPIを提供することができます.
以下は,前節で挙げたライブラリと「cyamli」を比較する表です.
「cyamli」の使い方と使用例
「cyamli」の使い方は,次の通りです.
使用例を以下に示します.
1. CLIをYAMLで定義
ここでは例として,データベースからテーブルの情報を取得するコンソールアプリケーションの作成を想定して,以下のようにCLIを定義します.
# cli.yaml name: demo description: demo app to get table information from databases subcommands: list: description: list tables options: -config: description: path to config file short: -c describe: description: show information of tables options: -config: description: path to config file short: -c -format: short: -f description: "output format: json or sql" default: json -unique: description: includes unique constraint information type: boolean -foreign-key: description: includes foreign key constraint information type: boolean -check: description: includes check constraint information type: boolean arguments: - name: tables variadic: true description: names of tables to be described
このYAMLファイルでは,コンソールアプリケーションについて以下の内容が定義されています.
- 名前が
demo
である. - サブコマンドとして
list
,describe
を持つ.
また,list
サブコマンドについては以下の内容が定義されています.
- 以下のオプション引数を持つ.
-config
:文字列型の値を取り,コンフィグファイルのパスを指定する.短縮バージョンとして-c
がある.
describe
サブコマンドについては以下の内容が定義されています.
- 以下のオプション引数を持つ.
- 以下の位置引数を持つ.
tables
:取得対象のテーブルを0個以上指定する.
2. CLIを解釈するAPIを自動生成
「cyamli」を実行してAPIを生成します. 「cyamli」の実行は以下のように行うことができます.
# cyamli のインストール go install "github.com/Jumpaku/cyamli/cmd/cyamli@latest" # cyamliの実行 # cli.yamlを入力し,cli.gen.goに出力する. cyamli golang -schema-path=cli.yaml -out-path=cli.gen.go
単に以下のように実行することもできます.
go run "github.com/Jumpaku/cyamli/cmd/cyamli@latest" golang -schema-path=cli.yaml -out-path=cli.gen.go
これにより,以下のようなAPIが生成されます(一部省略).
// Code generated by cyamli v0.0.11, DO NOT EDIT. package main // ... type Func[Input any] func(subcommand []string, input Input, inputErr error) (err error) type CLI struct { List CLI_List Describe CLI_Describe FUNC Func[CLI_Input] } type CLI_Input struct { } type CLI_List struct { FUNC Func[CLI_List_Input] } type CLI_List_Input struct { Opt_Config string } type CLI_Describe struct { FUNC Func[CLI_Describe_Input] } type CLI_Describe_Input struct { Opt_Check bool Opt_Config string Opt_ForeignKey bool Opt_Format string Opt_Unique bool Arg_Tables []string } // ... func NewCLI() CLI { return CLI{} } func Run(cli CLI, args []string) error { // ... }
このように,cyamliが生成するコードには以下のAPIが含まれます.
CLI
:ルートコマンドを表現する構造体CLI_Input
:ルートコマンドに渡すコマンドライン引数を表現する構造体CLI_<サブコマンド>
:サブコマンドを表現する構造体CLI_<サブコマンド>_Input
:サブコマンドに渡すコマンドライン引数を表現する構造体Run
:エントリーポイントとなる関数
3. 自動生成されたAPIに関数を割り当て
CLI
構造体とCLI_<サブコマンド>
構造体は,FUNC
フィールドを持ちます.FUNC
フィールドは,コマンドライン引数を表現する構造体を受け取ることが可能な関数の型を持っており,コマンドが実行するべき処理を設定することができます.
以下は,コマンドが実行するべき処理を設定したmain.goの例です.
// main.go package main import ( _ "embed" "fmt" "os" ) func main() { // CLIオブジェクトを生成 cli := NewCLI() // ルートコマンドの処理を設定 cli.FUNC = func(subcommand []string, input CLI_Input, inputErr error) (err error) { fmt.Printf("%#v\n", input) fmt.Println(cli.DESC_Detail()) return inputErr } // listサブコマンドの処理を設定 cli.List.FUNC = func(subcommand []string, input CLI_List_Input, inputErr error) (err error) { fmt.Printf("%#v\n", input) fmt.Println(cli.List.DESC_Detail()) return inputErr } // describeサブコマンドの処理を設定 cli.Describe.FUNC = func(subcommand []string, input CLI_Describe_Input, inputErr error) (err error) { fmt.Printf("%#v\n", input) fmt.Println(cli.Describe.DESC_Detail()) return inputErr } // コマンドライン引数の解釈と対応する処理の呼び出しを実行 if err := Run(cli, os.Args); err != nil { panic(err) } }
以下は,ルートコマンドの実行例です.
go build -o demo . && ./demo main.CLI_Input{} demo: demo app to get table information from databases Usage: $ demo Subcommands: describe: show information of tables list: list tables
自動生成されたヘルプテキストが利用できることがわかります.
以下は,list
サブコマンドの実行例です.
go build -o demo . && ./demo list -c=./demo.config main.CLI_List_Input{Opt_Config:"./demo.config"} list tables Usage: $ <program> list [<option>]... Options: -config=<string>, -c=<string> (default=""): path to config file
短縮バージョンのオプション引数-c
で指定した文字列"./demo.config"
を,オプション引数-config
の値として渡すことができたことがわかります.
以下は,describe
サブコマンドの実行例です.
go build -o demo . && ./demo describe -check Table1 Table2 Table3 main.CLI_Describe_Input{Opt_Check:true, Opt_Config:"", Opt_ForeignKey:false, Opt_Format:"json", Opt_Unique:false, Arg_Tables:[]string{"Table1", "Table2", "Table3"}} show information of tables Usage: $ <program> describe [<option>|<argument>]... [-- [<argument>]...] Options: -check[=<boolean>] (default=false): includes check constraint information -config=<string>, -c=<string> (default=""): path to config file -foreign-key[=<boolean>] (default=false): includes foreign key constraint information -format=<string>, -f=<string> (default="json"): output format: json or sql -unique[=<boolean>] (default=false): includes unique constraint information Arguments: [0:] [<tables:string>]... names of tables to be described
オプション引数-format
にデフォルト値である"json"
が渡されていることがわかります.
また,オプション引数-check
により,ブーリアン型の値true
が渡されていることがわかります.
さらに,位置引数を可変長とすることも可能であることがわかります.
まとめと展望
以下に本記事のまとめを示します.
- コンソールアプリケーションを作成する際には,CLI(サブコマンド,オプション引数,位置引数を含むコマンドライン引数)を定義・解釈する仕組みが必要となります.
- YAMLからCLIを自動生成するというアプローチにより,この仕組みを実現する「cyamli」を開発し,公開しました.
- 「cyamli」を紹介し,型付けされたAPIを提供できること,SSOTを実現できることを示しました.
以下に展望を示します.
- 「cyamli」は,その内部のコード生成処理を置き換えることで,様々なプログラミング言語をサポートすることが可能なため,今後はサポートするプログラミング言をGo以外にも広げていきたいと考えています.
- 本記事を読んでくださった方には,ぜひ「cyamli」を使ってみてもらいたいと思っています.