├── .gitignore ├── LICENSE ├── README.md └── gh-shell /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 John Keech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gh-shell 2 | Save your favorite shell commands in a portable history file backed by a gist. 3 | 4 | ## Usage 5 | ``` 6 | $ gh shell list 7 | $ gh shell add 8 | $ gh shell remove 9 | $ gh shell restore 10 | ``` 11 | 12 | Restoring the shell commands appends the list that is stored in the gist to your local shell history file (`$HISTFILE`). This allows you to save your favorite commands and then restore them on any machine, including as part of your dotfiles setup. 13 | -------------------------------------------------------------------------------- /gh-shell: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | 4 | usage () { 5 | echo "AVAILABLE COMMANDS" 6 | echo " list: View your saved shell commands" 7 | echo " add: Add a shell command to your list" 8 | echo " remove: Remove a shell command from your list" 9 | echo " restore: Append all commands from your list so your current shell's history file" 10 | echo "" 11 | echo "EXAMPLES" 12 | echo "$ gh shell list" 13 | echo "$ gh shell add " 14 | echo "$ gh shell remove " 15 | echo "$ gh shell restore" 16 | } 17 | 18 | get_gist_id () { 19 | gh api gists -q 'map(select(.files | has("gh-shell")))[0].id' 20 | } 21 | 22 | read_gist () { 23 | gh gist view -f gh-shell $1 24 | } 25 | 26 | list () { 27 | id=$(get_gist_id) 28 | if [[ -z "$id" ]]; then 29 | echo "You don't have any commands saved. Try adding one with 'gh shell add '." 30 | exit 1 31 | fi 32 | 33 | output=$(read_gist $id) 34 | if [[ -z "$output" ]]; then 35 | echo "You don't have any commands saved. Try adding one with 'gh shell add '." 36 | exit 1 37 | fi 38 | 39 | echo "$output" 40 | } 41 | 42 | create_gist () { 43 | command=$* 44 | echo "$command" | gh gist create -d "Saved commands for gh-shell" -f gh-shell - &>/dev/null 45 | } 46 | 47 | delete_gist () { 48 | gh gist delete $1 49 | } 50 | 51 | update_gist () { 52 | id=$1 53 | shift 54 | content=$* 55 | 56 | jq --arg c "$content" -n '{"files":{"gh-shell":{"content":$c}}}' \ 57 | | gh api gists/$id -X PATCH -H 'Accept: application/vnd.github.v3+json' --input - \ 58 | &>/dev/null 59 | } 60 | 61 | add () { 62 | shift 63 | command="$*" 64 | if [[ -z "$command" ]]; then 65 | echo "No command specified to add." 66 | echo "" 67 | usage 68 | exit 1 69 | fi 70 | 71 | id=$(get_gist_id) 72 | if [[ -z "$id" ]]; then 73 | create_gist $command 74 | else 75 | content=$(read_gist $id) 76 | content=$(echo -e "$content\n$command") 77 | update_gist $id "$content" 78 | fi 79 | } 80 | 81 | remove () { 82 | shift 83 | command="$*" 84 | if [[ -z "$command" ]]; then 85 | echo "No command specified to remove." 86 | echo "" 87 | usage 88 | exit 1 89 | fi 90 | 91 | id=$(get_gist_id) 92 | if [[ -z "$id" ]]; then 93 | # List doesn't exist. Nothing to remove. 94 | exit 0 95 | else 96 | content=$(read_gist $id) 97 | set +e 98 | content=$(echo "$content" | grep -Fxv "$command") 99 | set -e 100 | if [[ -z "$content" ]]; then 101 | # When the last command is removed, delete the whole gist 102 | delete_gist $id 103 | else 104 | update_gist $id "$content" 105 | fi 106 | fi 107 | } 108 | 109 | restore () { 110 | # When invoked through `gh` directly in a shell, the process chain should look something like 111 | # (shell) -> gh shell restore -> bash -> gh-shell 112 | # $PPID will point to the process id of `gh shell restore`. We need to find the parent 113 | # of this process to determine which shell the user is invoking this from so that we 114 | # can update the correct history file for that shell. 115 | parent_shell_pid=$(ps -o ppid= -p $PPID | tr -d '[:space:]') 116 | parent_shell=$(ps -p $parent_shell_pid -o comm=) 117 | case $parent_shell in 118 | *zsh) 119 | echo "✅ Detected zsh" 120 | file=${HISTFILE:-$HOME/.zsh_history} 121 | ;; 122 | *bash) 123 | echo "✅ Detected bash" 124 | file=${HISTFILE:-$HOME/.bash_history} 125 | ;; 126 | *) 127 | echo "⚠️ Could not detect shell ($parent_shell). Assuming bash." 128 | file=${HISTFILE:-$HOME/.bash_history} 129 | esac 130 | 131 | echo "$(list)" >> $file 132 | echo "History file $file updated" 133 | 134 | case $parent_shell in 135 | *zsh) 136 | if [[ -z "$TERM_SESSION_ID" ]]; then 137 | echo "Open a new shell or refresh history in the current shell by running 'fc -RI'" 138 | else 139 | # Special case for running in an Apple Terminal instance, which by default captures 140 | # terminal session history separately and merges from the shared history on launch 141 | echo "Open a new Terminal instance to refresh history" 142 | fi 143 | ;; 144 | *) 145 | echo "Open a new shell or refresh history in the current shell by running 'history -n'" 146 | esac 147 | } 148 | 149 | declare -A COMMANDS=( 150 | [usage]=usage 151 | [list]=list 152 | [add]=add 153 | [remove]=remove 154 | [restore]=restore 155 | ) 156 | 157 | "${COMMANDS[${1:-usage}]:-${COMMANDS[usage]}}" "$@" 158 | --------------------------------------------------------------------------------