Go言語の習作で「劣化curl」コマンドを作ってみた
よく訓練されたアップル信者、都元です。先日はcli-initというツールをご紹介しました。
今回はそれに引き続き、Goでちょっとしたプログラミングをしてみます。簡単にですが、curl
コマンドの劣化版を作ってみようと思います。
仕様
- 1回のコマンドで実行で1回のHTTPリクエストを行う。
- 対応するメソッドは GET, POST, PUT, DELETE の4種類。
- サブコマンドで get, post, put, delete を選択して使い分ける。
- HTTPレスポンスのbodyを標準出力に書き出す。
--data
オプションで、POST, PUT等のリクエストbodyを文字列で指定できる。- 環境変数
DEBUG
が空でない場合、リクエストとレスポンスの詳細を標準エラー出力に書き出す。
$ dcurl get http://example.com/foo $ dcurl post http://example.com/bar --data hogehoge $ DEBUG=true dcurl put http://example.com/bar --data hogehoge
目指すところはこんな感じ。いい感じで劣化してますね。ヘッダ指定? ナニソレ。
実装
とりあえず、前回ご紹介したcli-initを使う手順に従って、dcurl
というプロジェクトを作りました。cli-init
コマンドの実行時には-s get,post,put,delete
と指定。
まず各コマンドの実行内容としては、HTTPメソッド名が違うだけなので、メソッド名を指定してdoRequest
メソッドに処理委譲して終了。
func doGet(c *cli.Context) { doRequest(c, "GET") } func doPost(c *cli.Context) { doRequest(c, "POST") } // 略
引数の取り扱い
まず、postサブコマンドでは--data
っていうオプション(go界隈ではフラグって言うみたいですね)を使いますよ、という宣言が必要です。下記のFlags
に設定している内容がコレです。
その他、ノリでShortName
とかUsage
も埋めます。
var commandPost = cli.Command{ Name: "post", ShortName: "p", Usage: "Make POST request", Description: ` `, Flags: []cli.Flag{ cli.StringFlag{ Name: "data", Usage: "Body data", }, }, Action: doPost, }
ちなみに、HTTPリクエストでbody部を持つのはPOSTとPUTくらいだよなぁ、と思っていたんですが、一応「GETやDELETEにbodyがあってはならない、という訳ではない *1」ようなので、とりあえずどのメソッドでもbody指定できるようにしちゃいます。
そのため、別途変数 *2DefaultFlags
にフラグを宣言し…
var DefaultFlags = []cli.Flag{ cli.StringFlag{ Name: "data", Usage: "Body data", }, }
それぞれこんな感じにしました。
var commandGet = cli.Command{ Name: "get", ShortName: "g", Usage: "Make GET request", Description: ` `, Flags: DefaultFlags, Action: doGet, } var commandPost = cli.Command{ Name: "post", ShortName: "p", Usage: "Make POST request", Description: ` `, Flags: DefaultFlags, Action: doPost, } // 略
そもそも各コマンドに対してフラグ設定をするのではなく、下記のように(commands.go内ではなくmainメソッド内で)グローバルなオプションとして設定してしまう方法もあります。でもこの方法で--data
を設定した場合、help
等のサブコマンドに対しても--data
が指定できてしまうので、本当にグローバルなオプション以外は避けたほうが良いでしょう。
app.Flags = []cli.Flag{ cli.StringFlag{ Name: "profile", Value: "default", Usage: "profile name", }, }
このように設定した上で、--data
に指定した文字列を取得するのはctx.String("data")
でOK。文字列ではなく数値や真偽値を期待している場合はctx.Int("...")
やctx.Bool("...")
を使います。グローバルオプションの場合はctx.GlobalString("profile")
という感じで取り出します。
さて、ここまで準備ができたら、次にリクエスト先のURLを得る方法です。とりあえず引数が足りなかったらエラー終了させます。os.Exit
はプロセスの終了メソッドです。コードの頭の方にimport "os"
が必要です。
func getUrl(ctx *cli.Context) string { if len(ctx.Args()) < 1 { log.Fatal("require URL" + string(len(ctx.Args()))) os.Exit(1) } return ctx.Args()[0] }
もうちょっとキレイに書けるかもしれないっすね。もしかしたら、エラー処理は外に任せるほうがいいのかもしれません。この方がGo流でしょうか。
import "errors" // ... func getUrl(ctx *cli.Context) (string, err) { if len(ctx.Args()) < 1 { return nil, errors.New("require URL") } return ctx.Args()[0], nil }
HTTPリクエストを行う部分
あとは、Go標準の"net/http"
パッケージを使ってHTTPリクエストを実行します。
body := strings.NewReader(ctx.String("data")) req, _ := http.NewRequest(method, url, body) reqDump, _ := httputil.DumpRequestOut(req, true) debug("request = ", string(reqDump)) client := new(http.Client) res, err := client.Do(req) if err != nil { log.Fatal(err) } resDump, _ := httputil.DumpResponse(res, true) debug("response = ", string(resDump)) byteArray, _ := ioutil.ReadAll(res.Body) fmt.Println(string(byteArray))
上記実装にあたってはGo net/httpパッケージの概要とHTTPクライアント実装例 - Qiitaを参考にさせて頂きました。
デバッグログ出力の部分は、cli-initで作成したスケルトンに下記のような定義があったので、これをそのまま使わせてもらいました。log
パッケージでは基本的に標準エラー出力に出すんですね。
func debug(v ...interface{}) { if os.Getenv("DEBUG") != "" { log.Println(v...) } }
まとめ
さて、習作としてこんなものを作ってみました。本当に簡単に作れますね。
プロジェクト全体はこちらをご覧ください。 → dai0304/dcurl at 0.1.0
今後、このプロジェクトをごにょごにょと触っていくかもしれませんので、本エントリーの参考としてはv0.1.0のタグを参照すると良いとおもいます。