diff --git a/README.md b/README.md index 07ab0bc..2003316 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ normalize: Rename files and directories in the book to match a common structure. ``` +To see the rest, try `epubfile --help`. + ## Spec compliance epubfile does not rigorously enforce the epub spec and you can create noncompliant books with it. Basic errors are checked, and I am open to issues and comments regarding ways to improve spec-compliance without adding significant size or complexity to the library. I am prioritizing simplicity and ease of use over perfection. diff --git a/epubfile.py b/epubfile.py index bd20c79..df83fd4 100644 --- a/epubfile.py +++ b/epubfile.py @@ -14,6 +14,10 @@ import tinycss2 from voussoirkit import interactive from voussoirkit import pathclass +from voussoirkit import pipeable +from voussoirkit import vlogging + +log = vlogging.get_logger(__name__, 'epubfile') HTML_LINK_PROPERTIES = { 'a': ['href'], @@ -70,10 +74,6 @@ OPF_TEMPLATE = ''' {uuid} - author - aut - title - und @@ -91,9 +91,6 @@ NCX_TEMPLATE = ''' - - {title} - {navpoints} @@ -129,12 +126,12 @@ TEXT_TEMPLATE = ''' '''.strip() - # EPUB COMPRESSION ################################################################################ def compress_epub(directory, epub_filepath): directory = pathclass.Path(directory) epub_filepath = pathclass.Path(epub_filepath) + log.debug('Compressing %s to %s.', directory.absolute_path, epub_filepath.absolute_path) if epub_filepath in directory: raise ValueError('Epub inside its own directory') @@ -142,14 +139,14 @@ def compress_epub(directory, epub_filepath): if epub_filepath.extension != 'epub': epub_filepath = epub_filepath.add_extension('epub') - with zipfile.ZipFile(epub_filepath.absolute_path, 'w') as z: - z.write(directory.with_child('mimetype').absolute_path, arcname='mimetype') + with zipfile.ZipFile(epub_filepath, 'w') as z: + z.write(directory.with_child('mimetype'), arcname='mimetype') for file in directory.walk(): if file in [directory.with_child('mimetype'), directory.with_child('sigil.cfg')]: continue z.write( - file.absolute_path, - arcname=file.relative_to(directory), + file, + arcname=file.relative_to(directory).replace('\\', '/'), compress_type=zipfile.ZIP_DEFLATED, ) return epub_filepath @@ -157,9 +154,10 @@ def compress_epub(directory, epub_filepath): def extract_epub(epub_filepath, directory): epub_filepath = pathclass.Path(epub_filepath) directory = pathclass.Path(directory) + log.debug('Extracting %s to %s.', epub_filepath.absolute_path, directory.absolute_path) - with zipfile.ZipFile(epub_filepath.absolute_path, 'r') as z: - z.extractall(directory.absolute_path) + with zipfile.ZipFile(epub_filepath, 'r') as z: + z.extractall(directory) # XHTML TOOLS ################################################################################ @@ -295,6 +293,11 @@ def make_spine_item(id): # DECORATORS ################################################################################ def writes(method): + ''' + Indicates that the given method performs write operations to files inside + the book. The decorated method will raise ReadOnly if the book was opened + in read-only mode. + ''' @functools.wraps(method) def wrapped_method(self, *args, **kwargs): if self.read_only: @@ -335,7 +338,6 @@ class NotInSpine(EpubfileException): class ReadOnly(EpubfileException): error_message = 'Can\'t do {} in read-only mode.' - class Epub: def __init__(self, epub_path, *, read_only=False): ''' @@ -366,7 +368,7 @@ class Epub: def __init_from_dir(self, directory): self.is_zip = False - self.root_directory = pathclass.Path(directory, force_sep='/') + self.root_directory = pathclass.Path(directory) def __init_from_file_read_only(self, epub_path): # It may appear that is_zip is a synonym for read_only, but don't forget @@ -374,8 +376,8 @@ class Epub: # readonly dirs don't need a special init, all they have to do is # forbid writes. self.is_zip = True - self.root_directory = pathclass.Path(epub_path, force_sep='/') - self.zip = zipfile.ZipFile(self.root_directory.absolute_path) + self.root_directory = pathclass.Path(epub_path) + self.zip = zipfile.ZipFile(self.root_directory) def __init_from_file(self, epub_path): extract_to = tempfile.TemporaryDirectory(prefix='epubfile-') @@ -411,9 +413,22 @@ class Epub: If the book was opened as a read-only zip, we can read files out of the zip. ''' - p_path = self.root_directory.spawn(path) - if p_path in self.root_directory: - path = p_path.relative_to(self.root_directory, simple=True) + # When reading from a zip, root_directory is the zip file itself. + # So if the user is trying to read a filepath called + # D:\book.epub\dir1\file1.html, we need to convert it to the relative + # path dir1\file1.html + # But if they have already given us the relative path, we keep that. + normalized = path + if not isinstance(normalized, pathclass.Path): + normalized = pathclass.Path(normalized) + + if normalized in self.root_directory: + # The given path was an absolute path including the epub. + path = normalized.relative_to(self.root_directory, simple=True) + else: + # The given path was either a relative path already inside the epub, + # or an absolute path somewhere totally wrong. + path = os.fspath(path) # Zip files always use forward slash internally, even on Windows. path = path.replace('\\', '/') @@ -422,7 +437,7 @@ class Epub: return self.zip.open(path, 'r') if mode == 'r': return io.TextIOWrapper(self.zip.open(path, 'r'), encoding) - # At this time ZipFS is only used for read-only epubs anyway. + # At this time fopen_zip is only used for read-only epubs anyway. if mode == 'wb': return self.zip.open(path, 'w') if mode == 'w': @@ -459,7 +474,7 @@ class Epub: # Ensure we have a mimetype file. mimetype_file = self.root_directory.with_child('mimetype') if not mimetype_file.exists: - with self._fopen(mimetype_file.absolute_path, 'w', encoding='utf-8') as handle: + with self._fopen(mimetype_file, 'w', encoding='utf-8') as handle: handle.write(MIMETYPE_FILE_TEMPLATE) # Assert that all manifest items exist on disk. @@ -493,7 +508,7 @@ class Epub: writefile(root.join('mimetype'), MIMETYPE_FILE_TEMPLATE) writefile(root.join('META-INF/container.xml'), CONTAINER_XML_TEMPLATE) writefile(root.join('OEBPS/content.opf'), OPF_TEMPLATE.format(uuid=uid)) - writefile(root.join('OEBPS/toc.ncx'), NCX_TEMPLATE.format(uuid=uid, title='Unknown', navpoints='')) + writefile(root.join('OEBPS/toc.ncx'), NCX_TEMPLATE.format(uuid=uid, navpoints='')) writefile(root.join('OEBPS/Text/nav.xhtml'), NAV_XHTML_TEMPLATE.format(toc_contents='')) return cls(tempdir) @@ -518,15 +533,15 @@ class Epub: def read_container_xml(self): container_xml_path = self.root_directory.join('META-INF/container.xml') - container = self._fopen(container_xml_path.absolute_path, 'r', encoding='utf-8') + container = self._fopen(container_xml_path, 'r', encoding='utf-8') # 'xml' and 'html.parser' seem about even here except that html.parser # doesn't self-close. container = bs4.BeautifulSoup(container, 'xml') return container def read_opf(self, rootfile): - rootfile = pathclass.Path(rootfile, force_sep='/') - rootfile_xml = self._fopen(rootfile.absolute_path, 'r', encoding='utf-8').read() + rootfile = pathclass.Path(rootfile) + rootfile_xml = self._fopen(rootfile, 'r', encoding='utf-8').read() # 'html.parser' preserves namespacing the best, but unfortunately it # botches the items because it wants them to be self-closing # and the string contents come out. We will fix in just a moment. @@ -554,12 +569,12 @@ class Epub: if isinstance(container, bs4.BeautifulSoup): container = str(container) container_xml_path = self.root_directory.join('META-INF/container.xml') - container_xml = self._fopen(container_xml_path.absolute_path, 'w', encoding='utf-8') + container_xml = self._fopen(container_xml_path, 'w', encoding='utf-8') container_xml.write(container) @writes def write_opf(self): - with self._fopen(self.opf_filepath.absolute_path, 'w', encoding='utf-8') as rootfile: + with self._fopen(self.opf_filepath, 'w', encoding='utf-8') as rootfile: rootfile.write(str(self.opf)) # FILE OPERATIONS @@ -582,16 +597,16 @@ class Epub: content = fix_xhtml(content) if isinstance(content, str): - handle = self._fopen(filepath.absolute_path, 'w', encoding='utf-8') + handle = self._fopen(filepath, 'w', encoding='utf-8') elif isinstance(content, bytes): - handle = self._fopen(filepath.absolute_path, 'wb') + handle = self._fopen(filepath, 'wb') else: raise TypeError(f'content should be str or bytes, not {type(content)}.') with handle: handle.write(content) - href = filepath.relative_to(self.opf_filepath.parent, simple=True) + href = filepath.relative_to(self.opf_filepath.parent, simple=True).replace('\\', '/') href = urllib.parse.quote(href) manifest_item = make_manifest_item(id, href, mime) @@ -610,7 +625,7 @@ class Epub: automatically generated. ''' filepath = pathclass.Path(filepath) - with self._fopen(filepath.absolute_path, 'rb') as handle: + with self._fopen(filepath, 'rb') as handle: return self.add_file( id=filepath.basename, basename=filepath.basename, @@ -626,7 +641,7 @@ class Epub: spine_item = self.opf.spine.find('itemref', {'idref': id}) if spine_item: spine_item.extract() - os.remove(filepath.absolute_path) + os.remove(filepath) def get_filepath(self, id): href = self.get_manifest_item(id)['href'] @@ -655,9 +670,9 @@ class Epub: ) if is_text: - handle = self._fopen(filepath.absolute_path, mode, encoding='utf-8') + handle = self._fopen(filepath, mode, encoding='utf-8') else: - handle = self._fopen(filepath.absolute_path, mode + 'b') + handle = self._fopen(filepath, mode + 'b') return handle @@ -684,7 +699,7 @@ class Epub: if not new_filepath.extension: new_filepath = new_filepath.add_extension(old_filepath.extension) self.assert_file_not_exists(new_filepath) - os.rename(old_filepath.absolute_path, new_filepath.absolute_path) + os.rename(old_filepath, new_filepath) rename_map[old_filepath] = new_filepath if fix_interlinking: @@ -826,7 +841,7 @@ class Epub: current_meta = self.opf.metadata.find('meta', {'name': 'cover'}) if current_meta: - current_meta[content] = id + current_meta['content'] = id else: meta = make_meta_item(attrs={'name': 'cover', 'content': id}) self.opf.metadata.append(meta) @@ -838,7 +853,6 @@ class Epub: if linear_only: items = [x for x in items if x.get('linear') != 'no'] return [x['idref'] for x in items] - return ids @writes def set_spine_order(self, ids): @@ -980,7 +994,7 @@ class Epub: if new_filepath is None: return None - link = link._replace(path=new_filepath.relative_to(relative_to, simple=True)) + link = link._replace(path=new_filepath.relative_to(relative_to, simple=True).replace('\\', '/')) link = link._replace(path=urllib.parse.quote(link.path)) return link.geturl() @@ -1236,10 +1250,10 @@ class Epub: hash_anchor = f'#{header["id"]}' if nav_id: - relative = file_path.relative_to(nav_filepath.parent, simple=True) + relative = file_path.relative_to(nav_filepath.parent, simple=True).replace('\\', '/') toc_line['nav_anchor'] = f'{relative}{hash_anchor}' if ncx_id: - relative = file_path.relative_to(ncx_filepath.parent, simple=True) + relative = file_path.relative_to(ncx_filepath.parent, simple=True).replace('\\', '/') toc_line['ncx_anchor'] = f'{relative}{hash_anchor}' if current_list['level'] is None: @@ -1321,7 +1335,7 @@ class Epub: # location of all all manifest item hrefs. manifest_items = self.get_manifest_items(soup=True) old_filepaths = {item['id']: self.get_filepath(item['id']) for item in manifest_items} - old_ncx = self.get_ncx() + try: old_ncx_parent = self.get_filepath(self.get_ncx()).parent except Exception: @@ -1332,10 +1346,10 @@ class Epub: oebps.makedirs(exist_ok=True) self.write_opf() new_opf_path = oebps.with_child(self.opf_filepath.basename) - os.rename(self.opf_filepath.absolute_path, new_opf_path.absolute_path) + os.rename(self.opf_filepath, new_opf_path) container = self.read_container_xml() rootfile = container.find('rootfile', {'full-path': self.opf_filepath.basename}) - rootfile['full-path'] = new_opf_path.relative_to(self.root_directory, simple=True) + rootfile['full-path'] = new_opf_path.relative_to(self.root_directory, simple=True).replace('\\', '/') self.write_container_xml(container) self.opf_filepath = new_opf_path @@ -1348,15 +1362,15 @@ class Epub: if directory.exists: # On Windows, this will fix any incorrect casing. # On Linux it is inert. - os.rename(directory.absolute_path, directory.absolute_path) + os.rename(directory, directory) else: directory.makedirs() new_filepath = directory.with_child(old_filepath.basename) if new_filepath.absolute_path != old_filepath.absolute_path: rename_map[old_filepath] = new_filepath - os.rename(old_filepath.absolute_path, new_filepath.absolute_path) - manifest_item['href'] = new_filepath.relative_to(self.opf_filepath.parent, simple=True) + os.rename(old_filepath, new_filepath) + manifest_item['href'] = new_filepath.relative_to(self.opf_filepath.parent, simple=True).replace('\\', '/') self.fix_interlinking_opf(rename_map) for id in self.get_texts(): @@ -1372,7 +1386,6 @@ class Epub: if item['href'] in ['toc.ncx', 'Misc/toc.ncx']: item['media-type'] = 'application/x-dtbncx+xml' - # COMMAND LINE TOOLS ################################################################################ import argparse @@ -1382,13 +1395,12 @@ import string import sys from voussoirkit import betterhelp -from voussoirkit import winglob DOCSTRING = ''' Epubfile ======== -A simple python .epub scripting tool. +A simple Python .epub scripting tool. {addfile} @@ -1402,6 +1414,8 @@ A simple python .epub scripting tool. {merge} +{new} + {normalize} {setfont} @@ -1454,8 +1468,12 @@ generate_toc: holdit=''' holdit: - Extract the book so that you can manually edit the files on disk, then save - the changes back into the original file. + Extract the book so that you can manually edit the files on disk, then + compress them back into the original file. + + This is helpful when you want to do some file processing that is outside of + epubfile's scope. epubfile will save you the effort of extracting and + compressing the epub so you can focus on doing the file operations. > epubfile.py holdit book.epub '''.strip(), @@ -1468,7 +1486,7 @@ merge: flags: --demote_headers: - All h1 in the book will be demoted to h2, and so forth. So that the + All h1 in the book will be demoted to h2, and so forth, so that the headerfiles are the only h1s and the table of contents will generate with a good hierarchy. @@ -1479,7 +1497,7 @@ merge: In the headerfile, the

will start with the book's index, like "01. First Book" - -y | --autoyes: + --yes: Overwrite the output file without prompting. '''.strip(), @@ -1490,7 +1508,7 @@ new: > epubfile.py new book.epub flags: - -y | --autoyes: + --yes: Overwrite the file without prompting. '''.strip(), @@ -1511,7 +1529,11 @@ setfont: A stylesheet called epubfile_setfont.css will be created that sets * { font-family: ... !important } with a font file of your choice. - > epubfile.py setfont book.epub font.ttf + > epubfile.py setfont book.epub font.ttf + + flags: + --yes: + Overwrite the epubfile_setfont.css without prompting. '''.strip(), ) @@ -1525,7 +1547,7 @@ def addfile_argparse(args): for pattern in args.files: for file in pathclass.glob_files(pattern): - print(f'Adding file {file.absolute_path}.') + log.info('Adding file %s.', file.absolute_path) try: book.easy_add_file(file) except (IDExists, FileExists) as exc: @@ -1538,6 +1560,7 @@ def addfile_argparse(args): book.move_nav_to_end() book.save(args.epub) + return 0 def covercomesfirst(book): basenames = {i: book.get_filepath(i).basename for i in book.get_images()} @@ -1578,35 +1601,39 @@ def covercomesfirst(book): book.rename_file(rename_map) def covercomesfirst_argparse(args): - epubs = [epub for pattern in args.epubs for epub in winglob.glob(pattern)] + epubs = pathclass.glob_many(args.epubs) for epub in epubs: - print(epub) book = Epub(epub) + log.info('Moving %s\'s cover.', book) covercomesfirst(book) book.save(epub) + pipeable.stdout(epub.absolute_path) + return 0 def exec_argparse(args): - epubs = [epub for pattern in args.epubs for epub in winglob.glob(pattern)] + epubs = pathclass.glob_many(args.epubs) for epub in epubs: - print(epub) book = Epub(epub) exec(args.command) book.save(epub) + pipeable.stdout(epub.absolute_path) + return 0 def generate_toc_argparse(args): - epubs = [epub for pattern in args.epubs for epub in winglob.glob(pattern)] + epubs = pathclass.glob_many(args.epubs) books = [] for epub in epubs: book = Epub(epub) book.generate_toc(max_level=int(args.max_level) if args.max_level else None) book.save(epub) + return 0 def holdit_argparse(args): - epubs = [epub for pattern in args.epubs for epub in winglob.glob(pattern)] + epubs = pathclass.glob_many(args.epubs) books = [] for epub in epubs: book = Epub(epub) - print(f'{epub} = {book.root_directory.absolute_path}') + pipeable.stderr(f'{epub} = {book.root_directory.absolute_path}') books.append((epub, book)) input('Press Enter when ready.') @@ -1615,10 +1642,11 @@ def holdit_argparse(args): # So let's re-read it first. book.read_opf(book.opf_filepath) book.save(epub) + pipeable.stdout(epub.absolute_path) + return 0 def merge( input_filepaths, - output_filename, demote_headers=False, do_headerfile=False, number_headerfile=False, @@ -1631,7 +1659,7 @@ def merge( # Number books from 1 for human sanity. for (index, input_filepath) in enumerate(input_filepaths, start=1): - print(f'Merging {input_filepath.absolute_path}.') + log.info('Merging %s.', input_filepath.absolute_path) prefix = f'{rand_prefix}_{index:>0{index_length}}_{{}}' input_book = Epub(input_filepath) input_book.normalize_directory_structure() @@ -1691,79 +1719,87 @@ def merge( book.add_file(new_id, new_basename, content) book.move_nav_to_end() - book.save(output_filename) + return book def merge_argparse(args): - if os.path.exists(args.output): + output = pathclass.Path(args.output) + + if output.exists: if not (args.autoyes or interactive.getpermission(f'Overwrite {args.output}?')): raise ValueError(f'{args.output} exists.') - return merge( + book = merge( input_filepaths=args.epubs, - output_filename=args.output, demote_headers=args.demote_headers, do_headerfile=args.headerfile, number_headerfile=args.number_headerfile, ) + book.save(output) + pipeable.stdout(output.absolute_path) + return 0 def new_book_argparse(args): - if os.path.exists(args.epub): + output = pathclass.Path(args.epub) + if output.exists: if not (args.autoyes or interactive.getpermission(f'Overwrite {args.epub}?')): - raise ValueError(f'{args.epub} exists.') + raise ValueError(f'{output.absolute_path} exists.') book = Epub.new() - book.save(args.epub) + book.save(output) + pipeable.stdout(output.absolute_path) + return 0 def normalize_argparse(args): - epubs = [epub for pattern in args.epubs for epub in winglob.glob(pattern)] + epubs = pathclass.glob_many(args.epubs) for epub in epubs: - print(epub) + log.info('Normalizing %s.', epub.absolute_path) book = Epub(epub) book.normalize_opf() book.normalize_directory_structure() book.move_nav_to_end() book.save(epub) + pipeable.stdout(epub.absolute_path) + return 0 -def setfont_argparse(args): - book = Epub(args.epub) - +def setfont(book, new_font, autoyes=False): css_id = 'epubfile_setfont' css_basename = 'epubfile_setfont.css' + new_font = pathclass.Path(new_font) + new_font.assert_is_file() + try: book.assert_id_not_exists(css_id) except IDExists: - if not interactive.getpermission(f'Overwrite {css_id}?'): + if not (autoyes or interactive.getpermission(f'Overwrite {css_id}?')): return book.delete_file(css_id) - font = pathclass.Path(args.font) - for existing_font in book.get_fonts(): font_path = book.get_filepath(existing_font) - if font_path.basename == font.basename: + if font_path.basename == new_font.basename: font_id = existing_font break else: - font_id = book.easy_add_file(font) + font_id = book.easy_add_file(new_font) font_path = book.get_filepath(font_id) # The font_path may have come from an existing font in the book, so we have # no guarantees about its path layout. The css file, however, is definitely # going to be inside OEBPS/Styles since we're the ones creating it. # So, we should be getting the correct number of .. in the relative path. - family = font_path.basename - relative = font_path.relative_to(book.opf_filepath.parent.with_child('Styles')) + family = font_path.replace_extension('').basename + relative = font_path.relative_to(book.opf_filepath.parent.with_child('Styles')).replace('\\', '/') css = f''' @font-face {{ - font-family: '{family}'; + font-family: "{family}"; font-weight: normal; font-style: normal; src: url("{relative}"); }} * {{ - font-family: '{family}' !important; + font-family: "{family}" !important; }} ''' @@ -1776,69 +1812,77 @@ def setfont_argparse(args): for text_id in book.get_texts(): text_path = book.get_filepath(text_id) - relative = css_path.relative_to(text_path) soup = book.read_file(text_id, soup=True) head = soup.head if head.find('link', {'id': css_id}): continue link = soup.new_tag('link') link['id'] = css_id - link['href'] = css_path.relative_to(text_path.parent) + link['href'] = css_path.relative_to(text_path.parent).replace('\\', '/') link['rel'] = 'stylesheet' link['type'] = 'text/css' head.append(link) book.write_file(text_id, soup) - book.save(args.epub) +def setfont_argparse(args): + epubs = pathclass.glob_many(args.epubs) + for epub in epubs: + book = Epub(epub) + setfont(book, args.font, autoyes=args.autoyes) + book.save(epub) + pipeable.stdout(epub.absolute_path) + return 0 +@vlogging.main_decorator def main(argv): parser = argparse.ArgumentParser(description=__doc__) subparsers = parser.add_subparsers() p_addfile = subparsers.add_parser('addfile') p_addfile.add_argument('epub') - p_addfile.add_argument('files', nargs='+', default=[]) + p_addfile.add_argument('files', nargs='+') p_addfile.set_defaults(func=addfile_argparse) p_covercomesfirst = subparsers.add_parser('covercomesfirst') - p_covercomesfirst.add_argument('epubs', nargs='+', default=[]) + p_covercomesfirst.add_argument('epubs', nargs='+') p_covercomesfirst.set_defaults(func=covercomesfirst_argparse) p_exec = subparsers.add_parser('exec') - p_exec.add_argument('epubs', nargs='+', default=[]) - p_exec.add_argument('--command', dest='command', default=None, required=True) + p_exec.add_argument('epubs', nargs='+') + p_exec.add_argument('--command', default=None, required=True) p_exec.set_defaults(func=exec_argparse) p_generate_toc = subparsers.add_parser('generate_toc') - p_generate_toc.add_argument('epubs', nargs='+', default=[]) - p_generate_toc.add_argument('--max_level', '--max-level', dest='max_level', default=None) + p_generate_toc.add_argument('epubs', nargs='+') + p_generate_toc.add_argument('--max_level', '--max-level', default=None) p_generate_toc.set_defaults(func=generate_toc_argparse) p_holdit = subparsers.add_parser('holdit') - p_holdit.add_argument('epubs', nargs='+', default=[]) + p_holdit.add_argument('epubs', nargs='+') p_holdit.set_defaults(func=holdit_argparse) p_merge = subparsers.add_parser('merge') - p_merge.add_argument('epubs', nargs='+', default=[]) - p_merge.add_argument('--output', dest='output', default=None, required=True) - p_merge.add_argument('--headerfile', dest='headerfile', action='store_true') - p_merge.add_argument('--demote_headers', '--demote-headers', dest='demote_headers', action='store_true') - p_merge.add_argument('--number_headerfile', '--number-headerfile', dest='number_headerfile', action='store_true') - p_merge.add_argument('-y', '--autoyes', dest='autoyes', action='store_true') + p_merge.add_argument('epubs', nargs='+') + p_merge.add_argument('--output', required=True) + p_merge.add_argument('--headerfile', action='store_true') + p_merge.add_argument('--demote_headers', '--demote-headers', action='store_true') + p_merge.add_argument('--number_headerfile', '--number-headerfile', action='store_true') + p_merge.add_argument('-y', '--yes', '--autoyes', dest='autoyes', action='store_true') p_merge.set_defaults(func=merge_argparse) p_new = subparsers.add_parser('new') p_new.add_argument('epub') - p_new.add_argument('-y', '--autoyes', dest='autoyes', action='store_true') + p_new.add_argument('-y', '--yes', '--autoyes', dest='autoyes', action='store_true') p_new.set_defaults(func=new_book_argparse) p_normalize = subparsers.add_parser('normalize') - p_normalize.add_argument('epubs', nargs='+', default=[]) + p_normalize.add_argument('epubs', nargs='+') p_normalize.set_defaults(func=normalize_argparse) p_setfont = subparsers.add_parser('setfont') - p_setfont.add_argument('epub') - p_setfont.add_argument('font') + p_setfont.add_argument('epubs', nargs='+') + p_setfont.add_argument('--font', required=True) + p_setfont.add_argument('--yes', dest='autoyes', action='store_true') p_setfont.set_defaults(func=setfont_argparse) return betterhelp.subparser_main(