Skip to content

Instantly share code, notes, and snippets.

@T3sT3ro
Last active October 23, 2024 13:26
Show Gist options
  • Save T3sT3ro/bc06294f51091c0e34176c62683d538e to your computer and use it in GitHub Desktop.
Save T3sT3ro/bc06294f51091c0e34176c62683d538e to your computer and use it in GitHub Desktop.
Linux tips utility for helpful tips in CLI.

Linux tips for bash

Simple utility for managing and printing helpful markdown tips in terminal. Essentially a simple wrapper utility for jq with pretty printing. Anything it does can be done manually on the json file as well. Supports fetching hosted tips and easily sharing knowledge!

Made by Tooster, licensed MIT, full license text at https://mit-license.org/

terminal screenshot

Features

  • Tips stored in a familiar json format as string array, which if needed can be easily processed.
  • Track hosted tips by adding them to remotes with tips remtoe-add command. Remotes won't duplicate.
  • Easily fetch new and changed tips from tracked remotes with tips fetch. Tips won't duplicate.
  • Easily import other's collections of tips by importing and mergin their remotes with tips import-remotes.
  • If gum is installed it formats tips nicely.

Dependencies

  • required:
    • bash or something that runs bash
    • jq - for managing db data
    • sponge from moreutils - for updating db
    • curl - for upgrades and fetching tips from remotes
  • optional (recommended):
    • gum - for better interactive prompt
    • glow - for pretty markdown printing in CLI; if gum is insalled, it is not needed, but a nice program nonetheless

Installation/Deinstallation

# Debian based (uses apt and bash language)
curl https://gist.githubusercontent.com/T3sT3ro/bc06294f51091c0e34176c62683d538e/raw/install.sh | /bin/bash

For manual installation copy tips executable into appropriate place and copy tips.json, remotes.json into ~/.local/share/tips/. Create empty current file in this directory as well.

To uninstall, simply remove those files:

rm -rf ~/.local/share/tips
rm $(which tips)
# and remove appropriate entries from .bashrc or wher eyou called it

Configuration

You can use following environment variables to configure how tips (add them and export e.g. in .bashrc before calling tips):

  • TIPS_DIR - root location for tips data, defaults to ~/.local/share/tips. Holds tips.json (tips database), remotes.json (remotes tracker) and current (holding pointer to current tip)
  • TIPS_FORMATTER - the output of tips is piped through this. Defaults to gum format --theme=dark if gum is present. I highly recommend using glow, but cat is the default for safety.

Usage

Invoke in your .bashrc to print a tip on new terminal startup.

  • tips [next/current/previous]: select active tip and display it
  • tips -h: print full help, show arguments and usage
  • tips <n>: print n-th tip (modulo count), without changing active tip
  • tips all: displays all tips
  • tips stats: show number of entries, remotes and current tip index
  • tips remote-add <name> <remote_url>: add remote to remotes.json that will be used to fetch tips
  • tips import-remotes <url to remotes.json>: adds all remotes from url hosting remotes.json to you remotes
  • tips fetch: fetch tips from tracked remotes
  • tips upgrade: upgrade the tips script

More info after using tips --help.

Data format

  • remotes.json: a single json object mapping urls to remote names. Keys and JSON spec enforce uniqueness. The tips database is rebuilt on each tips fetch by merging respones from remotes.
    {
      "https://someRemote.com/tips.json": "some descriptive remote name", 
      "<url to another tips.json>": "<another remote name>"
    }
  • current: managed by tips, a single file declaring a variable holding current tip index in the tips.json array
  • tips.json: a volatile (overwritten on tips fetch), single JSON array containing strings with tip's. Each entry is a single tip. To use your own tips I recommnd creating a public (private gists not supported yet, auth token would be required) gist for tips.json in correct format and add the raw url (i.e. returning only tips.json, not html document) to your remotes via tips remote-add command.
    [
      "tip 1",
      "tip 2, in **markdown**, useful when displayed with formatter like __glow__"
    ]

Writing your own tips, importing other's tips

  1. Host tips.json in some accessible place, for example https://gist.github.com.
  2. Use tips remote-add <name> <raw url to tips.json> to start tracking hosted tips.json.
  3. Perform tips fetch to fetch and update/merge in tips from the tracked remotes.

You can also import all remotes from someone else if they host remotes.json: tips import-remotes <url to remotes.json> will merge your remotes with their remotes.

