强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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初始化失败内存不足
3URL 格式错误语法问题
5代理解析失败代理地址错误
6DNS 解析失败域名不存在
7连接失败服务未启动/防火墙
22HTTP 错误(4xx/5xx)使用 -f
23写入错误磁盘满/权限
26上传读取错误文件不存在
28超时--max-time--connect-timeout
35TLS 握手失败证书/版本问题
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

注意事项

  1. 引号规则:变量中包含空格或特殊字符时必须加引号 "$var"
  2. 子 Shell 陷阱:管道中的变量修改在父 Shell 中不可见
  3. 并发控制:使用 wait 等待后台任务完成
  4. 临时文件清理:使用 trap 确保退出时清理
  5. 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 的调试工具:详细输出、时间分析、协议诊断。