180 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			180 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/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(f"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? ")
 | |
|     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"))
 |