diff --git a/jrnl/cli.py b/jrnl/cli.py index 0ca54d65..50bde3ff 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -19,16 +19,14 @@ along with this program. If not, see . """ -import argparse import logging import packaging.version import platform -import re import sys -import jrnl - from . import install, plugins, util +from .commands import list_journals +from .parsing import parse_args_before_config from .Journal import PlainJournal, open_journal from .util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR, UserAbort @@ -36,186 +34,6 @@ log = logging.getLogger(__name__) logging.getLogger("keyring.backend").setLevel(logging.ERROR) -def parse_args(args=None): - parser = argparse.ArgumentParser() - parser.add_argument( - "-v", - "--version", - dest="version", - action="store_true", - help="prints version information and exits", - ) - - parser.add_argument( - "--diagnostic", - dest="diagnostic", - action="store_true", - help="outputs diagnostic information and exits", - ) - - parser.add_argument( - "-ls", dest="ls", action="store_true", help="displays accessible journals" - ) - parser.add_argument( - "-d", "--debug", dest="debug", action="store_true", help="execute in debug mode" - ) - - composing = parser.add_argument_group( - "Composing", - 'To write an entry simply write it on the command line, e.g. "jrnl yesterday at 1pm: Went to the gym."', - ) - - reading = parser.add_argument_group( - "Reading", - "Specifying either of these parameters will display posts of your journal", - ) - reading.add_argument( - "-from", dest="start_date", metavar="DATE", help="View entries after this date" - ) - reading.add_argument( - "-until", - "-to", - dest="end_date", - metavar="DATE", - help="View entries before this date", - ) - reading.add_argument( - "-contains", dest="contains", help="View entries containing a specific string" - ) - reading.add_argument( - "-on", dest="on_date", metavar="DATE", help="View entries on this date" - ) - reading.add_argument( - "-and", - dest="strict", - action="store_true", - help="Filter by tags using AND (default: OR)", - ) - reading.add_argument( - "-starred", - dest="starred", - action="store_true", - help="Show only starred entries", - ) - reading.add_argument( - "-n", - dest="limit", - default=None, - metavar="N", - help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.", - nargs="?", - type=int, - ) - reading.add_argument( - "-not", - dest="excluded", - nargs="?", - default=[], - metavar="E", - action="append", - help="Exclude entries with these tags", - ) - - exporting = parser.add_argument_group( - "Export / Import", "Options for transmogrifying your journal" - ) - exporting.add_argument( - "-s", - "--short", - dest="short", - action="store_true", - help="Show only titles or line containing the search tags", - ) - exporting.add_argument( - "--tags", - dest="tags", - action="store_true", - help="Returns a list of all tags and number of occurences", - ) - exporting.add_argument( - "--export", - metavar="TYPE", - dest="export", - choices=plugins.EXPORT_FORMATS, - help="Export your journal. TYPE can be {}.".format( - plugins.util.oxford_list(plugins.EXPORT_FORMATS) - ), - default=False, - const=None, - ) - exporting.add_argument( - "-o", - metavar="OUTPUT", - dest="output", - help="Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.", - default=False, - const=None, - ) - exporting.add_argument( - "--import", - metavar="TYPE", - dest="import_", - choices=plugins.IMPORT_FORMATS, - help="Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.".format( - plugins.util.oxford_list(plugins.IMPORT_FORMATS) - ), - default=False, - const="jrnl", - nargs="?", - ) - exporting.add_argument( - "-i", - metavar="INPUT", - dest="input", - help="Optionally specifies input file when using --import.", - default=False, - const=None, - ) - exporting.add_argument( - "--encrypt", - metavar="FILENAME", - dest="encrypt", - help="Encrypts your existing journal with a new password", - nargs="?", - default=False, - const=None, - ) - exporting.add_argument( - "--decrypt", - metavar="FILENAME", - dest="decrypt", - help="Decrypts your journal and stores it in plain text", - nargs="?", - default=False, - const=None, - ) - exporting.add_argument( - "--edit", - dest="edit", - help="Opens your editor to edit the selected entries.", - action="store_true", - ) - - exporting.add_argument( - "--delete", - dest="delete", - action="store_true", - help="Opens an interactive interface for deleting entries.", - ) - - # Everything else - composing.add_argument("text", metavar="", nargs="*") - - if not args: - args = [] - - # Handle '-123' as a shortcut for '-n 123' - num = re.compile(r"^-(\d+)$") - args = [num.sub(r"-n \1", arg) for arg in args] - - return parser.parse_intermixed_args(args) - - def guess_mode(args, config): """Guesses the mode (compose, read or export) from the given arguments""" compose = True @@ -282,17 +100,6 @@ def decrypt(journal, filename=None): ) -def list_journals(config): - """List the journals specified in the configuration file""" - result = f"Journals defined in {install.CONFIG_FILE_PATH}\n" - ml = min(max(len(k) for k in config["journals"]), 20) - for journal, cfg in config["journals"].items(): - result += " * {:{}} -> {}\n".format( - journal, ml, cfg["journal"] if isinstance(cfg, dict) else cfg - ) - return result - - def update_config(config, new_config, scope, force_local=False): """Updates a config dict with new values - either global if scope is None or config['journals'][scope] is just a string pointing to a journal file, @@ -332,30 +139,29 @@ Python 3.7 (or higher) soon. if manual_args is None: manual_args = sys.argv[1:] - args = parse_args(manual_args) + args = parse_args_before_config(manual_args) + + # import pprint + # pp = pprint.PrettyPrinter(depth=4) + # pp.pprint(args) configure_logger(args.debug) - if args.version: - version_str = f"{jrnl.__title__} version {jrnl.__version__}" - print(version_str) - sys.exit(0) - - if args.diagnostic: - print( - f"jrnl: {jrnl.__version__}\n" - f"Python: {sys.version}\n" - f"OS: {platform.system()} {platform.release()}" - ) + + # Run command if possible before config is available + if args.preconfig_cmd is not None: + args.preconfig_cmd(args) sys.exit(0) + # Load the config try: config = install.load_or_install_jrnl() except UserAbort as err: print(f"\n{err}", file=sys.stderr) sys.exit(1) - if args.ls: - print(list_journals(config)) + # Run command now that config is available + if args.postconfig_cmd is not None: + args.postconfig_cmd(config=config, args=args) sys.exit(0) log.debug('Using configuration "%s"', config) diff --git a/jrnl/commands.py b/jrnl/commands.py new file mode 100644 index 00000000..2c1f2d90 --- /dev/null +++ b/jrnl/commands.py @@ -0,0 +1,52 @@ +def deprecated_cmd(old_cmd, new_cmd, callback, **kwargs): + import sys + from .util import RESET_COLOR, WARNING_COLOR + + print( + WARNING_COLOR, + f"\nThe command {old_cmd} is deprecated and will be removed from jrnl soon.\n" + f"Please use {new_cmd} instead.\n", + RESET_COLOR, + file=sys.stderr, + ) + callback(**kwargs) + + +def preconfig_diagnostic(_): + import platform + import sys + import jrnl + + print( + f"jrnl: {jrnl.__version__}\n" + f"Python: {sys.version}\n" + f"OS: {platform.system()} {platform.release()}" + ) + + +def preconfig_version(_): + import jrnl + + version_str = f"{jrnl.__title__} version {jrnl.__version__}" + print(version_str) + + +def preconfig_command(args): + print("this is a pre-config command") + + +def postconfig_list(config, **kwargs): + print(list_journals(config)) + + +def list_journals(config): + from . import install + + """List the journals specified in the configuration file""" + result = f"Journals defined in {install.CONFIG_FILE_PATH}\n" + ml = min(max(len(k) for k in config["journals"]), 20) + for journal, cfg in config["journals"].items(): + result += " * {:{}} -> {}\n".format( + journal, ml, cfg["journal"] if isinstance(cfg, dict) else cfg + ) + return result diff --git a/jrnl/parsing.py b/jrnl/parsing.py new file mode 100644 index 00000000..b84ab1f3 --- /dev/null +++ b/jrnl/parsing.py @@ -0,0 +1,221 @@ +import argparse +import re + +from . import plugins +from .commands import preconfig_version +from .commands import preconfig_diagnostic +from .commands import postconfig_list +from .commands import deprecated_cmd + + +def parse_args_before_config(args=None): + """ + Argument parsing that is doable before the config is available. + Everything else goes into "text" for later parsing. + """ + parser = argparse.ArgumentParser() + parser.add_argument( + "-v", + "--version", + action="store_const", + const=preconfig_version, + dest="preconfig_cmd", + help="prints version information and exits", + ) + + parser.add_argument( + "--cmd1", + action="store_const", + const=lambda: print("cmd1"), + dest="preconfig_cmd", + ) + parser.add_argument( + "--diagnostic", + action="store_const", + const=preconfig_diagnostic, + dest="preconfig_cmd", + help="outputs diagnostic information and exits", + ) + + parser.add_argument( + "--ls", + "--list", + action="store_const", + const=postconfig_list, + dest="postconfig_cmd", + help="lists all configured journals", + ) + + parser.add_argument( + "-ls", + action="store_const", + const=lambda **kwargs: deprecated_cmd( + "-ls", "--ls or --list", callback=postconfig_list, **kwargs + ), + dest="postconfig_cmd", + help="displays accessible journals", + ) + + parser.add_argument( + "-d", "--debug", dest="debug", action="store_true", help="execute in debug mode" + ) + + composing = parser.add_argument_group( + "Composing", + 'To write an entry simply write it on the command line, e.g. "jrnl yesterday at 1pm: Went to the gym."', + ) + + reading = parser.add_argument_group( + "Reading", + "Specifying either of these parameters will display posts of your journal", + ) + reading.add_argument( + "-from", dest="start_date", metavar="DATE", help="View entries after this date" + ) + reading.add_argument( + "-until", + "-to", + dest="end_date", + metavar="DATE", + help="View entries before this date", + ) + reading.add_argument( + "-contains", dest="contains", help="View entries containing a specific string" + ) + reading.add_argument( + "-on", dest="on_date", metavar="DATE", help="View entries on this date" + ) + reading.add_argument( + "-and", + dest="strict", + action="store_true", + help="Filter by tags using AND (default: OR)", + ) + reading.add_argument( + "-starred", + dest="starred", + action="store_true", + help="Show only starred entries", + ) + reading.add_argument( + "-n", + dest="limit", + default=None, + metavar="N", + help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.", + nargs="?", + type=int, + ) + reading.add_argument( + "-not", + dest="excluded", + nargs="?", + default=[], + metavar="E", + action="append", + help="Exclude entries with these tags", + ) + + exporting = parser.add_argument_group( + "Export / Import", "Options for transmogrifying your journal" + ) + exporting.add_argument( + "-s", + "--short", + dest="short", + action="store_true", + help="Show only titles or line containing the search tags", + ) + exporting.add_argument( + "--tags", + dest="tags", + action="store_true", + help="Returns a list of all tags and number of occurences", + ) + exporting.add_argument( + "--export", + metavar="TYPE", + dest="export", + choices=plugins.EXPORT_FORMATS, + help="Export your journal. TYPE can be {}.".format( + plugins.util.oxford_list(plugins.EXPORT_FORMATS) + ), + default=False, + const=None, + ) + exporting.add_argument( + "-o", + metavar="OUTPUT", + dest="output", + help="Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.", + default=False, + const=None, + ) + exporting.add_argument( + "--import", + metavar="TYPE", + dest="import_", + choices=plugins.IMPORT_FORMATS, + help="Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.".format( + plugins.util.oxford_list(plugins.IMPORT_FORMATS) + ), + default=False, + const="jrnl", + nargs="?", + ) + exporting.add_argument( + "-i", + metavar="INPUT", + dest="input", + help="Optionally specifies input file when using --import.", + default=False, + const=None, + ) + exporting.add_argument( + "--encrypt", + metavar="FILENAME", + dest="encrypt", + help="Encrypts your existing journal with a new password", + nargs="?", + default=False, + const=None, + ) + exporting.add_argument( + "--decrypt", + metavar="FILENAME", + dest="decrypt", + help="Decrypts your journal and stores it in plain text", + nargs="?", + default=False, + const=None, + ) + exporting.add_argument( + "--edit", + dest="edit", + help="Opens your editor to edit the selected entries.", + action="store_true", + ) + + exporting.add_argument( + "--delete", + dest="delete", + action="store_true", + help="Opens an interactive interface for deleting entries.", + ) + + # Everything else + composing.add_argument("text", metavar="", nargs="*") + + if not args: + args = [] + + # Handle '-123' as a shortcut for '-n 123' + num = re.compile(r"^-(\d+)$") + args = [num.sub(r"-n \1", arg) for arg in args] + + # return parser.parse_args(args) + return parser.parse_intermixed_args(args) + + +def parse_args_after_config(args=None): + return None diff --git a/tests/test_parse_args.py b/tests/test_parse_args.py index f9af643e..9df8c200 100644 --- a/tests/test_parse_args.py +++ b/tests/test_parse_args.py @@ -1,4 +1,4 @@ -from jrnl.cli import parse_args +from jrnl.cli import parse_args_before_config as parse_args import pytest import shlex @@ -16,7 +16,6 @@ def expected_args(**kwargs): "debug": False, "decrypt": False, "delete": False, - "diagnostic": False, "edit": False, "encrypt": False, "end_date": None, @@ -25,16 +24,16 @@ def expected_args(**kwargs): "import_": False, "input": False, "limit": None, - "ls": False, "on_date": None, "output": False, + "preconfig_cmd": None, + "postconfig_cmd": None, "short": False, "starred": False, "start_date": None, "strict": False, "tags": False, "text": [], - "version": False, } return {**default_args, **kwargs} @@ -57,7 +56,11 @@ def test_delete_alone(): def test_diagnostic_alone(): - assert cli_as_dict("--diagnostic") == expected_args(diagnostic=True) + from jrnl.commands import preconfig_diagnostic + + assert cli_as_dict("--diagnostic") == expected_args( + preconfig_cmd=preconfig_diagnostic + ) def test_edit_alone(): @@ -134,7 +137,9 @@ def test_limit_shorthand_alone(): def test_list_alone(): - assert cli_as_dict("-ls") == expected_args(ls=True) + from jrnl.commands import postconfig_list + + assert cli_as_dict("--ls") == expected_args(postconfig_cmd=postconfig_list) def test_on_date_alone(): @@ -169,7 +174,9 @@ def test_text_alone(): def test_version_alone(): - assert cli_as_dict("--version") == expected_args(version=True) + from jrnl.commands import preconfig_version + + assert cli_as_dict("--version") == expected_args(preconfig_cmd=preconfig_version) # @see https://github.com/jrnl-org/jrnl/issues/520