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
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:
Take one argument (file path)
Use rsync to sync the file into a directory on a remote server
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!
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.
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.
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.
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.
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!");
}
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");
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.
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