はじめに
本記事はPowerShell でcurl.exe を使用し、REST APIを用いた操作をする際につまずいた内容と解決方法をまとめたものです。
職場で自由にlinuxが使えない方Windows環境に縛られている方 などのお役に立てれば幸いです。
通常ならInvoke-WebRequest
を使用しますが、今回クライアント証明書による認証が必要なためcurl.exe
を使用しています。
目次
環境
PowerShell
Name Value
---- -----
PSVersion 5.1.19041.5007
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.19041.5007
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
curl.exe
curl 8.9.1 (Windows) libcurl/8.9.1 Schannel zlib/1.3 WinIDN
Release-Date: 2024-07-31
Protocols: dict file ftp ftps http https imap imaps ipfs ipns mqtt pop3 pop3s smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS HSTS HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM SPNEGO SSL SSPI threadsafe Unicode UnixSockets
REST API は Redmine 5.0.9 に対して行った。
結論
- PowerSheell でlinux と同じようなcurl が使用したい場合は curl.exe で始める
-
[Console]::OutputEncoding
をREST API で帰ってくるデータの文字コードに合わせる-
[Console]::OutputEncoding=[Text.Encoding]::UTF8
など
-
- curl.exe でも
”
などがエスケープされるので回避するためにPOSTのデータはファイルに書き出す - curl.exe のファイル読み込みではBOM付きUTF-8を解釈できないためBOMなしUTF-8 で保存する
- PowerShell 5.1では
Set-Content
で-utf8NoBOM
が使えない - 代わりに
[Text.Encoding]::UTF8.GetBytes($reqJson) | Set-Content -Encoding Byte
を使う
- PowerShell 5.1では
つまずいた内容
curl が使えない
PowerShell でcurl example.com
をするとちゃんと帰ってくる。
だが、実態はInvoke-WebRequest
のエイリアス。
なので -d
とかを使おうとするとエラーになる。
PS Path\To\Desktop> curl -d "" example.com
Invoke-WebRequest : 引数 'example.com' を受け入れる位置指定パラメーターが見つかりません。
解決策
curl.exe
で実行ファイルを明示にする
変数に入れると文字化けする
curl.exe https://www.yahoo.co.jp/
をするとちゃんと帰ってくる
だが、
$res = https://www.yahoo.co.jp/
$res
とすると文字化けする。
<title>Yahoo! JAPAN</title><meta name="description" content="縺ゅ↑縺溘・豈取律繧偵い繝・・繝・・繝医☆繧区ュ蝣ア繝昴・繧ソ繝ォ縲よ、懃エ「縲√ル繝・繝シ繧ケ縲∝、ゥ豌励√せ繝昴・繝・√Γ繝シ繝ォ縲√す繝ァ繝・ヴ繝ウ繧ー縲√が繝シ繧ッ繧キ繝ァ繝ウ縺ェ縺ゥ萓ソ蛻ゥ縺ェ繧オ繝シ繝薙せ繧貞ア暮幕縺励※縺・∪縺吶・/><meta name="robots" content="noodp"/>
この事象が発生する原因は、PowerShellスクリプトが外部コマンドの実行結果を文字コードがSJISであることを期待しているため。
UTF-8 で帰ってきた外部コマンドの実行結果をPowerShellがSJISと思い文字コードの変換を行うため文字化けする。
解決策
詳しくは分からないが、PowerShellとやり取りする文字コードをUTF-8 にする。
[Console]::OutputEncoding=[Text.Encoding]::UTF8
をスクリプトの頭に書いた。
変数に入れると進捗状況が紛れ込む
今はどうも再現できないが レスポンスを変数に格納すると進捗状況が紛れ込むようになった。
再現出来たら詳細を書く。
解決策
標準エラー出力をファイルへリダイレクト
$(curl.exe example.com 2>garbage.txt)
ファイルではなく nul
(Linuxの/dev/nullに該当?)にするとエラーとなったため適当なファイルに吐き出している。
ステータスコードが欲しい!
curl.exe からステータスコードを取得できずに苦戦。
コマンド自体の成功、失敗を記録する$LASTEXITCODE
があるがステータスコードはとれないため不採用に。
解決策
かなり強引なやり方。-w "%{thhp_code}"
を使用した。
$res = curl.exe -w "{%thhp_code}" example.com
$statusCode = $res.Substring($res.Length - 3)
$resJson = $res.Substring(0,$res.Length - 3)
あまりにも強引すぎる
curl.exe でPOSTをするとエラーになる
上記の問題をクリアしたらcurl.exe で GETをすることはできるようになったと思う。
だが、POSTリクエストで -d
を使うとエラーになる
curl.exe -d "空白があると バグる" example.com
これを直接PowerShell で実行する分にはバグらなかった。(困惑)
結論から言うと -d
で設定したデータの""
やら
がcurl.exe 実行時、コマンドの一部と解釈されていることが原因。
エスケープしてみるとか試したがうまくいかない。
解決策
データを ファイルで書き出して -d "@data.txt"
のようにして参照する。
この方がエスケープとか考えなくていいから楽まであった。
書き出し方は後述。
POST データをファイルにしたのに400が返ってくる
ここでさらに問題が発生。
$data | Set-Content -Path "Path\To\Temp\data.txt" -Encoding UTF8
で書き出してPOSTを行ったが400 (Bad Request)
が返ってきた。
原因はSet-Content
のエンコードにある。
WindwosのUTF-8はUTF-8であるがBOMありなのである。
このBOMの有無はcurl.exeは吸収せずそのまま送るっぽいのでBOM付きでサーバに送信され、400
となった。
しかも立ち悪いことにSet-Content
でUTF-8 BOMなしを選べない。(PowerShell 7なら可能)
解決策
Qiitaの記事を参考にさせていただいた。
[Text.Encoding]::UTF8.GetBytes($data) | Set-Content -Encoding Byte -NoNewLine -Path .\data.txt
[Text.Encoding]::UTF8.GetBytes($data)
でBOMなしUTF-8に変換して Set-Content
の -Encoding Byte
でそのまま流しているらしい。
過程で作成したもの
- 受けたPOSTデータを標準出力に出力し、そのまま返すスクリプト
- Chat GPT に書いてもらいました
プロンプト
localhost:8081 にアクセスし、POSTリクエストのデータを受け取り標準出力すると同時に
まったく同一のデータを返すPowershellスクリプトを作成してください。
管理者権限が不要な構成でお願いします
スクリプト
# ポート8081でリスニングするためのHttpListenerオブジェクトを作成
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add("http://localhost:8081/")
$listener.Start()
Write-Host "Listening on http://localhost:8081/ ..."
# 無限ループでリクエストを処理
while ($true) {
# 非同期で接続待ち
$context = $listener.GetContext()
# POSTリクエストかを確認
if ($context.Request.HttpMethod -eq "POST") {
# リクエストからデータを取得
$reader = New-Object System.IO.StreamReader($context.Request.InputStream)
$requestData = $reader.ReadToEnd()
$reader.Close()
# 標準出力にリクエストデータを表示
Write-Host "Received POST data: $requestData"
# レスポンスとして同じデータを返す
$response = $context.Response
$buffer = [System.Text.Encoding]::UTF8.GetBytes($requestData)
$response.ContentLength64 = $buffer.Length
$response.OutputStream.Write($buffer, 0, $buffer.Length)
$response.OutputStream.Close()
}
}
# 停止する場合はCtrl+C
さいごに
ここまで見てくれてありがとうございます。
誤りなどありましたらコメントいただけますと幸いです。
Windowsは文字コードとかでかなり悩まされますね...
職場でもLinuxが(というかPythonとか)使いたい...
参考