Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLIツールとして使えるようにしてみよう #15

Closed
sotetsuk opened this issue Oct 7, 2020 · 0 comments · Fixed by #16
Closed

CLIツールとして使えるようにしてみよう #15

sotetsuk opened this issue Oct 7, 2020 · 0 comments · Fixed by #16

Comments

@sotetsuk
Copy link
Owner

sotetsuk commented Oct 7, 2020

前回、コードの再利用性を高めるために、パッケージングを行いました。コードの再利用性を高める方法はもう一つあり、それがここで紹介する CLI (Command Line Interface) ツールとしてコードを切り出すことです。

CLIツールのゴールのイメージは、次のような fizzbuzz-cli というコマンドから、前回までに作ってきた fizzbuzz 関数の結果を取得することになります。

$ fizzbuzz-cli 7 11 77 5 --fizz 7 --buzz 11
Fizz
Buzz
FizzBuzz
5

1. CLIツールからエントリーポイントとして呼ばれる関数を実装しよう

fizzbuzz.py に次のように main 関数を追加してみましょう。

import argparse
import sys


def fizzbuzz(n: int, fizz: int = 3, buzz: int = 5) -> str:
    ...


def main():
    parser = argparse.ArgumentParser(description='Fizz Buzz program.')
    parser.add_argument('nums', type=int, default=[], nargs='*',
                        help="Number to be applied to FizzBuzz function. If no arguments passed, read from stdin.")
    parser.add_argument('--fizz', type=int, default=3,
                        help='Number corresponds to Fizz.')
    parser.add_argument('--buzz', type=int, default=5,
                        help='Number corresponds to Fizz.')
    args = parser.parse_args()

    if args.nums:  # 数字の列が引数から渡された場合には、それらの数字にFizzBuzzを適用する
        sys.stderr.write("Reading numbers from arguments ...\n")
        for n in args.nums:
            sys.stdout.write(f"{fizzbuzz(n, fizz=args.fizz, buzz=args.buzz)}\n")
    else:  # 数字の列が引数から渡されなかった場合には、標準入力から数字を読み込む
        sys.stderr.write("Reading numbers from stdin ...\n")
        try:
            line = sys.stdin.readline()
            while line:
                sys.stdout.write(f"{fizzbuzz(int(line), fizz=args.fizz, buzz=args.buzz)}\n")
                line = sys.stdin.readline()
        except KeyboardInterrupt:
            return

2. CLIツールを作れるようにsetup.pyを変更しよう

entry_points として、CLI起動時に呼び出される関数を指定します。

image

3. インストールしてCLIツールを使ってみよう

$ make install を実行し、CLIツールをインストールします。その後、ターミナルを新しく開き、CLIツールを実行してみましょう。

$ fizzbuzz-cli 7 11 77 5 --fizz 7 --buzz 11
Reading numbers from arguments ...
Fizz
Buzz
FizzBuzz
5

ここで、 Reading numbers from arguments ...標準エラー、 その他の出力は 標準出力 から出力されていることに注意してください。

$ seq 1 15 | fizzbuzz-cli
Reading numbers from stdin ...
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

最後に、PRを作りマージしましょう。

4. 再利用性の高いCLIツールの設計

この設計では、CLIツールが標準入力から入力を受け取り、標準出力から出力を吐き出すことができる設計になっているので、他のUnixコマンドと親和性が高く、再利用性が高くなっています。例えば、 15の倍数が1から100の間にいくつあるかを数える操作は、次のように seqgrep コマンドをパイプを使って組み合わせることで実現できます。

seq 1 100 | fizzbuzz-cli | grep -c "FizzBuzz"                                                                                                                                                                                                                                                                 
Reading numbers from stdin ...
6

seq コマンドを利用することで、1から100の数字全てにFizzBuzzに適用するといった、似たような機能を自分で実装する手間が省けていることに着目してください。つまり、次のような機能は必要ありません。

$ fizzbuzz-cli --range 1 100

CLIツールを作る際には、様々な機能を付けるよりも単一機能の実装にのみに集中し、パイプを使って他のUnixコマンドと組み合わせることが容易になるよう意識しましょう。

特に、 入力を標準入力から一行ずつ受け取り、出力を標準出力から一行ずつ出力する ようにするのが一つのポイントになります。 実際、普段引数と一緒によく使っているコマンドの多くは、標準入力も受け付けることができます。

$ cat test.txt  # from args
test
$ echo "test" | cat  # from stdin
test

また、標準出力からの出力を壊さないように、人間が読む出力は(今回の例では Reading numbers from stdin ... など)、標準出力ではなく標準エラーから出力するよう心がけてください。機械が読むものは標準出力、人間が読むものは標準エラー とするのが基本的な形になります。

Unixコマンドは一つ一つはシンプルですが、組み合わせることで強力な武器となります。こんな有名な逸話があります。

テキストファイルからn個の最頻出な単語を取り出し、出現回数と共にソートした上で出力せよ。

という問題に対し、著名な計算機科学者のドナルド・クヌースが10ページに及ぶプログラムを書いたのに対し、パイプの作者 ダグラス・マキルロイ はパイプを使った非常にシンプルな解決法を提示しました(TODO: 出典):

$ cat n_freq.sh
#!/bin/bash
tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed ${1}q

$ cat README.md | ./n_freq.sh 10  # Example: n = 10
  19 python
  16 fizzbuzz
  13 tutorial
  13 sotetsuk
  13 https
  13 github
  13 dev
  12 com
  11 issues
   9 fizz

目次

  1. プロジェクトを初期化しよう
  2. 仕様を決めて、ユニットテストを追加してみよう
  3. ユニットテストが通るように本体を実装してみよう
  4. (Optional) doctestを導入してみよう
  5. 新しい機能を追加する前に、CIを導入してみよう
  6. 新しい機能を追加してみよう
  7. ライブラリとして使えるようにしてみよう
  8. [現在] CLIツールとして使えるようにしてみよう
  9. [つぎ] (Optional) 便利な外部ライブラリをインストールして使ってみよう (Pytest編)
  10. (Optional) 便利な外部ライブラリをインストールして使ってみよう (Click編)
  11. 型チェックを使ってみよう
  12. しっかりとしたREADMEを書こう
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant