Skip to content

Instantly share code, notes, and snippets.

@RandyMcMillan
Last active July 21, 2026 15:47
Show Gist options
  • Select an option

  • Save RandyMcMillan/7abd8016d5eaf3c493f68d70c9e743f4 to your computer and use it in GitHub Desktop.

Select an option

Save RandyMcMillan/7abd8016d5eaf3c493f68d70c9e743f4 to your computer and use it in GitHub Desktop.
gist-utility
#!/usr/bin/env bash
set -euo pipefail
INSTALL_DIR="/usr/local/bin"
SCRIPT_NAME="git-clone-gists"
CLONE_DIR="${CLONE_DIR:-gists}"
show_usage() {
cat <<EOF
Usage: $0 [install|uninstall|--help] [--force] [--user USERNAME] [filters] [github-token]
Defaults:
github-username randymcmillan
github-token optional token argument or GH_TOKEN env var
--gh-token read the token from a file path
--force overwrite existing gist directories instead of fetching
--filename match filenames containing the value
--language match gists with a file in the given language
--extension match gists with a file ending in the extension
--size match gists with a file size expression, e.g. >1000
--not exclude gists containing the value in metadata or filenames
--fork-only clone only forked gists
--anon recognized for parity, but unsupported by the REST API here
--stars recognized for parity, but unsupported by the REST API here
--query gist search syntax, e.g. 'cat stars:>100 user:octocat'
The script clones matching gists for the chosen user into ./gists and refreshes
~/gists to point at that directory.
EOF
}
install_script() {
if [ ! -d "$INSTALL_DIR" ]; then
echo "Error: installation directory '$INSTALL_DIR' does not exist."
exit 1
fi
cp "$0" "$INSTALL_DIR/$SCRIPT_NAME"
chmod +x "$INSTALL_DIR/$SCRIPT_NAME"
echo "Installed '$SCRIPT_NAME' to '$INSTALL_DIR/$SCRIPT_NAME'."
}
uninstall_script() {
rm -f "$INSTALL_DIR/$SCRIPT_NAME"
echo "Removed '$INSTALL_DIR/$SCRIPT_NAME'."
}
case "${1:-}" in
install)
install_script
exit 0
;;
uninstall)
uninstall_script
exit 0
;;
-h|--help|help)
show_usage
exit 0
;;
esac
FORCE_OVERWRITE=0
TARGET_USER=""
FILTER_FILENAME=""
FILTER_LANGUAGE=""
FILTER_EXTENSION=""
FILTER_SIZE=""
FORK_ONLY=0
INCLUDE_ANON=0
STARS_EXPR=""
SEARCH_QUERY=""
GITHUB_TOKEN_PATH=""
declare -a SEARCH_TERMS=()
declare -a SEARCH_NOT_TERMS=()
POSITIONAL=()
while [ $# -gt 0 ]; do
case "$1" in
--force|-f)
FORCE_OVERWRITE=1
shift
;;
--user)
if [ $# -lt 2 ]; then
echo "Error: --user requires a value." >&2
exit 1
fi
TARGET_USER="$2"
shift 2
;;
--filename)
if [ $# -lt 2 ]; then
echo "Error: --filename requires a value." >&2
exit 1
fi
FILTER_FILENAME="$2"
shift 2
;;
--language)
if [ $# -lt 2 ]; then
echo "Error: --language requires a value." >&2
exit 1
fi
FILTER_LANGUAGE="$2"
shift 2
;;
--extension)
if [ $# -lt 2 ]; then
echo "Error: --extension requires a value." >&2
exit 1
fi
FILTER_EXTENSION="$2"
shift 2
;;
--size)
if [ $# -lt 2 ]; then
echo "Error: --size requires a value." >&2
exit 1
fi
FILTER_SIZE="$2"
shift 2
;;
--not)
if [ $# -lt 2 ]; then
echo "Error: --not requires a value." >&2
exit 1
fi
SEARCH_NOT_TERMS+=("$2")
shift 2
;;
--fork-only)
FORK_ONLY=1
shift
;;
--anon)
INCLUDE_ANON=1
shift
;;
--stars)
if [ $# -lt 2 ]; then
echo "Error: --stars requires a value." >&2
exit 1
fi
STARS_EXPR="$2"
shift 2
;;
--query)
if [ $# -lt 2 ]; then
echo "Error: --query requires a value." >&2
exit 1
fi
SEARCH_QUERY="$2"
shift 2
;;
--gh-token)
if [ $# -lt 2 ]; then
echo "Error: --gh-token requires a file path." >&2
exit 1
fi
GITHUB_TOKEN_PATH="$2"
shift 2
;;
--)
shift
while [ $# -gt 0 ]; do
POSITIONAL+=("$1")
shift
done
;;
-*)
echo "Error: unknown flag '$1'." >&2
exit 1
;;
*)
POSITIONAL+=("$1")
shift
;;
esac
done
if [ -z "$TARGET_USER" ]; then
TARGET_USER="${POSITIONAL[0]:-randymcmillan}"
fi
if [ "${#POSITIONAL[@]}" -ge 2 ]; then
GITHUB_PAT="${POSITIONAL[1]}"
else
GITHUB_PAT="${GITHUB_PAT:-}"
fi
if [ -n "$GITHUB_TOKEN_PATH" ]; then
if [ ! -f "$GITHUB_TOKEN_PATH" ]; then
echo "Error: token file '$GITHUB_TOKEN_PATH' does not exist." >&2
exit 1
fi
GITHUB_PAT="$(<"$GITHUB_TOKEN_PATH")"
fi
if [ "$INCLUDE_ANON" -eq 1 ]; then
echo "Error: --anon is recognized for parity, but GitHub's REST API does not expose anonymous gist discovery here." >&2
exit 1
fi
if [ -n "$STARS_EXPR" ]; then
echo "Error: --stars is recognized for parity, but GitHub's REST API does not expose gist stars here." >&2
exit 1
fi
if [ -n "$FILTER_SIZE" ] && [[ ! "$FILTER_SIZE" =~ ^(>=|<=|>|<|=)?[0-9]+$ ]]; then
echo "Error: invalid --size expression '$FILTER_SIZE'." >&2
exit 1
fi
if [ -n "$FILTER_EXTENSION" ]; then
FILTER_EXTENSION="${FILTER_EXTENSION#.}"
fi
parse_search_query() {
local token negate_next=0
for token in $SEARCH_QUERY; do
case "$token" in
NOT)
negate_next=1
;;
user:*)
TARGET_USER="${token#user:}"
;;
filename:*)
FILTER_FILENAME="${token#filename:}"
;;
language:*)
FILTER_LANGUAGE="${token#language:}"
;;
extension:*)
FILTER_EXTENSION="${token#extension:}"
FILTER_EXTENSION="${FILTER_EXTENSION#.}"
;;
size:*)
FILTER_SIZE="${token#size:}"
;;
stars:*)
STARS_EXPR="${token#stars:}"
;;
anon:true)
INCLUDE_ANON=1
;;
fork:only)
FORK_ONLY=1
;;
*)
if [ "$negate_next" -eq 1 ]; then
SEARCH_NOT_TERMS+=("$token")
negate_next=0
else
SEARCH_TERMS+=("$token")
fi
;;
esac
done
}
if [ -n "$SEARCH_QUERY" ]; then
parse_search_query
fi
command -v gh >/dev/null
command -v git >/dev/null
command -v jq >/dev/null
if [ -n "$GITHUB_PAT" ]; then
export GH_TOKEN="$GITHUB_PAT"
fi
shopt -s nocasematch
echo "Starting Gist cloning process for user: $TARGET_USER"
mkdir -p "$CLONE_DIR"
ln -sfn "$PWD/$CLONE_DIR" "$HOME/gists"
cd "$CLONE_DIR"
mkdir -p "$TARGET_USER"
gist_matches_filters() {
local gist_json="$1"
local gist_description gist_url gist_fork_of gist_haystack
local filename language size
local filename_ok=0 language_ok=0 extension_ok=0 size_ok=0
local search_term search_not_term
local content_match=0
gist_description="$(jq -r '.description // ""' <<<"$gist_json")"
gist_url="$(jq -r '.html_url // ""' <<<"$gist_json")"
gist_fork_of="$(jq -r '.fork_of != null' <<<"$gist_json")"
gist_haystack="$(
jq -r '
[
(.description // ""),
(.html_url // ""),
(.files[]?.filename // ""),
(.files[]?.language // "")
] | map(select(. != null)) | join(" ")
' <<<"$gist_json"
)"
if [ "$FORK_ONLY" -eq 1 ] && [ "$gist_fork_of" != "true" ]; then
return 1
fi
if [ "${#SEARCH_NOT_TERMS[@]}" -gt 0 ]; then
for search_not_term in "${SEARCH_NOT_TERMS[@]}"; do
if [[ "$gist_haystack" == *"$search_not_term"* ]]; then
return 1
fi
done
fi
if [ "${#SEARCH_TERMS[@]}" -gt 0 ]; then
for search_term in "${SEARCH_TERMS[@]}"; do
if [[ "$gist_haystack" != *"$search_term"* ]]; then
content_match=1
break
fi
done
fi
if [ "$content_match" -eq 1 ]; then
return 1
fi
while IFS=$'\t' read -r filename language size; do
[ -z "${filename}${language}${size}" ] && continue
if [ -n "$FILTER_FILENAME" ] && [[ "$filename" == *"$FILTER_FILENAME"* ]]; then
filename_ok=1
fi
if [ -n "$FILTER_LANGUAGE" ] && [[ "$language" == "$FILTER_LANGUAGE" ]]; then
language_ok=1
fi
if [ -n "$FILTER_EXTENSION" ] && [[ "$filename" == *."$FILTER_EXTENSION" ]]; then
extension_ok=1
fi
if [ -n "$FILTER_SIZE" ] && size_expr_matches "$FILTER_SIZE" "$size"; then
size_ok=1
fi
done < <(jq -r '.files[]? | [.filename, (.language // ""), (.size | tostring)] | @tsv' <<<"$gist_json")
if [ -n "$FILTER_FILENAME" ] && [ "$filename_ok" -ne 1 ]; then
return 1
fi
if [ -n "$FILTER_LANGUAGE" ] && [ "$language_ok" -ne 1 ]; then
return 1
fi
if [ -n "$FILTER_EXTENSION" ] && [ "$extension_ok" -ne 1 ]; then
return 1
fi
if [ -n "$FILTER_SIZE" ] && [ "$size_ok" -ne 1 ]; then
return 1
fi
return 0
}
size_expr_matches() {
local expr="$1"
local size="$2"
local op limit
if [[ "$expr" =~ ^(>=|<=|>|<|=)?([0-9]+)$ ]]; then
op="${BASH_REMATCH[1]:-=}"
limit="${BASH_REMATCH[2]}"
else
return 1
fi
case "$op" in
'>')
[ "$size" -gt "$limit" ]
;;
'>=')
[ "$size" -ge "$limit" ]
;;
'<')
[ "$size" -lt "$limit" ]
;;
'<=')
[ "$size" -le "$limit" ]
;;
'=')
[ "$size" -eq "$limit" ]
;;
*)
[ "$size" -eq "$limit" ]
;;
esac
}
if ! GIST_JSON="$(
gh api --paginate --slurp "users/$TARGET_USER/gists?per_page=100"
)"; then
echo "Error: unable to list gists for $TARGET_USER. Authenticate with gh or pass a valid token." >&2
exit 1
fi
FOUND_COUNT=0
while IFS= read -r gist_json; do
[ -z "$gist_json" ] && continue
if ! gist_matches_filters "$gist_json"; then
continue
fi
GIST_ID="$(jq -r '.id' <<<"$gist_json")"
GIST_URL="$(jq -r '.html_url' <<<"$gist_json")"
GIST_DIR_NAME="$TARGET_USER/${GIST_ID:0:7}"
FOUND_COUNT=$((FOUND_COUNT + 1))
if [ -e "$GIST_DIR_NAME" ] && [ "$FORCE_OVERWRITE" -eq 1 ]; then
echo "Refreshing existing directory $GIST_URL"
rm -rf "$GIST_DIR_NAME"
elif [ -d "$GIST_DIR_NAME/.git" ]; then
echo "Fetching updates for $GIST_URL"
git -C "$GIST_DIR_NAME" fetch --all --prune >/dev/null
continue
elif [ -e "$GIST_DIR_NAME" ]; then
echo "Error: '$GIST_DIR_NAME' exists but is not a git repository. Re-run with --force." >&2
exit 1
fi
echo "Cloning $GIST_URL into gist/$GIST_DIR_NAME"
gh gist clone "$GIST_ID" "$GIST_DIR_NAME" >/dev/null
done < <(jq -c '.[][] | {id, html_url, description: (.description // ""), fork_of: (.fork_of != null), files: [ .files | to_entries[] | {filename: .value.filename, language: (.value.language // ""), size: .value.size} ] }' <<<"$GIST_JSON")
if [ "$FOUND_COUNT" -eq 0 ]; then
echo "No gists found for user $TARGET_USER."
exit 0
fi
echo "Found $FOUND_COUNT gists to clone."
echo "Gist cloning process completed."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment