zsh: add safe ncdu helper
This commit is contained in:
218
dotfiles/lib/functions/safe_ncdu
Executable file
218
dotfiles/lib/functions/safe_ncdu
Executable file
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
function _safe_ncdu_usage {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
safe_ncdu [scan] [ROOT] [-o OUTPUT]
|
||||
safe_ncdu open SNAPSHOT
|
||||
safe_ncdu top SNAPSHOT [LIMIT] [PATH]
|
||||
safe_ncdu excludes [ROOT]
|
||||
|
||||
Creates a compressed ncdu export while avoiding mounted descendants of ROOT.
|
||||
Default ROOT is /. Default OUTPUT is ~/.cache/ncdu/safe-ncdu-<root>-<timestamp>.json.zst.
|
||||
EOF
|
||||
}
|
||||
|
||||
function _safe_ncdu_require {
|
||||
local cmd
|
||||
for cmd in "$@"; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "safe_ncdu: missing required command: $cmd" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
function _safe_ncdu_root_name {
|
||||
local root="$1"
|
||||
if [[ "$root" == "/" ]]; then
|
||||
echo root
|
||||
else
|
||||
echo "${root#/}" | sed 's#[^A-Za-z0-9._-]#_#g'
|
||||
fi
|
||||
}
|
||||
|
||||
function _safe_ncdu_excludes {
|
||||
local root="${1:-/}"
|
||||
local root_real
|
||||
root_real="$(realpath -m "$root")" || return 1
|
||||
|
||||
# Exclude every mounted descendant. This catches FUSE/remote mounts such as
|
||||
# Keybase and also bind mounts such as /nix/store when scanning /.
|
||||
findmnt -R -rn -o TARGET "$root_real" 2>/dev/null \
|
||||
| awk -v root="$root_real" '
|
||||
$0 != root && length($0) > length(root) { print }
|
||||
'
|
||||
|
||||
# Static guardrails for known remote/special/noisy paths. ncdu treats
|
||||
# --exclude values as patterns, so globs are intentional here.
|
||||
cat <<EOF
|
||||
$HOME/keybase
|
||||
$HOME/.cache/keybase
|
||||
$HOME/.local/share/keybase
|
||||
$HOME/.config/keybase
|
||||
/home/*/keybase
|
||||
/keybase
|
||||
/var/lib/railbird
|
||||
/run/user/*/doc
|
||||
EOF
|
||||
}
|
||||
|
||||
function _safe_ncdu_scan {
|
||||
local root="/"
|
||||
local output=""
|
||||
local arg
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
arg="$1"
|
||||
case "$arg" in
|
||||
-h|--help)
|
||||
_safe_ncdu_usage
|
||||
return 0
|
||||
;;
|
||||
-o|--output)
|
||||
shift
|
||||
output="$1"
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echo "safe_ncdu: unknown option: $arg" >&2
|
||||
_safe_ncdu_usage >&2
|
||||
return 2
|
||||
;;
|
||||
*)
|
||||
root="$arg"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
_safe_ncdu_require ncdu findmnt realpath awk sed date mkdir tee sort uniq || return 1
|
||||
|
||||
local root_real root_name out_dir latest excludes_file
|
||||
root_real="$(realpath -m "$root")" || return 1
|
||||
root_name="$(_safe_ncdu_root_name "$root_real")"
|
||||
out_dir="$HOME/.cache/ncdu"
|
||||
mkdir -p "$out_dir"
|
||||
|
||||
if [[ -z "$output" ]]; then
|
||||
output="$out_dir/safe-ncdu-${root_name}-$(date +%Y%m%d-%H%M%S).json.zst"
|
||||
fi
|
||||
|
||||
excludes_file="${output}.excludes"
|
||||
_safe_ncdu_excludes "$root_real" | sort -u | tee "$excludes_file" >/dev/null
|
||||
|
||||
local -a exclude_args
|
||||
local exclude
|
||||
exclude_args=()
|
||||
while IFS= read -r exclude; do
|
||||
[[ -n "$exclude" ]] && exclude_args+=(--exclude "$exclude")
|
||||
done < "$excludes_file"
|
||||
|
||||
echo "safe_ncdu: scanning $root_real"
|
||||
echo "safe_ncdu: writing $output"
|
||||
echo "safe_ncdu: excludes recorded in $excludes_file"
|
||||
ncdu -0 -x -c "${exclude_args[@]}" -o "$output" "$root_real" || return $?
|
||||
|
||||
latest="$out_dir/latest-${root_name}.json.zst"
|
||||
ln -sfn "$output" "$latest"
|
||||
echo "safe_ncdu: latest symlink $latest"
|
||||
ln -sfn "$excludes_file" "${latest}.excludes"
|
||||
}
|
||||
|
||||
function _safe_ncdu_open {
|
||||
local snapshot="$1"
|
||||
if [[ -z "$snapshot" ]]; then
|
||||
echo "safe_ncdu open: missing SNAPSHOT" >&2
|
||||
return 2
|
||||
fi
|
||||
_safe_ncdu_require ncdu || return 1
|
||||
ncdu -r -f "$snapshot"
|
||||
}
|
||||
|
||||
function _safe_ncdu_top {
|
||||
local snapshot="$1"
|
||||
local limit="${2:-30}"
|
||||
local query_path="${3:-}"
|
||||
if [[ -z "$snapshot" ]]; then
|
||||
echo "safe_ncdu top: missing SNAPSHOT" >&2
|
||||
return 2
|
||||
fi
|
||||
_safe_ncdu_require zstdcat jq awk || return 1
|
||||
zstdcat "$snapshot" | jq -r --argjson limit "$limit" --arg path "$query_path" '
|
||||
def total:
|
||||
if type == "array" then
|
||||
((.[0].dsize // 0) + ([.[1:][] | total] | add // 0))
|
||||
elif type == "object" then
|
||||
(.dsize // 0)
|
||||
else
|
||||
0
|
||||
end;
|
||||
|
||||
def child_name:
|
||||
if type == "array" then .[0].name else .name end;
|
||||
|
||||
def descend($parts):
|
||||
if ($parts | length) == 0 then
|
||||
.
|
||||
else
|
||||
.[1:][]
|
||||
| select(type == "array" and .[0].name == $parts[0])
|
||||
| descend($parts[1:])
|
||||
end;
|
||||
|
||||
($path
|
||||
| sub("^/"; "")
|
||||
| split("/")
|
||||
| map(select(length > 0))) as $parts
|
||||
| (.[3] | descend($parts))[1:]
|
||||
| map({name: (if type == "array" then .[0].name else .name end), size: total})
|
||||
| sort_by(.size)
|
||||
| reverse
|
||||
| .[:$limit][]
|
||||
| [.name, .size]
|
||||
| @tsv
|
||||
' | awk -F'\t' '
|
||||
{
|
||||
size = $2
|
||||
unit = "B"
|
||||
if (size >= 1073741824) { size = size / 1073741824; unit = "GiB" }
|
||||
else if (size >= 1048576) { size = size / 1048576; unit = "MiB" }
|
||||
else if (size >= 1024) { size = size / 1024; unit = "KiB" }
|
||||
printf "%-90s %8.1f %s\n", $1, size, unit
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
function safe_ncdu {
|
||||
local subcommand="${1:-scan}"
|
||||
case "$subcommand" in
|
||||
scan)
|
||||
shift
|
||||
_safe_ncdu_scan "$@"
|
||||
;;
|
||||
open)
|
||||
shift
|
||||
_safe_ncdu_open "$@"
|
||||
;;
|
||||
top)
|
||||
shift
|
||||
_safe_ncdu_top "$@"
|
||||
;;
|
||||
excludes)
|
||||
shift
|
||||
_safe_ncdu_excludes "${1:-/}" | sort -u
|
||||
;;
|
||||
-h|--help|help)
|
||||
_safe_ncdu_usage
|
||||
;;
|
||||
*)
|
||||
_safe_ncdu_scan "$@"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
safe_ncdu "$@"
|
||||
Reference in New Issue
Block a user