Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
A bash script for gist management #bash #gist

gist - Manage your gist like a pro

All your notes, scripts, config files and snippets deserve version control and tagging!
gist is a simple bash script for gist management. It is lightweight(~700LOC) and dependency-free! Helps you to boost coding workflow.

Getting Started

# Install with script on GitHub Page
curl -L -o gist # This shortened link points to the bash script
sudo install -m755 gist /usr/local/bin/gist # Install this script wherever you like
# Fetch your gists and clone them into ~/gist as git repos
gist fetch
# List your gists
# Create a new gist
gist new
# Create private gist with files 'foo' and 'bar'
gist new -p foo bar
# Check information of your third gist
gist detail 3
# Get the path and cd to cloned repo with subshell
gist 3
# List your gists with tags instead of URL
gist tag
# Add tags to your third gist
gist tag 3
# Update the description of your third gist
gist edit 3
# Push changes in your third gist to the remote repo
gist push 3
# Delete gists with indices 3, 4 and 5
gist delete 3 4 5
# Or use Brace Expansion
gist delete {3..5}
# Export your third gist as a new Github repo with web page
gist github 3
# For more detail, read the helper message
gist help


OK...I lied, this script really depends on some very basic dev tools.

  • GNU coreutils
    Used to process text streams. Also, if you are on Mac and use sed, tail with BSD version, then it is still fine!
  • wget or curl
    Used to making request to
  • git
    Do not tell me you don't have it on your machine...
  • Python
    Both 2 and 3 works. It is used to process JSON response from and launch default browser.(Only built-in modules are used)

Basic Commands

Update and clone gists from Github

Run gist fetch to fetch your all gists with Github API and keep short information for each gist in a index file inside a given folder. (default to ~/gist/index)

  • Automatically Clone/Pull each gist with git into a given folder. (default to ~/gist/)
  • Run gist fetch star to fetch your starred gist
  • If token is not being set, then you cannot fetch your private gist

List your gists

Run gist to read index file (default to ~/gist/index) and list your gists with the following format:

<index> <gist-URL> <files-number> <comments-number> <description>

like the following:

  • Use gist star to show your starred gists
  • Use gist all to show your and starred gists
  • Index with prefix s is a starred gist, index with prefix p is a private gist
  • There are colorful hints for each gist in the following cases:
    • working Some changes are made locally but not yet do git commit, or you are not in master branch
    • ahead Your local HEAD is yet to be applied to upstream
    • outdated Your local HEAD is differs from the last fetched gists, do gist fetch to refresh index file and pull if needed

Create a new gist

Run gist new to create a new gist

  • You can create a new gist with 3 different ways:
    1. type the content by hand, run gist new
    2. use existing files, run gist new <file1> <file2>...
    3. from STDIN, like <command> | gist new
  • You can specify filename with --file, and description with --desc, like gist new --file new --desc 'a new gist'
  • If you don't specify filename or description, a prompt will shows up!

Modify a gist

Run gist <INDEX> to enter sub-shell with working directory of the given gist index (by default action). You can do some trick with custom action.(See action and Tips)

Since now a gist is a local cloned repo, it is your business to do git commit and git push. Use gist push <INDEX> is not recommended.

Clean unnecessary local repos

Say you delete gists with command gist delete <index-of-gist>..., the local git repositories are still at ~/gist/.
Run gist clean to move them into /tmp/gist/.


gist stores your configuraion inside ~/.config/gist.conf, with <key>=<value> format for each line. And just do source ~/.config/gist.conf at runtime.

~/.config/gist.conf is created automatically when you run gist at the first time, it only allows current user to read and write (permission 600).

Valid keys are user, token, folder, auto_sync, action, EDITOR, protocol and show_untagged. Use the following commands to set value:

# Set key with a given value
gist config <key> <value>

# Remove current value from a key
gist config <key>

# Or just modify ~/.config/gist.conf directly
gist config

Each key is for the following use cases:


Your Github username

If you use command which needs username and user is not being set, a prompt will shows up and requires your username and API token.

Use gist config user <your-github-username> to set the value if needed.


Your Github API token for the given username. It's scope should be with gist.

If you use command which needs it and it is not being set, A prompt will shows up and requires it. You can choose going to web page to create a new token, or just input an existing one directly.

Use gist config token <your-github-api-token> to set the value if needed.


[Optional] The folder you stores index file and git repos for each your gists and starred gists. Default to ~/gist/ if not being set.

Use gist config folder <prefered-directory> to set the value if needed.


[Optional] Automatically clone/update your gists and starred gists as git repos when doing gist fetch. Default to be true.

Use gist config auto_sync false to disable this feature.


[Optional] A custom action is performed when you do gist <INDEX> (like gist 3 for your third gist). If is being set, gist will cd to the cloned repo, and just simply use eval to perform action.

For example, you can use the following command to print the filename and its content of all files inside the given gist

gist config action 'tail -n +1 *'

If action is not being set, then a default action will be performed:

# Enter sub-shell with current shell or bash

Also, if you run gist <INDEX> with --no-action(or -n), then action would be ignored.


[Optional] Editor to open ~/.config/gist.conf. Default to be vi .

For example, use gist config EDITOR code to use VSCode instead.


[Optional] Protocol to clone git repo. Default to be HTTPS

Valid values are:

  • https
  • ssh

For example, use gist config protocol ssh to use SSH protocol instead.


[Optional] Whether to show untagged gists when using gist tag. Default to be true

Use gist config show_untagged false to disable this feature.

Filter gists

Filter by tags

gist treats trailing hashtags inside gist description as tags. For example, if a description is:

[Title] this is description #tag1 #tag2

When gist is performed, it only display description with part: [Title] this is description, and treat the trailing hashtags as tags of a gist.

Tag a gist

You can use the following command to add/remove tags:

# tag your third gist
gist tag 3

After it is finished, gist just calls Github API to apply new description onto the given gist.

List gists with tags

Use sub-command tag to list gists with tags instead of URLs.

# show tags for your gists
gist tag

Filter gists with tags

If arguments after gist tag are not indices of gist, then they will be treated as tag values. The output will be a list of gists with those tags

# Filter gists with tag1 and tag2
gist tag tag1 tag2

You can also use regex pattern as tag value:

# only show tagged gists
gist tag .+

Show existing tags

Use sub-command tags to show existing tags and pinned tags. They are sorted alphabetically.

gist tags

Pin/Unpin tags

Say you are working with gists with some meaningful tags. You can use sub-command pin to pin them, and filter your gists with pinned tags

# Pin tag1 and tag2, If a tag is pinned, then unpin it
gist pin tag1 tag2
# Disply gists with pinned tags
gist pin

Filter by pattern

You can search gists with pattern in description, filename or file contents with sub-command grep

# search by a simple string
gist grep string
# search by a pattern(heading string in a line)
gist grep '^string'

Filter by file languages

List gists with languages

You can use sub-command lan to List gists with file languages instead of URLs.

# show languages for your gists
gist lan

Filter gists with languages

# Filter gists with files in Shell and Yaml format

Index Range

You can specify the range of indices, works both on your owned gists and starred gists.

# only show gists with index 5 to 10
gist 5-10
# show gists from index 5
gist 5-
# show starred gists only to index s10
gist -s10
# only show gists with index 1 to 20
seq 20 | gist


Filter gists with pipe

If STDIN is from a pipe, then gist will only process gists with indices in the first column. So, you can concatenate the output of each sub-command.

# only show gists with index 1 to 20
seq 20 | gist
# List starred gist with Yaml file
gist star | gist lan Yaml
# Only List gists with tag1, pattern1 in description/filenames/contents and contains shell script
gist tag tag1 | gist grep pattern1 | gist lan SHELL

Git Workflow

Each gist is a git repository.
Although there are some limits on git push, like sub-directory is prohibited. But guess what?

  1. Push another branch to is allowed.
  2. Push tags is also allowed. And like repos in, you can get source file by reference with URL:<gist_id>/tar.gz/<YOUR TAG or BRANCH>

Useful action for gist repo

I strongly recommend using tig as your custom action. It is the most powerful git CLI tool as far as I know, and also easy to get in most of the Linux distros or Homebrew for mac. Give it a try!

If tig is installed, run the following command to configure it as custom action:

gist config action 'tig -all'

tig interface for history diagram:

Suppress action

If action is not being set, you will enter sub-shell by default. If you want suppress it and do not want to type --no-action every time, just use command ture to do nothing.

gist config action 'true'

Suppress hint

There are several environment variables or arguments can suppress hint or user confirm, like:

# List gists without hint
hint=false gist

# Just print the repo path with a given index
gist 3 --no-action
# Or shorter argument
gist 3 -n

