def _create_parser() -> ArgumentParser:
    """
    Creates argument parser for the CLI.
    :return: the argument parser
    """
    parser = ArgumentParser(prog=EXECUTABLE_NAME, description=f"{DESCRIPTION} (v{VERSION})")
    parser.add_argument(
        f"-{VERBOSE_SHORT_PARAMETER}", action="count", default=0,
        help="increase the level of log verbosity (add multiple increase further)")
    subparsers = parser.add_subparsers(dest=ACTION_CLI_PARAMETER_ACCESS, help="action")

    unlock_subparser = subparsers.add_parser(Action.UNLOCK.value, help="release a lock")
    unlock_subparser.add_argument(
        f"-{REGEX_KEY_ENABLED_SHORT_PARAMETER}", action="store_true", default=DEFAULT_REGEX_KEY_ENABLED,
        help="whether the key should be treated as a regular expression and to release all matching locks")

    lock_subparser = subparsers.add_parser(Action.LOCK.value, help="acquire a lock")
    lock_and_execute_subparser = subparsers.add_parser(Action.EXECUTE.value, help="call executable whilst holding lock")

    for subparser in (lock_subparser, lock_and_execute_subparser):
        subparser.add_argument(
            f"--{SESSION_TTL_LONG_PARAMETER}", type=float, default=DEFAULT_SESSION_TTL,
            help=f"time to live (ttl) in seconds of the session that will be created to hold the lock. Must be between "
                 f"{MIN_LOCK_TIMEOUT_IN_SECONDS}s and {MAX_LOCK_TIMEOUT_IN_SECONDS}s (inclusive). If set to "
                 f"{NO_EXPIRY_SESSION_TTL_CLI_PARAMETER_VALUE}, the session will not expire")
        subparser.add_argument(
            f"--{NON_BLOCKING_LONG_PARAMETER}", action="store_true",
            default=DEFAULT_NON_BLOCKING, help="do not block if cannot lock straight away")
        subparser.add_argument(
            f"--{TIMEOUT_LONG_PARAMETER}", default=DEFAULT_TIMEOUT, type=float,
            help="give up trying to acquire the key after this many seconds (where 0 is never)")
        subparser.add_argument(
            f"--{METADATA_LONG_PARAMETER}", default=DEFAULT_METADATA, type=str, action=_ParseJsonAction,
            help="additional metadata to add to the lock information (will be converted to JSON)")
        subparser.add_argument(
            f"--{ON_BEFORE_LOCK_LONG_PARAMETER}", default=[], type=str, nargs="+", action="append",
            help="path to executable that is to be called before an attempt is made to acquire a lock, where the lock "
                 "key is passed as the first argument. Any failures of this executable are ignored")
        subparser.add_argument(
            f"--{ON_LOCK_ALREADY_LOCKED_LONG_PARAMETER}", default=[], type=str, nargs="+", action="append",
            help="path to executable that is to be called after an attempt has been made to acquire a lock but failed "
                 "due to the lock already been taken, where the lock key is passed as the first argument. Any failures "
                 "of this executable are ignored")
        subparser.add_argument(
            f"-{LOCK_POLL_INTERVAL_SHORT_PARAMETER}", default=DEFAULT_LOCK_POLL_INTERVAL_GENERATOR(1), type=float,
            help="number of seconds between polls to acquire a locked lock")

    # XXX: probably a better way of iterating subparsers on `subparsers`
    for subparser in [unlock_subparser, lock_subparser, lock_and_execute_subparser]:
        subparser.add_argument(
            KEY_PARAMETER, type=str, help="the lock identifier")

    lock_and_execute_subparser.add_argument(
        EXECUTABLE_PARAMETER, type=str, help="to execute in shell")

    return parser