def generate_parsers() -> tuple[ArgumentParser, dict[str, ArgumentParser]]:
"""Generate the command-line parsers
Returns
-------
enderchest_parser : ArgumentParser
The top-level argument parser responsible for routing arguments to
specific action parsers
action_parsers : dict of str to ArgumentParser
The verb-specific argument parsers
"""
descriptions: dict[str, str] = {}
root_description: str = ""
for commands, description, _ in ACTIONS:
descriptions[commands[0]] = description
root_description += f"\n\t{commands[0]}\n\t\tto {description}"
enderchest_parser = ArgumentParser(
prog="enderchest",
description=(
f"v{get_versions()['version']}\n"
"\nsyncing and linking for all your Minecraft instances"
),
formatter_class=RawTextHelpFormatter,
)
enderchest_parser.add_argument(
"-v", # don't worry--this doesn't actually conflict with --verbose
"-V",
"--version",
action="version",
version=f"%(prog)s v{get_versions()['version']}",
)
# these are really just for the sake of --help
# (the parsed args aren't actually used)
enderchest_parser.add_argument(
"action",
help=f"The action to perform. Options are:{root_description}",
type=str,
)
enderchest_parser.add_argument(
"arguments",
nargs="*",
help="Any additional arguments for the specific action."
" To learn more, try: enderchest {action} -h",
)
action_parsers: dict[str, ArgumentParser] = {}
for verb, description in descriptions.items():
parser = ArgumentParser(
prog=f"enderchest {verb}",
description=description,
)
if verb != "test":
root = parser.add_mutually_exclusive_group()
if verb != "break":
root.add_argument(
"root",
nargs="?",
help=(
"optionally specify your root minecraft directory."
" If no path is given, the current working directory will be used."
),
type=Path,
)
root.add_argument(
"--root",
dest="root_flag",
help="specify your root minecraft directory",
type=Path,
)
# I'm actually okay with -vvqvqqv hilarity
parser.add_argument(
"--verbose",
"-v",
action="count",
default=0,
help="increase the amount of information that's printed",
)
parser.add_argument(
"--quiet",
"-q",
action="count",
default=0,
help="decrease the amount of information that's printed",
)
action_parsers[verb] = parser
# craft options
craft_parser = action_parsers[_create_aliases[0]]
craft_parser.add_argument(
"--from",
dest="copy_from",
help=(
"provide the URI (e.g. rsync://deck@my-steam-deck/home/deck/) of a"
" remote EnderChest installation that can be used"
" to boostrap the creation of this one."
),
)
craft_parser.add_argument(
"-r",
"--remote",
dest="remotes",
action="append",
help=(
"provide the URI (e.g. rsync://deck@my-steam-deck/home/deck/) of a"
" remote EnderChest installation to register with this one"
),
)
craft_parser.add_argument(
"-i",
"--instance",
dest="instance_search_paths",
action="append",
type=Path,
help="specify a folder to search for Minecraft installations in",
)
craft_parser.add_argument(
"--overwrite",
action="store_true",
help=(
"if there's already an EnderChest installation in this location,"
" overwrite its configuration"
),
)
# shulker box craft options
shulker_craft_parser = action_parsers[
f"{_create_aliases[0]} {_shulker_box_aliases[0]}"
]
shulker_craft_parser.add_argument(
"name",
help="specify the name for this shulker box",
)
shulker_craft_parser.add_argument(
"--priority",
"-p",
help="specify the link priority for this shulker box (higher = linked later)",
)
shulker_craft_parser.add_argument(
"-i",
"--instance",
dest="instances",
action="append",
help="only link instances with one of the provided names to this shulker box",
)
shulker_craft_parser.add_argument(
"-t",
"--tag",
dest="tags",
action="append",
help="only link instances with one of the provided tags to this shulker box",
)
shulker_craft_parser.add_argument(
"-e",
"--enderchest",
dest="hosts",
action="append",
help=(
"only link instances registered to one of the provided EnderChest"
" installations with this shulker box"
),
)
shulker_craft_parser.add_argument(
"-l",
"--folder",
dest="link_folders",
action="append",
help=(
"specify the name of a folder inside this shulker box"
" that should be linked completely"
),
)
shulker_craft_parser.add_argument(
"--overwrite",
action="store_true",
help=(
"if there's already a shulker box with the specified name,"
" overwrite its configuration"
),
)
# place options
place_parser = action_parsers["place"]
cleanup = place_parser.add_argument_group()
cleanup.add_argument(
"--keep-broken-links",
action="store_true",
help="do not remove broken links from instances",
)
cleanup.add_argument(
"--keep-stale-links",
action="store_true",
help=(
"do not remove existing links into the EnderChest,"
" even if the shulker box or instance spec has changed"
),
)
cleanup.add_argument(
"-k",
dest="keep_level",
action="count",
default=0,
help=(
"shorthand for the above cleanup options:"
" -k will --keep-stale-links,"
" and -kk will --keep-broken-links as well"
),
)
error_handling = place_parser.add_argument_group(
title="error handling"
).add_mutually_exclusive_group()
error_handling.add_argument(
"--stop-at-first-failure",
"-x",
action="store_true",
help="stop linking at the first issue",
)
error_handling.add_argument(
"--ignore-errors", action="store_true", help="ignore any linking errors"
)
error_handling.add_argument(
"--errors",
"-e",
choices=(
"prompt",
"ignore",
"skip",
"skip-instance",
"skip-shulker-box",
"abort",
),
default="prompt",
help=(
"specify how to handle linking errors"
" (default behavior is to prompt after every error)"
),
)
link_type = place_parser.add_mutually_exclusive_group()
link_type.add_argument(
"--absolute",
"-a",
action="store_true",
help="use absolute paths for all link targets",
)
link_type.add_argument(
"--relative",
"-r",
action="store_true",
help="use relative paths for all link targets",
)
# gather instance options
gather_instance_parser = action_parsers[f"gather {_instance_aliases[0]}"]
gather_instance_parser.add_argument(
"search_paths",
nargs="+",
action="extend",
type=Path,
help="specify a folder or folders to search for Minecraft installations",
)
instance_type = gather_instance_parser.add_mutually_exclusive_group()
instance_type.add_argument(
"--official",
"-o",
action="store_true",
help="specify that these are instances managed by the official launcher",
)
instance_type.add_argument(
"--mmc",
"-m",
action="store_true",
help="specify that these are MultiMC-like instances",
)
# gather server options
gather_server_parser = action_parsers["gather server"]
gather_server_parser.add_argument(
"server_home", type=Path, help="the working directory of the Minecraft server"
)
gather_server_parser.add_argument(
"--jar",
"-j",
type=Path,
help=(
"explicitly specify the path to the server JAR (in case it's outside"
" of the server's working directory of if there are multiple server"
" JAR files inside that folder)"
),
)
gather_server_parser.add_argument(
"--name", "-n", help="specify the name (alias) for the server"
)
gather_server_parser.add_argument(
"--tags",
"-t",
nargs="+",
action="extend",
help="specify any tags you want to apply to the server",
)
# gather remote options
gather_remote_parser = action_parsers[f"gather {_remote_aliases[0]}"]
gather_remote_parser.add_argument(
"remotes",
nargs="+",
action="extend",
help=(
"Provide URIs (e.g. rsync://deck@my-steam-deck/home/deck/) of any"
" remote EnderChest installation to register with this one."
" Note: you should not use this method if the alias (name) of the"
" remote does not match the remote's hostname (in this example,"
' "my-steam-deck").'
),
)
# list shulker box options
# list [instance] boxes options
list_boxes_parser = action_parsers[f"{_list_aliases[0]}"]
list_instance_boxes_parser = action_parsers[
f"{_list_aliases[0]} {_instance_aliases[0]}"
]
instance_name_docs = "The name of the minecraft instance to query"
list_boxes_parser.add_argument(
"--instance", "-i", dest="instance_name", help=instance_name_docs
)
list_instance_boxes_parser.add_argument("instance_name", help=instance_name_docs)
for parser in (list_boxes_parser, list_instance_boxes_parser):
parser.add_argument(
"--path",
"-p",
help=(
"optionally, specify a specific path"
" (absolute, relative, filename or glob pattern"
" to get a report of the shulker box(es) that provide that resource"
),
)
# list shulker options
list_shulker_box_parser = action_parsers[
f"{_list_aliases[0]} {_shulker_box_aliases[0]}"
]
list_shulker_box_parser.add_argument(
"shulker_box_name", help="the name of the shulker box to query"
)
# open / close options
for action in ("open", "close"):
sync_parser = action_parsers[action]
sync_parser.add_argument(
"--dry-run",
action="store_true",
help=(
"perform a dry run of the sync operation,"
" reporting the operations that will be performed"
" but not actually carrying them out"
),
)
sync_parser.add_argument(
"--exclude",
"-e",
action="extend",
nargs="+",
help="Provide any file patterns you would like to skip syncing",
)
sync_parser.add_argument(
"--timeout",
"-t",
type=int,
help=(
"set a maximum number of seconds to try to sync to a remote chest"
" before giving up and going on to the next one"
),
)
sync_confirm_wait = sync_parser.add_argument_group(
title="sync confirmation control",
description=(
"The default behavior when syncing EnderChests is to first perform a"
" dry run of every sync operation and then wait 5 seconds before"
" proceeding with the real sync. The idea is to give you time to"
" interrupt the sync if the dry run looks wrong. You can raise or"
" lower that wait time through these flags. You can also modify it"
" by editing the enderchest.cfg file."
),
).add_mutually_exclusive_group()
sync_confirm_wait.add_argument(
"--wait",
"-w",
dest="sync_confirm_wait",
type=int,
help="set the time in seconds to wait after performing a dry run"
" before the real sync is performed",
)
sync_confirm_wait.add_argument(
"--confirm",
"-c",
dest="sync_confirm_wait",
action="store_true",
help="after performing the dry run, explicitly ask for confirmation"
" before performing the real sync",
)
break_parser = action_parsers["break"]
break_parser.add_argument(
"instances",
nargs="*",
help="instead of breaking your entire EnderChest, just deregister and"
" copy linked resources into the specified instances (by name)",
)
# test pass-through
test_parser = action_parsers["test"]
test_parser.add_argument(
"--use-local-ssh",
action="store_true",
dest="use_local_ssh",
help=(
"By default, tests of SSH functionality will be run against a mock"
" SSH server. If you are running EnderChest on a machine you can SSH"
" into locally (by running `ssh localhost`) without requiring a password,"
" running the tests with this flag will produce more accurate results."
),
)
test_parser.add_argument(
"pytest_args",
nargs=argparse.REMAINDER,
help="any additional arguments to pass through to py.test",
)
return enderchest_parser, action_parsers