Pythonのdocoptを使ったコマンドライン引数の処理
PythonでCLIコマンドを作成する際にオプションの処理を行いたかったので、docoptを使って実装した。
docoptとは
コマンドライン引数として処理したいオプションなどをテキストベースで記述することができる。その内容から、実行時にコマンドライン引数のパーサーの生成と、パースを行う。
docoptはもともとはPythonのライブラリだったようだが、Command-line interface description language
と謳っているだけあって、各言語の実装が展開されている。
使い方
↓こんな感じで使用する。
"""Naval Fate. Usage: naval_fate.py ship new <name>... naval_fate.py ship <name> move <x> <y> [--speed=<kn>] naval_fate.py ship shoot <x> <y> naval_fate.py mine (set|remove) <x> <y> [--moored|--drifting] naval_fate.py -h | --help naval_fate.py --version Options: -h --help Show this screen. --version Show version. --speed=<kn> Speed in knots [default: 10]. --moored Moored (anchored) mine. --drifting Drifting mine. """ from docopt import docopt if __name__ == '__main__': arguments = docopt(__doc__) print(arguments)
実行すると以下のようになる。
$ naval_fate.py ship Guardian move 10 50 --speed=20 {'--drifting': False, '--help': False, '--moored': False, '--speed': '20', '--version': False, '<name>': ['Guardian'], '<x>': '10', '<y>': '50', 'mine': False, 'move': True, 'new': False, 'remove': False, 'set': False, 'ship': True, 'shoot': False}
Usage
などの規定のキーワードとフォーマットがあり、それに従ってテキストメッセージを書いておき(docstringに書くサンプルが多い)、それを利用してオプションのパーサーとすることができる。
optparserなどコード内でパースする内容を定義するものと違って、別途ヘルプを書かなくて良いので、この仕様は個人的にはありがたい。
docopt—language for description of command-line interfaces ↑こちらで実際に試すことができる。
コマンド・サブコマンドを処理したい
git
やaws-cli
のように、サブコマンドを使用したCLIアプリを実装したかったので調べてみた。公式などいくつかのサンプルを参考に最終的には以下のようにした。
myapp/ ├── __init__.py ├── __main__.py ├── commands │ ├── __init__.py │ ├── cmd1.py │ └── cmd2.py (snip...)
myapp/__init__.py
"""Usage: myapp <command> <subcommand> [<args>...] Commands: cmd1 Run cmd1 cmd2 Run cmd2 """ def main(): args = docopt(__doc__) command_name = args.pop('<command>') if command_name == 'help': # docstringをそのまま表示できるのが良い print(__doc__) return # search module from <command> try: command = importlib.import_module('myapp.commands.'+command_name) except Exception: # import_moduleでExceptionが起きた場合にはコマンドがない扱いにする # UnknownCommandErrorは別途定義している raise UnknownCommandError(__doc__, command_name) # search function from <subcommand> subcommand_name = args.pop('<subcommand>') # commandとして読み込んだモジュールからsubcommandを取得する # subcommandは関数として定義している(後述) subcommand = getattr(command, subcommand_name, None) if subcommand: # subcommandが取得できたら、再度コマンドライン引数のパースを実行する # subcommandの内容はテンプレートになっているので、 # global_optsを渡してformatして使用する subcommand_doc = subcommand.__doc__.format(**global_opts) if len(subcommand_doc.strip()) > 0: args = docopt(subcommand_doc, version=version) else: subcommand = gen_help(command.__doc__) subcommand(global_opts, args)
myapp/commands/cmd1.py
"""Usage: {app_name} cmd1 <subcommand> [options] [<args>...] Options: --opt=<opt> Option Commands: sub1 Sub command 1 sub2 Sub command 2 """ def sub1(global_opts, args): """Usage: {app_name} cmd1 sub1 --opt1=<opt1> [--flag] [<args>...] Options: --opt1=<opt1> Option --flag flag """ # ↑app_nameはdocoptに渡される前にformatされる do_something() def sub2(global_opts, args): """Usage: {app_name} cmd1 sub2 [<args>...] """ do_something()
こんな感じの実装にしてみた。
今のところ自分のユースケースはカバーできているが、もう少しコマンドが増えてくると考慮しないといけないことが出てくるかもしれない。
(移行しました)シェルスクリプトで正規表現を使ったチェック
↓↓↓こちらに移行しました↓↓↓
AWS Lambda+Goで、fujiwara/ridgeからapex/gatewayへ移行する
AWS Lambdaでは以前から、非公式にNode.jsをランタイムとして、Go実装のバイナリを実行させるツールがある。例えばApexとか。さらに、Apex、API Gatewayとの組み合わせを前提としたfujiwara/ridgeというライブラリがあり、net/http
のインターフェースにそった実装ができるようになっていた。
実際に会社でも個人的にも、Apex+Go+fujiwara/ridgeという組み合わせで稼働させているものがある。現時点ではランタイムはNode.jsで動いているが、公式にGoがサポートされるようになったので、移行をしたくてしばらく検証していた。
公式のライブラリ、aws/aws-lambda-goを使用した実装に直接入れ替えるのはしんどそうな感じだったが、apex/gatewayを見つけたのでそれを試してみた。
結果、問題なさそうだったので、とりあえず個人的なやつはそちらに移行した。
移行に伴う修正点
どちらもnet/http
のインターフェースを前提としているので、スムーズに移行できる。
ridgeのサンプルを変更すると以下になる。importするパッケージは当然変わるが、Multiplexerはそのまま使用できる。直接aws-lambda-goを使う必要がないので、大きな変更がなくて助かる。
--- main_old.go 2018-04-07 00:06:44.000000000 +0900 +++ main.go 2018-04-07 00:09:00.000000000 +0900 @@ -4,7 +4,7 @@ "fmt" "net/http" - "github.com/fujiwara/ridge" + "github.com/apex/gateway" ) var mux = http.NewServeMux() @@ -15,7 +15,7 @@ } func main() { - ridge.Run(":8080", "/api", mux) + gateway.ListenAndServe(":8080", mux) }
ちょっとapex/gatewayのコードを見てみる
https://github.com/apex/gateway/blob/master/gateway.go#L16
// ListenAndServe is a drop-in replacement for // http.ListenAndServe for use within AWS Lambda. // // ListenAndServe always returns a non-nil error. func ListenAndServe(addr string, h http.Handler) error { if h == nil { h = http.DefaultServeMux } lambda.Start(func(ctx context.Context, e events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { r, err := NewRequest(ctx, e) if err != nil { return events.APIGatewayProxyResponse{}, err } w := NewResponse() h.ServeHTTP(w, r) return w.End(), nil }) return nil }
ListenAndServe
の中では、lambda.Start
を呼び出しつつnet/http
のインターフェースに沿うように対応されている。w.End
でevents.APIGatewayProxyResponse
が返却されるはず。もう少し読み込んでみる。
https://github.com/apex/gateway/blob/master/response.go#L13
// ResponseWriter implements the http.ResponseWriter interface // in order to support the API Gateway Lambda HTTP "protocol". type ResponseWriter struct { out events.APIGatewayProxyResponse buf bytes.Buffer header http.Header wroteHeader bool closeNotifyCh chan bool }
ResponseWriter
はこんな構造になっている。
https://github.com/apex/gateway/blob/master/response.go#L39
// Write implementation. func (w *ResponseWriter) Write(b []byte) (int, error) { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } return w.buf.Write(b) }
Write
のなかではResponseWriter.buf
に対しての書き込みが行われるようになっている。
https://github.com/apex/gateway/blob/master/response.go#L77
// End the request. func (w *ResponseWriter) End() events.APIGatewayProxyResponse { w.out.IsBase64Encoded = isBinary(w.header) if w.out.IsBase64Encoded { w.out.Body = base64.StdEncoding.EncodeToString(w.buf.Bytes()) } else { w.out.Body = w.buf.String() } // notify end w.closeNotifyCh <- true return w.out }
ヘッダーを見て、バイナリのデータかどうか確認しているっぽい。AWS API Gatewayではバイナリデータを返す際にはBase64エンコードされている必要があるので、それにも対応している。
out.Body
が実際のレスポンスになるわけで、そこにResponseWriter.buf
に書き込まれたデータが代入される。
ResponseWriter.closeNofityCh
にtrue
が送信されるようになっているが、これはapex/gateway内部では利用していないっぽい。利用側に通知するためのチャネルのようだ。(経緯としてはfeat(Response) Add CloseNotify support. by wolfeidau · Pull Request #7 · apex/gatewayに書いてある)
最終的には、ResponseWriter
のout
がevents.APIGatewayProxyResponse
になっているので、それを返すようにしている。
気になるところ
ちゃんとコード全体を見ていないのだけど。
fujiwara/ridge
ではできていた以下の点が apex/gateway
でできるのかきちんと確認してない(必要性もないので)
- Proxy Integrationに対応していて、リクエストがあったパスの情報をきちんとMultiplexerで処理できるようになっていた
- 自分の要求としては
/
を前提にしているので、そこまで確認してない
- 自分の要求としては
- ローカルでの起動と実行
- 自分ではそこまで必要性がなく、実際今まで使ったことがないんだけど
参考
VSCodeをメモアプリとして使う
Boostnoteを使ってたんだけど、プレビュー周りの挙動が好きじゃなかったので、VSCodeでメモできるようにスクリプトを書いてみた。
雑な感じでzshのfunctionとして設定してみた。
function memo() { if [ -d $MEMO_HOME ]; then cd $MEMO_HOME local filename=$(date '+%Y%m%d_%H%M%S').md echo -e "# $(date '+%Y-%m-%d %H:%M:%S')\n" > ./src/$filename vs --wait --extensions-dir ./ext --goto src/$filename:2 ./src git add -A . git commit -m "add $filename" fi }
前提
MEMO_HOME
としてどこか指定しておく- 配下に
ext
、src
を作成しておく - extは拡張機能を置く
- できるだけ起動を軽くするため、普段使っている拡張機能は有効にならないようにしたい
- srcはメモのファイルを置く
- gitでも管理する
ext
はコミットしないようにgitignoreに追記しておく
挙動
VSCodeは以下のオプションをつけている。
実際使ってみると、ファイルの一覧が欲しくなったり、Alfredと連携したくなってきたので、そのうちまた修正したい。
AirPodsを買ったけどけっこう良かった
2週間くらい前にAirPodsを購入したけどけっこう良かった。今までにBluetoothイヤホンを2つ買ってたので、今回3つ目にしてようやく満足の行くものが買えたと思う。
とりあえずこだわりなくて、カナル型苦手って人はAirPodsおすすめ。
カナル型はしんどい
なぜかわからないけど市販されているBluetoothイヤホンはほとんどがカナル型になっている。でもカナル型は耳がいたい。
最初に買った3000円くらいのやつは耳が痛くなってやめた。
カナル型ではないフィリップスのイヤホン
しばらくしてからやっぱりワイヤレスのイヤホンが欲しくなって、たまたまヨドバシ寄ってみたときに見つけたフィリップスのイヤホンを買った。
たぶんこれ。
ヨドバシ.com - PHILIPS フィリップス SHB5250BK [Bluetoothイヤホン ブラック] 通販【全品無料配達】
でもこれも最初のやつもそうだったけど、iPhoneと一緒に使うときの音量の具合が非常に良くない。
普通の付属品のイヤホンと同じような感覚での音量調整ができず、音がデカい感じになってしまう。
あと、やっぱりiPhone付属品とはフィット感が若干違ってて、耳はスカスカするし首元のケーブルはなんか邪魔だし、あまり使い心地が良くなくて、結局使わなくなっていた。
AirPods普通に市販されてるのを知った
なんかauでしか売ってない気がしてて全然検討もしてなかったんだけど、最近会社の人で数人使っているっぽいのを見かけて、改めて調べてみた。すると普通にネットで買えるし、だったら迷わず買うか〜って感じで。
実際使ってみると
- 音量の問題はなく、有線イヤホンと同じ使い心地
- 首回りにワイヤがないのでマフラーしてても気にならない
- フィット感も問題なく、特に落ちたりズレたりの心配もない
って感じで今まで買ったイヤホンとは違ってけっこう使い心地が良かった。
充電については、イヤホン自体の充電はケースにしまうだけだし、ケースも頻繁に充電しなくても大丈夫そう。先週末に充電したけど、その時の残量は50%切ってなかったと思う。
嫌なとこ
耳に装着した時の音がデカい。
これが唯一の嫌なとこで、このボリュームは小さくできないっぽい。
保護ケース?
なんかアマゾンでお勧めされてた保護ケースのようなカラビナがついたものを買って、それにしまってる。
Amazon | AirPodsケース、AppleワイヤレスイヤホンAirPod用AhaStyleシリコンショックプルーフ保護カバー (グレー) | AHASTYLE | イヤホン・ヘッドホンケース
とにかくイヤホンについてはお手軽層なんで、買って良かった。
勉強するための時間の工夫
子育てエンジニアが時間取れないという話は度々目にする。
去年の何かのアドベントカレンダーで見た気がするし、最近ツイッターでも「子育て中は勉強の時間取れない」というものを見かけた。
ここでいう子育てってのは、たぶん乳幼児あたりを指しているとは思うが、自分もそうで、なかなかまとまった時間が取れていない。
とりあえず以下の時間帯で勉強できないか試行錯誤はしている。
- 通勤中
- お昼休み
- 子供の就寝後
- 早起きして
おそらくこれ以外の時間帯は無理っぽい。
通勤中
電車での移動時間は片道30分くらい。
10時出勤のおかげで、朝の電車はそこそこ空いていて、座れる日と座れない日が半々くらい。帰りは始発駅なので、時間に余裕がある時は座れるまで待てるが、そうでない時は座らずに帰る。
座れる時はブログを書くようにしている。スマホで書いているので、スピードは遅く、大体公開に至るまで1週間かかる。時間のムダなような気持ちと、何かしらアウトプットしないと、という気持ちでせめぎあっている感じ。
座れない時は、インプット中心。ツイッターとかで話題になってた記事やスマホでも読める(読みやすい)サイトを読んだりする。読書はあまりしてない。
お昼休み
食事はささっと済ませて45分ほど時間がある。
アウトプットではないが、PCが使えるので、気になるツールとかを触ってみたりとか、コード書いたりとかしている。後はネットで調べものが多いかな。
社内では個人タブレットの使用が禁止されているので、電子書籍はあっても会社では読めず。どうしても読破したい本があれば、紙の本で購入して読書していることもあるが、まれ。
子供の就寝後
週末、昼寝をしなかった場合には子供も早く寝る(10分とか)ので、その時は何か作業することが多い。でも12時過ぎには寝たいので、できても2時間くらい。
あとは早く帰れなかった時には、先に奥さんが寝かしつけしているので、そういう時も何か作業はできてる。
以前は寝付くまで起きてて、それから何かしら作業することをやっていた。でも、子供が寝ないことに対してイライラするようなことがあって、さすがに良くないので、寝かしつけをする時は一緒に早く寝るようにした。
早起きして
子供と一緒に早く寝て早起きした時や、なんか明け方早めに起きてしまった時に、勉強することができる。最近はちょっと起きれなくてやってないけど。
Kindleで読書していることが多い。あとは未読のブログを消化したりとかする。
全体的に
なんかPCを起動しようって気持ちにならないことが多くて、インプットが多い感じになってる。
あと、やっぱり勉強しないとって気持ちが湧いてこないこともあって、特に仕事が忙しくなると、疲れが先にくるので勉強する余裕がなくなる。
余暇時間も正直ダラダラ過ごしたいとも思うし、そういった誘惑もある。
たぶん、以前は勉強に充てていたであろう時間の分が子育てに回っていて、余暇時間というかダラダラしている時間はおそらく変わってない。
なので、やっぱりダラダラする時間を勉強の時間に変えていかないといけないんだと思う。