From ea736c897877185119d9de2d5fa6dbd2eb977901 Mon Sep 17 00:00:00 2001 From: Eric Torres Date: Sun, 17 Mar 2019 00:54:32 -0700 Subject: [PATCH] Reimplement quickdel script in python --- quickdel.sh => misc/quickdel.sh | 0 quickdel.py | 185 ++++++++++++++++++++++++++++++++ zsh/completions/_quickdel | 8 +- 3 files changed, 192 insertions(+), 1 deletion(-) rename quickdel.sh => misc/quickdel.sh (100%) create mode 100644 quickdel.py diff --git a/quickdel.sh b/misc/quickdel.sh similarity index 100% rename from quickdel.sh rename to misc/quickdel.sh diff --git a/quickdel.py b/quickdel.py new file mode 100644 index 0000000..6fa88e1 --- /dev/null +++ b/quickdel.py @@ -0,0 +1,185 @@ +#!/usr/bin/python3 +""" +quickdel - delete any file matching a query + +Dependencies +============ +* fd +* python-termcolor + +Command-Line Arguments +====================== +* -d, --directories-only +* -D, --directory TODO: implement this +* -e, --empty-only +* -E, --extension +* -f, --files-only +* -F, --force-directory-delete +* -i, --no-ignore +* -I, --no-ignore-vcs +* -l, --links-only +""" + +import argparse +import os +import os.path +import re +import shutil +import subprocess + +from termcolor import colored + +# ========== Constants ========== +FD_BIN = "/usr/bin/fd" +FD_OPTS = ["--hidden"] +# Matches 'y' or 'yes' only, ignoring case +USER_RESPONSE_YES = "^[Yy]{1}([Ee]{1}[Ss]{1})?$" + +E_NO_RESULTS = 1 +E_USER_RESPONSE_NO = 2 +E_INPUT_INTERRUPTED = 3 + + +# ========== Functions ========== +def color_file(filename): + """Return correct color code for filetype of filename. + + Example + ------- + >>> color_file('Test File', 'red') + '\x1b[31mTest String\x1b[0m' + + :param filename: file to determine color output for + :type filename: str + :return: filename with ANSII escape codes for color + :rtype: str + """ + if os.path.isdir(filename): + return colored(filename, "blue") + elif os.path.islink(filename): + return colored(filename, "green") + else: + return filename + + +# ========== Main Script ========== +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "-d", + "--directories-only", + action="store_const", + const=["--type", "directories"], + dest="fd_extra_opts", + help="filter results to directories", + ) + parser.add_argument( + "-e", + "--empty-only", + action="store_const", + const=["--type", "empty"], + dest="fd_extra_opts", + help="filter results to empty files and directories", + ) + parser.add_argument( + "-E", + "--extension", + action="append", + dest="extensions", + help="file extension", + metavar="ext", + ) + parser.add_argument( + "-f", + "--files-only", + action="store_const", + const=["--type", "file"], + dest="fd_extra_opts", + help="filter results to files", + ) + parser.add_argument( + "-F", + "--force-directory-delete", + action="store_true", + help="do not ignore non-empty directories, delete anyways", + ) + parser.add_argument( + "-I", + "--no-ignore-vcs", + action="store_const", + const="--no-ignore-vcs", + dest="fd_extra_opts", + help="do not ignore .gitignore", + ) + parser.add_argument( + "-i", + "--no-ignore", + action="store_const", + const="--no-ignore", + dest="fd_extra_opts", + help="do not ignore .gitignore and .fdignore", + ) + parser.add_argument( + "-l", + "--links-only", + action="store_const", + const=["--type", "symlink"], + dest="fd_extra_opts", + help="filter results to symlinks", + ) + parser.add_argument("patterns", nargs="+", help="file matching patterns") + + args = parser.parse_args() + + if args.fd_extra_opts is not None: + FD_OPTS.extend(args.fd_extra_opts) + if args.extensions is not None: + for ext in args.extensions: + FD_OPTS.extend(["--extension", ext]) + + files = [] + for pattern in args.patterns: + cmd = [FD_BIN, *FD_OPTS, pattern] + files.extend( + subprocess.run(cmd, capture_output=True, text=True).stdout.splitlines() + ) + files.sort() + + if files == []: + print(f"No results found, exiting") + exit(E_NO_RESULTS) + + # Pretty print all filenames + for index, filename in enumerate([color_file(f) for f in files], 1): + print(f"{index}. {filename}") + # Padding line + print() + + try: + user_response = input("Would you like to delete these files? ") + except KeyboardInterrupt: + exit(E_INPUT_INTERRUPTED) + + if re.match(USER_RESPONSE_YES, user_response) is None: + print("Operation cancelled") + exit(E_USER_RESPONSE_NO) + + # Remove files first + for f in filter(os.path.isfile, files): + os.remove(f) + + # Check -f, --force-directory-delete option + if args.force_directory_delete: + rmdir_func = shutil.rmtree + else: + rmdir_func = os.rmdir + + for d in filter(os.path.isdir, files): + try: + rmdir_func(d) + except OSError: + print( + f"{colored('Warning', 'yellow')}: {colored(d, 'blue')} is not empty, not deleting directory" + ) + + print(colored("\nDeletions complete", "green")) diff --git a/zsh/completions/_quickdel b/zsh/completions/_quickdel index 96222b2..566613a 100644 --- a/zsh/completions/_quickdel +++ b/zsh/completions/_quickdel @@ -4,11 +4,17 @@ local arguments arguments=( - {-d,--directories-only}'[only delete directories]' + {-d,--directories-only}'[filter results to directories]' + {-e,--empty-only}'[filter results to empty files and directories]' + {-f,--files-only}'[filter results to files]' + {-F,--force-directory-delete}'[do not ignore non-empty directories, delete anyways]' + {-E,--extension}'[file extension]' {-h,--help}'[print this help page]' {-i,--no-ignore}'[do not ignore .gitignore and .fdignore]' {-I,--no-ignore-vcs}'[do not ignore .gitignore]' + {-l,--links-only}'[filter results to symlinks]' '*:filename:_files' ) + _arguments -s $arguments