はじめに
普段Laravelを使っています。
最近Goの勉強を始めて、まだまだ勉強不足の中、ふとCLIツールを自作してみたいと思い立ちました。
普段何気なく使っているCLIツール(コマンド)ですが、そもそも裏側の仕組みは理解せず使っていました。
今回は、CLIツールの裏側や作るときの考え方を記事に残します。
前提
CLI(Command Line Interface)ツールとは、ターミナルから実行するプログラムのこと。
普段使っているls、git ~ 、docker ~は全てCLIツール。
そもそもコマンドを実行すると何が起きているのか
ls /etc
これを実行すると、OSは以下のことをやっている。
-
lsというプログラムを見つける -
/etcという文字列を引数として、プログラムに渡す - プログラムが起動し、受け取った引数を元に処理を実行する
ここで需要なのは、OSはプログラムに「文字列の配列」を渡すという点。
以下の場合、OSがプログラムに渡すのは["ls", "/etc", "/var"]になる。
配列の先頭には、必ず実行コマンド名が入り、2つ目以降にユーザーが渡した引数になる。
ls /etc /var
["ls", "/etc", "/var"]
↑ ↑
ユーザーが渡した引数
これはGo特有の話ではなく、OSの仕組み。
Goでは引数をどこで受け取るのか
OSが渡してくる文字列の配列をGoでは、os.Argsで受け取る。
他の言語でも受け取る場所が用意されていて、名前が違うだけ。
Go → os.Args
PHP → $argv
Python → sys.argv
C → main(int argc, char *argv[])
CLIツールの基本構造
どんなCLIツールも、以下のパーツに分解できる。
git commit -m "first commit"
| パーツ | 値 | 意味 |
|---|---|---|
| コマンド本体 | git |
実行するプログラム |
| サブコマンド | commit |
「何をするか」の指定 |
| フラグ | -m |
「こういう風にやって」という追加指示 |
| フラグの値 | "fix bug" |
フラグに渡す具体的なデータ |
ただし、すべてのCLIツールがすべてのパーツを使うわけではない。
▼コマンド(ls)、引数(/etc)
ls /etc
▼コマンド(ls)、フラグ(-la)、引数(/etc)
ls -la /etc
▼コマンド(git)、サブコマンド(commit)、フラグ(-m)、フラグの値("first commit")
git commit -m "first commit"
このように細分化される。
また、-mなどのフラグを使いたい場合は、flagパッケージを使う。
GoでCLIツールを作るときの3つの基本ルール
1. 終了コードを返す
プログラムが終了するときにOSに「成功したか失敗したか」を数字1文字で報告する仕組みのこと。
0 -> 成功
1 -> 失敗
身近な例で確認ができる。
ターミナルで以下を打つと、直前のコマンドの終了コードが見られる。
$ ls /etc # 存在するディレクトリ → 成功
$ echo $? # 直前の終了コードを表示
0 # 成功 = 0
$ ls /存在しない # 存在しないディレクトリ → 失敗
$ echo $?
2 # 失敗 = 0以外
なぜこれが必要かというと、コマンドを連結するときに「前が成功したら次をやる」という制御ができるから。
// 成功パターン
// main関数が普通に終わる = OSに終了コード0が返る
func main() {
fmt.Println("成功")
}
// 失敗パターン
// os.Exit(1)で終了コードを作成する(OSに1が返る)
func main() {
fmt.Fprintln(os.Stderr, "エラーが発生しました")
os.Exit(1)
}
2. 標準出力とエラー出力を分ける
ターミナルに文字を表示する経路は2つある。
標準出力(Stdout) :結果を表示
エラー出力(Stderr):エラーやログを表示
なぜ2つに分けているのか、lsコマンドで確認してみる。
存在するディレクトリ(/etc)と、存在しないディレクトリ(/none)を同時に渡してみる。
ls /etc /nonexistent
ls: cannot access '/none': No such file or directory # ← エラー(Stderr)
/etc:
hosts passwd ssl ... # ← 結果(Stdout)
「結果:Stdout、エラー:Stderr」と分けることが重要。
Goの場合以下のように書き分ける。
fmt.Println("処理結果: 42")
fmt.Fprintln(os.Stderr, "エラー: ファイルが見つかりません")
3. ヘルプメッセージを用意する
git -helpなどがあると便利。
基本的には、この仕組みはあった方が良い。
本来は自作しないといけないが、flagパッケージを使えば、自動でヘルプメッセージが生成される。
おわりに
CLIについて学習を進めていて、別にPHPでも作れるんだ。というのが驚きでした。
また、知らないことばかりで新鮮な気持ちで勉強できて楽しいです。
今作成しているものがあるので、ある程度完成したらプログラムなどを段階的に解説していこうと思います。