diff --git a/zsh/lib/interactive.zsh b/zsh/lib/interactive.zsh index 3702bf5..f03bafa 100644 --- a/zsh/lib/interactive.zsh +++ b/zsh/lib/interactive.zsh @@ -21,10 +21,77 @@ alias ls='ls --color=always' alias cgrep='grep --color=always' alias cdiff='git diff --color-words --no-index' +# Remove the files an archive extracted into the current directory. +# Lists what it will delete and asks first; -f skips the prompt, -n dry-runs. +# Directories are only removed if they end up empty. delzip() { - unzip -Z -1 "$@" | xargs -I{} rm -rf {} + emulate -L zsh + setopt local_options + + local dry=0 force=0 + while [[ $1 == -[nf] ]]; do + [[ $1 == -n ]] && dry=1 || force=1 + shift + done + (( $# )) || { print -ru2 "usage: delzip [-n|-f] ..."; return 2 } + + local zip entry parent + local -a entries files dirs + for zip in "$@"; do + [[ -f $zip ]] || { print -ru2 "delzip: no such file: $zip"; return 1 } + entries=("${(f)$(unzip -Z -1 -- $zip)}") || return 1 + for entry in $entries; do + [[ -n $entry ]] || continue + # zip-slip guard: never touch anything outside the cwd + if [[ $entry == (/|~)* || $entry == (|*/)..(|/*) ]]; then + print -ru2 "delzip: refusing unsafe path: $entry" + return 1 + fi + [[ $entry == */ ]] && dirs+=(${entry%/}) || files+=($entry) + # many archives omit directory entries, so infer them from the + # paths as well; unzip creates those dirs regardless + parent=${entry%/} + while [[ $parent == */* ]]; do + parent=${parent%/*} + dirs+=($parent) + done + done + done + dirs=(${(u)dirs}) + + # only keep what is actually on disk, so the list reflects reality; + # -L catches broken symlinks, which -e alone misses + local -a present + present=(); for entry in $files; do [[ -e $entry || -L $entry ]] && present+=($entry); done + files=($present) + present=(); for entry in $dirs; do [[ -d $entry ]] && present+=($entry); done + dirs=($present) + + (( $#files + $#dirs )) || { print -ru2 "delzip: nothing to remove here (already gone?)"; return 1 } + + if (( dry )); then + print -rl -- ${files} ${(O)dirs} + return 0 + fi + + if (( ! force )); then + [[ -t 0 ]] || { print -ru2 "delzip: not a terminal, use -f to skip the prompt"; return 1 } + print -r -- "delzip: remove ${#files} file(s) under ${PWD}:" + print -rl -- " "${^files[1,20]} + (( $#files > 20 )) && print -r -- " ... and $(( $#files - 20 )) more" + (( $#dirs )) && print -r -- "plus ${#dirs} directory/ies, if they end up empty." + read -q "?Proceed? [y/N] " || { print -r -- $'\naborted'; return 1 } + print + fi + + (( $#files )) && rm -f -- $files + # deepest first, so parents can empty out; rmdir refuses non-empty dirs + for entry in ${(O)dirs}; do + [[ -d $entry ]] && rmdir -- $entry 2>/dev/null + done + return 0 } -export MITMPROXY_SSLKEYLOGFILE='~/.mitmproxy/sslkeylogfile.txt' +export MITMPROXY_SSLKEYLOGFILE="$HOME/.mitmproxy/sslkeylogfile.txt" unset ZSH_AUTOSUGGEST_USE_ASYNC export LANG='en_US.UTF-8'