Shell Integration: Building Your CLI

The Goal

Transform this:

curl -s -H "Authorization: Bearer $(gopass show v3/work/myapp | sed '1,/^---$/d' | yq -r '.api_key')" \
  "https://api.myapp.com/tickets?status=open" | jq '.[] | {id, title}'

Into this:

myapp tickets '.[] | {id, title}'

Function Template

# ~/.zshrc or ~/.bashrc

# Generic API function template
api_name() {
  local endpoint="${1:-}"
  local jq_filter="${2:-.}"
  local method="${3:-GET}"

  # Load credentials once
  local creds
  creds=$(gopass show v3/work/api_name | sed '1,/^---$/d')

  local base_url
  base_url=$(echo "$creds" | yq -r '.base_url')

  local api_key
  api_key=$(echo "$creds" | yq -r '.api_key')

  curl -sS -X "$method" \
    -H "Authorization: Bearer $api_key" \
    -H "Content-Type: application/json" \
    "${base_url}/${endpoint}" \
    | jq "$jq_filter"
}

Real Examples

GitHub CLI

gh_api() {
  local endpoint="${1:-user}"
  local jq_filter="${2:-.}"

  curl -sS \
    -H "Authorization: Bearer $(gopass show v3/dev/github | sed '1,/^---$/d' | yq -r '.token')" \
    -H "Accept: application/vnd.github+json" \
    "https://api.github.com/${endpoint}" \
    | jq "$jq_filter"
}

# Usage:
gh_api user                           # Your profile
gh_api users/torvalds                 # User info
gh_api "repos/torvalds/linux" '.stargazers_count'  # Star count
gh_api "users/torvalds/repos?per_page=5" '.[].name'  # Repo names

JSONPlaceholder (Practice)

jsonph() {
  local endpoint="${1:-posts}"
  local jq_filter="${2:-.}"

  curl -sS "https://jsonplaceholder.typicode.com/${endpoint}" | jq "$jq_filter"
}

# Usage:
jsonph users                         # All users
jsonph users/1                       # User 1
jsonph posts '.[:5]'                 # First 5 posts
jsonph "posts?userId=1" 'length'     # Count user 1's posts

With POST Support

myapi() {
  local method="${1:-GET}"
  local endpoint="${2:-}"
  local data="${3:-}"
  local jq_filter="${4:-.}"

  local creds
  creds=$(gopass show v3/work/myapi | sed '1,/^---$/d')

  local args=(-sS -X "$method")
  args+=(-H "Authorization: Bearer $(echo "$creds" | yq -r '.api_key')")
  args+=(-H "Content-Type: application/json")

  if [[ -n "$data" ]]; then
    args+=(-d "$data")
  fi

  curl "${args[@]}" "$(echo "$creds" | yq -r '.base_url')/${endpoint}" | jq "$jq_filter"
}

# Usage:
myapi GET users                                    # List
myapi GET users/123                                # Get one
myapi POST users '{"name":"John"}'                 # Create
myapi PATCH users/123 '{"name":"Jane"}'            # Update
myapi DELETE users/123                             # Delete

gopass Structure

# gopass edit v3/work/myservice
---
base_url: https://api.myservice.com/v1
api_key: sk_live_xxxxxxxxxxxxx
username: myuser
# Optional extras
rate_limit: 100
timeout: 30

Helper Function

# Get any gopass YAML field
gpyq() {
  local path="$1"
  local field="$2"
  gopass show "$path" | sed '1,/^---$/d' | yq -r "$field"
}

# Usage:
gpyq v3/work/myapi '.api_key'
gpyq v3/work/myapi '.base_url'

Advanced Patterns

Caching

cached_api() {
  local cache_file="/tmp/api_cache_$(echo "$1" | md5sum | cut -d' ' -f1)"
  local cache_ttl=300  # 5 minutes

  if [[ -f "$cache_file" ]] && [[ $(($(date +%s) - $(stat -c %Y "$cache_file"))) -lt $cache_ttl ]]; then
    cat "$cache_file"
  else
    curl -sS "https://api.example.com/$1" | tee "$cache_file"
  fi | jq "${2:-.}"
}

Pagination

fetch_all_pages() {
  local endpoint="$1"
  local page=1
  local results="[]"

  while true; do
    local response
    response=$(curl -sS "https://api.example.com/${endpoint}?page=${page}&per_page=100")

    local count
    count=$(echo "$response" | jq 'length')

    [[ "$count" -eq 0 ]] && break

    results=$(echo "$results" "$response" | jq -s 'add')
    ((page++))
  done

  echo "$results"
}

# Usage:
fetch_all_pages users | jq 'length'  # Total count across all pages

Rate Limiting

rate_limited_api() {
  local last_call_file="/tmp/.api_last_call"
  local min_interval=1  # 1 second between calls

  if [[ -f "$last_call_file" ]]; then
    local last_call
    last_call=$(cat "$last_call_file")
    local now
    now=$(date +%s)
    local diff=$((now - last_call))

    if [[ $diff -lt $min_interval ]]; then
      sleep $((min_interval - diff))
    fi
  fi

  date +%s > "$last_call_file"
  curl -sS "https://api.example.com/$1"
}

Error Handling

safe_api() {
  local response
  local http_code

  response=$(curl -sS -w "\n%{http_code}" "https://api.example.com/$1")
  http_code=$(echo "$response" | tail -1)
  response=$(echo "$response" | sed '$d')

  case "$http_code" in
    200|201) echo "$response" | jq "${2:-.}" ;;
    401) echo "Error: Unauthorized. Check credentials." >&2; return 1 ;;
    403) echo "Error: Forbidden. Check permissions." >&2; return 1 ;;
    404) echo "Error: Not found: $1" >&2; return 1 ;;
    429) echo "Error: Rate limited. Slow down." >&2; return 1 ;;
    5*) echo "Error: Server error ($http_code)" >&2; return 1 ;;
    *) echo "Error: Unexpected status $http_code" >&2; return 1 ;;
  esac
}

Interactive Mode

# fzf integration for interactive selection
api_select() {
  local selected
  selected=$(curl -sS "https://api.example.com/users" \
    | jq -r '.[] | "\(.id)\t\(.name)\t\(.email)"' \
    | fzf --header="Select user" \
    | cut -f1)

  [[ -n "$selected" ]] && curl -sS "https://api.example.com/users/$selected" | jq
}

Tab Completion

# Zsh completion for your API function
_myapi() {
  local -a endpoints
  endpoints=(
    'users:List all users'
    'posts:List all posts'
    'tickets:List tickets'
    'config:Get configuration'
  )
  _describe 'endpoint' endpoints
}
compdef _myapi myapi

Full Example: Work Ticketing System

# Complete ticketing CLI
tickets() {
  local cmd="${1:-list}"
  shift

  local creds
  creds=$(gopass show v3/work/ticketing | sed '1,/^---$/d')
  local url
  url=$(echo "$creds" | yq -r '.base_url')
  local key
  key=$(echo "$creds" | yq -r '.api_key')

  _tickets_curl() {
    curl -sS -H "Authorization: Bearer $key" -H "Content-Type: application/json" "$@"
  }

  case "$cmd" in
    list)
      _tickets_curl "${url}/tickets?status=${1:-open}" | jq '.[] | {id, title, status, assignee}'
      ;;
    show)
      _tickets_curl "${url}/tickets/$1" | jq
      ;;
    create)
      _tickets_curl -X POST -d "$1" "${url}/tickets" | jq
      ;;
    close)
      _tickets_curl -X PATCH -d '{"status":"closed"}' "${url}/tickets/$1" | jq
      ;;
    mine)
      _tickets_curl "${url}/tickets?assignee=me" | jq '.[] | {id, title, status}'
      ;;
    *)
      echo "Usage: tickets {list|show|create|close|mine} [args]"
      ;;
  esac
}

# Usage:
# tickets list                    # Open tickets
# tickets list all                # All tickets
# tickets show 123                # Ticket details
# tickets create '{"title":"Bug"}' # Create ticket
# tickets close 123               # Close ticket
# tickets mine                    # My tickets

Exercises

  1. Create a function for JSONPlaceholder with GET/POST/DELETE support

  2. Add caching to your GitHub API function

  3. Build a function that handles pagination automatically

  4. Create tab completion for your custom API function

  5. Implement retry logic with exponential backoff