Source code for zmxtools.cli

import argparse
import logging
import sys
import traceback
from pathlib import Path
from typing import List, Optional, Sequence

from zmxtools import __version__, zar

log = logging.getLogger(__name__)

__all__ = ['unzar']

ZAR = '.zar'
ZIP = '.zip'


class CommandLineArgumentError(SyntaxError):
    """A class representing an error due to incorrect command line arguments."""


class UnzarArgumentParser(argparse.ArgumentParser):
    def __init__(self, name: str = 'unzar'):
        """Construct an argument parser specific for unzar."""
        super().__init__(
            description='Zemax archive files (.zar) unpacker and zipper.',
            usage=f"""
        > {name} filename.zar
        > {name} filename.zar -z
        > {name} -i filename.zar -o filename.zip
        > {name} -i filename.zar -o subfolder/filename.zip
        > {name} -i filename.zar -vv
        > {name} -h
        """,
            epilog='Error codes: 0 (OK), 1 (file skipped), 2 (incorrect argument), -1 (FATAL).\n' +
                   'Online: https://github.com/tttom/zmxtools, https://zmxtools.readthedocs.io',
            conflict_handler='resolve',
        )

        self.add_argument(
            '-v',
            '--verbosity',
            help='the level of verbosity',
            default=0,
            action='count',
        )
        self.add_argument(
            '-q',
            '--quiet',
            help='suppress output partially or completely (-qqqq)',
            default=0,
            action='count',
        )
        self.add_argument('-V', '--version', help='show version information before proceeding', action='store_true')
        self.add_argument('-i', '--input', type=str, nargs='*', help='the input archive')
        self.add_argument('-o', '--output', type=str, help='destination directory for the extracted archive')
        self.add_argument('-f', '--force', help='overwrite existing files if necessary', action='store_true')
        self.add_argument('-z', '--zip', help='create an archive instead of a directory', action='store_true')
        self.add_argument('archive_file_name', type=str, nargs='*', help='one or more names of input archives')

    def exit(self, status: int = 0, message: Optional[str] = None):
        """
        Custom handling of parser exit.

        :param status: The error code given by the parent class argparse.ArgumentParser.
        :param message: The message given by the parent class argparse.ArgumentParser.
        """
        raise CommandLineArgumentError(f'Error {status}, unrecognized command line argument: {message}')


[docs]def unzar(argv: Optional[Sequence[str]] = None) -> int: """ Function that can be called as a script. :param argv: An optional sequence of input arguments. :returns: The error code. 0: no error, 1: file skipped, 2: incorrect argument, -1: unexpected fatal error. """ # Parse the input arguments try: input_parser: UnzarArgumentParser = UnzarArgumentParser() input_args: argparse.Namespace = input_parser.parse_args(argv) verbosity_level: int = 2 + input_args.verbosity - input_args.quiet # between -inf and +inf, default 2==WARNING log_levels: List[int] = [logging.FATAL, logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG] log.level = log_levels[max(0, min(len(log_levels) - 1, verbosity_level))] log.debug(f'Parsed input arguments: {input_args}') version_message: str = (f'Running zmxtools unzar version {__version__}. ' + 'More info at: https://github.com/tttom/zmxtools' ) log.debug(version_message) if input_args.version and verbosity_level >= 0: sys.stdout.write(version_message) # i.e. a print that lints combined_input_files: List[str] = input_args.input if input_args.input is not None else [] combined_input_files += input_args.archive_file_name if input_args.archive_file_name is not None else [] if len(combined_input_files) == 0: log.error('No input zar archives specified.') if verbosity_level >= 0: input_parser.print_usage() return 2 log.info(f'Unpacking {len(combined_input_files)} archive(s)...') log.debug(f'with file names {combined_input_files}') nb_files_skipped = 0 for input_file in combined_input_files: log.info(f'Processing {input_file}...') input_full_file = Path(input_file.strip()) if input_full_file.suffix.lower() != ZAR: log.warning(f'Archive file "{input_full_file}" does not have the ".zar" extension!') # Check for direction on where to send the output if input_args.output is None: output_full_file = input_full_file.parent / ( input_full_file.stem if input_full_file.suffix.lower() == ZAR else input_full_file ) else: output_full_file = Path(input_args.output.strip()) / input_full_file.stem log.debug(f'Writing output to specified output directory: {output_full_file.parent}...') # Delegate the actual work if input_args.zip: output_full_file = output_full_file.with_suffix(ZIP) if input_args.force and output_full_file.exists(): log.info(f'The file "{output_full_file}" already exists, but forcing an overwrite...') output_full_file.unlink() if output_full_file.exists(): log.warning(f'The file "{output_full_file}" already exists, skipping it.' + ' Use unzar --force to overwrite.', ) nb_files_skipped += 1 else: zar.repack(input_full_file, output_full_file) else: zar.extract(input_full_file, output_full_file) except CommandLineArgumentError as clae: log.fatal(clae) return 2 except Exception as exc: log.fatal(f'An unexpected fatal error occurred: "{repr(exc)}"') log.info(traceback.format_exc()) return -1 # No error unless at least one file skipped return min(1, nb_files_skipped)
if __name__ == '__main__': sys.exit(unzar())