curl 深度教程 / 第 11 章:脚本编写
第 11 章:脚本编写
curl 真正的强大之处在于脚本化——将命令嵌入自动化流程,实现无人值守的数据传输、API 调用和系统监控。
11.1 配置文件
.curlrc 全局配置
# ~/.curlrc 文件(每次 curl 执行时自动加载)
# 以下为常用配置示例
# 默认 User-Agent
-A "MyApp/1.0 ([email protected])"
# 默认跟随重定向
-L
# 默认显示错误信息
-S
# 默认超时
--connect-timeout 10
--max-time 60
# 默认重试
--retry 3
--retry-delay 2
# 默认 CA 证书
--cacert /etc/ssl/certs/ca-certificates.crt
# 默认压缩
--compressed
使用 -K 指定配置文件
# 项目级配置文件
# project.curl.conf
-X POST
-H "Content-Type: application/json"
-H "Authorization: Bearer eyJhbGc..."
--connect-timeout 10
--max-time 30
--retry 3
# 使用配置文件
curl -K project.curl.conf -d '{"key":"value"}' https://api.example.com/data
# 配置文件中可以包含注释
# # 这是注释
# -H "Accept: application/json"
配置文件语法
# 配置文件支持的格式
# 长选项
--connect-timeout 10
# 短选项
-L
# 带值的选项
-H "Accept: application/json"
# URL(单独一行,不加引号也可)
https://api.example.com/data
# 每行一个参数
# 空行和 # 开头的行会被忽略
# 使用环境变量(Shell 展开后传入)
# -H "Authorization: Bearer ${API_TOKEN}"
多环境配置
# config/dev.conf
--url http://localhost:8080
-H "X-Environment: development"
# config/staging.conf
--url https://staging.example.com
-H "X-Environment: staging"
# config/prod.conf
--url https://api.example.com
-H "X-Environment: production"
# 使用不同环境配置
curl -K config/${ENV:-dev}.conf /api/health
11.2 Shell 变量管理
基本变量使用
# 从环境变量读取敏感信息
API_TOKEN="${API_TOKEN:?Error: API_TOKEN 未设置}"
curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com/data
# 构建动态 URL
BASE_URL="https://api.example.com"
VERSION="v2"
RESOURCE="users"
ID=42
curl "$BASE_URL/$VERSION/$RESOURCE/$ID"
# 使用变量构建查询参数
PAGE=1
PER_PAGE=50
SORT="created_at"
curl "$BASE_URL/$VERSION/$RESOURCE?page=$PAGE&per_page=$PER_PAGE&sort=$SORT"
安全地构建 JSON
# ❌ 不安全:字符串拼接(注入风险)
curl -d "{\"name\": \"$USER_NAME\"}" https://api.example.com/users
# ✅ 安全:使用 jq(正确处理特殊字符和转义)
DATA=$(jq -n \
--arg name "$USER_NAME" \
--arg email "$USER_EMAIL" \
--argjson age "$USER_AGE" \
'{name: $name, email: $email, age: $age}')
curl -d "$DATA" https://api.example.com/users
# 从文件读取模板并填充变量
# template.json: {"name": "__NAME__", "env": "__ENV__"}
sed "s/__NAME__/$USER_NAME/g; s/__ENV__/$DEPLOY_ENV/g" template.json | \
curl -d @- https://api.example.com/users
敏感信息管理
# 方式 1:环境变量(推荐)
export API_TOKEN="secret-token"
curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com/data
# 方式 2:从密码管理器读取
TOKEN=$(op item get api-token --fields password)
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/data
# 方式 3:.env 文件(确保不提交到 Git)
# .env
# API_TOKEN=secret-token
source .env
curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com/data
# 方式 4:stdin 输入
read -sp "API Token: " TOKEN
echo
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/data
unset TOKEN
# 方式 5:.netrc 文件
curl --netrc https://api.example.com/data
# ~/.netrc: machine api.example.com login admin password secret
11.3 批量请求
循环请求
# 从文件读取 URL 列表
while IFS= read -r url; do
[[ "$url" =~ ^#.*$ || -z "$url" ]] && continue
echo "请求: $url"
curl -sS "$url"
echo
done < urls.txt
# 批量获取用户信息
for user_id in $(seq 1 100); do
curl -sS "https://api.example.com/users/$user_id" \
| jq '{id: .id, name: .name, email: .email}'
done
# 批量创建资源
while IFS=, read -r name email role; do
[[ "$name" == "name" ]] && continue # 跳过表头
curl -sS -X POST "https://api.example.com/users" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg n "$name" --arg e "$email" --arg r "$role" \
'{name: $n, email: $e, role: $r}')" \
| jq '.id'
done < users.csv
批量请求与 –next
# 使用 --next 在单个 curl 进程中发送多个请求
curl \
https://api.example.com/users/1 \
--next \
https://api.example.com/users/2 \
--next \
https://api.example.com/users/3
# 不同方法的批量请求
curl \
-X POST -d '{"name":"用户1"}' -H "Content-Type: application/json" \
https://api.example.com/users \
--next \
-X POST -d '{"name":"用户2"}' -H "Content-Type: application/json" \
https://api.example.com/users \
--next \
-X POST -d '{"name":"用户3"}' -H "Content-Type: application/json" \
https://api.example.com/users
# 并行 + 顺序控制
# 使用 xargs 并行,使用 --next 批量
cat user_ids.txt | xargs -I {} -P 5 \
curl -sS "https://api.example.com/users/{}" -o "user_{}.json"
速率限制批量请求
# 控制 QPS(每秒请求数)
QPS=10
INTERVAL=$(echo "scale=3; 1 / $QPS" | bc)
while IFS= read -r url; do
curl -sS "$url" -o /dev/null &
sleep "$INTERVAL"
done < urls.txt
wait
# 使用 parallel 更优雅地控制
cat urls.txt | parallel -j 10 --delay 0.1 \
curl -sS {} -o "{#}.json"
11.4 错误处理
基本错误处理
# 检查 curl 的退出码
if curl -sS -o response.json https://api.example.com/data; then
echo "✅ 请求成功"
else
echo "❌ 请求失败,退出码: $?"
exit 1
fi
curl 退出码速查
| 退出码 | 含义 | 常见原因 |
|---|---|---|
| 0 | 成功 | — |
| 1 | 不支持的协议 | URL 格式错误 |
| 2 | 初始化失败 | 内存不足 |
| 3 | URL 格式错误 | 语法问题 |
| 5 | 代理解析失败 | 代理地址错误 |
| 6 | DNS 解析失败 | 域名不存在 |
| 7 | 连接失败 | 服务未启动/防火墙 |
| 22 | HTTP 错误(4xx/5xx) | 使用 -f 时 |
| 23 | 写入错误 | 磁盘满/权限 |
| 26 | 上传读取错误 | 文件不存在 |
| 28 | 超时 | --max-time 或 --connect-timeout |
| 35 | TLS 握手失败 | 证书/版本问题 |
| 47 | 重定向过多 | 循环重定向 |
| 52 | 服务器无响应 | 空响应 |
| 55 | 发送失败 | 网络中断 |
| 56 | 接收失败 | 网络中断 |
HTTP 状态码检查
# 使用 -w 获取 HTTP 状态码
HTTP_CODE=$(curl -sS -o response.json -w "%{http_code}" \
https://api.example.com/data)
case "$HTTP_CODE" in
200|201|204)
echo "✅ 成功 ($HTTP_CODE)"
;;
301|302|307|308)
echo "↪️ 重定向 ($HTTP_CODE)"
;;
400)
echo "⚠️ 请求错误 ($HTTP_CODE)"
cat response.json | jq '.error'
;;
401|403)
echo "🔐 认证/授权失败 ($HTTP_CODE)"
exit 2
;;
404)
echo "🔍 资源不存在 ($HTTP_CODE)"
exit 3
;;
429)
echo "🚦 请求过多 ($HTTP_CODE)"
RETRY_AFTER=$(curl -sI https://api.example.com/data \
| grep -i retry-after | awk '{print $2}' | tr -d '\r')
echo "等待 ${RETRY_AFTER:-60} 秒后重试..."
sleep "${RETRY_AFTER:-60}"
;;
500|502|503|504)
echo "💥 服务器错误 ($HTTP_CODE)"
exit 4
;;
*)
echo "❓ 未知状态码: $HTTP_CODE"
exit 5
;;
esac
完整的错误处理函数
#!/bin/bash
# api_call.sh - 带完整错误处理的 API 调用函数
api_call() {
local method="$1"
local url="$2"
local data="$3"
local max_retries=3
local retry_delay=5
for attempt in $(seq 1 $max_retries); do
local response_file=$(mktemp)
local header_file=$(mktemp)
local curl_args=(
-sS
-o "$response_file"
-D "$header_file"
-w "%{http_code}"
--connect-timeout 10
--max-time 30
-X "$method"
-H "Content-Type: application/json"
-H "Authorization: Bearer $API_TOKEN"
)
[[ -n "$data" ]] && curl_args+=(-d "$data")
local http_code
http_code=$(curl "${curl_args[@]}" "$url") || {
local exit_code=$?
rm -f "$response_file" "$header_file"
if [[ $attempt -lt $max_retries ]]; then
echo "⚠️ 请求失败 (exit=$exit_code),第 $attempt/$max_retries 次重试..." >&2
sleep $retry_delay
retry_delay=$((retry_delay * 2))
continue
else
echo "❌ 请求失败 (exit=$exit_code),已达最大重试次数" >&2
return $exit_code
fi
}
if [[ "$http_code" =~ ^2 ]]; then
cat "$response_file"
rm -f "$response_file" "$header_file"
return 0
elif [[ "$http_code" =~ ^5 ]] && [[ $attempt -lt $max_retries ]]; then
echo "⚠️ 服务器错误 ($http_code),第 $attempt/$max_retries 次重试..." >&2
rm -f "$response_file" "$header_file"
sleep $retry_delay
retry_delay=$((retry_delay * 2))
continue
else
echo "❌ HTTP 错误: $http_code" >&2
cat "$response_file" >&2
rm -f "$response_file" "$header_file"
return 1
fi
done
}
# 使用
USERS=$(api_call GET "https://api.example.com/users" "") || exit 1
echo "$USERS" | jq '.[] | .name'
11.5 重试逻辑
指数退避重试
#!/bin/bash
# exponential_retry.sh - 指数退避重试
exponential_retry() {
local max_retries=5
local delay=1
local max_delay=60
for i in $(seq 1 $max_retries); do
if "$@"; then
return 0
fi
if [ $i -lt $max_retries ]; then
local jitter=$((RANDOM % delay))
local sleep_time=$((delay + jitter))
[ $sleep_time -gt $max_delay ] && sleep_time=$max_delay
echo "重试 $i/$max_retries,等待 ${sleep_time}秒..." >&2
sleep $sleep_time
delay=$((delay * 2))
fi
done
echo "所有重试均失败" >&2
return 1
}
# 使用
exponential_retry curl -sS https://api.example.com/data
条件重试
# 仅在特定条件下重试
conditional_retry() {
local url="$1"
local max_retries=3
local delay=5
for i in $(seq 1 $max_retries); do
local response_file=$(mktemp)
local http_code
http_code=$(curl -sS -o "$response_file" -w "%{http_code}" "$url")
case "$http_code" in
200|201|204)
cat "$response_file"
rm -f "$response_file"
return 0
;;
429) # Too Many Requests - 需要更长等待
local retry_after=$(grep -i "retry-after" "$response_file" | awk '{print $2}' | tr -d '\r')
echo "速率限制,等待 ${retry_after:-30}秒..." >&2
rm -f "$response_file"
sleep "${retry_after:-30}"
;;
500|502|503|504) # 服务器错误 - 重试
echo "服务器错误 ($http_code),${delay}秒后重试 $i/$max_retries..." >&2
rm -f "$response_file"
sleep $delay
delay=$((delay * 2))
;;
401) # 认证失败 - 不重试
echo "认证失败 ($http_code)" >&2
rm -f "$response_file"
return 1
;;
*) # 其他错误 - 不重试
echo "HTTP 错误: $http_code" >&2
rm -f "$response_file"
return 1
;;
esac
done
echo "达到最大重试次数" >&2
return 1
}
11.6 日志记录
# 带日志的请求
LOG_FILE="api_calls.log"
log_request() {
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local method="$1"
local url="$2"
local http_code="$3"
local duration="$4"
echo "$timestamp | $method | $url | HTTP $http_code | ${duration}s" >> "$LOG_FILE"
}
# 使用
URL="https://api.example.com/data"
start_time=$(date +%s)
HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" "$URL")
end_time=$(date +%s)
DURATION=$((end_time - start_time))
log_request "GET" "$URL" "$HTTP_CODE" "$DURATION"
# 更详细的日志(使用 -w 输出为 JSON)
curl -sS -o response.json \
-w '{"http_code":%{http_code},"time_total":%{time_total},"size_download":%{size_download},"url":"%{url_effective}"}\n' \
https://api.example.com/data >> curl_stats.jsonl
请求计时与统计
# 使用 -w 记录详细计时
curl -o /dev/null -s -w '%{json}\n' https://example.com > timing.json
# 批量请求计时统计
echo "url,http_code,time_dns,time_connect,time_ttfb,time_total,size" > stats.csv
while IFS= read -r url; do
curl -o /dev/null -s \
-w '"%{url}",%{http_code},%{time_namelookup},%{time_connect},%{time_starttransfer},%{time_total},%{size_download}\n' \
"$url" >> stats.csv
done < urls.txt
11.7 管道与组合
curl 与其他工具的组合
# curl + jq:JSON 处理
curl -s https://api.example.com/users | jq '.[] | {name, email}'
# curl + grep:提取特定信息
curl -sI https://example.com | grep -i "content-type"
# curl + sed/awk:文本处理
curl -s https://example.com | grep -oP '(?<=href=")[^"]+'
# curl + xargs:批量操作
curl -s https://api.example.com/users | jq -r '.[].id' | \
xargs -I {} curl -s "https://api.example.com/users/{}"
# curl + tee:同时输出到文件和终端
curl -s https://api.example.com/data | tee response.json | jq .
# curl + pv:显示传输进度(管道模式)
curl -s https://example.com/largefile.tar.gz | pv | tar xzf -
# curl + sponge:原子性写入(避免半写状态)
curl -s https://api.example.com/config | sponge config.json
注意事项
- 引号规则:变量中包含空格或特殊字符时必须加引号
"$var" - 子 Shell 陷阱:管道中的变量修改在父 Shell 中不可见
- 并发控制:使用
wait等待后台任务完成 - 临时文件清理:使用
trap确保退出时清理 - ShellCheck:使用
shellcheck检查脚本质量
# 使用 trap 清理临时文件
TMPFILE=$(mktemp)
trap "rm -f $TMPFILE" EXIT
curl -sS https://api.example.com/data -o "$TMPFILE"
# 处理数据...
# 退出时自动清理
# 使用 shellcheck 检查脚本
shellcheck my_script.sh
扩展阅读
📖 下一章:第 12 章:调试与诊断 — 掌握 curl 的调试工具:详细输出、时间分析、协议诊断。