こうした要件でcurlを使ってREST APIを呼び出すシェルスクリプトを書こうとすると、認証エラーの扱い方がわからない、HTTPの4xxエラーをスクリプトが無視して処理を続けてしまう、といった問題に直面しやすい。
この記事では、curlとjqを組み合わせてREST APIを自動操作するシェルスクリプトの設計パターンを解説します。認証情報の安全な管理、HTTPステータスコードを使ったエラー検知、複数ページのレスポンスを取りまとめるページング設計まで、実際の実行結果を交えながら紹介します。
実行環境: RHEL 9.4 / Rocky Linux 9 / Ubuntu 24.04 LTS(bash 5.x、curl 7.76以上、jq 1.6以上)
この記事のポイント
・curl -s -oと-wでレスポンス本文とHTTPステータスコードを別々に受け取れる
・APIトークンはスクリプト内にハードコードせず環境変数かchmod 600ファイルで管理する
・HTTPステータスが4xx/5xxのときはスクリプトをexit 1で止める条件分岐が必須
・ページングはwhile true + 空配列でbreakの汎用パターンが実装しやすく可読性も高い
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜシェルスクリプトからREST APIを操作するのか
REST APIをシェルスクリプトから呼び出す場面は実務で頻繁にある。Python・Ruby・Goのような言語の実行環境を準備しなくても、bash・curl・jqの3つがあれば完結できるのが現場でのシェルの強みだ。代表的なユースケースを挙げると次のとおり。
・Slack Incoming Webhookでバックアップ失敗時にアラートを自動送信する
・GitHub APIでブランチ一覧やリリースタグを定期取得してレポートを自動生成する
・ZabbixのAPIでホスト設定をスクリプトから一括更新する
・クラウドサービスの認証APIで一時トークンを取得し、後続コマンドに引き渡す
ただし、単に
curl http://api.example.com/data と書いただけのスクリプトは、APIが4xxや5xxを返してもcurlの終了コードが0になり、エラーを見逃して処理を継続してしまいがちだ。本番運用に耐えるためには、エラー検知と適切なリカバリーの設計が欠かせない。curlとjqを組み合わせる基本パターン
1. GETリクエストでデータを取得する
最もシンプルな組み合わせは、curlでAPIを叩いてjqに渡すパイプ接続だ。GitHub公開APIを例に確認する。#!/bin/bash # GitHub APIからリポジトリの基本情報を取得する例 ORG="torvalds" REPO="linux" curl -s \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${ORG}/${REPO}" \ | jq '.full_name, .description, .stargazers_count'
"torvalds/linux" "Linux kernel source tree" 186462
-sオプション(silent)でcurlの進捗表示を抑制し、レスポンスのJSONだけをjqに渡している。jqのフィルタで必要なフィールドだけを抽出するのが基本形だ。2. POSTリクエストでJSONを送信する
SlackのIncoming WebhookなどJSONをPOSTするケースは次のように書く。#!/bin/bash # Slack Incoming WebhookにPOSTする例 WEBHOOK_URL="${SLACK_WEBHOOK_URL}" MESSAGE="バックアップ完了: $(date '+%Y-%m-%d %H:%M:%S')" # jqの--argでJSONを安全に生成する(変数内の特殊文字を自動エスケープ) PAYLOAD=$(jq -n --arg text "${MESSAGE}" '{text: $text}') curl -s -X POST \ -H "Content-Type: application/json" \ -d "${PAYLOAD}" \ "${WEBHOOK_URL}"
--argオプションでJSONを生成するのが安全だ。シェルで文字列を直接連結してJSON化しようとすると、改行やダブルクォートが混入したときにJSON構文エラーになりやすい。3. 配列レスポンスをjqで分解する
APIが配列を返す場合、jq '.[]'またはjq -r '.[] | .フィールド名'で各要素を展開できる。#!/bin/bash # GitHubのブランチ名を一覧取得する例 ORG="torvalds" REPO="linux" curl -s \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${ORG}/${REPO}/branches?per_page=5" \ | jq -r '.[].name'
master linux-rolling-lts
認証情報を安全に扱う設計
1. Bearer Tokenを環境変数で管理する
APIトークンをスクリプトに直接書き込むと、Gitにコミットしたときに流出するリスクがある。環境変数から読み込む設計が基本だ。#!/bin/bash # スクリプト先頭でトークンの存在を確認する if [[ -z "${GITHUB_TOKEN}" ]]; then echo "エラー: 環境変数 GITHUB_TOKEN が設定されていません" >&2 exit 1 fi curl -s \ -H "Authorization: Bearer ${GITHUB_TOKEN}" \ -H "Accept: application/vnd.github+json" \ "https://api.github.com/user" \ | jq '.login, .name'
GITHUB_TOKEN=xxxx を記述するか、後述のファイル読み込み方式を使う。シェルの環境変数の設定方法や読み込み方法については、シェル環境変数設定の基本も合わせて参照してほしい。
2. トークンをファイルで管理する(chmod 600 設計)
cronなど環境変数が渡せないケースでは、専用のトークンファイルを使う。#!/bin/bash # トークンファイルから読み込む設計 TOKEN_FILE="${HOME}/.config/github_token" if [[ ! -r "${TOKEN_FILE}" ]]; then echo "エラー: トークンファイルが見つかりません: ${TOKEN_FILE}" >&2 exit 1 fi API_TOKEN=$(< "${TOKEN_FILE}") curl -s \ -H "Authorization: Bearer ${API_TOKEN}" \ "https://api.github.com/user"
# トークンファイルのパーミッションを設定する(所有者のみ読み取り可能) chmod 600 ~/.config/github_token ls -la ~/.config/github_token # -rw------- 1 ec2-user ec2-user 41 8月 7 09:15 /home/ec2-user/.config/github_token
3. Basic認証を使うAPIへの対応
ZabbixやRedmineなど、Basic認証を使うAPIには-uオプションを使う。#!/bin/bash # Basic認証を使うAPIの例 API_USER="${ZABBIX_USER}" API_PASS="${ZABBIX_PASS}" curl -s \ -u "${API_USER}:${API_PASS}" \ -H "Content-Type: application/json" \ "https://zabbix.example.com/api_jsonrpc.php" \ -d '{"jsonrpc":"2.0","method":"apiinfo.version","params":[],"id":1}' \ | jq '.result'
-uの引数をスクリプトにハードコードしてはいけない。HTTPステータスコードでエラーを検知する設計
1. -w "%{http_code}"でステータスコードを分離して取得する
単純なパイプ接続(curl ... | jq ...)だと、APIが4xxや5xxを返してもcurlの終了ステータスは0になる。レスポンスボディとHTTPステータスコードを分離して取得するのが確実なエラー検知設計だ。#!/bin/bash # レスポンスボディとHTTPステータスコードを分離して取得する API_URL="https://api.github.com/repos/torvalds/linux" TMP_FILE=$(mktemp) # -o でボディをファイルへ、-w でステータスコードのみを変数に取り込む HTTP_STATUS=$(curl -s \ -H "Accept: application/vnd.github+json" \ -o "${TMP_FILE}" \ -w "%{http_code}" \ "${API_URL}") if [[ "${HTTP_STATUS}" -ge 200 && "${HTTP_STATUS}" -lt 300 ]]; then # 成功(2xx): jqでレスポンスを解析する jq '.full_name, .stargazers_count' "${TMP_FILE}" else # エラー(4xx/5xx): ステータスコードとボディを stderr に出力 echo "APIエラー: HTTP ${HTTP_STATUS}" >&2 cat "${TMP_FILE}" >&2 rm -f "${TMP_FILE}" exit 1 fi rm -f "${TMP_FILE}"
APIエラー: HTTP 401 {"message":"Requires authentication","documentation_url":"https://docs.github.com/..."}
-o "${TMP_FILE}"でレスポンスボディをファイルに保存し、-w "%{http_code}"でステータスコードのみを変数に取り込む。2つを同時にパイプで受け取ることはできないため、この「一時ファイル経由」が定番のパターンだ。2. API呼び出しを関数化して再利用する
複数のAPIを叩くスクリプトでは、エラー検知ロジックを関数化して再利用するのが保守性の高い設計だ。#!/bin/bash # Bearer Token認証付きのGETリクエスト関数 api_get() { local url="$1" local tmp tmp=$(mktemp) local status status=$(curl -s \ -H "Authorization: Bearer ${API_TOKEN}" \ -H "Accept: application/vnd.github+json" \ -o "${tmp}" \ -w "%{http_code}" \ "${url}") if [[ "${status}" -lt 200 || "${status}" -ge 300 ]]; then echo "API GET 失敗: HTTP ${status} URL=${url}" >&2 cat "${tmp}" >&2 rm -f "${tmp}" return 1 fi cat "${tmp}" rm -f "${tmp}" return 0 } # 使用例:失敗時はそのままスクリプトを終了する RESULT=$(api_get "https://api.github.com/orgs/torvalds/repos?per_page=5") || exit 1 echo "${RESULT}" | jq -r '.[].name'
ページング対応:複数ページのレスポンスを全件取得する設計
GitHubやGitLabなど多くのAPIは、一度に返せる件数に上限があり、複数ページに分割してレスポンスを返す。全件取得が必要な場面のページング設計パターンを見ていく。1. pageパラメータを使う汎用型
page=1から順に増やしてAPIを呼び出し、空の配列が返ってきたら終了する汎用パターンだ。#!/bin/bash # pageパラメータで全件取得するパターン fetch_all_items() { local base_url="$1" local per_page=100 local page=1 local all_results="[]" while true; do local result result=$(api_get "${base_url}?per_page=${per_page}&page=${page}") if [[ $? -ne 0 ]]; then echo "ページ ${page} の取得に失敗しました" >&2 return 1 fi # 空の配列([])が返ってきたらループ終了 local count count=$(echo "${result}" | jq 'length') if [[ "${count}" -eq 0 ]]; then break fi # 取得結果を累積する all_results=$(echo "${all_results} ${result}" | jq -s 'add') page=$((page + 1)) done echo "${all_results}" } # 使用例 ALL_REPOS=$(fetch_all_items "https://api.github.com/orgs/torvalds/repos") || exit 1 TOTAL=$(echo "${ALL_REPOS}" | jq 'length') echo "合計: ${TOTAL} 件取得" echo "${ALL_REPOS}" | jq -r '.[].name'
2. Linkヘッダーを使うGitHub/GitLab型
GitHubのAPIはLinkレスポンスヘッダーに次ページのURLを含める。-Dオプションでヘッダーをファイルに保存し、次ページURLを解析するパターンも実務でよく使われる。#!/bin/bash # Linkヘッダーから次ページURLを抽出する関数 extract_next_url() { local link_header="$1" # rel="next" のURLを正規表現で取り出す echo "${link_header}" | grep -o '<[^>]*>; rel="next"' | sed 's//;s/>.*$//' } # Linkヘッダー方式での全件取得 fetch_all_with_link() { local url="$1" local all_results="[]" while [[ -n "${url}" ]]; do local tmp_body tmp_header tmp_body=$(mktemp) tmp_header=$(mktemp) local status status=$(curl -s \ -H "Authorization: Bearer ${API_TOKEN}" \ -H "Accept: application/vnd.github+json" \ -D "${tmp_header}" \ -o "${tmp_body}" \ -w "%{http_code}" \ "${url}") if [[ "${status}" -ge 400 ]]; then echo "エラー: HTTP ${status}" >&2 rm -f "${tmp_body}" "${tmp_header}" return 1 fi all_results=$(echo "${all_results} $(cat ${tmp_body})" | jq -s 'add') local link_header link_header=$(grep -i '^Link:' "${tmp_header}" | tr -d '\r') url=$(extract_next_url "${link_header}") rm -f "${tmp_body}" "${tmp_header}" done echo "${all_results}" }
本記事のまとめ
シェルスクリプトでcurlとjqを使ったREST API自動操作の設計パターンをまとめる。| やりたいこと | 設計パターン | 注意点 |
|---|---|---|
| GETリクエスト + JSON解析 | curl -s URL | jq '.field' |
-sで進捗表示を抑制する |
| POSTでJSONを安全に送信 | jq -n --arg text "$MSG" '{text:$text}' |
変数直接連結は避けてjqで生成する |
| HTTPステータスコードを取得 | curl -s -o body.tmp -w "%{http_code}" URL |
一時ファイルでボディとコードを分離する |
| Bearer Token認証 | curl -H "Authorization: Bearer ${TOKEN}" URL |
トークンは環境変数か chmod 600 ファイルから読む |
| 4xx/5xxエラーで停止 | [[ "${HTTP_STATUS}" -ge 400 ]] && exit 1 |
エラーボディも stderr に出力して記録する |
| 全件ページング取得 | while true; do ... [[ count -eq 0 ]] && break; done |
jq length で空配列をbreakの判定に使う |
・APIトークンはスクリプト内へのハードコード厳禁。環境変数かchmod 600ファイルで管理する
・POSTのJSONボディは文字列連結ではなくjqの--argで生成することで特殊文字の問題を防ぐ
・ページングはpage=1から始めて空配列でbreakする汎用パターンが実装しやすく保守性も高い
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトでrsyncバックアップの世代管理を実装する方法|古い世代を自動削除してディスク枯渇を防ぐ設計パターン
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます