From 8616cdc5dd5d07a3c1bfa4ccd9f1082b3f154445 Mon Sep 17 00:00:00 2001 From: Ethan Dalool Date: Sat, 12 Feb 2022 19:50:00 -0800 Subject: [PATCH] Use new betterhelp. --- bitwise_or.py | 31 ++++--- blankimage.py | 68 ++++++++++++++ brename.py | 121 ++++++++++++------------ contentreplace.py | 104 ++++++++++++--------- directory_discrepancy.py | 31 +++---- fdroidapk.py | 64 +++++++------ filepull.py | 27 ++++-- fuchsiatransparent.py | 64 +++++++++++++ getcrx.py | 4 +- getpid.py | 32 +++---- gitcheckup.py | 137 ++++++++++++++++----------- icoconvert.py | 47 +++++++--- inodes.py | 18 +++- named_python.py | 42 ++++----- nosmartquotes.py | 42 ++++++--- prune_dirs.py | 19 ++-- rarpar.py | 193 +++++++++++++++++++++++--------------- reg_extension_icon.py | 83 +++++++++-------- reserve_disk_space.py | 40 ++++---- resize.py | 195 ++++++++++++++++++++++----------------- retry.py | 37 ++++++-- shortcut.py | 67 ++++++++------ stitch.py | 34 +++++-- svgrender.py | 86 +++++++++-------- tempeditor.py | 41 ++++---- threaded_dl.py | 102 ++++++++++---------- touch.py | 30 ++++-- wait_for_internet.py | 32 +++---- watchforlinks.py | 48 +++++----- 29 files changed, 1133 insertions(+), 706 deletions(-) create mode 100644 blankimage.py create mode 100644 fuchsiatransparent.py diff --git a/bitwise_or.py b/bitwise_or.py index e5326f0..534c4a5 100644 --- a/bitwise_or.py +++ b/bitwise_or.py @@ -1,11 +1,3 @@ -''' -bitwise_or -========== - -Merge two or more files by performing bitwise or on their bits. - -> bitwise_or file1 file2 --output file3 -''' import argparse import sys @@ -40,6 +32,8 @@ def bitwise_or_argparse(args): pass elif args.overwrite: pass + elif not pipeable.in_tty(): + return 1 elif not interactive.getpermission(f'Overwrite "{output.absolute_path}"?'): return 1 @@ -61,14 +55,27 @@ def bitwise_or_argparse(args): @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser( + description=''' + Merge two or more files by performing bitwise or on their bits. + That is, every byte of the output file will be the bitwise or of the + corresponding byte from all of the input files. + ''', + ) parser.add_argument('files', nargs='+') - parser.add_argument('--output', required=True) - parser.add_argument('--overwrite', action='store_true') + parser.add_argument('--output', required=True, type=pathclass.Path) + parser.add_argument( + '--overwrite', + action='store_true', + help=''' + Provide this flag if the output file already exists and you'd like to + overwrite it. + ''', + ) parser.set_defaults(func=bitwise_or_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/blankimage.py b/blankimage.py new file mode 100644 index 0000000..0b30e10 --- /dev/null +++ b/blankimage.py @@ -0,0 +1,68 @@ +import argparse +import PIL.Image +import sys + +from voussoirkit import betterhelp +from voussoirkit import vlogging + +log = vlogging.getLogger(__name__, 'blankimage') + +def blankimage_argparse(args): + if args.width and args.height: + size = (args.width, args.height) + else: + size = (512, 512) + + if args.color: + color = args.color + else: + color = (255, 255, 255, 255) + + image = PIL.Image.new('RGBA', size, color=color) + for filename in args.names: + image.save(filename) + + return 0 + +@vlogging.main_decorator +def main(argv): + parser = argparse.ArgumentParser( + description=''' + Create a blank image file. + ''', + ) + parser.add_argument( + 'names', + nargs='+', + help=''' + One or more filenames. The same image will be saved to each. + ''', + ) + parser.add_argument( + '--width', + default=None, + type=int, + help=''' + ''', + ) + parser.add_argument( + '--height', + default=None, + type=int, + help=''' + ''', + ) + parser.add_argument( + '--color', + default=None, + type=str, + help=''' + A hex color like #fff or #0a0a0a or #ff0000ff. + ''', + ) + parser.set_defaults(func=blankimage_argparse) + + return betterhelp.go(parser, argv) + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/brename.py b/brename.py index 7128a9e..5e0255f 100644 --- a/brename.py +++ b/brename.py @@ -1,19 +1,3 @@ -''' -Batch rename files by providing a string to be `eval`ed, using variable `x` as -the current filename. Yes I know this is weird, but for certain tasks it's just -too quick and easy to pass up. - -Examples: - -Prefix all the files: -brename.py "f'Test_{x}'" - -Rename files to their index with 0 padding: -brename.py "f'{index1:>03}{dot_ext}'" - -Keep the first word and extension: -brename.py "(x.split(' ')[0] + dot_ext) if ' ' in x else x" -''' import argparse import os import random @@ -125,53 +109,70 @@ def brename_argparse(args): recurse=args.recurse, ) -DOCSTRING = ''' -brename - batch file renaming -============================= - -> brename.py eval_string - -eval_string: - A string which will be evaluated by Python's eval. The name of the file or - folder will be in the variable `x`. In addition, many other variables are - provided for your convenience: - `quote` ("), `apostrophe` (') so you don't have to escape command quotes. - `hyphen` (-) because leading hyphens often cause problems with argparse. - `stringtools` entire stringtools module. See voussoirkit/stringtools.py. - `space` ( ), `dot` (.), `underscore` (_) so you don't have to add quotes to - your command while using these common characters. - `index` the file's index within the loop. - `index1` the file's index+1, in case you want your names to start from 1. - `parent` a pathclass.Path object for the directory containing the file. - `cwd` a pathclass.Path object for the cwd of this program session. - `noext` the name of the file, but without its extension. - `ext` the file's extension, with no dot. - --y | --yes: - Accept the results without confirming. - ---recurse: - Recurse into subfolders and rename those files too. - ---naturalsort: - Before renaming, the files will be sorted using natural sort instead of the - default lexicographic sort. Natural sort means that "My file 20" will come - before "My file 100" because 20<100. Lexicographic sort means 100 will come - first because 1 is before 2. - The purpose of this flag is so your index and index1 variables are applied - in the order you desire. -''' - def main(argv): - parser = argparse.ArgumentParser(description=__doc__) - - parser.add_argument('transformation', help='python command using x as variable name') - parser.add_argument('-y', '--yes', dest='autoyes', action='store_true') - parser.add_argument('--recurse', action='store_true') - parser.add_argument('--naturalsort', action='store_true') + parser = argparse.ArgumentParser( + description=''' + Batch rename files by providing a string to be `eval`ed, using variable `x` as + the current filename. Yes I know this is weird, but for certain tasks it's just + too quick and easy to pass up. + ''', + ) + parser.examples = [ + {'args': ['f\'Test_{x}\''], 'comment': 'Prefix all the files'}, + {'args': ['f\'{index1:>03}{dot_ext}\''], 'comment': 'Rename files to their index with 0 padding'}, + {'args': ['(x.split(space)[0] + dot_ext) if space in x else x'], 'comment': 'Keep the first word and extension'}, + ] + parser.add_argument( + 'transformation', + help=''' + A string which will be evaluated by Python's eval. The name of the file or + folder will be in the variable `x`. In addition, many other variables are + provided for your convenience: + `quote` ("), `apostrophe` (') so you don't have to escape command quotes. + `hyphen` (-) because leading hyphens often cause problems with argparse. + `stringtools` entire stringtools module. See voussoirkit/stringtools.py. + `space` ( ), `dot` (.), `underscore` (_) so you don't have to add quotes to + your command while using these common characters. + `index` the file's index within the loop. + `index1` the file's index+1, in case you want your names to start from 1. + `parent` a pathclass.Path object for the directory containing the file. + `cwd` a pathclass.Path object for the cwd of this program session. + `noext` the name of the file, but without its extension. + `ext` the file's extension, with no dot. + `dot_ext` the file's extension, with dot. + ''', + ) + parser.add_argument( + '-y', + '--yes', + dest='autoyes', + action='store_true', + help=''' + Accept the results without confirming. + ''', + ) + parser.add_argument( + '--recurse', + action='store_true', + help=''' + Recurse into subfolders and rename those files too. + ''', + ) + parser.add_argument( + '--naturalsort', + action='store_true', + help=''' + Before renaming, the files will be sorted using natural sort instead of the + default lexicographic sort. Natural sort means that "My file 20" will come + before "My file 100" because 20<100. Lexicographic sort means 100 will come + first because 1 is before 2. + The purpose of this flag is so your index and index1 variables are applied + in the order you desire. + ''', + ) parser.set_defaults(func=brename_argparse) - return betterhelp.single_main(argv, parser, DOCSTRING) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/contentreplace.py b/contentreplace.py index 2e6ffc8..3bb6aaa 100644 --- a/contentreplace.py +++ b/contentreplace.py @@ -1,36 +1,3 @@ -''' -contentreplace - find-and-replace en masse -========================================== - -> contentreplace filename_glob replace_from replace_to - -filename_glob: - A glob pattern that targets the files of interest. - -replace_from: - String to be replaced. - -replace_to: - String with which to replace. - -flags: ---recurse: - If provided, we will recurse into subdirectories and look for glob matches - there too. If not provided, only files in the cwd are affected. - ---regex: - If provided, the given replace_from, replace_to will be treated as regex - strings. If not provided, we use regular str.replace - ---clip_prompt: - If you want to do contentreplace with unicode that is difficult to enter - into your terminal, or multi-line strings that don't work as command line - arguments, this option might help you. The program will wait for you to put - the text of interest into your clipboard and press Enter. - ---yes: - If provided, replacements will occur automatically without prompting. -''' import argparse import codecs import pyperclip @@ -106,18 +73,69 @@ def contentreplace_argparse(args): @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) - - parser.add_argument('filename_glob') - parser.add_argument('replace_from') - parser.add_argument('replace_to') - parser.add_argument('--yes', dest='autoyes', action='store_true') - parser.add_argument('--recurse', action='store_true') - parser.add_argument('--regex', dest='do_regex', action='store_true') - parser.add_argument('--clip_prompt', '--clip-prompt', action='store_true') + parser = argparse.ArgumentParser( + description=''' + find-and-replace en masse + ''', + ) + parser.add_argument( + 'filename_glob', + help=''' + A glob pattern that targets the files of interest. + ''', + ) + parser.add_argument( + 'replace_from', + help=''' + String to be replaced. You can use backslash-escaped symbols like + \\n for newline. + ''', + ) + parser.add_argument( + 'replace_to', + help=''' + String with which to replace. Can use backslash-escaped symbols. + ''', + ) + parser.add_argument( + '--yes', + dest='autoyes', + action='store_true', + help=''' + If provided, replacements will occur automatically without prompting. + ''', + ) + parser.add_argument( + '--recurse', + action='store_true', + help=''' + If provided, we will recurse into subdirectories and look for glob matches + there too. If not provided, only files in the cwd are affected. + ''', + ) + parser.add_argument( + '--regex', + dest='do_regex', + action='store_true', + help=''' + If provided, the given replace_from, replace_to will be treated as regex + strings. If not provided, we use regular str.replace. + ''', + ) + parser.add_argument( + '--clip_prompt', + '--clip-prompt', + action='store_true', + help=''' + If you want to do contentreplace with unicode that is difficult to enter + into your terminal, or multi-line strings that don't work as command line + arguments, this option might help you. The program will wait for you to put + the text of interest into your clipboard and press Enter. + ''', + ) parser.set_defaults(func=contentreplace_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/directory_discrepancy.py b/directory_discrepancy.py index 4e7b3af..33f7c7c 100644 --- a/directory_discrepancy.py +++ b/directory_discrepancy.py @@ -1,16 +1,3 @@ -''' -directory_discrepancy -===================== - -This program compares two directory and shows which files exist in each -directory that do not exist in the other. - -> directory_discrepancy dir1 dir2 - -flags: ---recurse: - Also check subdirectories. -''' import argparse import sys @@ -48,14 +35,24 @@ def directory_discrepancy_argparse(args): @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) - + parser = argparse.ArgumentParser( + description=''' + This program compares two directory and shows which files exist in each + directory that do not exist in the other. + ''', + ) parser.add_argument('dir1') parser.add_argument('dir2') - parser.add_argument('--recurse', action='store_true') + parser.add_argument( + '--recurse', + action='store_true', + help=''' + Also check subdirectories. + ''', + ) parser.set_defaults(func=directory_discrepancy_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/fdroidapk.py b/fdroidapk.py index c29539d..615c483 100644 --- a/fdroidapk.py +++ b/fdroidapk.py @@ -1,22 +1,3 @@ -''' -fdroidapk - F-Droid APK downloader -================================== - -> fdroidapk package_names - -package_names: - One or more package names to download, separated by spaces. You can find - the package name in the URL on f-droid.org. - For example, com.nutomic.syncthingandroid from the URL - https://f-droid.org/en/packages/com.nutomic.syncthingandroid/ - ---destination path: - Alternative path to download the apk files to. Default is cwd. - ---folders: - If provided, each apk will be downloaded into a separate folder named after - the package. -''' import argparse import bs4 import io @@ -33,6 +14,7 @@ from voussoirkit import httperrors from voussoirkit import operatornotify from voussoirkit import pathclass from voussoirkit import pipeable +from voussoirkit import progressbars from voussoirkit import vlogging log = vlogging.getLogger(__name__, 'fdroidapk') @@ -52,7 +34,7 @@ def download_file(url, path): return downloady.download_file( url, path, - callback_progress=downloady.Progress2, + progressbar=progressbars.bar1_bytestring, timeout=30, ) @@ -88,8 +70,7 @@ def normalize_package_name(package_name): @pipeable.ctrlc_return1 def fpk_argparse(args): - destination = pathclass.Path(args.destination) - destination.assert_is_directory() + args.destination.assert_is_directory() return_status = 0 @@ -117,10 +98,10 @@ def fpk_argparse(args): apk_url = f'https://f-droid.org/repo/{apk_basename}' if args.folders: - this_dest = destination.with_child(package) + this_dest = args.destination.with_child(package) this_dest.makedirs(exist_ok=True) else: - this_dest = destination + this_dest = args.destination this_dest = this_dest.with_child(apk_basename) if this_dest.exists: @@ -145,14 +126,39 @@ def fpk_argparse(args): @operatornotify.main_decorator(subject='fdroidapk.py') @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser(description='F-Droid APK downloader.') - parser.add_argument('packages', nargs='+') - parser.add_argument('--folders', action='store_true') - parser.add_argument('--destination', default='.') + parser.add_argument( + 'packages', + nargs='+', + type=str, + help=''' + One or more package names to download, separated by spaces. You can find + the package name in the URL on f-droid.org. + For example, com.nutomic.syncthingandroid from the URL + https://f-droid.org/en/packages/com.nutomic.syncthingandroid/ + ''', + ) + parser.add_argument( + '--folders', + action='store_true', + help=''' + If provided, each apk will be downloaded into a separate folder named after + the package. + If omitted, the apks are downloaded into the destination folder directly. + ''', + ) + parser.add_argument( + '--destination', + default=pathclass.cwd(), + type=pathclass.Path, + help=''' + Alternative path to download the apk files to. Default is cwd. + ''', + ) parser.set_defaults(func=fpk_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/filepull.py b/filepull.py index 9e86232..906f4ff 100644 --- a/filepull.py +++ b/filepull.py @@ -1,6 +1,3 @@ -''' -Pull all of the files in nested directories into the current directory. -''' import argparse import os import sys @@ -9,10 +6,15 @@ from voussoirkit import interactive from voussoirkit import pathclass from voussoirkit import pipeable from voussoirkit import spinal +from voussoirkit import winglob -def filepull(pull_from='.', autoyes=False): +def filepull(pull_from='.', globs=None, autoyes=False): start = pathclass.Path(pull_from) - files = [file for d in start.listdir_directories() for file in d.walk_files()] + files = [ + file + for d in start.listdir_directories() + for file in spinal.walk(d, glob_filenames=globs) + ] if len(files) == 0: pipeable.stderr('No files to move') @@ -45,12 +47,23 @@ def filepull(pull_from='.', autoyes=False): return 1 def filepull_argparse(args): - return filepull(pull_from=args.pull_from, autoyes=args.autoyes) + return filepull(pull_from=args.pull_from, globs=args.glob, autoyes=args.autoyes) def main(argv): - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser( + description=''' + Pull all of the files in nested directories into the current directory. + ''', + ) parser.add_argument('pull_from', nargs='?', default='.') + parser.add_argument( + '--glob', + nargs='+', + help=''' + Only pull files whose basename matches any of these glob patterns. + ''', + ) parser.add_argument('-y', '--yes', dest='autoyes', action='store_true') parser.set_defaults(func=filepull_argparse) diff --git a/fuchsiatransparent.py b/fuchsiatransparent.py new file mode 100644 index 0000000..d063146 --- /dev/null +++ b/fuchsiatransparent.py @@ -0,0 +1,64 @@ +import argparse +import PIL.Image +import sys + +from voussoirkit import betterhelp +from voussoirkit import imagetools +from voussoirkit import pathclass +from voussoirkit import pipeable +from voussoirkit import vlogging + +log = vlogging.getLogger(__name__, 'fuchsiatransparent') + +FUCHSIA = (255, 0, 255, 255) +TRANSPARENT = (0, 0, 0, 0) + +def fuchsiatransparent_argparse(args): + patterns = pipeable.input_many(args.patterns) + files = pathclass.glob_many_files(patterns) + for file in files: + image = PIL.Image.open(file.absolute_path) + if image.mode == 'RGB': + image = image.convert('RGBA') + if image.mode == 'RGBA': + image = imagetools.replace_color(image, FUCHSIA, TRANSPARENT) + else: + log.info('Can\'t process %s', file.absolute_path) + continue + + if args.inplace: + outpath = file + else: + outname = file.replace_extension('').basename + '_transparent' + outpath = file.parent.with_child(outname).add_extension(file.extension) + + pipeable.stderr(outpath.absolute_path) + image.save(outpath.absolute_path) + return 0 + +@vlogging.main_decorator +def main(argv): + parser = argparse.ArgumentParser( + description=''' + Replace #FF00FF colored pixels with transparent. + ''', + ) + parser.add_argument( + 'patterns', + help=''' + One or more glob patterns for input files. + ''', + ) + parser.add_argument( + '--inplace', + action='store_true', + help=''' + Overwrite the input file instead of saving it as _transparent. + ''', + ) + parser.set_defaults(func=fuchsiatransparent_argparse) + + return betterhelp.go(parser, argv) + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/getcrx.py b/getcrx.py index 77031d7..48a0bca 100644 --- a/getcrx.py +++ b/getcrx.py @@ -127,6 +127,7 @@ def getcrx_argparse(args): else: auto_overwrite = None + return_status = 0 for extension_id in extension_ids: try: download_crx(extension_id, auto_overwrite=auto_overwrite) @@ -136,7 +137,8 @@ def getcrx_argparse(args): else: log.error(traceback.format_exc()) pipeable.stderr('Resuming...') - return 0 + return_status = 1 + return return_status @operatornotify.main_decorator(subject='getcrx') @vlogging.main_decorator diff --git a/getpid.py b/getpid.py index 38da917..e77f737 100644 --- a/getpid.py +++ b/getpid.py @@ -1,17 +1,3 @@ -''' -getpid -====== - -Get PIDs for running processes that match the given process name. - -Error level will be 0 if any processes are found, 1 if none are found. - -> getpid process_name - -Examples: -> getpid python.exe -> getpid chrome.exe -''' import argparse import psutil import sys @@ -32,12 +18,24 @@ def getpid_argparse(args): @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser( + description=''' + Get PIDs for running processes that match the given process name. - parser.add_argument('process_name') + Error level will be 0 if any processes are found, 1 if none are found. + ''', + ) + + parser.add_argument( + 'process_name', + type=str, + help=''' + Name like "python.exe" or "chrome" as it appears in your task manager / ps. + ''', + ) parser.set_defaults(func=getpid_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/gitcheckup.py b/gitcheckup.py index 53fed9c..702f9e8 100644 --- a/gitcheckup.py +++ b/gitcheckup.py @@ -1,50 +1,3 @@ -''' -gitcheckup -========== - -This program helps you check the commit and push status of your favorite git -repositories. The output looks like this: - -[ ][P] D:\\Git\\cmd (~1) -[C][P] D:\\Git\\Etiquette -[ ][P] D:\\Git\\voussoirkit (+1) -[C][ ] D:\\Git\\YCDL (↑3) - -To specify the list of git directories, you may either: -- Create a gitcheckup.txt file in the same directory as this file, where every - line contains an absolute path to the directory, or -- Pass directories as a series of positional arguments to this program. - -> gitcheckup.py -> gitcheckup.py dir1 dir2 - -flags: ---fetch: - Run `git fetch --all` in each directory. - ---pull: - Run `git pull --all` in each directory. - ---push: - Run `git push` in each directory. - ---run : - Run `git ` in each directory. You can use \- to escape - in your - git arguments, since they would confuse this program's argparse. - If this is used, any --fetch, --pull, --push is ignored. - ---add path: - Add path to the gitcheckup.txt file. - ---remove path: - Remove path from the gitcheckup.txt file. - -Examples: -> gitcheckup -> gitcheckup --fetch -> gitcheckup D:\\Git\\cmd D:\\Git\\YCDL --pull -> gitcheckup --run add README.md -''' import argparse import os import re @@ -349,19 +302,91 @@ def gitcheckup_argparse(args): @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser( + description=''' + This program helps you check the commit and push status of your favorite git + repositories. The output looks like this: - parser.add_argument('directories', nargs='*') - parser.add_argument('--fetch', dest='do_fetch', action='store_true') - parser.add_argument('--pull', dest='do_pull', action='store_true') - parser.add_argument('--push', dest='do_push', action='store_true') - parser.add_argument('--add', dest='add_directory') - parser.add_argument('--run', dest='run_command', nargs='+') - parser.add_argument('--remove', dest='remove_directory') + [ ][P] D:\\Git\\cmd (~1) + [C][P] D:\\Git\\Etiquette + [ ][P] D:\\Git\\voussoirkit (+1) + [C][ ] D:\\Git\\YCDL (↑3) + ''', + ) + parser.examples = [ + '', + '--fetch', + 'D:\\Git\\cmd D:\\Git\\YCDL --pull', + '--run add README.md', + ] + + parser.add_argument( + 'directories', + nargs='*', + help=''' + One or more directories to check up. + If omitted, you should have a file called gitcheckup.txt in the same + directory as this file, where every line contains an absolute path to + a directory. + ''', + ) + parser.add_argument( + '--fetch', + dest='do_fetch', + action='store_true', + help=''' + Run `git fetch --all` in each directory. + ''', + ) + parser.add_argument( + '--pull', + dest='do_pull', + action='store_true', + help=''' + Run `git pull --all` in each directory. + ''', + ) + parser.add_argument( + '--push', + dest='do_push', + action='store_true', + help=''' + Run `git push` in each directory. + ''', + ) + parser.add_argument( + '--run', + dest='run_command', + nargs='+', + type=str, + help=''' + Run `git ` in each directory. You can use \- to escape - in your + git arguments, since they would confuse this program's argparse. + If this is used, any --fetch, --pull, --push is ignored. + ''', + ) + parser.add_argument( + '--add', + dest='add_directory', + metavar='path', + type=str, + help=''' + Add path to the gitcheckup.txt file. + ''', + ) + parser.add_argument( + '--remove', + dest='remove_directory', + metavar='path', + type=str, + help=''' + Remove path from the gitcheckup.txt file. + ''', + ) parser.set_defaults(func=gitcheckup_argparse) try: - return betterhelp.single_main(argv, parser, docstring=__doc__) + return betterhelp.go(parser, argv) except GitCheckupException as exc: print(exc) return 1 diff --git a/icoconvert.py b/icoconvert.py index a926cba..633646e 100644 --- a/icoconvert.py +++ b/icoconvert.py @@ -85,11 +85,17 @@ # | 40 | n | Pixel bytes, r, g, b, a. | # |________|______________|_______________________________________________________| +import argparse import os import PIL.Image import sys +from voussoirkit import betterhelp from voussoirkit import imagetools +from voussoirkit import pipeable +from voussoirkit import vlogging + +log = vlogging.get_logger(__name__, 'icoconvert') ICO_HEADER_LENGTH = 6 ICON_DIRECTORY_ENTRY_LENGTH = 16 @@ -230,17 +236,36 @@ def images_to_ico(images): final_data = b''.join(datablobs) return final_data -if __name__ == '__main__': - try: - inputfiles = sys.argv[1:] - except Exception: - print('Please provide an image file') - raise SystemExit - print('Iconifying', inputfiles) - images = [load_image(filename) for filename in inputfiles] +def icoconvert_argparse(args): + log.info('Iconifying %s', args.files) + images = [load_image(filename) for filename in args.files] + final_data = images_to_ico(images) - name = os.path.splitext(inputfiles[0])[0] + '.ico' - output_file = open(name, 'wb') + + iconame = os.path.splitext(args.files[0])[0] + '.ico' + output_file = open(iconame, 'wb') output_file.write(final_data) output_file.close() - print('Finished %s.' % name) + pipeable.stderr(iconame) + return 0 + +@vlogging.main_decorator +def main(argv): + parser = argparse.ArgumentParser( + description=''' + Create a Windows .ico icon file from one or more images. + ''', + ) + parser.add_argument( + 'files', + nargs='+', + help=''' + One or more image files to put into the ico. + ''', + ) + parser.set_defaults(func=icoconvert_argparse) + + return betterhelp.go(parser, argv) + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/inodes.py b/inodes.py index ae7803e..9007141 100644 --- a/inodes.py +++ b/inodes.py @@ -13,12 +13,22 @@ def inodes_argparse(args): return 0 def main(argv): - parser = argparse.ArgumentParser(description=__doc__) - - parser.add_argument('patterns', nargs='+') + parser = argparse.ArgumentParser( + description=''' + Show the st_dev, st_ino of files. + ''', + ) + parser.add_argument( + 'patterns', + nargs='+', + help=''' + One or more glob patterns. Supports pipeable !c clipboard, !i stdin + lines of patterns. + ''', + ) parser.set_defaults(func=inodes_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/named_python.py b/named_python.py index d3d01e2..7f1a255 100644 --- a/named_python.py +++ b/named_python.py @@ -1,22 +1,3 @@ -''' -named_python -============ - -Because Python is interpreted, when you look at the task manager / process list -you'll see that every running python instance has the same name, python.exe. -This script helps you name the executables so they stand out. - -For the time being this script doesn't automatically call your new exe, you -have to write a second command to actually run it. I tried using -subprocess.Popen to spawn the new python with the rest of argv but the behavior -was different on Linux and Windows and neither was really clean. - -> named_python name - -Examples: -> named_python myserver && python-myserver server.py --port 8080 -> named_python hnarchive && python-hnarchive hnarchive.py livestream -''' import argparse import os import sys @@ -40,12 +21,29 @@ def namedpython_argparse(args): return 0 def main(argv): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser( + description=''' + Because Python is interpreted, when you look at the task manager / process list + you'll see that every running python instance has the same name, python.exe. + This script helps you name the executables so they stand out. - parser.add_argument('name') + For the time being this script doesn't automatically call your new exe, you + have to write a second command to actually run it. I tried using + subprocess.Popen to spawn the new python with the rest of argv but the behavior + was different on Linux and Windows and neither was really clean. + ''', + ) + parser.add_argument( + 'name', + type=str, + help=''' + If you invoke this script with python.exe, a hardlink python-{name}.exe + will be created. Also works with pythonw. + ''', + ) parser.set_defaults(func=namedpython_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/nosmartquotes.py b/nosmartquotes.py index 175c5be..a6778c4 100644 --- a/nosmartquotes.py +++ b/nosmartquotes.py @@ -1,17 +1,10 @@ -''' -no smart quotes -=============== - -Replace smart quotes and smart apostrophes with regular ASCII values. - -Just say no to smart quotes! -''' import argparse import os import sys from voussoirkit import betterhelp from voussoirkit import pathclass +from voussoirkit import pipeable from voussoirkit import spinal from voussoirkit import vlogging @@ -27,8 +20,9 @@ def replace_smartquotes(text): return text def nosmartquotes_argparse(args): + globs = list(pipeable.input_many(args.patterns)) files = spinal.walk( - glob_filenames=args.filename_glob, + glob_filenames=globs, exclude_filenames={THIS_FILE}, recurse=args.recurse, ) @@ -43,19 +37,39 @@ def nosmartquotes_argparse(args): continue file.write('w', text, encoding='utf-8') - print(file.absolute_path) + pipeable.stdout(file.absolute_path) return 0 @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser( + description=''' + Replace smart quotes and smart apostrophes with regular ASCII values. - parser.add_argument('filename_glob') - parser.add_argument('--recurse', action='store_true') + Just say no to smart quotes! + ''', + ) + parser.examples = [ + '*.md --recurse', + ] + parser.add_argument( + 'patterns', + nargs='+', + help=''' + One or more glob patterns for input files. + ''', + ) + parser.add_argument( + '--recurse', + action='store_true', + help=''' + If provided, recurse into subdirectories and process those files too. + ''', + ) parser.set_defaults(func=nosmartquotes_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/prune_dirs.py b/prune_dirs.py index b9e2f17..c43a860 100644 --- a/prune_dirs.py +++ b/prune_dirs.py @@ -1,11 +1,3 @@ -''' -This program deletes all empty directories which are children of the given -starting directory. The starting directory itself will not be deleted even -if it is empty. - -> prune_dirs . -> prune_dirs C:\\somepath -''' import argparse import os import sys @@ -46,12 +38,17 @@ def prune_dirs_argparse(args): @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) - + parser = argparse.ArgumentParser( + description=''' + This program deletes all empty directories which are children of the given + starting directory. The starting directory itself will not be deleted even + if it is empty. + ''', + ) parser.add_argument('starting') parser.set_defaults(func=prune_dirs_argparse) - return betterhelp.single_main(argv, parser, docstring=__doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/rarpar.py b/rarpar.py index 0d213f7..526fa31 100644 --- a/rarpar.py +++ b/rarpar.py @@ -460,64 +460,6 @@ def rarpar( # COMMAND LINE ##################################################################################### -DOCSTRING = ''' -rarpar -====== - -> rarpar path - -path: - The input file or directory to rarpar. - ---volume X | X% | min(A, B) | max(A, B): - Split rars into volumes of this many megabytes. Should be - An integer number of megabytes, or; - A percentage "X%" to calculate volumes as X% of the file size, down to - a 1 MB minimum, or; - A string "min(A, B)" or "max(A, B)" where A and B follow the above rules. - ---rec X: - An integer to generate X% recovery record in the rars. - See winrar documentation for information about recovery records. - ---rev X: - An integer to generate X% recovery volumes. - Note that winrar's behavior is the number of revs will always be less than - the number of rars. If you don't split volumes, you will have 1 rar and - thus 0 revs even if you ask for 100% rev. - See winrar documentation for information about recovery volumes. - ---par X: - A number to generate X% recovery with par2. - ---basename X: - A basename for the rar and par files. You will end up with - basename.partXX.rar and basename.par2. - Without this argument, the default basename is "{basename} ({timestamp})". - Your string may include {basename}, {timestamp} and/or {date} including the - braces to insert that value there. - ---compression X: - Level of compression. Can be "store" or "max" or integer 0-5. - ---password X: - A password with which to encrypt the rar files. - ---workdir X: - The directory in which the rars and pars will be generated while the - program is working. - ---moveto X: - The directory to which the rars and pars will be moved after the program - has finished working. - ---recycle: - The input file or directory will be recycled at the end. - ---dry: - Print the commands that will be run, but don't actually run them. -''' - def rarpar_argparse(args): status = 0 try: @@ -549,24 +491,127 @@ def rarpar_argparse(args): def main(argv): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('path') - parser.add_argument('--volume') - parser.add_argument('--rec') - parser.add_argument('--rev') - parser.add_argument('--par') - parser.add_argument('--basename') - parser.add_argument('--compression') - parser.add_argument('--password') - parser.add_argument('--profile', dest='rar_profile') - parser.add_argument('--workdir', default='.') - parser.add_argument('--moveto') - parser.add_argument('--recycle', dest='recycle_original', action='store_true') - parser.add_argument('--dictionary', dest='dictionary_size') - parser.add_argument('--solid', action='store_true') - parser.add_argument('--dry', action='store_true') + parser.add_argument( + 'path', + type=pathclass.Path, + help=''' + The input file or directory to rarpar. + ''', + ) + parser.add_argument( + '--volume', + help=''' + Split rars into volumes of this many megabytes. Should be: + - An integer number of megabytes, or + - A percentage "X%" to calculate volumes as X% of the file size, down to + a 1 MB minimum, or + - A string "min(A, B)" or "max(A, B)" where A and B follow the above rules. + ''', + ) + parser.add_argument( + '--rec', + type=int, + help=''' + An integer to generate X% recovery record in the rars. + See winrar documentation for information about recovery records. + ''', + ) + parser.add_argument( + '--rev', + type=int, + help=''' + An integer to generate X% recovery volumes. + Note that winrar's behavior is the number of revs will always be less than + the number of rars. If you don't split volumes, you will have 1 rar and + thus 0 revs even if you ask for 100% rev. + See winrar documentation for information about recovery volumes. + ''', + ) + parser.add_argument( + '--par', + type=int, + help=''' + A number to generate X% recovery with par2. + ''', + ) + parser.add_argument( + '--basename', + type=str, + help=''' + A basename for the rar and par files. You will end up with + basename.partXX.rar and basename.par2. + Without this argument, the default basename is "{basename} ({timestamp})". + Your string may include {basename}, {timestamp} and/or {date} including the + braces to insert that value there. + ''', + ) + parser.add_argument( + '--compression', + help=''' + Level of compression. Can be "store" or "max" or integer 0-5. + ''', + ) + parser.add_argument( + '--password', + type=str, + help=''' + A password with which to encrypt the rar files. + ''', + ) + parser.add_argument( + '--profile', dest='rar_profile', + ) + parser.add_argument( + '--workdir', + type=pathclass.Path, + default='.', + help=''' + The directory in which the rars and pars will be generated while the + program is working. + ''', + ) + parser.add_argument( + '--moveto', + type=pathclass.Path, + help=''' + The directory to which the rars and pars will be moved after the program + has finished working. + ''', + ) + parser.add_argument( + '--recycle', + dest='recycle_original', + action='store_true', + help=''' + The input file or directory will be recycled at the end. + ''', + ) + parser.add_argument( + '--dictionary', + dest='dictionary_size', + help=''' + Larger dictionary sizes can improve compression in exchange for higher + memory usage. Accepted values are 128k, 256k, 512k, 1m, 2m, 4m, 8m, 16m, + 32m, 64m, 128m, 256m, 512m, 1g. + ''' + ) + parser.add_argument( + '--solid', + action='store_true', + help=''' + Generate a 'solid' rar archive. See winrar's documentation for details. + ''', + ) + parser.add_argument( + '--dry', + action='store_true', + help=''' + Print the commands that will be run, but don't actually run them. + ''', + ) parser.set_defaults(func=rarpar_argparse) - return betterhelp.single_main(argv, parser, DOCSTRING) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/reg_extension_icon.py b/reg_extension_icon.py index 97f21e3..25a761f 100644 --- a/reg_extension_icon.py +++ b/reg_extension_icon.py @@ -1,32 +1,3 @@ -''' -reg_extension_icon -================== - -This script edits the windows registry HKEY_CLASSES_ROOT to assign a file -extension icon and optionally a human-friendly name string. - -Must run as administrator. - -WARNING, if the extension is already associated with a program, or is otherwise -connected to a progid, this will break it. - -> reg_extension_icon ico_file - -ico_file: - Filepath of the icon file. - ---extension: - If you omit this option, your file should be named "png.ico" or "py.ico" to - set the icon for png and py types. If the name of your ico file is not the - name of the extension you want to control, specify the extension here. - ---name: - A human-friendly name string which will show on Explorer under the "Type" - column and in the properties dialog. - ---shellopen: - A command-line string to use as the shell\open\command -''' import argparse import sys import winreg @@ -97,16 +68,56 @@ def extension_registry_argparse(args): ) def main(argv): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser( + description=''' + This script edits the windows registry HKEY_CLASSES_ROOT to assign a file + extension icon and optionally a human-friendly name string. - parser.add_argument('ico_file') - parser.add_argument('--extension', default=None) - parser.add_argument('--name', default=None) - parser.add_argument('--shellopen', default=None) - parser.add_argument('--yes', dest='autoyes', action='store_true') + Must run as administrator. + + WARNING, if the extension is already associated with a program, or is otherwise + connected to a progid, this will break it. + ''', + ) + parser.add_argument( + 'ico_file', + help=''' + Filepath of the icon file. + ''', + ) + parser.add_argument( + '--extension', + default=None, + help=''' + If you omit this option, your file should be named "png.ico" or "py.ico" to + set the icon for png and py types. If the name of your ico file is not the + name of the extension you want to control, specify the extension here. + ''', + ) + parser.add_argument( + '--name', + type=str, + default=None, + help=''' + A human-friendly name string which will show on Explorer under the "Type" + column and in the properties dialog. + ''', + ) + parser.add_argument( + '--shellopen', + default=None, + help=''' + A command-line string to use as the shell\\open\\command + ''', + ) + parser.add_argument( + '--yes', + dest='autoyes', + action='store_true', + ) parser.set_defaults(func=extension_registry_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/reserve_disk_space.py b/reserve_disk_space.py index 7dbea1e..84c1d80 100644 --- a/reserve_disk_space.py +++ b/reserve_disk_space.py @@ -1,17 +1,3 @@ -''' -reserve_disk_space -================== - -Exits with status of 0 if the disk has the requested amount of space, 1 if not. - -> reserve_disk_space reserve [drive] - -reserve: - A string like "50g" or "100 gb" - -drive: - Filepath to the drive you want to check. Defaults to cwd drive. -''' import argparse import sys @@ -39,13 +25,29 @@ def reserve_disk_space_argparse(args): @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) - - parser.add_argument('reserve') - parser.add_argument('drive', nargs='?', default='.') + parser = argparse.ArgumentParser( + description=''' + Exits with status of 0 if the disk has the requested amount of space, 1 if not. + ''', + ) + parser.add_argument( + 'reserve', + type=str, + help=''' + A string like "50g" or "100 gb" + ''', + ) + parser.add_argument( + 'drive', + nargs='?', + default='.', + help=''' + Filepath to the drive you want to check. + ''', + ) parser.set_defaults(func=reserve_disk_space_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/resize.py b/resize.py index eb25096..2bf00d9 100644 --- a/resize.py +++ b/resize.py @@ -1,65 +1,3 @@ -''' -resize -====== - -Resize image files. - -> resize patterns - -patterns: - One or more glob patterns for input files. - Uses pipeable to support !c clipboard, !i stdin lines of glob patterns. - -flags: ---width X: ---height X: - New dimensions for the image. If either of these is omitted, then that - dimension will be calculated automatically based on the aspect ratio. - ---break_aspect_ratio: - If provided, the given --width and --height will be used exactly. You will - need to provide both --width and --height. - - If omitted, the image will be resized to fit within the bounds provided by - --width and --height while preserving its aspect ratio. - ---output X: - A string that controls the output filename format. Suppose the input file - was myphoto.jpg. You can use these variables in your format string: - {base} = myphoto - {filename} = myphoto.jpg - {width} = an integer - {height} = an integer - {extension} = .jpg - - You may omit {extension} from your format string and it will automatically - be added to the end, unless you already provided a different extension. - - If your format string only designates a basename, output files will go to - the same directory as the corresponding input file. If your string contains - path separators, all output files will go to that directory. - The directory - part is not formatted with the variables. - ---inplace: - Overwrite the input files. Cannot be used along with --output. - Be careful! - ---nearest: - If provided, use nearest-neighbor scaling to preserve pixelated images. - If omitted, use antialiased scaling. - ---only_shrink: - If the input image is smaller than the requested dimensions, do nothing. - Useful when globbing in a directory with many differently sized images. - ---quality X: - JPEG compression quality. - ---scale X: - Scale the image by factor X. - Use this option instead of --width, --height. -''' import argparse import os import PIL.Image @@ -77,15 +15,6 @@ log = vlogging.getLogger(__name__, 'resize') OUTPUT_INPLACE = sentinel.Sentinel('output inplace') DEFAULT_OUTPUT_FORMAT = '{base}_{width}x{height}{extension}' -def resize_core( - image, - height=None, - only_shrink=False, - scale=None, - width=None, - ): - pass - def resize( filename, *, @@ -215,21 +144,121 @@ def resize_argparse(args): @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser(description='Resize image files.') + parser.examples = [ + 'myphoto.jpg --scale 0.5 --inplace', + '*.jpg --only_shrink --width 500 --height 500 --output thumbs\\{base}.jpg', + 'sprite*.png sprite*.bmp --width 1024 --nearest --output {base}_big{extension}', + ] - parser.add_argument('patterns', nargs='+') - parser.add_argument('--width', type=int, default=None) - parser.add_argument('--height', type=int, default=None) - parser.add_argument('--inplace', action='store_true') - parser.add_argument('--nearest', dest='nearest_neighbor', action='store_true') - parser.add_argument('--only_shrink', '--only-shrink', action='store_true') - parser.add_argument('--break_aspect_ratio', '--break-aspect-ratio', action='store_true') - parser.add_argument('--output', default=None) - parser.add_argument('--scale', type=float, default=None) - parser.add_argument('--quality', type=int, default=100) + parser.add_argument( + 'patterns', + nargs='+', + type=str, + help=''' + One or more glob patterns for input files. + Uses pipeable to support !c clipboard, !i stdin lines of glob patterns. + ''', + ) + parser.add_argument( + '--width', + type=int, + default=None, + help=''' + New width of the image. If --width is omitted and --height is given, then + width will be calculated automatically based on the aspect ratio. + ''', + ) + parser.add_argument( + '--height', + type=int, + default=None, + help=''' + New width of the image. If --height is omitted and --width is given, then + height will be calculated automatically based on the aspect ratio. + ''', + ) + parser.add_argument( + '--break_aspect_ratio', + '--break-aspect-ratio', + action='store_true', + help=''' + If provided, the given --width and --height will be used exactly. You will + need to provide both --width and --height. + + If omitted, the image will be resized to fit within the bounds provided by + --width and --height while preserving its aspect ratio. + ''', + ) + parser.add_argument( + '--inplace', + action='store_true', + help=''' + Overwrite the input files. Cannot be used along with --output. + Be careful! + ''', + ) + parser.add_argument( + '--nearest', + '--nearest_neighbor', + '--nearest-neighbor', + dest='nearest_neighbor', + action='store_true', + help=''' + If provided, use nearest-neighbor scaling to preserve pixelated images. + If omitted, use antialiased scaling. + ''', + ) + parser.add_argument( + '--only_shrink', + '--only-shrink', + action='store_true', + help=''' + If the input image is smaller than the requested dimensions, do nothing. + Useful when globbing in a directory with many differently sized images. + ''', + ) + parser.add_argument( + '--output', + default=None, + help=''' + A string that controls the output filename format. Suppose the input file + was myphoto.jpg. You can use these variables in your format string: + {base} = myphoto + {filename} = myphoto.jpg + {width} = an integer + {height} = an integer + {extension} = .jpg + + You may omit {extension} from your format string and it will automatically + be added to the end, unless you already provided a different extension. + + If your format string only designates a basename, output files will go to + the same directory as the corresponding input file. If your string contains + path separators, all output files will go to that directory. + The directory part is not formatted with the variables. + ''', + ) + parser.add_argument( + '--scale', + type=float, + default=None, + help=''' + Scale the image by this factor, where 1.00 is regular size. + Use this option instead of --width, --height. + ''', + ) + parser.add_argument( + '--quality', + type=int, + default=100, + help=''' + JPEG compression quality. + ''' + ) parser.set_defaults(func=resize_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/retry.py b/retry.py index aa28fbd..c1bb9af 100644 --- a/retry.py +++ b/retry.py @@ -2,6 +2,7 @@ import argparse import subprocess import sys import time +from voussoirkit import betterhelp class NoMoreRetries(Exception): pass @@ -43,15 +44,39 @@ def retry_argparse(args): ) def main(argv): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser( + description=''' + Run a command line command multiple times until it returns 0. + ''', + ) - parser.add_argument('command', nargs='+') - parser.add_argument('--limit', type=int, default=None) - parser.add_argument('--sleep', type=float, default=None) + parser.add_argument( + 'command', + nargs='+', + help=''' + A command line command. You may need to put this after -- to avoid + confusion with arguments to this program. + ''', + ) + parser.add_argument( + '--limit', + type=int, + default=None, + help=''' + Maximum number of retries before giving up. + ''', + ) + parser.add_argument( + '--sleep', + type=float, + default=None, + help=''' + Number of seconds of sleep between each retry. + ''', + ) parser.set_defaults(func=retry_argparse) - args = parser.parse_args(argv) - return args.func(args) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/shortcut.py b/shortcut.py index 7ea26e9..ac4312c 100644 --- a/shortcut.py +++ b/shortcut.py @@ -38,29 +38,6 @@ def shortcut(lnk_name, target, start_in=None, icon=None): shortcut.write() return lnk -DOCSTRING = ''' -shortcut -======== - -> shortcut lnk_path target - -lnk_path: - The filepath of the lnk file you want to create. - -target: - The filepath of the target file and any additional arguments separated - by spaces. If you want to include an argument that starts with hyphens, - consider putting this last and use `--` to indicate the end of named - arguments. For example: - > shortcut game.lnk --icon game.ico -- javaw.exe -jar game.jar - ---start-in: - Directory to use as CWD for the program. - ---icon: - Path to an .ico file. -''' - def shortcut_argparse(args): try: lnk = shortcut( @@ -77,14 +54,48 @@ def shortcut_argparse(args): def main(argv): parser = argparse.ArgumentParser(description=__doc__) + parser.examples = [ + 'game.lnk --icon game.ico -- javaw.exe -jar game.jar', + 'game.lnk --icon game.ico -- 4 8 6', + ] - parser.add_argument('lnk_name') - parser.add_argument('target', nargs='+') - parser.add_argument('--start_in', '--start-in', '--startin', default=None) - parser.add_argument('--icon', default=None) + parser.add_argument( + 'lnk_name', + help=''' + The filepath of the lnk file you want to create. + ''', + ) + parser.add_argument( + 'target', + nargs='+', + type=int, + help=''' + The filepath of the target file and any additional arguments separated + by spaces. If you want to include an argument that starts with hyphens, + consider putting this last and use `--` to indicate the end of named + arguments, since they might otherwise be mistaken for arguments to this + program. + ''', + ) + parser.add_argument( + '--start_in', + '--start-in', + '--startin', + default=None, + help=''' + Directory to use as CWD for the program. + ''', + ) + parser.add_argument( + '--icon', + default=None, + help=''' + Path to an .ico file. + ''', + ) parser.set_defaults(func=shortcut_argparse) - return betterhelp.single_main(argv, parser, DOCSTRING) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/stitch.py b/stitch.py index 9028dcf..3454098 100644 --- a/stitch.py +++ b/stitch.py @@ -2,6 +2,7 @@ import PIL.Image import argparse import sys +from voussoirkit import betterhelp from voussoirkit import pathclass from voussoirkit import pipeable from voussoirkit import sentinel @@ -49,14 +50,35 @@ def main(argv): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('image_files', nargs='+') - parser.add_argument('--output', required=True) - parser.add_argument('--horizontal', action='store_true') - parser.add_argument('--vertical', action='store_true') - parser.add_argument('--gap', type=int, default=0) + parser.add_argument( + '--output', + required=True, + ) + parser.add_argument( + '--horizontal', + action='store_true', + help=''' + Stitch the images together horizontally. + ''', + ) + parser.add_argument( + '--vertical', + action='store_true', + help=''' + Stitch the images together vertically. + ''', + ) + parser.add_argument( + '--gap', + type=int, + default=0, + help=''' + This many pixels of transparent gap between each row / column. + ''', + ) parser.set_defaults(func=stitch_argparse) - args = parser.parse_args(argv) - return args.func(args) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/svgrender.py b/svgrender.py index e523a53..32ba566 100644 --- a/svgrender.py +++ b/svgrender.py @@ -1,29 +1,3 @@ -''' -svgrender -========= - -Calls the Inkscape command line to render svg files to png. A link to inkscape -should be on your PATH. - -> svgrender svg_file scales - -scales: - One or more integers. Each integer will be the size of one output file. - -flags: ---destination: - A path to a directory where the png files should be saved. By default, - they go to the same folder as the svg file. - ---y: - By default, the scales control the width of the output image. - Pass this if you want the scales to control the height. - ---basename-only: - By default, the png filenames will have suffixes like _{scale}. - Pass this if you want the png to have the same name as the svg file. - Naturally, this only works if you're only using a single scale. -''' import argparse import glob import os @@ -79,11 +53,10 @@ def svgrender(filepath, scales, destination, scale_suffix=True, axis='x'): def svgrender_argparse(args): svg_paths = glob.glob(args.svg_filepath) - scales = [int(x) for x in args.scales] for svg_path in svg_paths: svgrender( svg_path, - scales, + scales=args.scales, destination=args.destination, scale_suffix=args.scale_suffix, axis='y' if args.y else 'x', @@ -93,16 +66,57 @@ def svgrender_argparse(args): @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) - - parser.add_argument('svg_filepath') - parser.add_argument('scales', nargs='+') - parser.add_argument('--destination', default=None) - parser.add_argument('--y', dest='y', action='store_true') - parser.add_argument('--basename_only', '--basename-only', dest='scale_suffix', action='store_false') + parser = argparse.ArgumentParser( + description=''' + Calls the Inkscape command line to render svg files to png. A link to Inkscape + should be on your PATH. + ''', + ) + parser.add_argument( + 'svg_filepath', + help=''' + Input svg file to be rendered. + ''', + ) + parser.add_argument( + 'scales', + type=int, + nargs='+', + help=''' + One or more integers. Each integer will be the size of one output file. + ''', + ) + parser.add_argument( + '--destination', + default=None, + help=''' + A path to a directory where the png files should be saved. By default, + they go to the same folder as the svg file. + ''', + ) + parser.add_argument( + '--y', + dest='y', + action='store_true', + help=''' + By default, the scales control the width of the output image. + Pass this if you want the scales to control the height. + ''', + ) + parser.add_argument( + '--basename_only', + '--basename-only', + dest='scale_suffix', + action='store_false', + help=''' + By default, the png filenames will have suffixes like _{scale}. + Pass this if you want the png to have the same name as the svg file. + Naturally, this only works if you're only using a single scale. + ''', + ) parser.set_defaults(func=svgrender_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': main(sys.argv[1:]) diff --git a/tempeditor.py b/tempeditor.py index 2b6d8e8..26738e9 100644 --- a/tempeditor.py +++ b/tempeditor.py @@ -1,21 +1,3 @@ -''' -tempeditor -========== - -This program allows you to use your preferred text editor as an intermediate -step in a processing pipeline. The user will use the text editor to edit a temp -file, and when they close the editor the contents of the temp file will be sent -to stdout. - -Command line usage: - -> tempeditor [--text X] - ---text X: - The initial text in the document. - Uses pipeable to support !c clipboard, !i stdin. - If not provided, the user starts with a blank document. -''' import argparse import os import shlex @@ -90,12 +72,29 @@ def tempeditor_argparse(args): @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser( + description=''' + This program allows you to use your preferred text editor as an + intermediate step in a processing pipeline. The user will use the text + editor to edit a temp file, and when they close the editor the contents + of the temp file will be sent to stdout. + ''', + ) - parser.add_argument('--text', dest='initial_text', default=None) + parser.add_argument( + '--text', + dest='initial_text', + default=None, + type=str, + help=''' + The initial text in the document. + Uses pipeable to support !c clipboard, !i stdin. + If not provided, the user starts with a blank document. + ''', + ) parser.set_defaults(func=tempeditor_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/threaded_dl.py b/threaded_dl.py index 0fbe529..b8c2a9a 100644 --- a/threaded_dl.py +++ b/threaded_dl.py @@ -1,40 +1,3 @@ -''' -threaded_dl -=========== - -> threaded_dl links thread_count filename_format - -links: - The name of a file containing links to download, one per line. - Uses pipeable to support !c clipboard, !i stdin lines of urls. - -thread_count: - Integer number of threads to use for downloading. - -filename_format: - A string that controls the names of the downloaded files. Uses Python's - brace-style formatting. Available formatters are: - - {basename}: The name of the file as indicated by the URL. - E.g. example.com/image.jpg -> image.jpg - - {extension}: The extension of the file as indicated by the URL, including - the dot. E.g. example.com/image.jpg -> .jpg - - {index}: The index of this URL within the sequence of all downloaded URLs. - Starts from 0. - - {now}: The unix timestamp at which this download job was started. It might - be ugly but at least it's unambiguous when doing multiple download batches - with similar filenames. - -flags: ---bytespersecond X: - Limit the overall download speed to X bytes per second. Uses - bytestring.parsebytes to support strings like "1m", "500k", "2 mb", etc. - ---headers X: - ; - ---timeout X: - Integer number of seconds to use as HTTP request timeout for each download. -''' import argparse import ast import os @@ -194,7 +157,7 @@ def threaded_dl( ui_thread.join() def ui_thread_func(meter, pool, stop_event): - if pipeable.OUT_PIPE: + if pipeable.stdout_pipe(): return while not stop_event.is_set(): @@ -236,17 +199,62 @@ def threaded_dl_argparse(args): @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) - - parser.add_argument('url_file') - parser.add_argument('thread_count', type=int) - parser.add_argument('filename_format', nargs='?', default='{now}_{index}_{basename}') - parser.add_argument('--bytespersecond', default=None) - parser.add_argument('--timeout', default=15) - parser.add_argument('--headers', nargs='+', default=None) + parser = argparse.ArgumentParser() + parser.add_argument( + 'url_file', + metavar='links', + help=''' + The name of a file containing links to download, one per line. + Uses pipeable to support !c clipboard, !i stdin lines of urls. + ''', + ) + parser.add_argument( + 'thread_count', + type=int, + help=''' + Integer number of threads to use for downloading. + ''', + ) + parser.add_argument( + 'filename_format', + nargs='?', + type=str, + default='{now}_{index}_{basename}', + help=''' + A string that controls the names of the downloaded files. Uses Python's + brace-style formatting. Available formatters are: + - {basename}: The name of the file as indicated by the URL. + E.g. example.com/image.jpg -> image.jpg + - {extension}: The extension of the file as indicated by the URL, including + the dot. E.g. example.com/image.jpg -> .jpg + - {index}: The index of this URL within the sequence of all downloaded URLs. + Starts from 0. + - {now}: The unix timestamp at which this download job was started. It might + be ugly but at least it's unambiguous when doing multiple download batches + with similar filenames. + ''', + ) + parser.add_argument( + '--bytespersecond', + default=None, + help=''' + Limit the overall download speed to X bytes per second. Uses + bytestring.parsebytes to support strings like "1m", "500k", "2 mb", etc. + ''', + ) + parser.add_argument( + '--timeout', + default=15, + help=''' + Integer number of seconds to use as HTTP request timeout for each download. + ''', + ) + parser.add_argument( + '--headers', nargs='+', default=None, + ) parser.set_defaults(func=threaded_dl_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/touch.py b/touch.py index 26b2cc2..b80389a 100644 --- a/touch.py +++ b/touch.py @@ -1,10 +1,8 @@ -''' -Create the file, or update the last modified timestamp. -''' import argparse import os import sys +from voussoirkit import betterhelp from voussoirkit import pipeable from voussoirkit import winglob @@ -24,13 +22,31 @@ def touch_argparse(args): return 0 def main(argv): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser( + description=''' + Create blank files, or update mtimes of existing files. + ''', + ) - parser.add_argument('patterns', nargs='+') + parser.add_argument( + 'patterns', + type=str, + nargs='+', + help=''' + One or more filenames or glob patterns. + ''', + ) + parser.add_argument( + '--sleep', + type=float, + default=None, + help=''' + Sleep for this many seconds between touching each file. + ''', + ) parser.set_defaults(func=touch_argparse) - args = parser.parse_args(argv) - return args.func(args) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/wait_for_internet.py b/wait_for_internet.py index 340f57a..967f3d5 100644 --- a/wait_for_internet.py +++ b/wait_for_internet.py @@ -1,16 +1,3 @@ -''' -wait_for_internet -================= - -This program will block until internet access is available. It can be useful to -run this program before running another program that expects an internet -connection. - -> wait_for_internet timeout - -timeout: - An integer number of seconds, after which to give up and return 1. -''' import argparse import sys @@ -27,12 +14,23 @@ def wait_for_internet_argparse(args): @vlogging.main_decorator def main(argv): - parser = argparse.ArgumentParser(description=__doc__) - - parser.add_argument('timeout', type=int) + parser = argparse.ArgumentParser( + description=''' + This program will block until internet access is available. It can be useful to + run this program before running another program that expects an internet + connection. + ''', + ) + parser.add_argument( + 'timeout', + type=int, + help=''' + An integer number of seconds, after which to give up and return 1. + ''', + ) parser.set_defaults(func=wait_for_internet_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/watchforlinks.py b/watchforlinks.py index eb9fc0c..db3ecfd 100644 --- a/watchforlinks.py +++ b/watchforlinks.py @@ -1,21 +1,3 @@ -''' -watchforlinks -============= - -This program will continuously watch your clipboard for URLs and save them to -individual files. The files will have randomly generated names. The current -contents of your clipboard will be erased. - -> watchforlinks [extension] - -extension: - The saved files will have this extension. - If not provided, the default is "generic". - -flags: ---regex X: - A regex pattern. Only URLs that match this pattern will be saved. -''' import argparse import pyperclip import re @@ -64,13 +46,35 @@ def watchforlinks_argparse(args): return 0 def main(argv): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser( + description=''' + This program will continuously watch your clipboard for http:// and + https:// URLs and save them to individual files. The files will have + randomly generated names. The current contents of your clipboard will + be erased. + ''', + ) - parser.add_argument('extension', nargs='?', default='generic') - parser.add_argument('--regex', default=None) + parser.add_argument( + 'extension', + nargs='?', + type=str, + default='generic', + help=''' + The saved files will have this extension. + ''', + ) + parser.add_argument( + '--regex', + type=str, + default=None, + help=''' + A regex pattern. Only URLs that match this pattern will be saved. + ''', + ) parser.set_defaults(func=watchforlinks_argparse) - return betterhelp.single_main(argv, parser, __doc__) + return betterhelp.go(parser, argv) if __name__ == '__main__': raise SystemExit(main(sys.argv[1:]))