TODO

  • if gh is installed, use api requests + glow to edit and add new tips remotely straight from the CLI
  • support some kidn of auth tokens/a method to send extra curl data to access scured private tips if needed. gh auth
  • support editing/toggling remotes/tips with the help of gum tools like gum select
  • compare with fortune program?
  • add version info and migrations scripts if db format ever changes (based on commit info?)
  • 1:1 mapping between remote url and remote name to avoid duplicate urls/names
  • add tip tags, with enabling/disabling tags and browsing a category
  • local tips-local.json if needed - not sure if I want to add that, I won't personaly use it and I deem them unnecessary if private tips can be used instead.
  • check out stow for easy installation

Feedback

Feel free to leave any comments or suggestions ❤️

Thanks

To the ones who shared their knowledge with me, and the authors of glow, gum, curl, jq and moreutils 👍

# <prompt> <suffix in fallback> <default in fallback>
promptConfirm() {
if command -v gum 2>&1 >/dev/null; then
gum confirm "$1"
return $?
fi
read -srn 1 -p "$1 $2 " yesno; echo;
[[ ${yesno:-$3} =~ [yY] ]];
return $?
}
# required
if ! { type -p jq && type -p sponge && type -p curl; } >/dev/null; then
promptConfirm "Install prerequisites?" "(Y/n)" "Y" && sudo apt install jq moreutils curl
else
echo "required components already installed"
fi
# additional CLI util packages
if ! { type -p gum; } >/dev/null; then
promptConfirm "Install gum (better CLI input utils)?" "(Y/n)" "Y" && sudo apt install gum
else
echo "`gum` already installed"
fi
sudo -k # clear sudo privilage from this script for safety
# installation customization
TIPS_SH="${TIPS_SH:-$HOME/.local/bin/tips}"
TIPS_DIR="${TIPS_DIR:-$HOME/.local/share/tips}"
TIPS_DB="$TIPS_DIR/tips.json"
TIPS_REMOTES="$TIPS_DIR/remotes.json"
TIPS_CURRENT="$TIPS_DIR/current"
REPO_URL=https://gist.githubusercontent.com/T3sT3ro/bc06294f51091c0e34176c62683d538e
safeDownload() {
# $1:dst_path, $2: hint $3: relative url from base path
[[ (! -f "$1") || $(promptConfirm "$1 ($2) already exists, overwrite?" "(Y/n)" "Y") ]] && curl -o "$1" "$REPO_URL/$3"
}
mkdir -p "$TIPS_DIR"
safeDownload "$TIPS_SH" "script" "raw/tips"
safeDownload "$TIPS_DB" "db" "raw/tips.json"
safeDownload "$TIPS_REMOTES" "remotes" "raw/remotes.json"
touch "$TIPS_CURRENT"
chmod +x ~/.local/bin/tips
APPEND_TO_BASHRC="
# TIPS
alias tip='tips'
eval \$(tips completions)
tips"
tput setaf 3; echo "[recommended] add the following to .bashrc:"; tput sgr0
echo "$APPEND_TO_BASHRC";
if promptConfirm "append to .bashrc?" "(Y/n)" "Y"; then
echo "$APPEND_TO_BASHRC" >> ~/.bashrc
fi
{
"https://gist.githubusercontent.com/T3sT3ro/bc06294f51091c0e34176c62683d538e/raw/tips.json": "official tips repository"
}
#!/bin/bash
# author: Tooster
# license: MIT, full license text at https://mit-license.org/
# source: https://gist.github.com/T3sT3ro/bc06294f51091c0e34176c62683d538e
#
# Reads the tips json file, prints it and advances to a next tip. With some util arguments to make working with it easier
#
# requirements:
# - jq - for tips database (json) management
# - sponge (from `moreutils` package) - for updating file
# recommended:
# - gum - for nice prompting and markdown printing (glow not needed when using `gum format`) :https://github.com/charmbracelet/gum
# - glow - for nice markdown printing: https://github.com/charmbracelet/glow
#
# for now, not strictly following https://clig.dev/
if [[ $1 == "completions" ]]; then
echo 'complete -W "all n next c current p previous stats remote-add import-remotes fetch upgrade completions help" tips'
exit 0;
fi;
HELP="\
Utility for printing and managing helpful tips in the terminal,
similar to IDEs' tips shown on startup. For use in startup script.
Can fetch new tips from web and add remotes. Data stored by default in '~/.local/share/tips/'.
Usage:
tips [arguments]
Arguments:
n, next (default) move to and print next tip
c, current print current tip
p, previous move to and print previous tip
<number> print n-th tip from current databse
all prints all tips
stats prints the tips database stats
remote-add <name> <url> add remote repository url for fetching tips
import-remotes <url> import all remotes from url hosting remotes.json
fetch fetches tips from remotes in 'remotes' file, merges with current tips.db
upgrade upgrades tips utility (overwrites current installation in .local/bin/tips)
completions generate completions
help, -h, -? print this message"
# configurable
TIPS_DIR="${TIPS_DIR:-$HOME/.local/share/tips}"
TIPS_FORMATTER=${TIPS_FORMATTER:-$(command -v gum >/dev/null && echo "gum format --theme=dark" || echo cat)}
# fixed
TIPS_DB="$TIPS_DIR/tips.json"
TIPS_REMOTES="$TIPS_DIR/remotes.json"
TIPS_CURRENT="$TIPS_DIR/current"
TIPS_URL="https://gist.githubusercontent.com/T3sT3ro/bc06294f51091c0e34176c62683d538e"
TIPS_UPGRADE_URL="$TIPS_URL/raw/tips"
# <prompt> <suffix in fallback> <default in fallback>
promptConfirm() {
if command -v gum 2>&1 >/dev/null; then
gum confirm "$1"
return $?
fi
read -srn 1 -p "$1 $2 " yesno; echo;
[[ ${yesno:-$3} =~ [yY] ]];
return $?
}
# used with MOVE parameter will move to tips + $MOVE
# used with TIP parameter will use this number instead of reading from 'current' file
displayTip() {
SIZE=$(jq '. | length' "$TIPS_DB")
if [[ -z $TIP ]]; then
source "$TIPS_CURRENT"
else
CURRENT=$TIP
fi
if [[ $? -ne 0 || -z $SIZE ]]; then
echo "$0: missing file or invalid format: $TIPS_DB" >&2;
exit 2;
fi;
OFFSET=${MOVE:-0}
CURRENT=$((((CURRENT+OFFSET)%SIZE+SIZE)%SIZE))
[[ ! -z $MOVE ]] && declare -p CURRENT >"$TIPS_CURRENT"
jq -r --arg current $CURRENT '. as $tips | "# TIP #\($current):\n\($tips[$current | tonumber])"' "$TIPS_DB"
}
# legacy: unsorted unique entries:
# .tips[] | [0].tips += .[1].tips | .[0] | .tips |= (map({key:.,value:1}) | from_entries | keys_unsorted)
shopt -s extglob
SUBCOMMAND="${1:-next}"
shift 1
case "$SUBCOMMAND" in
p|previous)
MOVE=-1; displayTip | $TIPS_FORMATTER
;;
c|current)
MOVE=0; displayTip | $TIPS_FORMATTER
;;
n|next)
MOVE=+1; displayTip | $TIPS_FORMATTER
;;
?([+-])+([[:digit:]]))
TIP=$SUBCOMMAND; displayTip | $TIPS_FORMATTER
;;
all)
jq -r '. | to_entries | map("# TIP #\(.key)\n\(.value)\n") | .[]' "$TIPS_DB" | $TIPS_FORMATTER
;;
remote-add)
if [[ $# < 2 || -z $1 || -z $2 ]]; then
echo -e "usage: tips remote-add <name> <url to tips.json>" >&2
exit 1
fi;
jq -s --arg name "$1" --arg url "$2" '(.[] // {}) + {($url): $name} ' "$TIPS_REMOTES" | sponge "$TIPS_REMOTES"
echo "Added '$1': '$2'"
echo "Run 'tips fetch' to downlaod tips."
;;
import-remotes)
if [[ -z $1 ]]; then
echo -e "usage: tips import-remotes <url to remotes.json>" >&2
exit 1
fi;
{ curl -f "$1"; cat remotes.json; } | jq -s 'add' | sponge "$TIPS_REMOTES";
echo >&2 "Imported all remotes. Run 'tips fetch' to downlaod tips."
;;
fetch)
jq -r '. | to_entries | map([.key, .value] | @tsv)[]' "$TIPS_REMOTES" |
while read -r URL NAME; do
echo "Updating '$NAME': $URL" >&2;
tput setaf 1 >&2
curl -fsS "$URL?r=$RAND" # random parameter to mitigate caching issues on CDN such as gist.github.com
tput sgr0 >&2
done |
if output=$(set -o pipefail; jq -sr '. | flatten | unique'); then
echo "$output" > "$TIPS_DB"
echo "Tips database updated" >&2
else
echo "Update aborted" >&2
fi
;;
upgrade)
TIPS_SH=$(which tips)
if [[ ! -f $TIPS_SH ]] || promptConfirm "Overwrite existing program '$TIPS_SH'? (y/N)"; then
echo "Saving to $TIPS_SH..."
curl -fsSo "$TIPS_SH" "$TIPS_UPGRADE_URL"
fi
;;
stats)
source "$TIPS_CURRENT"
echo "databse: $TIPS_DB"
echo "remotes: $(jq 'length' "$TIPS_REMOTES")"
echo "entries: $(jq 'length' "$TIPS_DB")"
echo "current: $CURRENT"
;;
-h|-?|help|--help)
echo "$HELP"
;;
*)
echo "$HELP"
exit 1;
;;
esac
[
"You can use fzf's `Ctr-t` to fuzzy search files, `Alt-c` to fuzzy cd into directory and use `<TAB>` in fzf to select multiple files. Smart fzf completion can be triggered by writing `**` and triggering `<TAB>`.",
"`bat` can be used instead of `cat`",
"`ALT+.` recalls last argument from previous command",
"`sponge` is a nice utility for overwriting the file that you read from in the same pipe!",
"`read a b c < <(someProgram)` can read space separated values into `a`, `b`, and `c` variables",
"bash `${param:-default}` substitutions if `param` is not set\n\n[more on bash substitutions](https://tldp.org/LDP/abs/html/parameter-substitution.html)",
"bash `#`, `##`, `%`, `%%` substitutions (`${param%%pattern}`) removes the shortest/longest prefix/suffix pattern from param\n\n[more on bash substitutions](https://tldp.org/LDP/abs/html/parameter-substitution.html)",
"`jq` can read lines and parse them into an array with `jq -nR [inputs]`",
"`jq` can merge objects using `objA * objB`",
"`nvchad` is a nice sensible default for neovim",
"https://charm.sh is a TUI and tool swissknife — terminal components, interactive forms, inter-machine encrypted store and more",
"`gum` by https://charm.sh is a TUI swissknife: confirmation/input/file prompts, selects, radio, viewers, markdow formatter and others",
"use `nl` to add numbers to lines",
"`du -h -t <min> -t -<max> | sort -h` prints sorted files of size [min..max] in human readable format",
"`{ echo 'a'; echo 'b'} | ...` will concat the output of two programs and pass it to the pipe",
"Use `xsv` to manage CSV files in terminal. Combine with `gum table` and `jq` for some powerfull editing in CLI.",
"`lsd` is a fancier `ls` with nerdfont icons and colors",
"`fd` is a modern replacement for find with sane defaults and syntax",
"*Procrastination check:* are you working on a detail?",
"bash logical operators syntax:\n`[[ cmd1 ]] && [[ ! cmd2 ]] || cmd3`",
"in bash (rather readline) `CTRL+SHIFT+_` triggers undo",
"vim uses `%` to jump between maching brackets",
"in vim, `Ctrl+a`/`Ctrl-x` increments/decrements numbers",
"in bash, you can redirect and pipe creatively, for example `for ... done | jq -s ...`",
"`curl ifconfig.co` displays your current public IP!",
"`fzf` adds custom frontend to a reverse-history-search triggered with `CTRL+R` in bash!",
"bash has `${PIPESTATUS[]}` variable for inspecting errors in pipes",
"to prevent bash from continuing failed pipe, consider using `set -e` and `set -o pipefail` in a subshell",
"`cd -` in bash takes you to previous location",
"`difft` is a new, free semantic diffing tool powered by TreeSitter https://difftastic.wilfred.me.uk/",
"`zcat` can be used to prints contents of zipped files without unzipping manually",
"`zgrep` is like grep, but it can search for pattern in potentially zipped files",
"You can enable vim mode in bash with `set -o vi`, pretty revolutionary!",
"Use `CTRL+X`, `CTRL+E` in `zsh` to edit command in default editor!",
"Useful zsh plugins and commands: 'copyfile', 'copypath'",
"`<Enter>`+`~`+`.` to kill ssh session that hung up (there are other commands than `.` as well!)",
"`tac` is similar to `cat`, buuut... it prints from the end! Useful for viewing logs!",
"`ncdu` > `du`!",
"`ack`,`ag`,`rg` > `grep`!\n> https://beyondgrep.com/feature-comparison/",
"`preview` is awesome!: `alias preview=\"fzf --preview 'bat --color \\\"always\\\" {}'\"`",
"`sd` > `sed` — is a painless replacer with intuitive interface and JS like regexes",
"`zellij` is a great terminal multiplexer!",
"consider `mosh` as an alternative to `ssh` if you need secure connection while roaming with poor network connection!"
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment