Help writing simple command line application

Disclaimer: This is a veeeery newbie level question. I have only gotten past the "Slice" chapter of the Rust book, but I need to write a simple command line application to do a task for me, so I took it as an opportunity to learn a bit of Rust :slight_smile:

What am I trying to do

So, I am trying to write a simple application to publish a file to my blog. This is what it needs to do:

  1. Take one argument (file path)
  2. Use rsync to sync the file into a directory on a remote server
  3. Call a local script (which makes Zola (static site generator) rebuild my site on the remote server)

I realize I can probably just write a simple shell script for this purpose, but since I do not know how to work with arguments in shell scripts yet, I would appreciate if you can show me a Rusty way to do it, so that I can learn about taking command line arguments and using them to issue other commands to my OS in Rust.

Thank you in advance!

EDIT:
Just to add, I am aware that there is a Rust CLI application book, but it is still a little over my head and I don't think I should use external crates for something as tiny as this. Please correct me if I am wrong!

Specifically my questions are:

  • How do I take command line arguments?
  • How do I call rsync or my local script from within my application?

Hey,

Accepting Command Line Arguments section of Rust docs has the following snippet that should get you going through your first blocker:

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);
}

Going beyond you should take a look at: std::process::Command Link

Hope this helps,
K

1 Like

Thank you, I will go from there and see how I can get it to work!

Good luck!

Here's what I have come up with so far:

use std::env;
use std::process::Command;

fn main() {
    let args: Vec<String> = env::args().collect();
    let file = &args[1];
    
    println!("Synchronizing {} to server...", file);
    
    let mut rsync = Command::new("rsync -avz {} destination/", file);

    rsync.status().expect("rsync failed to execute");
}

I am running into an issue though:
If I execute this by running cargo run example.txt, I get the following error:

error[E0061]: this function takes 1 parameter but 2 parameters were supplied
  --> src/main.rs:10:21
   |
10 |     let mut rsync = Command::new("rsync -avz {} destination/", file);
   |                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected 1 parameter

Could you help me with figuring out a different way of using the argument I passed to my application as one of the arguments to pass to rsync? As far as I understand, the compiler believes that I am passing file as a second argument, while I am just trying to use it as part of the command.

You can use format! to do this:

Command::new(format!("rsync -avz {} destination/", file))
1 Like

Another (perhaps cleaner -- think about what happens if the filename has spaces in it!) option is to use Command's builder methods to assemble the command you'd like to execute:

    let mut rsync = Command::new("rsync")
        .arg("-avz")
        .arg(file)
        .arg("destination/");

You may also want to throw a .arg("--") in between the -avz and your filename, too, to ensure you're correctly dealing with filename starting with a -. I understand these details are somewhat tangential to you just getting something working, but thought it was worth mentioning anyway. :smile:

3 Likes

I did end up with something similar to this, but I encountered an error with this way of doing it:

let mut rsync = Command::new("rsync")
                        .arg("-avz")
                        .arg("-e ssh -p 12345")
                        .arg(source)
                        .arg(destination);

rsync.status().expect("rsync failed to execute");

If I do it like this, the compiler complains:

error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:11:21
   |
11 |     let mut rsync = Command::new("rsync")
   |                     ^^^^^^^^^^^^^^^^^^^^^ creates a temporary which is freed while still in use                                               
...
15 |                             .arg(destination);
   |                                              - temporary value is freed at the end of this statement                                          
...
19 |     println!("{:?}", rsync); //for debugging
   |                      ----- borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value

Honestly I do not understand the issue, since I am using let and it does not look like it's going out of scope or something to me.

I was able to work around it like this though:

    let mut rsync = Command::new("rsync");
    rsync.arg("-avz")
         .arg("-e ssh -p 12345")
         .arg(source)
         .arg(destination);

And I finally got everything working the way I wanted it to :slight_smile:

Actually it's not an option but fix. Command("rsymc -avz foo.txt") won't work and you should split arguments manually. If you type this command the shell script like bash do the split, but with Command there's no shell and it's up to you.

1 Like

And you are right, I found that it does not work the way I first tried there :slight_smile:

1 Like

To summarize, this is what I ended up with:

use std::env;
use std::process::Command;

fn main() {
    let args: Vec<String> = env::args().collect();
    let source = &args[1];
    let destination = "username@123.456.78.90:~/path/to/folder/"; 
// ^^^ hardcoded destination to reduce user input since this doesnt
// change for me. If I were to make this a proper tool for people to use,
// I could instead take this from a config file or something similar
    
    println!("\nSynchronizing {} to server...\n", source); 
    
    let mut rsync = Command::new("rsync");
    rsync.arg("-avz")
         .arg("-e ssh -p 12345")  // tells rsync which port to use
         .arg(source)
         .arg(destination);
    
    rsync.status().expect("rsync failed to execute");
//  For debugging changes, comment above and uncomment below to just see output
//  println!("{:?}", rsync);

    
    println!("\nSuccess!");
}
1 Like

Did this actually work? I wouldn't have expected rsync to parse these parameters as a single argument!

In other words, what you've done is equivalent to:

rsync -avz "-e ssh -p 12345" "<source>" "username@123.456.78.90:~/path/to/folder/"

Regarding your temporary variable issue: Command::new() returns a Command, but Command::arg() returns a &mut Command, which means that the result of your arg calls cannot outlast your initial Command. Your options are to either set Command::new() to a variable, as you did, or to include your entire call sequence in one fluent chain:

let status = Command::new("rsync")
    .arg("-avz")
    .arg("-e ssh -p 12345")
    .arg(source)
    .arg(destination);
    .status()
    .expect("rsync failed to execute");
1 Like

Believe me, I was just as surprised as you are, but for some reason it works. I had a hard time figuring out how to get escape sequences to work for this case, tried that out of frustration and somehow it worked.

That clears it up a little, thank you!

I also ended up implementing error handling for when there are no arguments provided; the Rust Book chapter linked above contained all the information I needed and with a bit of fiddling, I got it to work :slight_smile:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.