• Your first CLI tool with Rust
  • Original author: Jeremie Veillet
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: JackEggie
  • Proofread by: TloveYing

In the wonderful world of programming, you’ve probably heard of this new language called Rust. It is an open source system-level programming language. It focuses on performance, memory security and parallelism. You can write low-level applications with it just like C/C++.

You’ve probably seen it on the Web Assembly site. Rust can compile WASM applications, and you can find many examples on the Web Assembly FAQ. It is also considered the cornerstone of Servo, a high-performance browser engine implemented in Firefox.

That might make you cringe, but that’s not what we’re talking about here. We’ll show you how to use it to build command-line tools, and you might find it interesting.

Why Rust?

Okay, let me get this straight. I could have done the command-line tool in any other language or framework. I could choose C, Go, Ruby, etc. I could even use the classic bash.

In 2018, I wanted to learn something new, and Rust piqued my curiosity, as well as my need to build simple gadgets to automate some processes at work and on personal projects.

The installation

You can set up your development environment using Rustup, which is the main entry point for installing and configuring all Rust tools on your machine.

If you are working on Linux or MacOS, use the following command to complete the installation:

$ curl <https://sh.rustup.rs> -sSf | sh
Copy the code

If you are using Windows, you will also need to download an EXE from the Rustup website and run it.

If you are running Windows 10, I recommend using WSL to complete the installation. These are the steps required for installation, and we are now ready to create our first Rust application!

Your first Rust application

What we’re going to do here is build a UNIX utility modeled after CAT, or at least a simplified version, which we’ll call KT. This application will take a file path as input and display the contents of the file in the terminal’s standard output.

To create the basic framework for this application, we will use a tool called Cargo. It is Rust’s package manager and can be thought of as the NPM for Rust tools (for Javascript developers) or Bundler (for Ruby developers).

Open your terminal, go to the path where you want to store the source code, and type the following code.

$ cargo init kt
Copy the code

This will create a directory called KT that already contains the basic structure of our application.

If we CD into the directory, we will see the directory structure. And, conveniently, the project already initializes Git by default. That’s great!

$ cd kt/
  |
  .git/
  |
  .gitignore
  |
  Cargo.toml
  |
  src/
Copy the code

The Cargo. Toml file contains the basic information and dependency information for our application. Again, think of it as your application’s package.json or Gemfile file.

The SRC/directory contains the source files for the application, and we can see that there is only one main.rs file. Examining the contents of the file, we can see that there is only one main function.

fn main() {
    println!("Hello, world!");
}
Copy the code

Try building the project. With no external dependencies, it should build very quickly.

$cargo build the Compiling kt v0.1.0 (/ Users/jeremie/Development/Kitty) Finished dev [unoptimized + debuginfo] target (s)in2.82 sCopy the code

In development mode, you can execute the binary by calling cargo Run (cargo run — my_arg to pass command line arguments).

$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.07s
Running `target/debug/kt`
Hello, world!
Copy the code

Congratulations, you have created and run your first Rust application by following these steps! 🎉

Parse the first command line argument

As I mentioned earlier in the article, we are trying to build a simplified version of the cat command. Our goal is to simulate the behavior of cat by running the kt myfile.txt command and output the contents of the file at the terminal.

We could have handled the parsing of parameters ourselves, but fortunately, a Rust tool can help simplify the process: It’s called Clap.

This is a high-performance command-line argument parser that makes it easy to manage command-line arguments.

The first step in using the tool is to open the Cargo. Toml file and add the specified dependencies to it. It doesn’t matter if you’ve never worked with.toml files, which are very similar to.ini files in Windows. This file format is common in Rust.

In this file, you will see that some information has been filled in, such as author, version, and so on. We just need to add the dependency under [dependencies].

[dependencies]
clap = "2.32"
Copy the code

After saving the files, we need to rebuild the project so that we can use the dependent libraries. There is no need to worry if Cargo downloads a file other than Clap, since Clap also has its required dependencies.

