Created
June 15, 2020 13:41
-
-
Save philpennock/87cc71c6630723ca44c8eb40cf44a039 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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