aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/populate/src/cli.rs
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/populate/src/cli.rs')
-rw-r--r--pkgs/populate/src/cli.rs212
1 files changed, 212 insertions, 0 deletions
diff --git a/pkgs/populate/src/cli.rs b/pkgs/populate/src/cli.rs
new file mode 100644
index 0000000..c059c0f
--- /dev/null
+++ b/pkgs/populate/src/cli.rs
@@ -0,0 +1,212 @@
+//! populate's command line.
+
+use crate::spec::TargetParts;
+
+/// What the command line asks for.
+#[derive(Debug, PartialEq)]
+pub enum Command {
+ Help,
+ Version,
+ Populate(Options),
+}
+
+/// The arguments of populating.
+#[derive(Debug, Default, PartialEq)]
+pub struct Options {
+ pub backup: bool,
+ pub force: bool,
+ pub prune: bool,
+ /// The file the sources are in, or - for standard input.
+ pub sources: String,
+ /// The target, as given, with the options changing its parts applied.
+ pub target: TargetParts,
+ pub sudo: bool,
+ pub ssh_args: Vec<String>,
+ /// Whether the target is the machine populate runs on, if --local or
+ /// --remote says so.
+ pub local: Option<bool>,
+}
+
+pub const USAGE: &str = "\
+usage: populate [OPTION]... SOURCES [TARGET]
+
+Delivers the sources in the file SOURCES, a JSON object, or standard input
+if SOURCES is -, to the target directory TARGET.
+
+TARGET is [USER@][HOST][:PORT][PATH]. USER defaults to $LOGNAME, HOST to
+the name of the machine, and PATH to /var/src. A target is local if its
+user is $LOGNAME, and its host is localhost or the name of the machine;
+populate reaches other targets via ssh.
+
+Options:
+ --backup copy the target directory to a backup first
+ --force create the target directory's sentinel if it's missing
+ --prune remove what isn't a source from the target directory
+ --sudo deliver as root, via sudo on the target
+ --ssh-arg ARG pass ARG to ssh; repeatable
+ --user USER the target's user; empty for ssh's default
+ --host HOST the target's host
+ --port PORT the port ssh connects to
+ --path PATH the target directory
+ --local the target is the machine populate runs on
+ --remote the target isn't, even if it looks so
+ -h, --help print this and exit
+ -V, --version print populate's version and exit
+";
+
+/// The command line, without the program's name. Options and the other
+/// arguments may come in any order, up to --.
+pub fn parse(args: &[String]) -> Result<Command, String> {
+ let mut options = Options::default();
+ let mut positional: Vec<String> = Vec::new();
+ let mut overrides: Vec<(&'static str, String)> = Vec::new();
+ let mut args = args.iter();
+ while let Some(arg) = args.next() {
+ if arg == "--" {
+ positional.extend(args.by_ref().cloned());
+ break;
+ }
+ if arg == "-" || !arg.starts_with('-') {
+ positional.push(arg.clone());
+ continue;
+ }
+ let (name, inline) = match arg.split_once('=') {
+ Some((name, value)) if name.starts_with("--") => (name, Some(value.to_string())),
+ _ => (arg.as_str(), None),
+ };
+ let mut value = |name: &str| -> Result<String, String> {
+ match &inline {
+ Some(value) => Ok(value.clone()),
+ None => args
+ .next()
+ .cloned()
+ .ok_or_else(|| format!("option {name} needs an argument")),
+ }
+ };
+ match name {
+ "-h" | "--help" => return Ok(Command::Help),
+ "-V" | "--version" => return Ok(Command::Version),
+ "--backup" => options.backup = true,
+ "--force" => options.force = true,
+ "--prune" => options.prune = true,
+ "--sudo" => options.sudo = true,
+ "--local" => options.local = Some(true),
+ "--remote" => options.local = Some(false),
+ "--ssh-arg" => options.ssh_args.push(value(name)?),
+ "--user" => overrides.push(("user", value(name)?)),
+ "--host" => overrides.push(("host", value(name)?)),
+ "--port" => overrides.push(("port", value(name)?)),
+ "--path" => overrides.push(("path", value(name)?)),
+ _ => return Err(format!("unknown option {name}")),
+ }
+ if inline.is_some()
+ && matches!(
+ name,
+ "--backup" | "--force" | "--prune" | "--sudo" | "--local" | "--remote"
+ )
+ {
+ return Err(format!("option {name} takes no argument"));
+ }
+ }
+ let (sources, target) = match positional.as_slice() {
+ [sources] => (sources.clone(), String::new()),
+ [sources, target] => (sources.clone(), target.clone()),
+ [] => return Err("SOURCES is missing".to_string()),
+ _ => return Err("too many arguments".to_string()),
+ };
+ options.sources = sources;
+ options.target = TargetParts::parse(&target)?;
+ for (part, value) in overrides {
+ let target = &mut options.target;
+ match part {
+ "user" => target.user = Some(value),
+ "host" => target.host = Some(value),
+ "port" => target.port = Some(value),
+ _ => target.path = Some(value),
+ }
+ }
+ if let Some(path) = &options.target.path {
+ if path.is_empty() {
+ return Err("the target's path is empty".to_string());
+ }
+ }
+ Ok(Command::Populate(options))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn parse(args: &[&str]) -> Result<Command, String> {
+ super::parse(&args.iter().map(|a| a.to_string()).collect::<Vec<_>>())
+ }
+
+ fn options(args: &[&str]) -> Options {
+ match parse(args) {
+ Ok(Command::Populate(options)) => options,
+ other => panic!("{other:?}"),
+ }
+ }
+
+ #[test]
+ fn targets() {
+ let o = options(&["s.json", "root@host:2222/var/src"]);
+ assert_eq!(o.sources, "s.json");
+ assert_eq!(
+ o.target,
+ TargetParts {
+ user: Some("root".to_string()),
+ host: Some("host".to_string()),
+ port: Some("2222".to_string()),
+ path: Some("/var/src".to_string()),
+ }
+ );
+ assert_eq!(options(&["-"]).target, TargetParts::default());
+ let o = options(&["--host", "2001:db8::1", "-", "root@x", "--port=22"]);
+ assert_eq!(o.target.user.as_deref(), Some("root"));
+ assert_eq!(o.target.host.as_deref(), Some("2001:db8::1"));
+ assert_eq!(o.target.port.as_deref(), Some("22"));
+ }
+
+ #[test]
+ fn flags() {
+ let o = options(&[
+ "--backup",
+ "--prune",
+ "--sudo",
+ "--ssh-arg",
+ "-o",
+ "--ssh-arg=X=y",
+ "--remote",
+ "s",
+ ]);
+ assert!(o.backup && o.prune && o.sudo && !o.force);
+ assert_eq!(o.ssh_args, ["-o", "X=y"]);
+ assert_eq!(o.local, Some(false));
+ // After --, arguments are no options.
+ assert_eq!(options(&["--", "--sources"]).sources, "--sources");
+ }
+
+ #[test]
+ fn errors() {
+ assert_eq!(parse(&["--help", "--bogus"]), Ok(Command::Help));
+ assert_eq!(
+ parse(&["--bogus"]),
+ Err("unknown option --bogus".to_string())
+ );
+ assert_eq!(parse(&[]), Err("SOURCES is missing".to_string()));
+ assert_eq!(
+ parse(&["a", "b", "c"]),
+ Err("too many arguments".to_string())
+ );
+ assert_eq!(
+ parse(&["s", "--user"]),
+ Err("option --user needs an argument".to_string())
+ );
+ assert_eq!(
+ parse(&["--prune=yes", "s"]),
+ Err("option --prune takes no argument".to_string())
+ );
+ assert!(parse(&["s", "host:"]).is_err());
+ }
+}