curlコマンドによるデータ送信あれこれ

最近Webアプリを作ることがあって、動作チェックのためにcurlコマンドを色々使った。オプションが色々あって、データ送信の方法に分かりにくいのところもあったので、使い方をまとめてみる。

なぜ分かりにくいか

理由1: 送信方法が色々ある

単にデータ送信と言っても、やり方がいくつかある。Webアプリに対してデータを送る方法は大きく4つあると思う。それぞれの場合に応じたオプションを使わなければ成らない。また、そもそも作られたサービスがどの形式を期待しているかを知っていなければならない。

  • URLエンコードによるPOST
  • マルチパートによるPOST
  • REST APIでのPOST
  • クエリ文字列

理由2: データの渡し方も色々ある

送るデータを指定する方法もいくつかあり、やはりオプションを使い分けなければならない。

  • データをコマンド中で指定する
  • データを含むをファイルを指定する
    • name=content形式のcontentのみをファイルで指定する
    • name=content全体がファイルにある
  • データをエンコードするorしない

ということで、それぞれの方法に対応するオプションを見てみる。

URLエンコードによるPOST

まずは以下のようなフォーム入力から考える。この場合はPOSTメソッドでデータが送られ、ボディはURLエンコード (application/x-www-form-urulencoded) 形式になる。

<form action="test.cgi">
  <input type="text" name="text1"><br>
  <input type="text" name="text2"><br>
  <input type="submit">
</form>

例えば2つのテキストボックスに、それぞれ"a&b"、"あ"と入力すると、送られるリクエストは以下のようになる(実際にはブラウザはもっと多くのヘッダを送るがここでは省略)。

POST /test.cgi HTTP/1.1
Content-Type: application/x-www-form-urlencoded

text1=a%26b&text2=%E3%81%82

見てのとおり、いくつかの文字はエンコードされている('&'はASCIIで0x26、また"あ"はUTF-8で0xe3, 0x81, 0x82だ)。

curlでURLエンコードによるPOSTをしたい場合、基本となるのは--data-urlencodeオプションだ。場合によっては-dや--data-rawを使う。また、データの指定方法にいくつか種類がある。

--data-urlencode name=content

--data-urlencode name=contentと指定すれば、contentをURLエンコードしてくれる。また、複数回指定できる。

$ curl --data-urlencode "text1=a&b" --data-urlencode "text2=あ" http://example.org/test.cgi
Content-Type: application/x-www-form-urlencoded

text1=a%26b&text2=%E3%81%82

また、contentを空にすることもできる(フォームのテキストボックスに何も入力しない場合と同じになる)。

$ curl --data-urlencode text1= --data-urlencode text2= http://example.org/test.cgi
Content-Type: application/x-www-form-urlencoded

text1=&text2=

--data-urlencode name@filename

URLエンコードは基本的にname=contentの形式だが、contentがファイルに保存されている場合はname@filenameとすると指定したファイルを読んで送信する。

例えばtext1の値がpost-data1.txt(内容は"a&b")にあり、text2の値がpost-data2.txt(内容は"あ")にあるとき、以下のようにする。
(2017/11/28: このサンプルが間違っていたのを修正しました。)

$ curl --data-urlencode text1@post-data1.txt --data-urlencode text2@post-data2.txt http://example.org/test.cgi
Content-Type: application/x-www-form-urlencoded

text1=a%26b&text2=%E3%81%82

なお、ファイルをテキストエディタで作る場合は末尾に改行コードが入る場合があることに注意。

--data-urlencode @filename

contentだけでなく、name=contentそのものがファイルにある場合は@filename形式を使う。例えばpost-data3.txtとpost-data4.txtに送りたいデータ(それぞれ"text1=a&b"、"text2=あ")がある場合は以下のようになる。

$ curl --data-urlencode @post-data3.txt --data-urlencode @post-data4.txt http://example.org/test.cgi
Content-Type: application/x-www-form-urlencoded

text1=a%26b&text2=%E3%81%82

--data-urlencode content, --data-urlencode =content

これらを使うとname=content形式でないデータを送れるが、どういう場合に使うかは不明。例えば単に"a&b"というデータを送信する場合は以下のようにする。

$ curl --data-urlencode "a&b" http://example.org/test.cgi
Content-Type: application/x-www-form-urlencoded

a%26b

ただし、データ中に'@'や'='があると別の形式と見なされる。'@'や'='そのものを送りたい場合は=contentにする。先頭の'='は送られず、またデータ中の'@'、'='はURLエンコードされて("%3D", "%40"になる)送られる。

$ curl --data-urlencode "=a=b@c" http://example.org/test.cgi
Content-Type: application/x-www-form-urlencoded

a%3Db%40c

-d, --data, --data-ascii

--data-urlencodeは指定したデータをURLエンコードするが、エンコード済みのデータを指定したいときは-dを使う(--dataや--data-asciiも同じ)。データに対して何も処理されないため、URLエンコードの仕様に合わないデータが送られることもありえる。また、複数回指定すると'&'が挟まれる。

例えば、またtext1に"a&b"、text2に"あ"を入れる場合、以下の2つの方法がある(結果は同じ)。

$ curl -d "text1=a%26b" -d "text2=%E3%81%82" http://example.org/test.cgi
$ curl -d "text1=a%26b&text2=%E3%81%82" http://example.org/test.cgi
Content-Type: application/x-www-form-urlencoded

text1=a%26b&text2=%E3%81%82

-d @filename

エンコード済みのデータがファイルにある場合、-d @filenameでそのファイルを送る。post-data5.txtの内容が"text1=a%26b&text2=%E3%81%82"のとき、以下のコマンドで上と同じ結果になる。

$ curl -d @post-data5.txt http://example.org/test.cgi

また、ファイル中の改行は無視される。つまり、

x=1
2

というファイルを指定すると、"x=12"が送られる。なぜ改行を削るのかはマニュアルには記載がないので分からない。

改行も含めて完全にそのまま送りたい場合は--data-binaryを使うが、フォーム入力のチェックをするときには使うことはないだろう。URLエンコード形式ではそもそも改行コードはそのままでは送れず、エンコードする必要があるからだ。--data-binaryの使い方は後述する。

なお、--data-urlencodeでは改行コードもエンコードされて送られる。

マルチパートによるPOST

フォーム入力でファイルを送る場合(<input type="file">がある場合)、enctypeにmultipart/form-dataを指定する(そうしないとファイルの中身が送信されない)。

例:

<form action="test.cgi" method="post" enctype="multipart/form-data">
  <input type="text" name="text1"><br>
  <input type="file" name="file1"><br>
  <input type="submit">
</form>

上記のフォームのテキストボックスに"a&b"、ファイルにhello.txtを指定し、またhello.txtの内容が"HELLO"だとすると、リクエストは以下のようになる。ボディ全体のContent-Typeはmultipart/form-dataであり、中に複数のパートが含まれる。ファイルを指定した場合はContent-Dispositionにfilenameが入り、またパートのContent-Typeも付けられる。一方、テキストボックスの方にはこれらは付かない。

POST /test.cgi HTTP/1.1
Content-Type: multipart/form-data; boundary=------------------------d7719356dd7979e0

--------------------------d7719356dd7979e0
Content-Disposition: form-data; name="text1"

a&b
--------------------------d7719356dd7979e0
Content-Disposition: form-data; name="file1"; filename="hello.txt"
Content-Type: text/plain

HELLO
--------------------------d7719356dd7979e0--

-F, --form

curlでマルチパート形式で送るときは-F (--form) オプションを使う。引数はname=content形式で、ファイルを送るときは@filenameとする。例えば上の例をcurlで行うときは以下のようにする。

$ curl -F text1=foo -F file1=@hello.txt http://example.org/test.cgi

@と<の使い分け

上のように、<input type="file">を再現したいときはname=@filenameを使う。一方、type="file"でないもの(上の例のtext1)のデータをファイルを使って指定したいときは、'@'の代わりに'<'を使う。例えばfoo.txtというファイルの内容が"foo"になっているとき、以下のようにすると同じリクエストになる。

$ curl -F "text1=<foo.txt" -F file1=@hello.txt http://example.org/test.cgi

パートのContent-Typeとファイル名の指定

