180 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			180 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
#!/usr/bin/env 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 file_scripts.error as error
 | 
						|
import file_scripts.search as search
 | 
						|
 | 
						|
from termcolor import colored
 | 
						|
 | 
						|
# ========== Constants ==========
 | 
						|
fd_opts = ["--hidden"]
 | 
						|
# Matches 'y' or 'yes' only, ignoring case
 | 
						|
USER_RESPONSE_YES = r"^[Yy]{1}([Ee]{1}[Ss]{1})?$"
 | 
						|
 | 
						|
 | 
						|
# ========== 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", "directory"],
 | 
						|
        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 = set()
 | 
						|
    for p in args.patterns:
 | 
						|
        files.update(
 | 
						|
            search.find_files(opts=fd_opts, capture_text=True, pattern=p).splitlines()
 | 
						|
        )
 | 
						|
    files = sorted(files)
 | 
						|
 | 
						|
    if files == []:
 | 
						|
        print("No results found, exiting")
 | 
						|
        exit(error.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? [y/N]: ")
 | 
						|
    except KeyboardInterrupt:
 | 
						|
        exit(error.E_INTERRUPT)
 | 
						|
 | 
						|
    if re.match(USER_RESPONSE_YES, user_response) is None:
 | 
						|
        print("Operation cancelled")
 | 
						|
        exit(error.E_USER_RESPONSE_NO)
 | 
						|
 | 
						|
    # Remove files first
 | 
						|
    for f in [fi for fi in files if os.path.isfile(fi)]:
 | 
						|
        os.remove(f)
 | 
						|
 | 
						|
    # Check -f, --force-directory-delete option
 | 
						|
    # shutil.rmtree forcibly removes directories
 | 
						|
    # os.rmdir removes directories only if they are empty
 | 
						|
    rmdir_func = shutil.rmtree if args.force_directory_delete else 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"))
 |