Skip to content

Instantly share code, notes, and snippets.

@Magnus167
Last active May 5, 2026 21:01
Show Gist options
  • Select an option

  • Save Magnus167/b0a21c26a8eea95df8ee51c4e275316a to your computer and use it in GitHub Desktop.

Select an option

Save Magnus167/b0a21c26a8eea95df8ee51c4e275316a to your computer and use it in GitHub Desktop.
dev-tools-ubuntu.md

Boots-on-the-ground dev tools

  1. git
  2. python3 / python3-pip
  3. docker
  4. gh — GitHub CLI.
  5. ripgrep (rg) — grep, but absurdly fast. Single most life-changing tool here.
  6. fzf — fuzzy finder; pipes into everything.
  7. jq — JSON swiss army knife.
  8. uv — fast Python package/project manager (rust-powered, replaces pip/venv/pyenv).
  9. cargo / rust — the Rust toolchain.
  10. go — the Go toolchain.
  11. zoxidecd that learns your habits; z proj jumps to most-used "proj" dir.
  12. lazygit — TUI for git.
  13. lazydocker — TUI for docker.
  14. bottom (btm) — system/process monitor TUI.
  15. glow — render markdown in the terminal.
  16. yq — jq, but for YAML.
  17. gron — flattens JSON so you can grep it.
  18. jnv — interactive jq REPL.
  19. harlequin — SQL IDE in the terminal (DuckDB/Postgres/etc).
  20. visidata — TUI spreadsheet for any tabular file.

And my own edition:

  1. clipcat - a one-line util to copy contents of a file to your clipboard from the terminal

usage: clipcat filename installation: add the following line to the .bashrc or other shell profile

clipcat() { base64 -w0 "$1" | sed 's/.*/\x1b]52;c;&\x07/' > /dev/tty; }
#!/usr/bin/env bash
# Installer for the dev-tools-ubuntu list:
# https://gist.github.com/Magnus167/b0a21c26a8eea95df8ee51c4e275316a
#
# Strategy:
# Phase 1 (serial): apt repo setup + single apt-get install for system pkgs.
# Phase 2 (serial): Docker (uses apt internally, can't share apt's lock).
# Phase 3 (parallel): every remaining tool launches as its own background job.
# Chains with a dependency (rust->jnv, uv->{harlequin,visidata})
# run inside a single subshell so the dependent step waits
# only on its own prerequisite, not the whole world.
#
# Targets: Debian/Ubuntu on x86_64 or aarch64.
# Idempotent: each step skips if the binary is already on PATH.
set -uo pipefail
LOG_DIR="$(mktemp -d -t devtools-install-XXXXXX)"
BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
mkdir -p "$BIN_DIR"
echo "==> devtools installer"
echo " logs: $LOG_DIR"
echo " bindir: $BIN_DIR"
echo
# ---- arch detection ----
case "$(uname -m)" in
x86_64) DEB_ARCH=amd64; GO_ARCH=amd64; UNAME_ARCH=x86_64 ;;
aarch64|arm64) DEB_ARCH=arm64; GO_ARCH=arm64; UNAME_ARCH=aarch64 ;;
*) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;;
esac
SUDO=""
if [ "${EUID:-$(id -u)}" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi
# Ensure curl exists before we lean on it everywhere.
if ! command -v curl >/dev/null 2>&1; then
$SUDO apt-get update -y
$SUDO DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates
fi
# ---- background job tracking ----
declare -a PIDS=()
declare -a NAMES=()
launch() {
# launch <name> <function-name>
local name="$1"; local fn="$2"
(
echo "[$name] start"
if "$fn" >"$LOG_DIR/$name.log" 2>&1; then
echo "[$name] ok"
else
echo "[$name] FAILED (see $LOG_DIR/$name.log)"
exit 1
fi
) &
PIDS+=("$!")
NAMES+=("$name")
}
wait_all() {
local fail=0
for i in "${!PIDS[@]}"; do
if ! wait "${PIDS[$i]}"; then
fail=1
fi
done
return $fail
}
# ===================================================================
# Phase 1: apt repos + system packages (serial, single apt transaction)
# ===================================================================
echo "==> phase 1: apt repos + system packages"
# GitHub CLI repo (so gh comes from upstream, not stale distro pkg).
if [ ! -f /etc/apt/sources.list.d/github-cli.list ]; then
$SUDO mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| $SUDO tee /etc/apt/keyrings/githubcli-archive-keyring.gpg >/dev/null
$SUDO chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$DEB_ARCH signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
| $SUDO tee /etc/apt/sources.list.d/github-cli.list >/dev/null
fi
$SUDO apt-get update -y
$SUDO DEBIAN_FRONTEND=noninteractive apt-get install -y \
git \
python3 python3-pip python3-venv \
ripgrep fzf jq \
gh \
curl wget ca-certificates gnupg tar unzip xz-utils \
build-essential pkg-config libssl-dev
# ===================================================================
# Phase 2: Docker (serial; its install script also calls apt-get)
# ===================================================================
echo "==> phase 2: docker"
if command -v docker >/dev/null 2>&1; then
echo "[docker] already installed"
else
if curl -fsSL https://get.docker.com | $SUDO sh >"$LOG_DIR/docker.log" 2>&1; then
echo "[docker] ok"
else
echo "[docker] FAILED (see $LOG_DIR/docker.log)"
fi
fi
# ===================================================================
# Phase 3: parallel installers
# ===================================================================
echo "==> phase 3: parallel installers"
# ---- helpers ----
gh_latest_tag() {
# $1 = "owner/repo"; prints tag without leading 'v'
curl -fsSL "https://api.github.com/repos/$1/releases/latest" \
| grep -Po '"tag_name": "v?\K[^"]*'
}
# ---- rust (rustup) ----
install_rust() {
if [ -x "$HOME/.cargo/bin/cargo" ] || command -v cargo >/dev/null 2>&1; then
echo "rust already installed"; return 0
fi
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --no-modify-path --default-toolchain stable
}
install_jnv() {
local cargo="$HOME/.cargo/bin/cargo"
[ -x "$cargo" ] || cargo="$(command -v cargo || true)"
[ -n "$cargo" ] || { echo "no cargo found"; return 1; }
if command -v jnv >/dev/null 2>&1 || [ -x "$HOME/.cargo/bin/jnv" ]; then
echo "jnv already installed"; return 0
fi
"$cargo" install jnv
}
# ---- uv ----
install_uv() {
if [ -x "$HOME/.local/bin/uv" ] || command -v uv >/dev/null 2>&1; then
echo "uv already installed"; return 0
fi
curl -LsSf https://astral.sh/uv/install.sh | sh
}
uv_bin() {
if [ -x "$HOME/.local/bin/uv" ]; then
echo "$HOME/.local/bin/uv"
else
command -v uv
fi
}
install_harlequin() {
local uv; uv="$(uv_bin)" || { echo "no uv"; return 1; }
"$uv" tool install harlequin
}
install_visidata() {
local uv; uv="$(uv_bin)" || { echo "no uv"; return 1; }
"$uv" tool install visidata
}
# ---- go ----
install_go() {
if command -v go >/dev/null 2>&1 && go version 2>/dev/null | grep -q 'go1\.'; then
echo "go already installed"; return 0
fi
local ver="1.23.4"
local tmp; tmp=$(mktemp -d)
curl -fsSL "https://go.dev/dl/go${ver}.linux-${GO_ARCH}.tar.gz" -o "$tmp/go.tgz"
$SUDO rm -rf /usr/local/go
$SUDO tar -C /usr/local -xzf "$tmp/go.tgz"
rm -rf "$tmp"
echo "go installed to /usr/local/go (add /usr/local/go/bin to PATH)"
}
# ---- zoxide ----
install_zoxide() {
command -v zoxide >/dev/null 2>&1 && { echo "zoxide already installed"; return 0; }
curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh
}
# ---- lazygit ----
install_lazygit() {
command -v lazygit >/dev/null 2>&1 && { echo "lazygit already installed"; return 0; }
local ver arch tmp
ver=$(gh_latest_tag jesseduffield/lazygit)
case "$UNAME_ARCH" in x86_64) arch=x86_64 ;; aarch64) arch=arm64 ;; esac
tmp=$(mktemp -d)
curl -fsSL "https://github.com/jesseduffield/lazygit/releases/download/v${ver}/lazygit_${ver}_Linux_${arch}.tar.gz" \
-o "$tmp/lg.tgz"
tar -xzf "$tmp/lg.tgz" -C "$tmp"
install -m 0755 "$tmp/lazygit" "$BIN_DIR/lazygit"
rm -rf "$tmp"
}
# ---- lazydocker ----
install_lazydocker() {
command -v lazydocker >/dev/null 2>&1 && { echo "lazydocker already installed"; return 0; }
local ver arch tmp
ver=$(gh_latest_tag jesseduffield/lazydocker)
case "$UNAME_ARCH" in x86_64) arch=x86_64 ;; aarch64) arch=arm64 ;; esac
tmp=$(mktemp -d)
curl -fsSL "https://github.com/jesseduffield/lazydocker/releases/download/v${ver}/lazydocker_${ver}_Linux_${arch}.tar.gz" \
-o "$tmp/ld.tgz"
tar -xzf "$tmp/ld.tgz" -C "$tmp"
install -m 0755 "$tmp/lazydocker" "$BIN_DIR/lazydocker"
rm -rf "$tmp"
}
# ---- bottom (btm) ----
install_bottom() {
command -v btm >/dev/null 2>&1 && { echo "bottom already installed"; return 0; }
local ver tmp deb
ver=$(gh_latest_tag ClementTsang/bottom)
tmp=$(mktemp -d)
deb="$tmp/bottom.deb"
curl -fsSL "https://github.com/ClementTsang/bottom/releases/download/${ver}/bottom_${ver}-1_${DEB_ARCH}.deb" -o "$deb"
$SUDO dpkg -i "$deb"
rm -rf "$tmp"
}
# ---- glow ----
install_glow() {
command -v glow >/dev/null 2>&1 && { echo "glow already installed"; return 0; }
local ver arch tmp
ver=$(gh_latest_tag charmbracelet/glow)
case "$UNAME_ARCH" in x86_64) arch=x86_64 ;; aarch64) arch=arm64 ;; esac
tmp=$(mktemp -d)
curl -fsSL "https://github.com/charmbracelet/glow/releases/download/v${ver}/glow_${ver}_Linux_${arch}.tar.gz" \
-o "$tmp/glow.tgz"
tar -xzf "$tmp/glow.tgz" -C "$tmp"
local glow_bin; glow_bin=$(find "$tmp" -maxdepth 3 -type f -name glow | head -n1)
install -m 0755 "$glow_bin" "$BIN_DIR/glow"
rm -rf "$tmp"
}
# ---- yq ----
install_yq() {
command -v yq >/dev/null 2>&1 && { echo "yq already installed"; return 0; }
local arch
case "$UNAME_ARCH" in x86_64) arch=amd64 ;; aarch64) arch=arm64 ;; esac
curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${arch}" \
-o "$BIN_DIR/yq"
chmod +x "$BIN_DIR/yq"
}
# ---- gron ----
install_gron() {
command -v gron >/dev/null 2>&1 && { echo "gron already installed"; return 0; }
local ver arch tmp
ver=$(gh_latest_tag tomnomnom/gron)
case "$UNAME_ARCH" in x86_64) arch=amd64 ;; aarch64) arch=arm64 ;; esac
tmp=$(mktemp -d)
curl -fsSL "https://github.com/tomnomnom/gron/releases/download/v${ver}/gron-linux-${arch}-${ver}.tgz" \
-o "$tmp/gron.tgz"
tar -xzf "$tmp/gron.tgz" -C "$tmp"
install -m 0755 "$tmp/gron" "$BIN_DIR/gron"
rm -rf "$tmp"
}
# ---- chained installs ----
# rust -> jnv (jnv waits for rust *only*; doesn't block other tools)
launch_rust_then_jnv() {
(
echo "[rust] start"
if install_rust >"$LOG_DIR/rust.log" 2>&1; then
echo "[rust] ok"
else
echo "[rust] FAILED (see $LOG_DIR/rust.log)"
exit 1
fi
echo "[jnv] start"
if install_jnv >"$LOG_DIR/jnv.log" 2>&1; then
echo "[jnv] ok"
else
echo "[jnv] FAILED (see $LOG_DIR/jnv.log)"
exit 1
fi
) &
PIDS+=("$!"); NAMES+=("rust+jnv")
}
# uv -> harlequin + visidata in parallel
launch_uv_then_pytools() {
(
echo "[uv] start"
if install_uv >"$LOG_DIR/uv.log" 2>&1; then
echo "[uv] ok"
else
echo "[uv] FAILED (see $LOG_DIR/uv.log)"
exit 1
fi
# harlequin and visidata are independent — fan out.
(
echo "[harlequin] start"
install_harlequin >"$LOG_DIR/harlequin.log" 2>&1 \
&& echo "[harlequin] ok" \
|| { echo "[harlequin] FAILED (see $LOG_DIR/harlequin.log)"; exit 1; }
) &
h_pid=$!
(
echo "[visidata] start"
install_visidata >"$LOG_DIR/visidata.log" 2>&1 \
&& echo "[visidata] ok" \
|| { echo "[visidata] FAILED (see $LOG_DIR/visidata.log)"; exit 1; }
) &
v_pid=$!
rc=0
wait "$h_pid" || rc=1
wait "$v_pid" || rc=1
exit $rc
) &
PIDS+=("$!"); NAMES+=("uv+harlequin+visidata")
}
launch_rust_then_jnv
launch_uv_then_pytools
launch go install_go
launch zoxide install_zoxide
launch lazygit install_lazygit
launch lazydocker install_lazydocker
launch bottom install_bottom
launch glow install_glow
launch yq install_yq
launch gron install_gron
echo
echo "==> waiting for ${#PIDS[@]} background jobs..."
echo " (tail any log live with: tail -f $LOG_DIR/<name>.log )"
echo
if wait_all; then
echo
echo "==> all installs done."
else
echo
echo "==> finished with errors. Check failed logs in: $LOG_DIR" >&2
fi
# ===================================================================
# Phase 4: shell rc setup (idempotent) + verification report
# ===================================================================
echo
echo "==> phase 4: shell rc setup + verification"
# Detect login shell from $SHELL (we're running under bash, so $SHELL
# reflects the user's actual login shell, not this script's interpreter).
LOGIN_SHELL_NAME="$(basename "${SHELL:-/bin/bash}")"
case "$LOGIN_SHELL_NAME" in
zsh) RC="$HOME/.zshrc" ;;
bash) RC="$HOME/.bashrc" ;;
*) RC="$HOME/.bashrc" ;;
esac
touch "$RC"
echo " rc file: $RC (login shell: $LOGIN_SHELL_NAME)"
echo
# ---- PATH ----
if grep -qF '.local/bin:$HOME/.cargo/bin:/usr/local/go/bin' "$RC"; then
echo "[PATH] already set"
else
{
echo ''
echo '# Added by install.sh — dev tools PATH'
echo 'export PATH="$HOME/.local/bin:$HOME/.cargo/bin:/usr/local/go/bin:$PATH"'
} >> "$RC"
echo "[PATH] appended to $RC"
fi
# ---- zoxide init ----
if grep -qE '^[^#]*zoxide init' "$RC"; then
echo "[zoxide] init already set"
elif ! command -v zoxide >/dev/null 2>&1 && ! [ -x "$HOME/.local/bin/zoxide" ]; then
echo "[zoxide] binary not found — skipping rc entry"
else
{
echo ''
echo '# Added by install.sh — zoxide'
echo "eval \"\$(zoxide init $LOGIN_SHELL_NAME)\""
} >> "$RC"
echo "[zoxide] init appended for $LOGIN_SHELL_NAME"
fi
# ---- fzf keybindings + completion ----
if [ "$LOGIN_SHELL_NAME" = "zsh" ]; then
FZF_KB=/usr/share/doc/fzf/examples/key-bindings.zsh
FZF_CO=/usr/share/doc/fzf/examples/completion.zsh
else
FZF_KB=/usr/share/doc/fzf/examples/key-bindings.bash
FZF_CO=/usr/share/bash-completion/completions/fzf
fi
if [ -r "$FZF_KB" ]; then
if grep -qF "$FZF_KB" "$RC"; then
echo "[fzf] keybindings already sourced"
else
echo "[ -f $FZF_KB ] && source $FZF_KB" >> "$RC"
echo "[fzf] keybindings appended"
fi
else
echo "[fzf] keybindings file not found at $FZF_KB — skipping"
fi
if [ -r "$FZF_CO" ]; then
if grep -qF "$FZF_CO" "$RC"; then
echo "[fzf] completion already sourced"
else
echo "[ -f $FZF_CO ] && source $FZF_CO" >> "$RC"
echo "[fzf] completion appended"
fi
else
echo "[fzf] completion file not found at $FZF_CO — skipping"
fi
# ---- clipcat function (OSC 52 clipboard copy) ----
if grep -qE '^[[:space:]]*clipcat\(\)' "$RC"; then
echo "[clipcat] already defined in $RC"
else
{
echo ''
echo '# Added by install.sh — clipcat: copy file contents to terminal clipboard via OSC 52'
echo 'clipcat() { base64 -w0 "$1" | sed '\''s/.*/\x1b]52;c;&\x07/'\'' > /dev/tty; }'
} >> "$RC"
echo "[clipcat] appended to $RC"
fi
# ---- docker group ----
if ! command -v docker >/dev/null 2>&1; then
echo "[docker] not installed — skipping group check"
elif id -nG "$USER" | tr ' ' '\n' | grep -qx docker; then
echo "[docker] $USER already in 'docker' group"
else
echo "[docker] adding $USER to 'docker' group..."
if command -v sudo >/dev/null 2>&1; then
sudo usermod -aG docker "$USER" \
&& echo "[docker] added — log out/in (or run: newgrp docker) to apply"
else
echo "[docker] sudo not found; run as root: usermod -aG docker $USER"
fi
fi
# ---- verification report ----
echo
echo "==> verification"
echo
# shellcheck disable=SC1090
source "$RC" 2>/dev/null || true
ok() { printf " \033[32m✓\033[0m %s\n" "$1"; }
miss() { printf " \033[31m✗\033[0m %s\n" "$1"; }
echo "PATH segments:"
for seg in "$HOME/.local/bin" "$HOME/.cargo/bin" /usr/local/go/bin; do
if echo ":$PATH:" | grep -qF ":$seg:"; then
ok "$seg"
else
miss "$seg (not in PATH)"
fi
done
echo
echo "Tools on PATH:"
for t in git python3 docker gh rg fzf jq uv cargo go zoxide lazygit lazydocker btm glow yq gron jnv harlequin vd; do
if command -v "$t" >/dev/null 2>&1; then
ok "$t ($(command -v "$t"))"
else
miss "$t (not found)"
fi
done
echo
echo "Docker group:"
if id -nG "$USER" | tr ' ' '\n' | grep -qx docker; then
ok "$USER is in 'docker' group (in /etc/group)"
if groups | tr ' ' '\n' | grep -qx docker; then
ok "and the current shell sees the group"
else
miss "but current shell hasn't picked it up — log out/in or 'newgrp docker'"
fi
else
miss "$USER not in 'docker' group"
fi
echo
echo "==> done. Open a new shell (or 'source $RC') so PATH/zoxide/fzf take effect."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment