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
-
Create a function for JSONPlaceholder with GET/POST/DELETE support
-
Add caching to your GitHub API function
-
Build a function that handles pagination automatically
-
Create tab completion for your custom API function
-
Implement retry logic with exponential backoff