パートのContent-Typeは自動的に付けられる。どのように決めているかは不明だが、多分拡張子で判断していると思う。自分でContent-Typeを指定したいときは';'で区切ってtype=xxxを書く。

また、name=@filenameでファイルを指定した場合には、";filename=yyy"でContent-Dispositionヘッダ中のfilenameも指定することができる。

フィールド ファイル名 内容 Content-Type ヘッダ中のfilename
text1 (なし) foo text/plain (なし)
file1 hello.txt HELLO text/html foo.txt
$ curl -F "text1=foo;type=text/plain" -F "file1=@hello.txt;filename=test.txt;type=text/plain" http://example.org/test.cgi
Content-Type: multipart/form-data; boundary=------------------------a30b1104258b2e26

--------------------------a30b1104258b2e26
Content-Disposition: form-data; name="text1"
Content-Type: text/plain

foo
--------------------------a30b1104258b2e26
Content-Disposition: form-data; name="file1"; filename="test.txt"
Content-Type: text/plain

HELLO
--------------------------a30b1104258b2e26--

複数ファイルの指定 (multiple)

HTML5では、1つのinput要素にmultiple属性を指定することで、複数のファイルを送信できるようになった。

<form action="test.cgi" method="post" enctype="multipart/form-data">
  <input type="file" name="files" multiple="multiple"><br>
  <input type="submit">
</form>

このフォームで、2つのファイルa.txt, b.txt(内容はそれぞれ"AA", "BB")を指定すると、以下のようなリクエストが送られる。

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryC15BKaGNEiNN0Evx

------WebKitFormBoundaryC15BKaGNEiNN0Evx
Content-Disposition: form-data; name="files"; filename="a.txt"
Content-Type: text/plain

AA
------WebKitFormBoundaryC15BKaGNEiNN0Evx
Content-Disposition: form-data; name="files"; filename="b.txt"
Content-Type: text/plain

BB
------WebKitFormBoundaryC15BKaGNEiNN0Evx--

curlでは-Fを複数回使えば同じことができる。

$ curl -F "files=@a.txt" -F "files=@b.txt" http://example.org/test.cgi

REST APIでのPOST

ブラウザを使わないHTTPベースのサービス(REST APIなどと呼ばれるもの)の場合はボディに直接データを入れることが多い。例えば、"<a>hello</a>"というXMLを送るときは以下のようなリクエストになる。

POST /test.cgi HTTP/1.1
Content-Length: 12
Content-Type: application/xml

<a>hello</a>

--data-binary

URLエンコードやマルチパート形式を使わず、データをそのまま送る場合は--data-binaryを使い、かつ-HでContent-Typeを指定する(指定しないとapplication/x-www-form-urlencodedになってしまう)。また、'@'によるファイル名の指定も可能。-Fと違い、ファイル名を指定してもContent-Typeを自動で付けられることはないため、自分で指定する必要がある。

hello.xmlの内容が"<a>hello</a>"のとき、以下の2つはどちらも上と同じリクエストになる。

$ curl -H "Content-Type: application/xml" --data-binary "<a>hello</a>" http://example.org/test.cgi
$ curl -H "Content-Type: application/xml" --data-binary @hello.xml http://example.org/test.cgi

クエリ文字列

form要素のmethod属性にgetを指定すると、データはクエリ文字列としてURLの後ろに付く。

<form action="test.cgi" method="get">
  <input type="text" name="text1"><br>
  <input type="text" name="text2"><br>
  <input type="submit">
</form>

例えば上記のフォームを含むHTMLがhttp://example.orgにあり、このテキストボックスにそれぞれ"foo", "bar"を入れると、http://example.org/text.cgi?text1=foo&text2=bar にGETメソッドが送られる。

クエリ文字列もURLの一部なので直接指定することもできるが、その場合は自分でエンコードしなければならない。--data-urlencodeを指定し、かつ-G (--get)でGETメソッドを強制するとエンコードを任せることができる。以下の2つはどちらも同じリクエストになる。

$ curl -G --data-urlencode "text1=a&b" http://example.org/test.cgi
$ curl "http://example.org/test.cgi?text1=a%26b"