# zsh/.functions.zsh -- small shell functions I actually use day to day.
# Kept out of .aliases because these need positional args or control flow.
# Loaded from .zshrc via: source "$ZDOTDIR/.functions.zsh"
# mkcd <dir>: make and enter a directory in one step.
mkcd() {
[[ -n $1 ]] || { print -r -- "usage: mkcd <dir>" >&2; return 2; }
mkdir -p -- "$1" && cd -- "$1"
}
# up [n]: go up n directories (default 1). `up 3` ~= `cd ../../..`.
up() {
local n=${1:-1} path=""
for (( i = 0; i < n; i++ )); do
path+="../"
done
cd -- "$path" || return
}
# gcb <branch>: git create-branch. Creates from origin/main by default so
# I stop accidentally branching off whatever is currently checked out.
gcb() {
[[ -n $1 ]] || { print -r -- "usage: gcb <branch> [base]" >&2; return 2; }
local base=${2:-origin/main}
git fetch --quiet origin main
git switch -c "$1" "$base"
}
# gclean: delete every merged local branch except main/master/current.
gclean() {
local current
current=$(git symbolic-ref --quiet --short HEAD) || return
git branch --merged main 2>/dev/null \
| grep -vE "^\*|^\+| (main|master|$current)\$" \
| xargs -r git branch -d
}
# glog [n]: one-line git log with color, default 20 entries.
glog() {
git log --oneline --decorate --color=auto -n "${1:-20}"
}
# grep-ls: rg files matching a pattern, one path per line, deduplicated.
grep-ls() {
[[ -n $1 ]] || { print -r -- "usage: grep-ls <pattern>" >&2; return 2; }
rg --files-with-matches --no-messages "$@"
}
# extract <file>: unpack most archive formats without remembering the flags.
extract() {
[[ -f $1 ]] || { print -r -- "extract: $1 is not a file" >&2; return 2; }
case $1 in
*.tar.bz2|*.tbz2) tar xjf "$1" ;;
*.tar.gz|*.tgz) tar xzf "$1" ;;
*.tar.xz|*.txz) tar xJf "$1" ;;
*.tar.zst) tar --zstd -xf "$1" ;;
*.tar) tar xf "$1" ;;
*.zip) unzip "$1" ;;
*.rar) unrar x "$1" ;;
*.7z) 7z x "$1" ;;
*.gz) gunzip "$1" ;;
*.bz2) bunzip2 "$1" ;;
*.xz) unxz "$1" ;;
*.Z) uncompress "$1" ;;
*) print -r -- "extract: don't know how to open $1" >&2; return 2 ;;
esac
}
# mkpass [n]: printable random password of length n (default 24).
mkpass() {
local n=${1:-24}
LC_ALL=C tr -dc 'A-Za-z0-9!@#%^&*_+-' </dev/urandom | head -c "$n"
print
}
# port <n>: show what is listening on TCP port n. Linux + macOS friendly.
port() {
[[ -n $1 ]] || { print -r -- "usage: port <number>" >&2; return 2; }
if (( $+commands[ss] )); then
ss -ltnp "sport = :$1" 2>/dev/null
else
lsof -iTCP:"$1" -sTCP:LISTEN -P -n 2>/dev/null
fi
}
# fcd: fzf-based cd, bound to alt-c in the keymap.
fcd() {
local dir
dir=$(
fd --type d --hidden --follow --exclude .git 2>/dev/null \
| fzf --prompt='cd> ' --height=40% --reverse
) || return
cd -- "$dir"
}
# serve [port]: one-off python static server with a sensible default.
serve() {
local port=${1:-8080}
print -r -- "serving $PWD on http://localhost:$port"
python3 -m http.server "$port"
}
# json <file>: pretty-print json, fall back to python if jq is missing.
json() {
if (( $+commands[jq] )); then
jq . "${1:-/dev/stdin}"
else
python3 -m json.tool "${1:-/dev/stdin}"
fi
}
# manp <cmd>: open a man page in preview with nice typography.
manp() {
[[ -n $1 ]] || { print -r -- "usage: manp <command>" >&2; return 2; }
man -t "$1" | open -f -a Preview
}
# weather [city]: quick weather via wttr.in -- no keys required.
weather() {
local q=${1:-}
curl -fsSL "https://wttr.in/${q}?format=3"
}
# mvcd <src>: mv a file and cd to the destination dir.
mvcd() {
[[ $# -ge 2 ]] || { print -r -- "usage: mvcd <src>... <dst>" >&2; return 2; }
mv "$@" && cd -- "${@: -1}"
}
# note: append a line to today's TIL file; picks up my tilstream layout.
note() {
local d=${TIL_ROOT:-$HOME/til}/posts
mkdir -p -- "$d"
local f="$d/$(date +%Y-%m-%d).md"
[[ -s $f ]] || print "---\ntitle: $(date +%Y-%m-%d)\ndate: $(date +%Y-%m-%d)\n---\n" >> "$f"
print -r -- "- $*" >> "$f"
print -r -- "appended to $f"
}