Check for unset command-line arguments

I was trying to set up some command-line arguments and came across this method with clap. I don't mind to use something else, but let me explain my problem. Here is the code I have now:

fn main() {
    use std::process;
    use std::process::{Command, Stdio};
    use clap::{Arg, App};

    let matches = App::new("Test")
        .version("0.1.0")
        .author("me <somebody@mail.ru>")
        .about("DevOps In Rust")
        .arg(Arg::with_name("File")
                 .short("f")
                 .takes_value(true)
                 .help("Path to file of hosts."))
        .arg(Arg::with_name("Execute")
                 .short("e")
                 .takes_value(true)
                 .help("Commands to be quotes. Use quotes for muliple commands."))
        .arg(Arg::with_name("Host")
                 .short("l")
                 .takes_value(true)
                 .help("Ip or domain of host"))
        .arg(Arg::with_name("User")
                 .short("u")
                 .takes_value(true)
                 .help("Username. Should have sudo, but not enforced."))
        .arg(Arg::with_name("Port")
                 .short("p")
                 .takes_value(true)
                 .help("Specify port if different from 22."))
        .arg(Arg::with_name("Password")
                 .short("w")
                 .takes_value(true)
                 .help("Password if SSH keys are not configured."))
        .get_matches();

    let user = matches.value_of("User").unwrap();
    println!("{}", user);

    let host = matches.value_of("Host").unwrap();
    println!("{}", host);

    let password = matches.value_of("Password").unwrap();
    println!("{}", password);

    let file = matches.value_of("File").unwrap();
    println!("{}", file);

    let exec = matches.value_of("Execute").unwrap();
    println!("{}", exec);

    let port = matches.value_of("Port").unwrap();
    println!("{}", port);
}

If I run it with all arguments set from the command-line it runs as expected right now:

$ ./target/release/system -u user -l localhost -w Cheese -f file -e date -p 22
user
localhost
Cheese
file
date
22

If you run this without all the arguments right now it will fail as Rust shouldn't t print it out an unset value. I wanted to perform a check if arguments are set and perform different actions depending on what the user chose. I found some stuff about using is_empty() and tried using the following:

if !Port.is_empty() {
    let port = matches.value_of("Port").unwrap();
    println!("{}", port);
}

When I tried and if statement with "!Port.is_empty();" I got an error that Port wasn't in scope with that line.

$ cargo build --release
   Compiling system v0.1.0 (/home/user/rust/system)
error[E0425]: cannot find value `Port` in this scope
  --> src/main.rs:90:9
   |
90 |     if !Port.is_empty() {
   |         ^^^^ not found in this scope

I'm unsure how this could be out of scope as without the if around those same lines Port is found just fine.

Happy to hear anyone's suggestions.

Thanks!

  1. Rust is case-sensitive. Port and port are two different names. Use lowercase letters for variable names.

  2. Variables exist only after you declare them. let port must be before all uses of port. You can't have if port {let port}.

I'm aware of how Rust and most languages are case sensitive and X is not x. Port is the command-line option, not the new variable 'port'. I'm using those as separate values. I also mentioned that the first code works without the if statement around it. The problem is checking if Port is empty or not.

Thanks

The identifier Port doesn’t appear let-bound anywhere in your code, but a string containing the word “Port” is used to get a value which you later store in the variable port. That’s why wrapping the definition of port in an if causes an error; if you really wanted to do this you’d have to write if matches.value_of(“Port”).is_some(). But that’s awkward; if you want a default port you could use matches.value_of(“Port”).or(8080) or whatever (the argument to or should be of the same type as the value inside the Option from value_of, I haven’t used clap before so I’m not sure what the type should be). If you just want to execute something for side effects or to do something else Option has a variety of useful functions.

1 Like

Well, matches.value_of(“Port”).is_some() does get the job done. Unsure why it was felt to be awkward, but it has to look better than me putting a variable into another variable if there's no reason for that. I can work with this.

Thank you!

You can also use pattern matching to reduce some of the repetition:

if let Some(port) = matches.value_of("Port") {
    println!("{}", port);
}

or

match matches.value_of("Port") {
    Some(port) => { println!("{}", port); }
    None       => { println!("Port not specified"); }
}
1 Like

This was much appreciated:

match matches.value_of("Port") {
    Some(port) => { println!("{}", port); }
    None       => { println!("Port not specified"); }
}

I was going to do an if/else statement, but that is much cleaner.

Thanks again!

“Awkward” was not very precise of me! Here’s what I meant: writing and executing matches.value_of(“Port”) twice (once for the is_some and once for the unwrap) introduces some repetition and unnecessary work, so it’s preferable to avoid separately checking for non-None-ness and getting the value by using either match/if let or Options various helper functions.

1 Like

I'm unsure if this is related or needs a new topic. The previous suggestions worked for checking if one argument is set, but what about multiples? I know this ugly, but if I do something like this I can detect if multiple values are set:

if matches.value_of("User").is_some() && matches.value_of("Host").is_some() && matches.value_of("Exec").is_some() {
    println!("It worked");
}

The above won't work for me because I won't be able to pass the values to call a function later within that if statement's body. I tried a lot of things like this, which is not syntactically correct:

match matches.value_of("User", "Host", "Exec"){
Some(user, host, exec) => { single(&user, &host, &exec); }
None => { println!("Wut...?") }
}

Hopefully that at least gets the point across of what I'm trying to do. I'm just wondering about how to check if multiple arguments set at the same time. Tried searching, but all the documentation I found seems to be for matching patterns in an OR type situation. I need to check them with AND type logic.

Thank you

You can use iterators to reduce the repetition: (not tested)

let args = ["User", "Host", "Exec"]
    .iter()
    .copied()
    .map(|arg| matches.value_of(arg))
    .collect::<Option<Vec<_>>>();
match args.as_ref().map(Vec::as_slice) {
    Some([user, host, exec]) => single(&user, &host, &exec),
    _ => println!("Wut...?"),
}

You can wrap this in a function to reduce repetition.

Also note that you used Execute to define the argument, but Exec to retrieve it. They need to be kept consistent.

1 Like

Well, this does work, and is beautiful compared to what I had. I did have to remove the dbg!() part and just use the matches statement within it. This was perfect though.

Thanks again!

Well, I may have spoken too soon. I started to do a second group of arguments like this:

let args = ["User", "Host", "Pass", "Exec"]
.iter()
.copied()
.map(|arg| matches.value_of(arg))
.collect::<Option<Vec<_>>>();
match args.as_ref().map(Vec::as_slice) {
Some([user, host, pass, exec]) => double(&user, &host, &pass, &exec),
_ => (),
}

let args = ["User", "Host", "Exec"]
.iter()
.copied()
.map(|arg| matches.value_of(arg))
.collect::<Option<Vec<_>>>();
match args.as_ref().map(Vec::as_slice) {
Some([user, host, exec]) => ssh_it(&user, &host, &exec),
_ => (),
}

The problem is that the second part of this code doesn't relize there are extra arguments it shouldn't have and tries to login again when it shouldn't and fails:

$ ./target/release/tester -u user -l 192.168.x.x -w cheese -e date
Mon 03 Aug 2020 09:27:15 PM CDT

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error { code: -18, msg: "Username/PublicKey combination invalid" }', src/main.rs:41:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

I tried putting if statements around them, but they seemed to get ignored. I know someone mentioned putting these in their own functions, but since I'm only using these to call functions it seems that would be a little unnecessary. Any more suggestions?

Thank you

Oops, sorry, that dbg! was my mistake :smiley: I forgot to remove it when I copied the code.

As for the second one, well, that seems like an SSH-related issue. I'm not sure either :neutral_face:

Well, the SSH error isn't the issue. Even though the password was supplied, the second instance of that code does not check, take or pass the password value. The user I'm testing in that case requires a password. So when it goes to the second instance that password is not there and the login fails as it should. The problem is the second instance shouldn't be called at all if the first instance has. Is there a way I can add a check or skip these when they aren't needed.

Sorry if that wasn't clear.

Which arguments are required and which are optional?

I think the cleanest approach if you do different things on different combinations of valid arguments is something like this. We’ll say A1 is mandatory, A3 is purely optional, and depending on whether A2 is given we’ll do different things.

match [“A1”, “A2”, “A3”].iter().map(|a| matches.value_of(a)).collect::<Vec<_>>() {
  [Some(a1), Some(a2), a3] => do_a2(a1, a3.or(false)),
  [Some(a1), None, a3] => do_no_a2(a1, a3.or(true))
  _ => print_usage()
}

For a bonus you could let the match evaluate to a Result or an exit code.

2 Likes

Well, I need to have different sets of arguments calling different functions. So even if using Port is not required, it needs to call a function when used and a different function without it. So I think I'm looking for absolutes. Maybe some pseudo code would help:

if ("User", "Host", "Exec") {
     first_call(&User, &Host, &Exec); 
}

if ("User", "Host", "Pass", "Exec") {
     second_call(&User, &Host, &Pass, &Exec);
}

Would you have any suggestions something like that? I have a lot more separate functions for different options. Otherwise I wouldn't be so picky.

I did try to make this method work:

match ["User", "Host", "Pass", "Exec"].iter().map(|a| matches.value_of(a)).collect::<Vec<_>>() {
[Some(user), Some(host), Some(pass), Some(exec)] => double(&user, &host, &pass, &exec),
[Some(user), Some(host), None(pass), Some(exec)] => ssh_it(&user, &host, &exec),
_ => (),
}

This gets so many compiler errors I'm embarrassed to post it. That's on me though. I do see that this could be used to call different functions, but I'm lost on the syntax. Could anyone point me in the right direction?