From f996cf93ed09341c682c0511918da56c34780214 Mon Sep 17 00:00:00 2001 From: oxiedi Date: Thu, 15 Jul 2021 22:49:22 +0500 Subject: [PATCH] Add completion for rmlint --- src/_rmlint | 407 +++++++++++++++++++++++++++++++++++++++++++++++++ src/_rmlint.sh | 42 +++++ 2 files changed, 449 insertions(+) create mode 100644 src/_rmlint create mode 100644 src/_rmlint.sh diff --git a/src/_rmlint b/src/_rmlint new file mode 100644 index 0000000..2b6f141 --- /dev/null +++ b/src/_rmlint @@ -0,0 +1,407 @@ +#compdef rmlint + +# Copyright (c) 2021 Github zsh-users - http://github.com/zsh-users +# +# Permission is hereby granted, without written agreement and without +# licence or royalty fees, to use, copy, modify, and distribute this +# software and to distribute modified versions of this software for any +# purpose, provided that the above copyright notice and the following +# two paragraphs appear in all copies of this software. +# +# In no event shall the Zsh Development Group be liable to any party for +# direct, indirect, special, incidental, or consequential damages arising out +# of the use of this software and its documentation, even if the Zsh +# Development Group have been advised of the possibility of such damage. +# +# The Zsh Development Group specifically disclaim any warranties, including, +# but not limited to, the implied warranties of merchantability and fitness +# for a particular purpose. The software provided hereunder is on an "as is" +# basis, and the Zsh Development Group have no obligation to provide +# maintenance, support, updates, enhancements, or modifications. +# +# Description +# ----------- +# +# Zsh completion for rmlint 2.10.1 (https://github.com/sahib/rmlint) +# +# Authors +# ------- +# +# * oxiedi (https://github.com/oxiedi) + +(( $+functions[_rmlint_types] )) || +_rmlint_types() { + compset -P '*[+-]' + # FIXME: all values before `-` are swallowed by `*`, which breaks deduplication of the swallowed values + # TODO: respect `prefix-needed` + _values -s ',' 'list [defaults]' \ + 'all[enables all lint types]' \ + 'defaults[enables all lint types, but nonstripped]' \ + 'minimal[defaults minus emptyfiles and emptydirs]' \ + 'minimaldirs[defaults minus emptyfiles, emptydirs and duplicates, but with duplicatedirs]' \ + 'none[disable all lint types]' \ + '(badids bi)'{badids,bi}'[find files with bad UID, GID or both]' \ + '(badlinks bl)'{badlinks,bl}'[find bad symlinks pointing nowhere valid]' \ + '(emptydirs ed)'{emptydirs,ed}'[find empty directories]' \ + '(emptyfiles ef)'{emptyfiles,ef}'[find empty files]' \ + '(nonstripped ns)'{nonstripped,ns}'[find nonstripped binaries]' \ + '(duplicates df)'{duplicates,df}'[find duplicate files]' \ + '(duplicatedirs dd)'{duplicatedirs,dd}'[find duplicate directories (This is the same -D!)]' +} + +(( $+functions[__rmlint_setup_formatter_descriptions] )) || +__rmlint_setup_formatter_descriptions() { + typeset -gA formatter_descriptions=( + csv 'output all found lint as comma-separated-value list' + sh 'output all found lint as shell script' + json 'print a JSON-formatted dump of all found reports' + py 'outputs a python script and a JSON document, just like the json formatter' + uniques 'outputs all unique paths found during the run, one path per line' + stamp 'outputs a timestamp of the time rmlint was run' + progressbar 'shows a progressbar' + pretty 'shows all found items in realtime nicely colored' + summary 'shows counts of files and their respective size after the run. Also list all written output files' + fdupes 'prints an output similar to the popular duplicate finder fdupes(1)' + ) +} + +(( $+functions[_rmlint_output] )) || +_rmlint_output() { + local -A formatter_descriptions + __rmlint_setup_formatter_descriptions + if compset -P "(${(kj:|:)formatter_descriptions}):"; then + _alternative \ + 'files:file:_files' \ + 'outputs:output:(stdout stderr)' + else + local -a outputs + local f d + for f d in ${(kv)formatter_descriptions}; do + outputs+=( "$f:$d" ) + done + _describe -t outputs 'output' outputs -S ':' -q + fi +} + +(( $+functions[_rmlint_config] )) || +_rmlint_config() { + local -A formatter_descriptions + __rmlint_setup_formatter_descriptions + unset 'formatter_descriptions['{py,pretty,summary}']' + local -a match mbegin mend + if compset -P "(#b)(${(kj:|:)formatter_descriptions}):"; then + case $match[1] in + (csv) + _values 'option' \ + 'no_header[do not write a first line describing the column headers]' \ + 'unique[include unique files in the output]' + ;; + (sh) + local context state state_descr line + _values 'option' \ + 'cmd[specify a user defined command to run on duplicates]: :_cmdstring' \ + 'handler[define a comma separated list of handlers to try on duplicate files in that given order until one handler succeeds]:handler:->handler' \ + 'link[shortcut for -c sh:handler=clone,reflink,hardlink,symlink]' \ + 'hardlink[shortcut for -c sh:handler=hardlink,symlink]' \ + 'symlink[shortcut for -c sh:handler=symlink]' + case $state in + (handler) + _values -s ',' $state_descr \ + 'clone[try to clone both files with the FIDEDUPERANGE ioctl(3p) (or BTRFS_IOC_FILE_EXTENT_SAME on older kernels)]' \ + 'reflink[try to reflink the duplicate file to the original]' \ + 'hardlink[replace the duplicate file with a hardlink to the original file]' \ + 'symlink[tries to replace the duplicate file with a symbolic link to the original]' \ + 'remove[remove the file using rm -rf]' \ + 'usercmd[use the provided user defined command (-c sh:cmd=something)]' + ;; + esac + ;; + (json) + _values 'option' \ + 'unique[include unique files in the output]' \ + 'no_header[print the header with metadata]:boolean [true]:(false true)' \ + 'no_footer[print the footer with statistics]:boolean [true]:(false true)' \ + 'oneline[print one json document per line]:boolean [false]:(true false)' + ;; + (uniques) + _values 'option' \ + 'print0[do not put newlines between paths but zero bytes]' + ;; + (stamp) + _values 'option' \ + 'iso8601[write an ISO8601 formatted timestamps or seconds since epoch]:boolean:(true false)' + ;; + (progressbar) + _values 'option' \ + 'update_interval[number of milliseconds to wait between updates (default: 50)]: :_guard "[0-9]#" "update interval (milliseconds) [50]"' \ + 'ascii[do not attempt to use unicode characters, which might not be supported by some terminals]' \ + 'fancy[use a more fancy style for the progressbar]' + ;; + (fdupes) + _values 'option' \ + 'omitfirst[omits the first line of each set of duplicates]' \ + 'sameline[does not print newlines between files, only a space]' + ;; + esac + else + local -a formatters + local f d + for f d in ${(kv)formatter_descriptions}; do + formatters+=( "$f:$d" ) + done + _describe -t formatters 'formatter' formatters -S ':' + fi +} + +(( $+functions[_rmlint_algorithm] )) || +_rmlint_algorithm() { + local -a tmp=( sha{1,256,512} sha3-{256,384,512} blake{2s,2b,2sp,2bp} highway{64,128,256} ) + _alternative \ + '512bit-algorithms:512-bit:(blake2b blake2bp sha3-512 sha512)' \ + '384bit-algorithms:384-bit:(sha3-384)' \ + '256bit-algorithms:256-bit:(blake2s blake2sp sha3-256 sha256 highway256 metro256 metrocrc256)' \ + '160bit-algorithms:160-bit:(sha1)' \ + '128bit-algorithms:128-bit:(md5 murmur metro metrocrc)' \ + '64bit-algorithms:64-bit:(highway64 xxhash)' \ + "cryptographic-algorithms:cryptographic:($tmp)" \ + 'non-cryptographic-algorithms:non-cryptographic:(metro metro256 xxhash murmur)' \ + 'not-useful-algorithms:not-useful:(cumulative paranoid ext)' +} + +(( $+functions[_rmlint_sort] )) || +_rmlint_sort() { + local -A letter_descriptions=( + s 'sort by size of group' + a 'sort alphabetically by the basename of the original' + m 'sort by mtime of the original' + p 'sort by path-index of the original' + o 'sort by natural found order (might be different on each run)' + n 'sort by number of files in the group' + ) + local -a letters + local l d + for l d in ${(kv)letter_descriptions}; do + letters+=( "${l}[$d]" "${(U)l}[$d (in reverse order)]" ) + done + _values -s '' 'order' $letters +} + +(( $+functions[__rmlint_describe_multipliers] )) || +__rmlint_describe_multipliers() { + local -a multipliers=( + 'C:1^1' + 'W:2^1' + 'B:512^1' + 'K:1000^1' + 'KB:1024^1' + 'M:1000^2' + 'MB:1024^2' + 'G:1000^3' + 'GB:1024^3' + 'T:1000^4' + 'TB:1024^4' + 'P:1000^5' + 'PB:1024^5' + 'E:1000^6' + 'EB:1024^6' + ) + _describe -t multiplier 'multiplier' multipliers "$@" +} + +(( $+functions[__rmlint_multipliers] )) || +__rmlint_multipliers() { + compset -P '[0-9]##' || return + __rmlint_describe_multipliers "$@" +} + +(( $+functions[_rmlint_size] )) || +_rmlint_size() { + ! __rmlint_multipliers && [[ -z $PREFIX ]] && _message -e "${1:-size}" +} + +(( $+functions[_rmlint_size_range] )) || +_rmlint_size_range() { + if compset -P '[0-9]##'; then + if compset -P '(C|W|B|K|KB|M|MB|G|GB|T|TB|P|PB|E|EB)-'; then + _rmlint_size 'max' + else + __rmlint_describe_multipliers -S '-' -q + fi + elif [[ -z $PREFIX ]]; then + _message -e 'min' + fi +} + +(( $+functions[_rmlint_iso8601_or_unix_timestamp] )) || +_rmlint_iso8601_or_unix_timestamp() { + _alternative \ + 'dates:iso8601_timestamp: _dates -f "%FT%T"' \ + 'seconds:unix_timestamp:_guard "[0-9]#" "seconds since epoch"' +} + +(( $+functions[_rmlint_rank] )) || +_rmlint_rank() { + # TODO: {r,R,x,X} + _values -s '' 'criteria [pOma]' \ + 'm[keep lowest mtime (oldest)]' 'M[keep highest mtime (newest)]' \ + 'a[keep first alphabetically]' 'A[keep last alphabetically]' \ + 'p[keep first named path]' 'P[keep last named path]' \ + 'd[keep path with lowest depth]' 'D[keep path with highest depth]' \ + 'l[keep path with shortest basename]' 'L[keep path with longest basename]' \ + 'r[keep paths matching regex]' 'R[keep path not matching regex]' \ + 'x[keep basenames matching regex]' 'X[keep basenames not matching regex]' \ + 'h[keep file with lowest hardlink count]' 'H[keep file with highest hardlink count]' \ + 'o[keep file with lowest number of hardlinks outside of the paths traversed by rmlint]' \ + 'O[keep file with highest number of hardlinks outside of the paths traversed by rmlint]' +} + +(( $+functions[_rmlint_percent] )) || +_rmlint_percent() { + if compset -P '(100|[1-9][0-9]|[1-9])'; then + compadd "$@" -- '%' + elif [[ -z $PREFIX ]]; then + _message -e 'percent%%' + fi +} + +(( $+functions[_rmlint_clamp] )) || +_rmlint_clamp() { + _alternative \ + "factor: :_guard '((|0)(|.[0-9]#)|1(|.0#))' 'fac.tor [$1]'" \ + 'percent:percent%%:_rmlint_percent' \ + 'multiplier:offset: _rmlint_size "offset"' +} + +(( $+functions[_rmlint_files_or_separator] )) || +_rmlint_files_or_separator() { + if (( $words[(i)-] < CURRENT )); then + [[ -z $words[CURRENT] ]] && compadd "$@" -S '' -- - + return + fi + local -a alts=( 'files:file:_files' ) + (( $line[(I)//] || $+opt_args[--equal] )) || alts+=( 'separator:separator:(//)' ) + _alternative $alts +} + +_rmlint() { + local curcontext="$curcontext" state state_descr + local -a line + local -i ret=1 + typeset -A opt_args + + _arguments -s -w -C : \ + '(-T --types)'{-T,--types}'=[configure the types of lint rmlint will look for]: :_rmlint_types' \ + '*'{-o,--output}'=[configure the way rmlint outputs its results]:spec:_rmlint_output' \ + '*'{-O,--add-output}'=[configure the way rmlint outputs its results (preserve defaults)]:spec:_rmlint_output' \ + '*'{-c,--config}'=[configure a format]:spec:_rmlint_config' \ + '(-z --perms)'{-z,--perms}'=[only look into file if it is readable, writable or executable by the current user]: :_values -s "" perms r w x' \ + '(-a --algorithm)'{-a,--algorithm}'=[choose the algorithm to use for finding duplicate files]:algo:_rmlint_algorithm' \ + '*'{-p,--paranoid}'[increase the paranoia of rmlint'\''s duplicate algorithm]' \ + '*'{-P,--less-paranoid}'[decrease the paranoia of rmlint'\''s duplicate algorithm]' \ + '*'{-v,--loud}'[increase the verbosity]' \ + '*'{-V,--quiet}'[decrease the verbosity]' \ + '(-g --progress)'{-g,--progress}'[show a progressbar with sane defaults]' \ + '(-G --no-progress)'{-G,--no-progress}'[do not show a progressbar with sane defaults (default)]' \ + '(-D --merge-directories)'{-D,--merge-directories}'[makes rmlint use a special mode where all found duplicates are collected and checked if whole directory trees are duplicates]' \ + '(-j --honour-dir-layout)'{-j,--honour-dir-layout}'[only recognize directories as duplicates that have the same path layout]' \ + '(-y --sort-by)'{-y,--sort-by}'=[during output, sort the found duplicate groups by criteria described by order]:order:_rmlint_sort' \ + '(-w --with-color)'{-w,--with-color}'[use color escapes for pretty output (default)]' \ + '(-W --no-with-color)'{-W,--no-with-color}'[disable color escapes for pretty output]' \ + '(- *)'{-h,--help}'[show a shorter reference help text]' \ + '(- *)--help-all[show all help options]' \ + '(- *)'{-H,--show-man}'[show the full man page]' \ + '(- *)--version[print the version of rmlint]' \ + '(-s --size)'{-s,--size}'=[only consider files as duplicates in a certain size range]:range:_rmlint_size_range' \ + '(-d --max-depth)'{-d,--max-depth}'=[only recurse up to this depth]: :_guard "[0-9]#" "depth"' \ + '(-l --hardlinked)'{-l,--hardlinked}'[hardlinked files are treated as duplicates (default)]' \ + '--keep-hardlinked[rmlint will not delete any files that are hardlinked to an original in their respective group]' \ + '(-L --no-hardlinked)'{-L,--no-hardlinked}'[only one file (of a set of hardlinked files) is considered, all the others are ignored]' \ + '(-f --followlinks)'{-f,--followlinks}'[follow symbolic links]' \ + '(-F --no-followlinks)'{-F,--no-followlinks}'[ignore symbolic links completely]' \ + '(-@ --see-symlinks)'{-@,--see-symlinks}'[see symlinks and treats them like small files with the path to their target in them (default)]' \ + '(-x --no-crossdev)'{-x,--no-crossdev}'[stay always on the same device]' \ + '(-X --crossdev)'{-X,--crossdev}'[allow crossing mountpoints (default)]' \ + '(-r --hidden)'{-r,--hidden}'[traverse hidden directories]' \ + '(-R --no-hidden)'{-R,--no-hidden}'[don'\''t traverse hidden directories (default)]' \ + '--partial-hidden[traverse duplicate hidden directories]' \ + '(-b --match-basename)'{-b,--match-basename}'[only consider those files as dupes that have the same basename]' \ + '(-B --unmatched-basename)'{-B,--unmatched-basename}'[only consider those files as dupes that do not share the same basename]' \ + '(-e --match-with-extension)'{-e,--match-with-extension}'[only consider those files as dupes that have the same file extension]' \ + '(-E --no-match-with-extension)'{-E,--no-match-with-extension}'[don'\'t' consider those files as dupes that have the same file extension (default)]' \ + '(-i --match-without-extension)'{-i,--match-without-extension}'[only consider those files as dupes that have the same basename minus the file extension]' \ + '(-I --no-match-without-extension)'{-I,--no-match-without-extension}'[don'\'t' consider those files as dupes that have the same basename minus the file extension (default)]' \ + '(-n --newer-than-stamp)'{-n,--newer-than-stamp}'=[only consider files (and their size siblings for duplicates) newer than a certain modification time (mtime)]:timestamp_filename:_files' \ + '(-N --newer-than)'{-N,--newer-than}'=[don'\'t' consider files (and their size siblings for duplicates) newer than a certain modification time (mtime)]: :_rmlint_iso8601_or_unix_timestamp' \ + '(-k --keep-all-tagged)'{-k,--keep-all-tagged}'[don'\''t delete any duplicates that are in tagged paths]' \ + '(-K --keep-all-untagged)'{-K,--keep-all-untagged}'[don'\''t delete any duplicates that are in non-tagged paths]' \ + '(-m --must-match-tagged)'{-m,--must-match-tagged}'[only look for duplicates of which at least one is in one of the tagged paths]' \ + '(-M --must-match-untagged)'{-M,--must-match-untagged}'[only look for duplicates of which at least one is in one of the non-tagged paths]' \ + '(-S --rank-by)'{-S,--rank-by}'=[sort the files in a group of duplicates into originals and duplicates by one or more criteria]: :_rmlint_rank' \ + '--replay[read an existing json file and re-output it]' \ + '(-C --xattr)'{-C,--xattr}'[shortcut for --xattr-read, --xattr-write, --write-unfinished]' \ + '--xattr-read[read cached checksums from the extended file attributes]' \ + '--xattr-write[write cached checksums from the extended file attributes]' \ + '--xattr-clear[clear cached checksums from the extended file attributes]' \ + '(-U --write-unfinished)'{-U,--write-unfinished}'[include files in output that have not been hashed fully, i.e. files that do not appear to have a duplicate]' \ + '(-t --threads)'{-t,--threads}'=[the number of threads to use during file tree traversal and hashing (default: 16)]: :_guard "[0-9]#" "threads [16]"' \ + '(-u --limit-mem)'{-u,--limit-mem}'=[apply a maximum number of memory to use for hashing and --paranoid]:size: _rmlint_size' \ + '(-q --clamp-low)'{-q,--clamp-low}'=[only look at the content of files in the range of from low to (including) high (default: 0)]: : _rmlint_clamp 0' \ + '(-Q --clamp-top)'{-Q,--clamp-top}'=[only look at the content of files in the range of from low to (including) high (default: 1.0)]: : _rmlint_clamp 1.0' \ + '(-Z --mtime-window)'{-Z,--mtime-window}'=[only consider those files as duplicates that have the same content and the same modification time (mtime) within a certain window of T seconds (default: -1)]: :_guard "[0-9]#" "mtime window (seconds) [-1]"' \ + '--with-fiemap[enable reading the file extents on rotational disk in order to optimize disk access patterns (default)]' \ + '--without-fiemap[disable reading the file extents on rotational disk in order to optimize disk access patterns]' \ + '--gui[start the optional graphical frontend to rmlint called Shredder]:*: :->gui' \ + '--hash[make rmlint work as a multi-threaded file hash utility]:*: :->hash' \ + '--equal[check if the paths given on the commandline all have equal content]: :_rmlint_files_or_separator' \ + '(-0 --stdin0)'{-0,--stdin0}'[read null-separated file list from stdin]' \ + '--backup[do create backups of previous result files]' \ + '--no-backup[do not create backups of previous result files]' \ + '--dedupe[dedupe matching extents from source to dest (if filesystem supports)]:*:: := ->dedupe' \ + '--dedupe-xattr[check extended attributes to see if the file is already deduplicated]' \ + '--dedupe-readonly[(--dedupe option) even dedupe read-only snapshots (needs root)]' \ + '--is-reflink[test if two files are reflinks (share same data extents)]:*:: := ->reflink' \ + '*: :_rmlint_files_or_separator' && return + + case $state in + (gui) + _arguments -s -w : \ + '(- *)'{-h,--help}'[show help options]' \ + {-a,--add-location}'[add locations to locations view]' \ + {-s,--scan}'[add location to scan (as untagged path)]' \ + {-S,--scan-tagged}'[add location to scan (as tagged path)]' \ + {-l,--load-script}'[show `script` in editor view]' \ + '*'{-v,--verbose}'[be more verbose]' \ + '*'{-V,--less-verbose}'[be less verbose]' \ + {-c,--show-settings}'[show the settings view]' \ + '(- *)--version[show the version of Shredder]' && ret=0 + ;; + (hash) + _arguments -s -w : \ + '(- *)'{-h,--help}'[show help options]' \ + {-a,--algorithm}'[digest type \[bLAKE2B\]]:type:_rmlint_algorithm' \ + {-t,--num-threads}'[number of hashing threads \[8\]]: :_guard "[0-9]#" "threads [8]"' \ + {-b,--buffer-mbytes}'[megabytes read buffer \[256 MB\]]: :_guard "[0-9]#" "buffer (MB) [256]"' \ + {-i,--ignore-order}'[print hashes in order completed, not in order entered (reduces memory usage)]' \ + '*:file:_files' && ret=0 + ;; + (dedupe) + _arguments -s -w : \ + '-r[enable deduplication of read-only \[btrfs\] snapshots (requires root)]' \ + '(-V)*-v' \ + '(-v)*-V' \ + ':src:_files' \ + ':dst:_files' && ret=0 + ;; + (reflink) + _arguments -s -w : \ + '(-V)*-v' \ + '(-v)*-V' \ + ':file1:_files' \ + ':file2:_files' && ret=0 + ;; + esac + + return ret +} + +_rmlint "$@" diff --git a/src/_rmlint.sh b/src/_rmlint.sh new file mode 100644 index 0000000..434fbf7 --- /dev/null +++ b/src/_rmlint.sh @@ -0,0 +1,42 @@ +#compdef rmlint.sh -P rmlint.*.sh + +# Copyright (c) 2021 Github zsh-users - http://github.com/zsh-users +# +# Permission is hereby granted, without written agreement and without +# licence or royalty fees, to use, copy, modify, and distribute this +# software and to distribute modified versions of this software for any +# purpose, provided that the above copyright notice and the following +# two paragraphs appear in all copies of this software. +# +# In no event shall the Zsh Development Group be liable to any party for +# direct, indirect, special, incidental, or consequential damages arising out +# of the use of this software and its documentation, even if the Zsh +# Development Group have been advised of the possibility of such damage. +# +# The Zsh Development Group specifically disclaim any warranties, including, +# but not limited to, the implied warranties of merchantability and fitness +# for a particular purpose. The software provided hereunder is on an "as is" +# basis, and the Zsh Development Group have no obligation to provide +# maintenance, support, updates, enhancements, or modifications. +# +# Description +# ----------- +# +# Zsh completion for shell scripts produced by rmlint (https://github.com/sahib/rmlint) +# +# Authors +# ------- +# +# * oxiedi (https://github.com/oxiedi) + +_arguments -s : \ + '(-)-h[show help message]' \ + '-d[do not ask before running]' \ + '-x[keep rmlint.sh; do not autodelete it]' \ + '-p[recheck that files are still identical before removing duplicates]' \ + '-r[allow deduplication of files on read-only btrfs snapshots (requires sudo)]' \ + '(-d -x)-n[do not perform any modifications, just print what would be done (implies -d and -x)]' \ + '-c[clean up empty directories while deleting duplicates]' \ + '-q[do not show progress]' \ + '-k[keep the timestamp of directories when removing duplicates]' \ + '-i[ask before deleting each file]'