$cargo build Updating crates. IO index Downloaded Clap V2.32.0 Downloaded atty v0.2.11 BitFlags v1.0.4 Downloaded ANsi_term V0.11.0 Downloaded VEC_map V0.8.1 Downloaded Textwrap V0.10.0 Downloaded LIBC V0.2.48 Unicode-width v0.1.5 Compiling libc v0.2.48 Compiling strsim V0.7.0 Compiling bitflags v1.0.4 Compiling ansi_term v0.11.0 Compiling VEC_map v0.8.1 Compiling Textwrap v0.10.0 The Compiling atty v0.2.11 Compiling clap v2.32.0 Compiling kt v0.1.0 (/ home/jeremie/Development/kt) Finished dev [unoptimized + debuginfo] target(s)in33.92 sCopy the code

That’s all we need to configure, so we can start by writing some code to read our first command-line argument.

Open the main.rs file. We must explicitly declare that we want to use the Clap library.

extern crate clap;

use clap::{Arg, App};

fn main() {}
Copy the code

The Extern Crate keyword is used to import dependency libraries; you simply add it to the main file and any source file of your application can reference it. The use part refers to which module of the clap you are going to use in the file.

Rust module (Module)

Rust has a modular system that enables reuse of code in an organized manner. A module is a namespace that contains function or type definitions, and you can choose whether those definitions are visible outside of its module (public/private). – Rust document

What we declare here is that we want to use the Arg and App modules. We want our application to have a FILE parameter that will contain a FILE path. The Clap can help us implement this feature quickly. There is a very pleasant way to call methods in a chain fashion.

fn main() {
    let matches = App::new("kt")
      .version("0.1.0 from")
      .author("Jérémie Veillet. [email protected]")
      .about("A drop in cat replacement written in Rust")
      .arg(Arg::with_name("FILE")
            .help("File to print.")
            .empty_values(false)
        )
      .get_matches();
}
Copy the code

Compile and execute again, and it shouldn’t output much other than the compile warning on the variable matches (for Ruby developers, it’s possible to prefix the variable with _, which tells the compiler it’s optional).

If you pass -h or -v to the application, it automatically generates a help message and version information. I don’t know how you feel about this thing, but I think it 🔥🔥🔥.

$ cargo run -- -h
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/kt -h`
kt 0.1.0
Jérémie Veillet. [email protected]
A drop-in cat replacement written in Rust

 USAGE:
    kt [FILE]

 FLAGS:
    -h, --help       Prints help information
    -V, --version    Prints version information

 ARGS:
    <FILE>    File to print.

$ cargo run --- -V
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running target/debug/kt -V
kt 0.1.0
Copy the code

We can also try to start the program with no arguments and see what happens.

$ cargo run --
Finished dev [unoptimized + debuginfo] target(s) in0.03 s Running ` target/debug/kt `Copy the code

Nothing happened. This is the default behavior that should happen every time you build a command-line tool. I don’t think any action should ever be triggered without passing any parameters to the application. Even if this is not always true, in most cases, never do something the user never intended to do.

Now that we have the arguments, we can delve into how to capture this command-line argument and display something in standard output.

To do this, use the value_of method in Clap. Please refer to the documentation to see how this works.

fn main() {
    let matches = App::new("kt")
      .version("0.1.0 from")
      .author("Jérémie Veillet. [email protected]")
      .about("A drop in cat replacement written in Rust")
      .arg(Arg::with_name("FILE")
            .help("File to print.")
            .empty_values(false)
      )
      .get_matches();

     if let Some(file) = matches.value_of("FILE") {
        println!("Value for file argument: {}", file); }}Copy the code

At this point, you can run the application and pass in a random string as an argument, which will be displayed in your console.

$ cargo run -- test.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
  Running `target/debug/kt test.txt`
Value for file argument: test.txt
Copy the code

Note that we are not actually verifying the existence of this file at this time. So how do we do that?

There is a standard library that allows us to check the existence of a file or directory in a very simple way. It is the STD :: Path library. It has an exists method that checks for the existence of a file.

As mentioned earlier, use the use keyword to add the dependent library, and then write the following code. As you can see, we use the if-else condition to control printing some text in the output. println! Method writes stdout to the standard output, while eprintln! The standard error output stderr is written.

extern crate clap;

use clap::{Arg, App};
use std::path::Path;
use std::process;

 fn main() {
    let matches = App::new("kt")
      .version("0.1.0 from")
      .author("Jérémie Veillet. [email protected]")
      .about("A drop in cat replacement written in Rust")
      .arg(Arg::with_name("FILE")
            .help("File to print.")
            .empty_values(false)
        )
      .get_matches();

     if let Some(file) = matches.value_of("FILE") {
        println!("Value for file argument: {}", file);
        if Path::new(&file).exists() {
            println!("File exist!!");
        }
        else{ eprintln! ("[kt Error] No such file or directory.");
            process::exit(1); // Standard exit code for program error termination}}}Copy the code

We’re almost done! Now we need to read the contents of the file and display the results in STdout.

Again, we’ll use a standard library called File to read files. We will use the open method to read the contents of the file, and then write it to a string object that will be displayed in stdout.

extern crate clap;

use clap::{Arg, App};
use std::path::Path;
use std::process;
use std::fs::File;
use std::io::{Read};

fn main() {
    let matches = App::new("kt")
      .version("0.1.0 from")
      .author("Jérémie Veillet. [email protected]")
      .about("A drop in cat replacement written in Rust")
      .arg(Arg::with_name("FILE")
            .help("File to print.")
            .empty_values(false)
        )
      .get_matches();
    if let Some(file) = matches.value_of("FILE") {
        if Path::new(&file).exists() {
           println!("File exist!!");
           let mut f = File::open(file).expect("[kt Error] File not found.");
           let mut data = String::new();
           f.read_to_string(&mut data).expect("[kt Error] Unable to read the file.");
           println!("{}", data);
        }
        else{ eprintln! ("[kt Error] No such file or directory.");
            process::exit(1); }}}Copy the code

Build and run the code again. Congratulations to you! We now have a fully functional tool! 🍾

$cargo build the Compiling kt v0.1.0 (/ home/jeremie/Development/kt) Finished dev [unoptimized + debuginfo] target (s)in 0.70s
$ cargo run -- ./src/main.rs
    Finished dev [unoptimized + debuginfo] target(s) in0.03s Running 'target/debug/ kt. / SRC /main.rs' File exist!! extern crate clap; use clap::{Arg, App}; use std::path::Path; use std::process; use std::fs::File; use std::io::{Read}; fnmain() {
    let matches = App::new("kt")
      .version("0.1.0 from")
      .author("Jérémie Veillet. [email protected]")
      .about("A drop in cat replacement written in Rust")
      .arg(Arg::with_name("FILE")
            .help("File to print.")
            .empty_values(false)
        )
      .get_matches();

     if let Some(file) = matches.value_of("FILE") {
        if Path::new(&file).exists() { println! ("File exist!!");
            let mut f = File::open(file).expect("[kt Error] File not found.");
            let mut data = String::new();
            f.read_to_string(&mut data).expect("[kt Error] Unable to read the file."); println! ("{}", data);
        }
        else{ eprintln! ("[kt Error] No such file or directory.");
            process::exit(1); }}}Copy the code

Improve a little bit

Our application can now take a parameter and display the result in STdout.

We can tweak the performance of the entire print phase slightly by using Writeln! Instead of a println! . This is well explained in the Rust output tutorial. Along the way, we can clean up some code, remove unnecessary prints, and fine-tune possible error scenarios.

extern crate clap;

use clap::{Arg, App};
use std::path::Path;
use std::process;
use std::fs::File;
use std::io::{Read, Write};

fn main() {
    let matches = App::new("kt")
      .version("0.1.0 from")
      .author("Jérémie Veillet. [email protected]")
      .about("A drop in cat replacement written in Rust")
      .arg(Arg::with_name("FILE")
            .help("File to print.")
            .empty_values(false)
        )
      .get_matches();

     if let Some(file) = matches.value_of("FILE") {
        if Path::new(&file).exists() {
            match File::open(file) {
                Ok(mut f) => {
                    let mut data = String::new();
                    f.read_to_string(&mut data).expect("[kt Error] Unable to read the file.");
                    let stdout = std::io::stdout(); // Get the global stdout object
                    let mut handle = std::io::BufWriter::new(stdout); // Optional: Wrap handle in a buffer
                    match writeln!(handle, "{}", data) {
                        Ok(_res) => {},
                        Err(err) => { eprintln! ("[kt Error] Unable to display the file contents. {:? }", err);
                            process::exit(1); }}},Err(err) => { eprintln! ("[kt Error] Unable to read the file. {:? }", err);
                    process::exit(1); }}},else{ eprintln! ("[kt Error] No such file or directory.");
            process::exit(1); }}}Copy the code
$ cargo run -- ./src/main.rs
  Finished dev [unoptimized + debuginfo] target(s) inS Running 'target/debug/kt./ SRC /main.rs' extern crate clap; use clap::{Arg, App}; use std::path::Path; use std::process; use std::fs::File; use std::io::{Read, Write}; fnmain() {
    let matches = App::new("kt")
      .version("0.1.0 from")
      .author("Jérémie Veillet. [email protected]")
      .about("A drop in cat replacement written in Rust")
      .arg(Arg::with_name("FILE")
            .help("File to print.")
            .empty_values(false)
        )
      .get_matches();

     if let Some(file) = matches.value_of("FILE") {
        if Path::new(&file).exists() {
            match File::open(file) {
                Ok(mut f) => {
                    let mut data = String::new();
                    f.read_to_string(&mut data).expect("[kt Error] Unable to read the file.");
                    letstdout = std::io::stdout(); // Get the global stdout objectletmut handle = std::io::BufWriter::new(stdout); // Optional: Wrap handle in a buffer match Writeln! (handle,"{}", data) { Ok(_res) => {}, Err(err) => { eprintln! ("[kt Error] Unable to display the file contents. {:? }", err);
                            process::exit(1); }, } } Err(err) => { eprintln! ("[kt Error] Unable to read the file. {:? }", err);
                    process::exit(1); }}},else{ eprintln! ("[kt Error] No such file or directory.");
            process::exit(1); }}}Copy the code

We’re done! We did our simplified version of the cat command 🤡 in about 45 lines of code, and it worked pretty well!

Build standalone applications

So how do you build the application and install it on the file system? Ask Cargo for help!

Cargo Build accepts a –release flag bit so that we can specify the final version of the executable we want.

$cargo build --release Compiling libc v0.2.48 Compiling unicode-width v0.1.5 Compiling ansi_term v0.11.0 Compiling Bitflags v1.0.4 Compiling Vec_map v0.8.1 Compiling strsim v0.7.0 Compiling Textwrap v0.10.0 Compiling atty v0.2.11 The Compiling clap v2.32.0 Compiling kt v0.1.0 (/ home/jeremie/Development/kt) Finished release/optimized target (s)in28.17 sCopy the code

The generated executable is located in this subdirectory:./target/release/kt.

You can copy this file into your PATH environment variable, or use a cargo command to install it automatically. The application will be installed in the ~/.cargo/bin/ directory (make sure this directory is in the PATH environment variable of ~/.bashrc or ~/.zshrc).

$cargo install -- path. Installing kt v0.1.0 (/ home/jeremie/Development/kt) Finished release/optimized target (s)in0.03 s Installing/home/jeremie /. Cargo/bin/ktCopy the code

Now we can call our application directly from the terminal using the KT command! \o/

$kt -V kt 0.1.0Copy the code

conclusion

We created a command-line gadget with just a few lines of Rust code that takes a file path as input and displays the contents of that file in stdout.

You can find all the source code for this article in the GitHub repository.

It’s your turn to improve the tool!

  • You can add a command-line argument to control whether to add a line number to the output (-nOption).
  • Show only part of the file and then press on the keyboardENTERKey to display the rest.
  • usekt myfile.txt myfile2.txt myfile3.txtThis syntax opens multiple files at once.

Don’t hesitate to tell me what you did with it! 😎

Special thanks to Anais 👍 for helping to revise this article

Further exploration

  • Cat: Wikipedia page for the CAT utility.
  • kt-rs
  • Rust Cookbook
  • Clap: A full-featured, high-performance Rust command-line argument parser.
  • Reqwest: A simple and powerful Rust HTTP client.
  • Serde: A serialization framework for Rust.
  • Crates. IO: Tool registration site for the Rust community.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.