//! Command-line surface. //! //! The sidecar is configured by file and environment (see [`crate::config`]); this is deliberately //! not a second configuration mechanism. It exists so the binary can be *driven by an installer* //! rather than only by a human reading its logs: //! //! - `--print-config` resolves the configuration exactly as a normal start would — including //! generating the auth token on first run — and prints it as JSON on stdout. That is the //! supported way to obtain the token for the website's Admin → Shard form. Before this existed, //! the only way to read it back was to scrape the startup log or parse `sidecar.toml`. //! - `--config ` names the config file without having to export `UOLINK_CONFIG`, so a //! diagnostic run can point at an installed config from any working directory. //! //! Hand-rolled rather than pulled from a crate: four flags, no subcommands, no completions. A //! dependency here would be larger than the code it replaced. /// What this invocation should do. Everything except `Run` prints and exits. #[derive(Debug, PartialEq, Eq)] pub enum Mode { /// Normal operation: bind the shard listener and the web server. Run, /// Resolve config, print it as JSON, exit. PrintConfig, Help, Version, } #[derive(Debug, PartialEq, Eq)] pub struct Cli { pub mode: Mode, /// `--config `, which outranks `$UOLINK_CONFIG`. pub config: Option, } pub const USAGE: &str = "\ uo-link sidecar — bridges a ServUO shard to the Runic Gateway website. Usage: uo-link-sidecar [OPTIONS] Options: --print-config Resolve the configuration, print it as JSON, and exit. Runs first-run setup like a normal start does: if the config file is missing it is written, and a blank auth token is generated and saved. The JSON CONTAINS THE AUTH TOKEN in clear text. --config Path to sidecar.toml. Overrides $UOLINK_CONFIG; defaults to ./sidecar.toml. -V, --version Print the sidecar and protocol versions and exit. -h, --help Print this help and exit. Configuration lives in sidecar.toml; environment variables override the file: UOLINK_CONFIG, UOLINK_SHARD_BIND, UOLINK_WEB_BIND, UOLINK_WEB_TOKEN, UOLINK_DB_PATH "; /// Parses arguments **without** the program name. /// /// Returns the message to print on stderr when the arguments are unusable; the caller exits `2`. pub fn parse>(args: I) -> Result { let mut mode = Mode::Run; let mut config = None; let mut it = args.into_iter(); while let Some(arg) = it.next() { match arg.as_str() { "--print-config" => mode = Mode::PrintConfig, "-h" | "--help" => { return Ok(Cli { mode: Mode::Help, config, }) } "-V" | "--version" => { return Ok(Cli { mode: Mode::Version, config, }) } "--config" => { // `--config` with nothing after it would otherwise silently fall through and start // the sidecar against the default config — the opposite of what was asked for. let path = it .next() .ok_or_else(|| "--config requires a path".to_string())?; config = Some(path); } _ => match arg.strip_prefix("--config=") { Some("") => return Err("--config requires a path".into()), Some(path) => config = Some(path.to_string()), None => return Err(format!("unrecognized argument: {arg}")), }, } } Ok(Cli { mode, config }) } #[cfg(test)] mod tests { use super::*; fn parse_str(args: &[&str]) -> Result { parse(args.iter().map(|s| s.to_string())) } #[test] fn no_arguments_runs_the_sidecar() { let cli = parse_str(&[]).unwrap(); assert_eq!(cli.mode, Mode::Run); assert_eq!(cli.config, None); } #[test] fn print_config_is_recognized() { assert_eq!( parse_str(&["--print-config"]).unwrap().mode, Mode::PrintConfig ); } #[test] fn config_accepts_both_spellings() { let spaced = parse_str(&["--config", "/etc/runicgateway/sidecar.toml"]).unwrap(); let equals = parse_str(&["--config=/etc/runicgateway/sidecar.toml"]).unwrap(); assert_eq!( spaced.config.as_deref(), Some("/etc/runicgateway/sidecar.toml") ); assert_eq!(spaced, equals); } #[test] fn config_combines_with_print_config() { let cli = parse_str(&["--config", "c.toml", "--print-config"]).unwrap(); assert_eq!(cli.mode, Mode::PrintConfig); assert_eq!(cli.config.as_deref(), Some("c.toml")); } #[test] fn a_path_is_never_swallowed_as_a_flag() { // `--config --print-config` takes the next token as the path, wrong as that path is. The // alternative — treating it as a missing value — guesses at intent. let cli = parse_str(&["--config", "--print-config"]).unwrap(); assert_eq!(cli.mode, Mode::Run); assert_eq!(cli.config.as_deref(), Some("--print-config")); } #[test] fn config_without_a_value_is_an_error() { assert!(parse_str(&["--config"]).is_err()); assert!(parse_str(&["--config="]).is_err()); } #[test] fn unknown_arguments_are_rejected() { // Silently ignoring a typo'd flag would start a sidecar that is not what was asked for. let err = parse_str(&["--pirnt-config"]).unwrap_err(); assert!(err.contains("--pirnt-config"), "{err}"); assert!(parse_str(&["/etc/runicgateway/sidecar.toml"]).is_err()); } #[test] fn help_and_version_win_immediately() { assert_eq!(parse_str(&["--help", "--bogus"]).unwrap().mode, Mode::Help); assert_eq!(parse_str(&["-h"]).unwrap().mode, Mode::Help); assert_eq!( parse_str(&["--version", "--bogus"]).unwrap().mode, Mode::Version ); assert_eq!(parse_str(&["-V"]).unwrap().mode, Mode::Version); } }