# Delete your third gist without confirmation
gist delete 3 --force
#!/usr/bin/env bash
# Author: Hsieh Chin Fan (typebrook) <>
# License: MIT
# --
# gist
# Description: Manage your gists with git and Github API v3
# Usage: gist [command] [<args>]
# [star|s|all|a] List your gists, use 'star' or 's' for your starred gists,
# 'all' or 'a' for both your and starred gists. Format for each line is:
# fetch, f [star|s] Update the local list of your gists, 'star' for your starred gists
# <INDEX> [-n|--no-action] Show the path of local gist repo and do custom actions(enter sub-shell by default)
# new, n [-d |--desc <description>] [-p] <FILE>... create a new gist with files
# new, n [-d |--desc <description>] [-p] [-f|--file <FILE_NAME>] create a new gist from STDIN
# grep, g <PATTERN> Grep gists by description, filename and content with a given pattern
# tag, t <INDEX> Modify tags for a gist
# tag, t <TAG>... Grep gists with tags
# tag, t List gist with tags and pinned tags
# tags, tt List all tags and pinned tags
# pin, p <TAG>... Pin/Unpin tags
# pin, p Grep gists with pinned tags
# lan, l <PATTERN>... Grep gists with languages
# lan, l List gist with languages of files
# detail, d <INDEX> Show the detail of a gist
# edit, e <INDEX> ["NEW_DESCRIPTRION"] Edit description of the given gist
# delete, D <INDEX>... [--force] Delete gists by given indices
# push, P <INDEX> Push changes by git (Well... better to make commit by youself)
# clean, C Clean local repos of removed gists
# config, c Configure with editor, will show all valid keys
# config, c <VALID_KEY> [value] Configure a single option. If no value is specified, apply default setting
# user, u <USER> Get list of gists with a given Github user
# github, G <INDEX> Export selected gist as a new Github repo
# id <INDEX> Print the gist ID
# url <INDEX> Print the gist URL
# help, h Show this help message
# Example:
# gist fetch (update the list of gists from
# gist (Show your gists)
# gist 3 (show the repo path of your 3rd gist, and do custom actions)
# gist 3 --no-action (show the repo path of your 3rd gist, and do not perform actions)
# gist new foo bar (create a new gist with files foo and bar)
# gist tag (Show your gists with tags)
# gist tag 3 (Add tags to your 3rd gist)
# gist tag .+ (show tagged gists)
# Since now a gist is a local cloned repo
# It is your business to do git commit and git push
declare -r NAME=${GISTSCRIPT:-$(basename $0)} #show hint and helper message with current script name
declare -r GITHUB_API=
declare -r GIST_DOMAIN=
declare -r CONFIG=~/.config/gist.conf; mkdir -p ~/.config
declare -r per_page=100
declare -ar INDEX_FORMAT=('index' 'public' 'gist_id' 'tags_string' 'blob_code' 'file_array' 'file_num' 'comment_num' 'author' 'created_at' 'updated_at' 'description')
declare -ar VALID_CONFIGS=('user' 'token' 'folder' 'auto_sync' 'action' 'EDITOR' 'protocol' 'show_untagged' 'pin')
declare -A CONFIG_VALUES=([auto_sync]='true|false' [protocol]='https|ssh' [show_untagged]='true|false')
if [[ ! -t 0 ]]; then
export mark=.
export mark=[^s] # By defaut, only process user's gists, not starred gist
# Default configuration
python() { type python3 >&/dev/null && python3 "$@" || python "$@"; }
if [[ ! -t 1 && -z $hint ]]; then
# Shell configuration
set -o pipefail
[[ $TRACE == 'true' ]] && set -x
# clean temporary files
tmp_dir=$(mktemp -d)
trap "[[ '$DEBUG' == 'true' ]] && tail -n +1 $tmp_dir/* >log 2>/dev/null; rm -r $tmp_dir" EXIT
# Mac compatibility
tmp_file() {
if [[ $(uname) == Darwin ]]; then
TMPDIR=$tmp_dir mktemp -t $1
mktemp -p $tmp_dir -t $1.XXXXXX
tac() { sed -e '1! G; h; $!d'; } # An easy way to reverse file content both on Linux and Darwin
mtime() {
if [[ $(uname) == Darwin ]]; then
stat -x $1 | grep Modify | cut -d' ' -f2-
stat -c %y $1
# This function determines which http get tool the system has installed and returns an error if there isnt one
getConfiguredClient() {
if command -v curl &>/dev/null; then
elif command -v wget &>/dev/null; then
elif command -v http &>/dev/null; then
echo "Error: This tool requires either curl, wget, or httpie to be installed." >&2
return 1
# Allows to call the users configured client without if statements everywhere
# TODO return false if code is not 20x
http_method() {
local METHOD=$1; shift
local header_opt; local header; local data_opt
case "$configuredClient" in
curl) [[ -n $token ]] && header_opt="--header" header="Authorization: token $token"
[[ $METHOD =~ (POST|PATCH) ]] && data_opt='--data'
curl -X "$METHOD" -A curl -s $header_opt "$header" $data_opt ${HEADER:+-D $HEADER} "@$http_data" "$@" ;;
wget) [[ -n $token ]] && header_opt="--header" header="Authorization: token $token"
[[ $METHOD =~ (POST|PATCH) ]] && data_opt='--body-file'
wget --method="$METHOD" -qO- $header_opt "$header" $data_opt "$http_data" "$@" ;;
httpie) [[ -n $token ]] && header="Authorization:token $token"
[[ $METHOD =~ (POST|PATCH) ]] && data_opt="@$http_data"
http -b "$METHOD" "$@" "$header" "$data_opt" ;;
esac 2>&1 \
| tee $(tmp_file HTTP.$METHOD) \
|| { echo "Error: no active internet connection" >&2; return 1; }
# Parse JSON from STDIN with string of python commands
_process_json() {
python -c "from __future__ import print_function; import sys, json; $1"
return "$?"
# Handle configuration cases
_configure() {
[[ $# == 0 ]] && ${EDITOR} $CONFIG && return 0
local key=$1; local value="$2"; valid_configs=${VALID_CONFIGS[@]}
[[ ! ${key} =~ ^(${valid_configs// /|})$ ]] \
&& echo "Not a valid key for configuration, use <${valid_configs[@]// /|}> instead." \
&& return 1
case $key in
while [[ ! $value =~ ^[[:alnum:]]+$ ]]; do
[[ -n $value ]] && echo "Invalid username"
read -r -p "Github username: " value </dev/tty
done ;;
[[ -n $value && ${#value} -ne 40 && ! $value =~ ^(\$|\`) ]] && echo 'Invalid token format, it is not 40 chars' >&2 && return 1 ;;
eval $key="$value"
echo $key="$value" >>$CONFIG
# Prompt for token
# TODO check token scope contains gist, ref:
_ask_token() {
echo -n "Create a new token from web browser? [Y/n] "
read -r answer < /dev/tty
if [[ ! $answer =~ ^(N|n|No|NO|no)$ ]]; then
python -mwebbrowser
while [[ ! $token =~ ^[[:alnum:]_-]{40,}$ ]]; do
[[ -n $token ]] && echo "Invalid token"
read -r -p "Paste your token here (Ctrl-C to skip): " token </dev/tty
echo token=$token >>$CONFIG
# Check configuration is fine with user setting
_validate_config() {
[[ $1 =~ ^(c|config|h|help|u|user|update) ]] && return 0
if [[ -z $user ]]; then
echo 'Hi fellow! To access your gists, I need your Github username'
echo "Also a personal token with scope which allows \"gist\"!"
_configure user && _ask_token && init=true
elif [[ -z $token && $1 =~ ^(n|new|e|edit|D|delete)$ ]]; then
echo 'To create/edit/delete a gist, a token is needed' >&2 && return 1
elif [[ -z $token && $1 =~ ^(f|fetch)$ && $2 =~ ^(s|star)$ ]]; then
echo 'To get user starred gists, a token is needed' >&2 && return 1
# Apply current configuration into config file
_reformat_config() {
local line_token=$(sed -ne '/^token=/h; ${x;p}' $CONFIG); line_token=${line_token:-token=$token}
for key in ${VALID_CONFIGS[@]}; do
[[ $key == token ]] && echo $line_token && continue # Because format may like this: token=$(COMMAND-TO-GET-TOKEN), just leave it untouched
local value="${!key}"; local valid_values=${CONFIG_VALUES[$key]}
if [[ -n "$value" ]] && [[ -z $valid_values || "$value" =~ ^($valid_values)$ ]]; then
echo -n $key="'$value'"
declare -g $key="$value" # apply current value
echo -n $key=
declare -g $key=${valid_values%%|*} # apply the first valid value
echo -e "${valid_values:+\t\t# Valid: $valid_values}"
done >"$CONFIG"
# Load configuration
_apply_config() {
unset ${VALID_CONFIGS[@]}
if [[ -e $CONFIG ]]; then
source "$CONFIG" 2>/dev/null
umask 0077 && touch "$CONFIG"
_validate_config "$@" || return 1
[[ -z $folder || ! -w $(dirname "$folder") ]] && folder=~/gist && mkdir -p $folder
INDEX=$folder/index; [[ -e $INDEX ]] || touch $INDEX
EDITOR=${EDITOR:-$(type vim &>/dev/null && echo vim || echo vi)}
# extract trailing hashtags from description
_trailing_hashtags() {
grep -Eo " #[$TAG_CHAR #]+$" <<<"$1" | sed -Ee "s/.* [$TAG_CHAR]+//g"
_color_pattern() {
sed -Ee "s/$1/\\\e[33m\1\\\e[0m/g"
_pattern_pinned_tags() {
echo '('$(sed -E 's/ /[[:space:]]|/g; s/\./[^ ]/g; s/$/[[:space:]]/' <<<"$@")')'
# Return git status of a given repo
# $1 for repo path, $2 for blob_code from last fetch
_check_repo_status() {
if [[ $2 == NONE ]]; then
return 0
elif [[ ! -d $1 ]]; then
if [[ $auto_sync != false ]]; then
echo "\e[32m[cloning]\e[0m";
echo "\e[32m[Not cloned yet]\e[0m";
cd "$1" || exit 1
# git status is not clean or working on non-master branch
if [[ -n $(git status --short) || $(git branch) =~ "\* $(_remote_head)" ]] &>/dev/null; then
echo "\e[36m[working]\e[0m"
# files contents are not the same with the last time called GIST API, so warn user to call 'gist fetch'
[[ $(_blob_code "$1") != "$2" ]] 2>/dev/null && local status="\e[31m[outdated]\e[0m"
# current HEAD is newer than remote, warn user to call 'git push'
[[ -n $(git cherry origin) ]] 2>/dev/null && local status="\e[31m[ahead]\e[0m"
echo "$status"
# check given index is necessary to handle
_index_pattern() {
if [[ -z "$INPUT" ]]; then
echo .+
echo "($(sed -Ee '/^ {5,}/ d; s/^ *//; /^$/ q' <<<"$INPUT" | cut -d' ' -f1 | xargs | tr ' ' '|'))"
_show_hint() {
if [[ $display == 'tag' && -n $pin ]]; then
local pinned_tags=( $pin )
echo > /dev/tty
echo Pinned tags: "${pinned_tags[*]/#/#} " > /dev/tty
elif [[ $hint != 'false' ]]; then
local mtime="$(mtime $INDEX | cut -d'.' -f1)"
echo > /dev/tty
echo "Last updated at $mtime" > /dev/tty
echo "Run \"$NAME fetch\" to keep gists up to date, or \"$NAME help\" for more details" > /dev/tty
# Display the list of gist, show username for starred gist
# If hint=false, do not print hint to tty. If mark=<pattern>, filter index with regex
# If display=tag/language, print tags/languages instead or url
_print_records() {
if [[ ! -s $INDEX ]]; then
echo "Index file is empty, please run commands "$NAME fetch" or "$NAME create""
return 0
local PWD=$(pwd)
sed -Ee "/^$mark/ !d; /^$(_index_pattern) / !d" $INDEX \
| while read -r "${INDEX_FORMAT[@]}"; do
local message="$(printf '%-56s' ${GIST_DOMAIN}/${gist_id})"
if [[ $display == 'tag' ]]; then
[[ $show_untagged == 'false' && $tags_string == ',' ]] && continue
message="$(printf '% 45s' "${tags_string//,/ }") "
elif [[ $display == 'language' ]]; then
message="$(tr ',' '\n' <<< $file_array | sed -Ee 's/.+=/#/' | uniq | xargs)"
message="$(printf '% 45s' "$message")"
local extra="$(printf "%-4s" "$file_num $comment_num")"
[[ $PWD == $folder/$gist_id ]] && extra="$(tput setaf 13)>>> $(tput sgr0)"
local status=''; status=$(_check_repo_status "${folder}/${gist_id}" "$blob_code")
[[ $index =~ ^s ]] && description="$(printf "%-12s" [${author}]) ${description}"
raw_output="$(printf "%-4s" "$index") $message $extra ${status:+${status} }$(_color_pattern '^(\[.+\])' <<<"$description")"
decorator=$(( $(grep -o '\\e\[0m' <<<"$raw_output" | wc -l) *9 ))
echo -e "$raw_output" | cut -c -$(( $(tput cols) +decorator ))
done \
| if [[ $display == 'tag' && -n $pin ]]; then
local pinned_tags=($pin); local pattern="$(_pattern_pinned_tags ${pinned_tags[@]/#/#})"
echo -e "$(_color_pattern "$pattern")"
[[ -z $INPUT ]] && _show_hint || true
# Grep description, filename or file content with a given pattern
# TODO add option to configure case-sensitive
_grep_content() {
if [[ -z $1 ]]; then echo 'Please give a pattern' && return 1; fi
sed -Ee "/^$(_index_pattern) / !d" $INDEX \
| while read -r "${INDEX_FORMAT[@]}"; do
# grep from description
if grep --color=always -iq "$1" <<<"$description"; then
hint=false mark="$index " _print_records
local repo=${folder}/${gist_id}
[[ -d $repo ]] && cd $repo || continue
local result=$({
# grep from filenames
ls $repo | grep --color=always -Ei "$1"
# grep from content of files
# Abort error message to prevent weird file name, for example:
grep --color=always -EHi -m1 "$1" * 2>/dev/null | head -1
[[ -n $result ]] && cd - >/dev/null && hint=false mark="$index " _print_records && sed -e 's/^/ /' <<<"$result"
# Parse JSON object of the result of gist fetch
_parse_gists() {
_process_json '
raw = json.load(sys.stdin)
for gist in raw:
print(gist["public"], end=" ")
print(gist["html_url"], end=" ")
print(",".join(file["raw_url"] for file in gist["files"].values()), end=" ")
print(",".join(file["filename"].replace(" ", "-") + "=" + str(file["language"]).replace(" ", "-") for file in gist["files"].values()), end=" ")
print(len(gist["files"]), end=" ")
print(gist["comments"], end=" ")
print(gist["owner"]["login"], end=" ")
print(gist["created_at"], end=" ")
print(gist["updated_at"], end=" ")
# Parse response from 'gist fetch' to the format for index file
_parse_response() {
_parse_gists \
| while read -r "${INDEX_FORMAT[@]:1:1}" html_url file_url_array "${INDEX_FORMAT[@]:5:7}"; do
local gist_id=${html_url##*/}
local blob_code=$(echo "$file_url_array" | tr ',' '\n' | sed -E -e 's#.*raw/(.*)/.*#\1#' | sort | cut -c -7 | paste -s -d '-' -)
local hashtags_suffix="$(_trailing_hashtags "$description")"
local hashtags="$(echo $hashtags_suffix | xargs)"
local tags_string="${hashtags// /,}"; [[ -z $tags_string ]] && tags_string=','
eval echo "${INDEX_FORMAT[@]/#/$}"
# Get a single JSON object of gist from response, and update index file
_update_gist() {
local record="$(sed -e '1 s/^/[/; $ s/$/]/' | index=$1 _parse_response)"
[[ -n $record ]] && sed -i'' -Ee "/^$1 / s^.+^$record^" $INDEX
# Get latest list of gists from Github API
_fetch_gists() {
local route=${route:-users/$user/gists}
if [[ $mark == s ]]; then
# set global variable HEADER in http_method, so prevent using pipe
HEADER=$(tmp_file HEADER)
http_method GET $GITHUB_API/$route${1} | _parse_response
# consider if HEADER is not exist
_fetch_gists_with_pagnation() {
_fetch_gists "?per_page=$per_page" >> $1
while true; do
local next_page=''
[[ -e $HEADER ]] && next_page=$(sed -Ene '/^[lL]ink: / s/.+page=([[:digit:]]+)>; rel=\"next\".+/\1/p' $HEADER)
[[ -z $next_page ]] && break
[[ $hint != false ]] && printf "%-4s gists fetched\n" $(( ($next_page -1) * $per_page )) >/dev/tty
_fetch_gists "?per_page=$per_page&page=$next_page" >> $1
done || return 1
# Update index file by GITHUB API with pagnation
_update_gists() {
echo "Fetching $user's gists from $GITHUB_API..."
local fetched_records=$(tmp_file fetched)
_fetch_gists_with_pagnation $fetched_records || { echo Something screwed; exit 1; }
[[ ! -s $fetched_records ]] && echo 'Not a single valid gist' && return 0
sed -i'' -Ee "/^$mark/ d" $INDEX
extra='s0 True b0d2e7e67aa50298fdf8111ae7466b56 #bash,#gist 0316236-13154b2-768f3e5,gist=Shell,test.bats=Shell 3 30 typebrook 2019-12-26T06:49:40Z 2021-05-05T09:42:00Z A bash script for gist management'
[[ $mark == s ]] && echo $extra >> $INDEX
<$fetched_records tac | nl -s' ' \
| while read -r "${INDEX_FORMAT[@]:0:2}" extra; do
local prefix=''
[[ $public == False ]] && prefix=p; [[ $mark == s ]] && prefix=s
echo $prefix$index $public $extra
done >> $INDEX
[[ $auto_sync != false ]] && (_sync_repos &> /dev/null &)
# Fetch gists for a given user
_query_user() {
local fetched_records=$(tmp_file fetched)
route=users/$1/gists _fetch_gists_with_pagnation $fetched_records
<$fetched_records tac | nl -s' ' \
| while read -r ${INDEX_FORMAT[@]}; do
echo $index ${GIST_DOMAIN}/${gist_id} $author $file_num $comment_num $description | cut -c -"$(tput cols)"
done || { echo "Failed to query $1's gists"; exit 1; }
_remote_head() {
[ -d .git/ ] || return 1
local origin_head_content="$(cat .git/refs/remotes/origin/HEAD)"
echo ${origin_head_content##*/}
# Return the unique code for current commit, to compare repo status and the result of 'gist fetch'
# Because there is no way to get commit SHA with 'gist fetch'
_blob_code() {
cd "$1" || return 1
cd "$1" && git ls-tree $(_remote_head) | cut -d' ' -f3 | cut -c-7 | sort | paste -sd '-'
_pull_if_needed() {
sed -ne "/$1 / p" "$INDEX" \
| while read -r "${INDEX_FORMAT[@]}"; do
local repo=$folder/$1
local blob_code_local=$(_blob_code "$repo")
cd "$repo" \
&& [[ $blob_code_local != "$blob_code" ]] \
&& [[ $(git rev-parse origin/HEAD) == $(git rev-parse $(_remote_head)) ]] \
&& git pull &
# Update local git repos
_sync_repos() {
comm -1 <(ls -A "$folder" | sort) \
<(while read -r ${INDEX_FORMAT[@]}; do echo $index $gist_id; done < "$INDEX" | sed -ne "/^$mark/ p" | cut -d' ' -f2 | sort) \
| {
# clone repos which are not in the local
sed -ne '/^\t/ !p' <<<$result \
| xargs -I{} --max-procs 8 git clone "$(_repo_url {})" $folder/{} &
# if repo is cloned, do 'git pull' if remote repo has different blob objects
sed -ne '/^\t/ s/\t//p' <<<$result \
| while read GIST_ID; do
_pull_if_needed $GIST_ID
# Get the url where to clone repo, take user and repo name as parameters
_repo_url() {
if [[ $protocol == 'ssh' ]]; then
echo "$1.git"
echo "${GIST_DOMAIN}/$1.git"
# Get gist id from index files
_gist_id() {
read -r ${INDEX_FORMAT[@]} <<<"$(sed -ne "/^$1 / p" $INDEX)"
if [[ -z $GIST_ID || ! $1 =~ [0-9a-z]+ ]]; then
echo -e "$(hint=false _print_records | sed -Ee 's/^( *[0-9a-z]+)/\\e[5m\1\\e[0m/')"
echo -e "Invalid index: \e[33m$1\e[0m"
echo 'Use the indices blinking instead (like 1 or s1)'
return 1
# set gist id either by given index or current directory
_set_gist_id() {
if [[ -z $1 ]]; then
[[ $(dirname $(pwd)) == $folder ]] && GIST_ID=$(basename $(pwd)) && return 0
_gist_id "$1" || return 1
# Show path of repo by gist ID, and perform action
_goto_gist() {
echo "${folder}/${GIST_ID}"
touch "${folder}/${GIST_ID}"
if [[ $* =~ (-n|--no-action) || $PIPE_TO_SOMEWHERE == true ]]; then
return 0
elif [[ -z $action ]]; then
action='echo You are inside subshell now, press \<CTRL-D\> to exit; echo; ls; ${SHELL:-bash}'
cd "${folder}/${GIST_ID}" && eval "$action"
# Return the path of local repo with a given index
_goto_gist_by_index() {
[[ $1 =~ (-n|--no-action) ]] && set -- $2 -n # move '-n' as the last argument
_gist_id "$1" || return 1
if [[ ! -d $folder/$GIST_ID ]]; then
echo 'Cloning gist as repo...'
if git clone "$(_repo_url "$GIST_ID")" "$folder/$GIST_ID"; then
echo 'Repo is cloned' > /dev/tty
echo 'Failed to clone the gist' > /dev/tty
return 1
_goto_gist "$@"
# Delete gists with given indices
# Specify --force to suppress confirmation
_delete_gist() {
if [[ ! $* =~ '--force' ]]; then
read -n1 -p "Delete gists above? [y/N] " response
[[ ! $response =~ ^(y|Y)$ ]] && return 0 || echo
for i in "$@"; do
_gist_id "$i" &> /dev/null || continue
http_method DELETE "$GITHUB_API/gists/${GIST_ID}" \
&& echo "$i is deleted, but the local git repo is still at $folder/${GIST_ID}" \
&& sed -i'' -Ee "/^$i / d" $INDEX
# Remove repos which are not in index file anymore
_clean_repos() {
comm -23 <(find $folder -maxdepth 1 -type d | sed -e '1d; s#.*/##' | sort) \
<(while read -r ${INDEX_FORMAT[@]}; do echo $gist_id; done < "$INDEX" | sort 2> /dev/null ) \
| while read -r dir; do
mkdir -p /tmp/gist
mv $folder/"$dir" /tmp/gist/ && echo $folder/"$dir" is moved to /tmp/gist/
# Parse JSON object of gist user comments
_parse_comment() {
_process_json '
raw = json.load(sys.stdin);
for comment in raw:
print("|", "user:", comment["user"]["login"])
print("|", "created_at:", comment["created_at"])
print("|", "updated_at:", comment["updated_at"])
print("|", comment["body"])
# Show the detail of a gist
# TODO add parameter --comment to fetch comments
_show_detail() {
_set_gist_id $1 || return 1
sed -En -e "/[^ ]+ [^ ]+ ${GIST_ID} / {p; q}" $INDEX \
| while read -r "${INDEX_FORMAT[@]}"; do
echo -e Desc: $(_color_pattern '^(\[.+\])' <<<"$description")
echo -e Tags: ${tags_string//,/ }
echo -e Site: ${GIST_DOMAIN}/${GIST_ID}
echo -e APIs:${GIST_ID}
echo -e created_at: $created_at
echo -e updated_at: $updated_at
echo -e files:
tr ',' '\n' <<<${file_array//=/ } | column -t | sed -e 's/^/ /'
# Open Github repository import page
_export_to_github() {
_gist_id "$1" || return 1
echo Put the folowing URL into web page:
echo -n "${GIST_DOMAIN}/${GIST_ID}.git"
python -mwebbrowser
_id_to_index() {
while read -r ${INDEX_FORMAT[@]}; do
[[ ! $index =~ s && ${gist_id} == $1 ]] && echo $index
done <$INDEX
# Simply commit current changes and push to remote
_push_to_remote() {
_set_gist_id $1 || return 1
cd "${folder}/${GIST_ID}"
local index=$(_id_to_index ${GIST_ID})
if [[ -n $(git status --short) ]]; then
git add . && git commit -m 'update'
if [[ -n $(git cherry) ]]; then
git push && \
http_method GET "$GITHUB_API/gists/${GIST_ID}" | _update_gist $index
# Set filename/description/permission for a new gist
_set_gist() {
description=''; filename=''; public=True
while [[ -n "$*" ]]; do case $1 in
-d | --desc)
shift; shift;;
-f | --file)
shift; shift;;
ls "${files[@]}" > /dev/null || return 1
# Let user type the content of gist before setting filename
_new_file() {
tmp_file=$(tmp_file CREATE)
if [[ -z $INPUT ]]; then
echo "Type a gist. <Ctrl-C> to cancel, <Ctrl-D> when done" > /dev/tty
cat > "$tmp_file"
echo "$INPUT" > "$tmp_file"
echo > /dev/tty
[[ -z $1 ]] && read -e -r -p 'Type file name: ' filename < /dev/tty
mv "$tmp_file" $tmp_dir/"$filename"
echo $tmp_dir/"$filename"
# Parse JSON object of a single gist
_gist_body() {
_process_json "
import os.path
files_json = {}
files = sys.stdin.readline().split()
description = sys.stdin.readline().replace('\n','')
for file in files:
with open(file, 'r') as f:
files_json[os.path.basename(file)] = {'content':}
print(json.dumps({'public': $public, 'files': files_json, 'description': description}))
# Create a new gist with files. If success, also update index file and clone the repo
_create_gist() {
_set_gist "$@" || return 1
[[ -z ${files[*]} ]] && files+=($(_new_file "$filename"))
[[ -z $description ]] && read -e -r -p 'Type description: ' description < /dev/tty
local index=$([[ $public == False ]] && echo p)$(( $(sed -e '/^s/ d' $INDEX | wc -l) +1 ))
echo 'Creating a new gist...'
http_data=$(tmp_file PATLOAD.CREATE)
echo -e "${files[*]}\n$description" \
| _gist_body > "$http_data" \
&& http_method POST $GITHUB_API/gists \
| sed -e '1 s/^/\[/; $ s/$/\]/' \
| index=$index _parse_response \
| tee -a $INDEX \
| while read -r "${INDEX_FORMAT[@]}"; do
git clone "$(_repo_url $gist_id)" ${folder}/${gist_id}
# shellcheck disable=2181
if [[ $? -eq 0 ]]; then
echo 'Gist is created'
INPUT=$(tail -1 $INDEX | cut -d' ' -f1) hint=false _print_records
echo 'Failed to create gist'
# Update description of a gist
_edit_gist() {
local index=$1; shift
_gist_id "$index" || return 1
if [[ -z "$@" ]]; then
read -r "${INDEX_FORMAT[@]}" <<<"$(sed -ne "/^$index / p" $INDEX)"
read -e -p 'Edit description: ' -i "$description" -r DESC < /dev/tty
tags=( ${tags_string//,/ } )
DESC="$DESC ${tags[*]}"
http_data=$(tmp_file PAYLOAD.EDIT)
echo '{' \"description\": \""${DESC//\"/\\\"}"\" '}' > "$http_data"
http_method PATCH "${GITHUB_API}/gists/${GIST_ID}" | _update_gist $index \
&& hint=false mark="$index " _print_records \
|| echo 'Fail to modify gist description'
# Print helper message
usage() {
sed -Ene "/^#/ !q; 1,/^# --/ d; s/^# //p; s/^( *|Usage: )gist/\1$NAME/" "$0"
# Check remote urls of all repos match current protocol in configuration file
# If not, update them
_check_protocol() {
find $folder -maxdepth 1 -mindepth 1 -type d \
| while read -r repo; do
cd "$repo" || exit 1
git remote set-url origin $(_repo_url $(basename $(pwd)))
_tag_gist() {
# if no tag is given, show gist list with tags
if [[ -z $* ]]; then
display=tag _print_records
# if user want to change tags of a gist
elif _gist_id $1 &>/dev/null; then
_show_detail $1 | sed 3,6d && echo
read -r "${INDEX_FORMAT[@]}" <<<"$(sed -ne "/^$1 / p" $INDEX)"
local tags="$(sed -e 's/,//g; s/#/ /g; s/^ //g' <<<"$tags_string")"
read -e -p 'Edit tags: ' -i "$tags" -r -a new_tags < /dev/tty
local hashtags=( $(sed -Ee 's/#+/#/g' <<<${new_tags[@]/#/#}) )
($0 edit $1 "${description}${hashtags:+ }${hashtags[@]}" &>/dev/null &)
# if user want to filter gists with given tags
local pattern="($(sed -E 's/([^ ]+)/#\1/g; s/ /[[:space:]]|/g; s/\./[^ ]/g' <<<"$@") )"
hint=false mark=${INPUT:+.} display=tag _print_records | grep --color=always -E "$pattern"
# show all tags and pinned tags
_show_tags() {
local pinned_tags=( $pin )
local pattern=$(_pattern_pinned_tags "${pinned_tags[@]/#/#}")
local tags=$(while read -r "${INDEX_FORMAT[@]}"; do
echo ${tags_string//,/ }
done < $INDEX | tr ' ' '\n' | sed -e '/^$/d' | sort -u)
for prefix in {0..9} {a..z} {A-Z} [^0-9a-zA-Z]; do
local line=$(echo $tags | grep -Eo "#$prefix[^ ]+" | tr '\n' ' ')
[[ -z $line ]] && continue
# add color to pinned tags
echo -e $(_color_pattern "$pattern" <<<"$line")
if [[ ${#pinned_tags} == 0 ]]; then
echo "Run \"$NAME pin <tag1> <tag2>...\" to pin/unpin tags"
echo Pinned tags: "${pinned_tags[@]/#/#}"
# pin/unpin tags
_pin_tags() {
# if no arguments, print gists with pinned tags
if [[ -z $* && -n $pin ]]; then
hint=false _tag_gist $pin
local new_pinned=( $(echo $pin $* | tr ' ' '\n' | sort | uniq -u | xargs) )
for tag in "${new_pinned[@]}"; do
if [[ $tag =~ [p]*[0-9]+ ]]; then
echo Invalid tag: $tag
return 1
done || exit 1
sed -i'' -e "/^pin=/ d" "$CONFIG" && echo pin=\'"${new_pinned[*]}"\' >> "$CONFIG"
# show languages of files in gists
_gists_with_languages() {
local pattern="($(sed -E 's/([^ ]+)/#\1/g; s/ /|/g' <<<"$@"))"
display=language _print_records | grep --color=always -Ei "$pattern"
_gists_with_range() {
[[ ! $* =~ ^s?[0-9]*-s?[0-9]*$ ]] && echo 'Invalid range' && exit 1
local prefix=''; [[ $* =~ s ]] && prefix=s
local maximum=$(sed -Ene "/^${prefix:-[^s]}/ p" $INDEX | wc -l)
local range=$(sed -Ee "s/s//g; s/^-/1-/; s/-$/-$maximum/; s/-/ /" <<< "$*")
INPUT=$(seq $range | sed -e "s/^/p?$prefix/")
hint=false _print_records
_access_last_index() {
GIST_ID=$(ls -tup $folder | grep / | head -1)
_goto_gist "$@"
_apply_config "$@" || exit 1
if [[ $init ]]; then _update_gists; exit 0; fi
shopt -s extglob
case "$1" in
_print_records ;;
star | s)
mark=s; _print_records ;;
all | a)
mark=.; _print_records ;;
fetch | f)
[[ $2 =~ ^(s|star)$ ]] && mark=s || mark=[^s]
_update_gists ;;
new | n)
_create_gist "$@" ;;
edit | e)
_edit_gist "$@" ;;
sync | S)
_sync_repos ;;
detail | d)
_show_detail "$@" ;;
_set_gist_id "$1" && echo ${GIST_ID} ;;
_set_gist_id "$1" && echo${GIST_ID} ;;
delete | D)
_delete_gist "$@" ;;
clean | C)
_clean_repos ;;
config | c)
_configure "$@" && _apply_config && sed -ne "/^$1=/p" $CONFIG && (_check_protocol &>/dev/null &) ;;
user | u)
_query_user "$@" ;;
grep | g)
_grep_content "$@" ;;
github | G)
_export_to_github "$1" ;;
push | P)
_push_to_remote "$1" ;;
tag | t)
_tag_gist "$@" ;;
tags | tt)
_show_tags ;;
pin | p)
_pin_tags "$@" ;;
lan | l)
_gists_with_languages "$@" ;;
mark=.; _gists_with_range "$@" ;;
last | L)
_access_last_index "$@" ;;
help | h)
usage ;;
_goto_gist_by_index "$@" ;;
#!/usr/bin/env bats
export TOOL_NAME='gist'
export GIST_USER='phamhsieh'
export GIST_API_TOKEN='dd43dc9949a5b4a1d6c7''b779f13af357282016e4'
@test "Testing ${TOOL_NAME} tool" {
echo "${TOOL_NAME}"
@test "The help command should print usage" {
run ./gist help
[[ "$status" -eq 0 ]]
[[ "${lines[0]}" = "${TOOL_NAME}" ]]
@test "Use config command to add configuarion for user" {
run ./gist config user ${GIST_USER}
[ "$status" -eq 0 ]
[ "${lines[0]}" = "user='${GIST_USER}'" ]
@test "Use config command to add configuarion for token" {
run ./gist config token ${GIST_API_TOKEN}
[ "$status" -eq 0 ]
[ "${lines[0]}" = "token=${GIST_API_TOKEN}" ]
@test "The new command should create a new public gist with gist command" {
hint=false run ./gist new --file gist --desc 'Manage gist like a pro' gist
[ "$status" -eq 0 ]
[[ "${lines[-1]}" =~ ([0-9]+ +[0-9a-z]+) ]]
@test "The fetch command should fetch user gists" {
hint=false run ./gist fetch
[ "$status" -eq 0 ]
[[ "${lines[-1]}" =~ ([0-9]+ +[0-9a-z]+) ]]
@test "The fetch command should fetch starred gists" {
hint=false run ./gist fetch star
[ "$status" -eq 0 ]
echo ${lines[-1]}
[[ "${lines[-1]}" =~ (Not a single valid gist|^ *s[0-9]+ +[0-9a-z]+) ]]
@test "No arguments prints the list of gists" {
hint=false run ./gist
[ "$status" -eq 0 ]
[[ "${lines[-1]}" =~ ([0-9]+ +[0-9a-z]+) ]]
@test "Specify an index to return the path of cloned repo" {
run ./gist 1 --no-action
[ "$status" -eq 0 ]
[[ "${lines[-1]}" =~ (${HOME}/gist/[0-9a-z]+) ]]
@test "The edit command should modify the description of a gist" {
./gist edit 1 "Modified description"
run ./gist detail 1
[ "$status" -eq 0 ]
[[ "${lines[0]}" =~ (Modified description$) ]]
@test "The delete command should delete specified gists" {
run ./gist delete 1 --force
[ "$status" -eq 0 ]
@test "The user command should get the list of public gists from a user" {
hint=false run ./gist user defunkt
[ "$status" -eq 0 ]
[[ "${lines[0]}" =~ ([0-9a-z]+ defunkt) ]]
Copy link

idkjs commented May 7, 2020

Just downloaded cp'd, relinked, reloaded. gist fetch works!

gist new does not.

~/dotfiles/gist master*
❯ gist new
Type description: testing
Creating a new gist...
Traceback (most recent call last):
  File "<string>", line 4, in <module>
KeyError: 'html_url'
Failed to create gist

Copy link

typebrook commented May 7, 2020

Thanks for your report!
This script heavily depends on GNU Coreutils, so some commands(or arguments) may differs on Mac.
Since I not a Mac user and didn't test it enough, this script should contains multiple errors when working on Mac.

Luckily a Mac became unoccupied in my office recently, so I can debug with it tomorrow.
When it is done, I'll get back to you.

Copy link

idkjs commented May 7, 2020

Very cool of you. Looking forward to it. Cool script. Was messing with pkg/github but cant get it to do anything. Will look for your update. Peace to you, sir.

Copy link

typebrook commented May 8, 2020


Traceback (most recent call last):
 File "<string>", line 4, in <module>
KeyError: 'html_url'
Failed to create gist

This means the response from gist create API cannot be parsed. I thinks it is because your token is invalid.

Latest revision add an option(DEBUG=true) to keep http response from into file log, please run:

DEBUG=true ./gist new
... # get error message
cat log

to see if the response is like:

  "message": "Bad credentials",
  "documentation_url": ""

If so, then just create a new token:

# remove current token
./gist config token
# will show a prompt to ask if create a new token
./gist new

Copy link

typebrook commented May 8, 2020

By the way, for pkg/github you mentioned, it is not well maintained and use deprecated API to get token

As I mentioned above, that's why I call browser to help user to create a new access token for gist API.

curl -u "$username:$password"
That way, I don't need a browser to get the token.

Authorization API is going to be deprecated this year (Check here), so getting token via a browser is necessary.

Copy link

idkjs commented May 8, 2020

~/dotfiles/gist/bin master*
❯ DEBUG=true ./gist new                                                              
mktemp: illegal option -- p
usage: mktemp [-d] [-q] [-t prefix] [-u] template ...
       mktemp [-d] [-q] [-u] -t prefix 
Type a gist. <Ctrl-C> to cancel, <Ctrl-D> when done
./gist: line 651: : No such file or directory

Type file name: tb gist
mv: rename  to /var/folders/rt/7lc5vcw16459dszl8djk35fh0000gn/T/tmp.CeDSYCoj/tb gist: No such file or directory
Type description: tb gist test
Creating a new gist...
mktemp: illegal option -- p
usage: mktemp [-d] [-q] [-t prefix] [-u] template ...
       mktemp [-d] [-q] [-u] -t prefix 
./gist: line 685: : No such file or directory
Failed to create gist

Failed to create gist

~/dotfiles/gist master*
❯ cat log

~/dotfiles/gist master*

log empty

Copy link

idkjs commented May 8, 2020

Thanks for info on other package.

Just ran it again:

❯ DEBUG=true ./gist new
mktemp: illegal option -- p
usage: mktemp [-d] [-q] [-t prefix] [-u] template ...
       mktemp [-d] [-q] [-u] -t prefix 
Type a gist. <Ctrl-C> to cancel, <Ctrl-D> when done
./gist: line 651: : No such file or directory

Type file name: 

Maybe -p is deprecated ->

Logging in seems to be going ok. I can _fetch_gists without issue.

Copy link

typebrook commented May 9, 2020


❯ DEBUG=true ./gist new
mktemp: illegal option -- p
usage: mktemp [-d] [-q] [-t prefix] [-u] template ...
       mktemp [-d] [-q] [-u] -t prefix 
Type a gist. <Ctrl-C> to cancel, <Ctrl-D> when done
./gist: line 651: : No such file or directory

Type file name: 

Damn... I didn't check mktemp compatibility for BSD version and just pushed a new revision, what a shame...
I think latest revision solves this, please re-download it and try again.

Also, I need to add a proper Bats test on this gist ASAP and do it before every update, or something like this issue would keep happening.

Again, many thanks for your feedback. Have a good day man!

Copy link

d0ruk commented Dec 9, 2021

Hello @typebrook,

I get an Invalid token error when I try to paste ghp_A7Gcexxxxxx.

However, I can use a token of this format when I manually edit the gist.conf file.

Am I doing something wrong?

Copy link

typebrook commented Dec 13, 2021

Pattern check here misses underscore, so it always fails.

Fixed this issue in latest revision, thanks for your report!.

Copy link

d0ruk commented Dec 13, 2021


When I run this;

sudo install -m755 gist /usr/bin/gist
gist config protocol ssh
gist config auto_sync true
gist config EDITOR code
gist f

I need to enter my token several times, and I can't fetch after setting configs.


Is it normal for the script to prompt me for init for each config? How should I enter the token after a fresh install?

Copy link

typebrook commented Dec 14, 2021

This is a logic error when calling _validate_config. After this function finished, the key-value pairs in config file should be fed into variables.

This issue is fixed in latest revision.

Copy link

d0ruk commented Dec 14, 2021

I successfully fetched gists, and then the following;


I think this message comes directly from the git CLI. Can you tell me what the error is?

Copy link

typebrook commented Dec 15, 2021

I think it is because:

  1. After gist fetch, we do git clone for each gist in the background (26 gist repos at the same time, so 26 different processes at the background)
  2. Because you did gist config protocol ssh, git tries to use ssh protocol to connect with remote host (which is with ip
  3. ssh try to connect, but its key not trusted by you. So prompt shows up. (And because there are 26 processes doing the same things, the prompts flush you screen)

To resolve this, first you need to trust Run the following command the add key into known hosts:

ssh-keyscan -H >>~/.ssh/known_hosts

Copy link

d0ruk commented Dec 18, 2021


That worked. Didn't know about ssh-keyscan.

Thanks for the revisions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment