Skip to content

Instantly share code, notes, and snippets.

@philpennock
Created June 15, 2020 13:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save philpennock/87cc71c6630723ca44c8eb40cf44a039 to your computer and use it in GitHub Desktop.
Save philpennock/87cc71c6630723ca44c8eb40cf44a039 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
set -euo pipefail
# Switched to bash so that we could bind variables for GraphQL
# TODO:
# * Rewrite in Go
# * Handle paging, iterate
# * Consider session keyring with timeout for caching PATs if had to be pulled from PGP-encrypted files
# * see if there's a Go implementation of the jq language
# * ability to take graphql queries from files/fds and munge into the correct
# format.
# + standard config dir where I can drop files and refer to them just by filename not pathname
# + compile some in, for stuff I particularly want for github?
: "${GITHUB_PAT_FILE:="$HOME/.config/pdp/github-pat"}"
readonly http_user_agent='github_curl/0.3 (@philpennock)'
readonly rest_http_accept='Accept: application/vnd.github.v3+json, application/json'
readonly graphql_idl_http_accept='Accept: application/vnd.github.v4.idl'
http_accept="$rest_http_accept"
readonly sent_payload_type_hdr='Content-Type: application/json'
progname="$(basename "$0")"
info() { printf "%s: %s\n" "$progname" "$*"; }
warn() { info "$@" >&2; }
die() {
warn "$@"
exit 1
}
VERBOSE=false
verbose() {
$VERBOSE || return 0
info "$@" >&2
}
need_pat() {
if test -n "${GITHUB_PAT:-}"; then return 0; fi
if test -f "$GITHUB_PAT_FILE"; then
GITHUB_PAT="$(sed -n '/^[^#]/p' <"$GITHUB_PAT_FILE")"
return 0
fi
if test -f "${GITHUB_PAT_FILE}.asc"; then
GITHUB_PAT="$(gpg -d "${GITHUB_PAT_FILE}.asc" | sed -n '/^[^#]/p')"
return 0
fi
case "$(uname -s)" in
Darwin)
# security add-generic-password -a 'GitHub PAT - laptop tooling' -j 'updated 2018-03-19' -s github_curl -T =github_curl -w
# -- regenerated that date, for freshness
set +e
GITHUB_PAT="$(security find-generic-password -a 'GitHub PAT - laptop tooling' -s github_curl -w)"
set -e
test -n "${GITHUB_PAT:-}" && return 0
;;
Linux)
set +e
GITHUB_PAT="$(secret-tool lookup account 'GitHub PAT - laptop tooling' service github_curl)"
set -e
test -n "${GITHUB_PAT:-}" && return 0
;;
esac
warn "need a GitHub Personal Access Token, \"$GITHUB_PAT_FILE\" does not exist"
return 1
}
cmd_curl() {
# <https://developer.github.com/changes/2017-10-12-git-signing/>
# Git signing APIs into v3 of API, Accept MIME change:
# application/vnd.github.cryptographer-preview → application/vnd.github.v3+json
if [ -n "${GITHUB_PAT:-}" ]; then
verbose "curl/authenticated/x-oauth-basic: $*"
curl \
-su "${GITHUB_PAT:?}:x-oauth-basic" \
--user-agent "$http_user_agent" -H "$http_accept" \
"$@"
else
verbose "curl/noauth: $*"
curl -s --user-agent "$http_user_agent" -H "$http_accept" "$@"
fi
}
github_fetch_path() {
local uri_path="${1:?need a URI path on the remote side}"
: "${2:?need a JSON query for jq command}"
shift
# -e let's us distinguish between "description present but empty string" and "description not present"
m_curl "https://api.github.com${uri_path:?}" | jq -e -r "$@"
}
usage() {
cat <<EOUSAGE
Usage: ${progname} [-p|-P] [-I|-J|-i] [-hsv] [-A type] [-m method] [-g [-V K=V]...] /path [json query]
-g Use GraphQL APIv4
-V K=V Bind variable for GraphQL
-A Accept, override HTTP Accept: header
-I use curl -I, headers-only
-i use curl -i, headers and then raw response
-J no JSON, just emit raw response
-m M use M(ethod) instead of GET
-s use stdin for curl data
-p public, do not need PAT
-P public if no PAT, but private if PAT available
-C jq forced in color
-M jq forced in mono
-v verbose, show CURL to stderr
-h this help
NB: options do not re-order, so that -foo after /path is part of the JSON query
NB: to see graphql schema, try: github_curl -C /graphql | less
EOUSAGE
}
die_usage() {
usage >&2
die "$@"
}
want_headers=false
want_include_headers=false
should_need_pat=true
will_use_pat=false
want_emit_raw=false
want_graphql=false
opt_jq_color=false
opt_jq_mono=false
opt_method=''
opt_stdin=false
declare -A gVars
while getopts ':A:CIJMPV:ghim:psv' arg; do
case "$arg" in
A)
# this doesn't handle case variations for Accept, but really it's just a
# guard, you should just supply the type
http_accept="Accept: ${OPTARG#Accept:}"
;;
C) opt_jq_color=true; opt_jq_mono=false ;;
I) want_headers=true ;;
J) want_emit_raw=true ;;
M) opt_jq_mono=true; opt_jq_color=false ;;
P) will_use_pat=true ;;
V)
varkey="${OPTARG%%=*}"
varval="${OPTARG#*=}"
[[ "$varkey" != "$OPTARG" ]] || die "variables must be key=value format"
# We should probably rewrite to Python or Go for this
gVars["$varkey"]="$varval"
;;
g) want_graphql=true ;;
h)
usage
exit 0
;;
i) want_include_headers=true ;;
m) opt_method="$OPTARG" ;;
p) should_need_pat=false ;;
s) opt_stdin=true ;;
v) VERBOSE=true ;;
:) die_usage "missing required option for -$OPTARG; see -h for help" ;;
\?) die_usage "unknown option -$OPTARG; see -h for help" ;;
*) die_usage "unhandled option -$arg; CODE BUG" ;;
esac
done
shift $((OPTIND - 1))
[ $# -gt 0 ] || die_usage "need a query or endpoint-path"
if $want_headers && $want_emit_raw; then
die "incompatible options -I and -J"
elif $want_headers && $want_include_headers; then
die "incompatible options -I and -i"
elif $want_include_headers && $want_emit_raw; then
die "incompatible options -i and -J"
fi
if $opt_jq_color; then
jq() { command jq -C "$@"; }
elif $opt_jq_mono; then
jq() { command jq -M "$@"; }
fi
$should_need_pat && { need_pat || $will_use_pat; }
query="$1"
shift
if [ $# -eq 0 ]; then
set '.'
fi
if $want_graphql; then
# We don't need to change types, the graphql_idl_http_accept is one option,
# which returns a JSON object where the .data field has everything, but in a
# different format.
readonly endpoint='https://api.github.com/graphql'
vars='{}'
for vk in "${!gVars[@]}"; do
vars="$(printf '%s\n' "$vars" | jq --arg vk "$vk" --arg vv "${gVars[$vk]}" '.[$vk]=$vv')"
done
emit_stdin() {
echo '{}' | jq --arg query "$query" --argjson vars "$vars" \
'.query=$query | .variables=$vars'
}
if $VERBOSE; then
printf >&2 'GraphQL query:\n'
emit_stdin | sed $'s/^/\t/' >&2
fi
if $want_headers; then
emit_stdin | cmd_curl -I -X POST -d @- "${endpoint:?}"
elif $want_include_headers; then
emit_stdin | cmd_curl -i -X POST -d @- "${endpoint:?}"
elif $want_emit_raw; then
emit_stdin | cmd_curl -X POST -d @- "${endpoint:?}"
else
emit_stdin | cmd_curl -X POST -d @- "${endpoint:?}" | jq -e -r "$@"
fi
exit 0
fi
case "$query" in
/*) path="$query" ;;
*) path="/$query" ;;
esac
readonly endpoint="https://api.github.com${path:?}"
if [[ -n "$opt_method" ]]; then
if $opt_stdin; then
m_curl() { cmd_curl -X "$opt_method" -d @- -H "$sent_payload_type_hdr" "$@"; }
else
m_curl() { cmd_curl -X "$opt_method" -H "$sent_payload_type_hdr" "$@"; }
fi
else
m_curl() { cmd_curl "$@"; }
fi
if $want_headers; then
m_curl -I "${endpoint:?}"
elif $want_include_headers; then
m_curl -i "${endpoint:?}"
elif $want_emit_raw; then
m_curl "${endpoint:?}"
else
github_fetch_path "$path" "$@"
fi
# vim: set sw=2 et :
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment