Code Monkey home page Code Monkey logo

command-line-rust's Introduction

Command-Line Rust: A Project-Based Primer for Writing Rust CLIs

This is the code repository for the Command-Line Rust (O'Reilly, 2022/2024, ISBN 9781098109417)

Author

Ken Youens-Clark [email protected]

Synopsis

For several consecutive years, Rust has been voted "most loved programming language" in Stack Overflow's annual developer survey. This open source systems programming language is now used for everything from game engines and operating systems to browser components and virtual reality simulation engines. But Rust is also an incredibly complex language with a notoriously difficult learning curve.

Rather than focus on the language as a whole, this guide teaches Rust using a single small, complete, focused program in each chapter. Author Ken Youens-Clark shows you how to start, write, and test each of these programs to create a finished product. You'll learn how to handle errors in Rust, read and write files, and use regular expressions, Rust types, structs, and more.

Discover how to:

  • Use Rust's standard libraries and data types to create command-line programs
  • Write and test Rust programs and functions
  • Read and write files, including stdin, stdout, and stderr
  • Document and validate command-line arguments
  • Write programs that fail gracefully
  • Parse raw and delimited text
  • Use and control randomness

Git Branches

The book was originally published in 2022, when the clap (command-line argument parser) crate was a v2.33. The book was updated in 2024 to use clap v4, which has two patterns for parsing, builder (similar to the original v2.33 code) and derive. The branches are organized as follows:

  • main: Contains the clap v4 derive pattern
  • clap_v4_builder: Contains the clap v4 builder pattern
  • clap_v2: Contains the original programs from the 2022 version of the book that use clap v2.33

Rust

command-line-rust's People

Contributors

ajo2995 avatar kyclark avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

command-line-rust's Issues

cargo test fails for hello1 and hello2 cases in 02_echor

Issue Description:

When running cargo test for the hello1 and hello2 cases in the 02_echor code, the tests fail with the following error:

---- hello2 stdout ----
thread 'hello2' panicked at 'Unexpected stdout, failed diff original var
├── original: Hello there
├── diff:
└── var as str: Hello there

thread 'hello1' panicked at 'Unexpected stdout, failed diff original var
├── original: Hello there
├── diff:
└── var as str: Hello there

Chapter 3: tests failed because of the new line

It seems that the cat wouldn't add the new line at the last position. However, the solution of this repo used println! to print the result. It causes the test failed.

After I change the code to the following, the tests pass

pub fn run(config: Config) -> MyResult<()> {
    for filename in &config.files {
        match open(&filename) {
            Err(err) => eprintln!("{}: {}", filename, err),
            Ok(buf) => {
                let mut line_num = 0usize;
                let mut lines: Vec<String> = vec![];
                for line_result in buf.lines() {
                    let line = line_result?;
                    line_num += 1;
                    if config.number_lines {
                        lines.push(format!("{:>6}\t{}", line_num, line));
                    } else if config.number_nonblank_lines {
                        if line.is_empty() {
                            line_num -= 1;
                            lines.push("".into());
                        } else {
                            lines.push(format!("{:>6}\t{}", line_num, line))
                        }
                    } else {
                        lines.push(format!("{}", line))
                    }
                }

                let result = lines.join("\n");
                print!("{}", result)
            }
        }
    }
    Ok(())
}

confusing branch names

While v2 is out of the question, readme says main is 'clap v4 derive pattern' and clap_v4_builder is 'clap v4 builder pattern'. But actual branches are more than that: main, clap_v4_builder, clap_v4_derive, and clap v4.
image

If readme is correct, what are these other branches? It's confusing. I think it'll be more helpful to readers to cut down branches (or at least update readme).

[catr] tests unreliable?

Hey there,
I'm following along the examples in the book and I can't seem to get the tests for catr to pass...
image

When I use the debugger to get some idea it seems that the test doesn't read the expected outputs correctly:
image

But if I do a hex-dump of the file contents it seems to be just fine?
image
I tried re-generating the outputs with the script in this repository but that doens't seem to help.
Anyone have an idea?

The code is from the tests itself, unmodified:

fn run(args: &[&str], expected_file: &str) -> TestResult {
    let expected = fs::read_to_string(expected_file)?;
    Command::cargo_bin(PRG)?
        .args(args)
        .assert()
        .success()
        .stdout(expected);
    Ok(())
}

The example of testing the `ls` command via `cargo test` doesn't work on Windows. Here's how to fix it.

On page 31 of the Kindle Edition of the book, the following example code from the book doesn't work on Windows:

use std::process::Command; 

#[test]
fn runs() {
    let mut cmd = Command::new("ls"); 
    let res = cmd.output(); 
    assert!(res.is_ok()); 
}

The reason is because there is no separate EXE file corresponding to ls on Windows.

Windows' ls command is actually an internal component of the powershell executable.

To fix it, what actually must be done is to pass "powershell" as the command and then give it "ls" as an argument.

Thus, the following code (in contrast), works on Windows:

use std::process::Command;

#[test]
fn runs() {
    let mut cmd = Command::new("powershell");
    cmd.arg("ls");
    let res = cmd.output();
    assert!(res.is_ok());
}

Perhaps this mistake was made by emulating Windows on Linux (which is not real Windows) or by not testing it on Windows at all.

Anyway, thanks for your time and for writing the book!

FWIW: `find` output differs from book (and appears to be expected)

TL; DR

The output differs system to system regardless of the find implementation (BSD vs GNU) that is used.

Overview

This is less of a bug report and more of a FYI for others going through the book. No matter the version of find that I tested with the output did not match what the book shows.

Version of repo used:

$ git log -n 1
commit 10d983f68e84b9c94057da6ecf555bc419e11999 (HEAD -> main, origin/main, origin/HEAD)
Author: Ken Youens-Clark <[email protected]>
Date:   Tue Mar 19 11:35:02 2024 -0700

    readme

From the book

CH7, page 144-145 (epub) of the book notes that the BSD find output is on the left with GNU version on Linux on the right:

$ find .                          $ find .
.                                 .
./g.csv                           ./d
./a                               ./d/d.txt
./a/a.txt                         ./d/d.tsv
./a/b                             ./d/e
./a/b/b.csv                       ./d/e/e.mp3
./a/b/c                           ./d/b.csv
./a/b/c/c.mp3                     ./f
./f                               ./f/f.txt
./f/f.txt                         ./g.csv
./d                               ./a
./d/b.csv                         ./a/a.txt
./d/d.txt                         ./a/b
./d/d.tsv                         ./a/b/c
./d/e                             ./a/b/c/c.mp3
./d/e/e.mp3                       ./a/b/b.csv

Ubuntu 20.04 (WSLv2)

$ git clone https://github.com/kyclark/command-line-rust
$ cd command-line-rust/07_findr/tests/inputs
$ find .
.
./a
./a/a.txt
./a/b
./a/b/b.csv
./a/b/c
./a/b/c/c.mp3
./g.csv
./f
./f/f.txt
./d
./d/d.tsv
./d/d.txt
./d/e
./d/e/e.mp3
./d/b.csv

Ubuntu 22.04 (WSLv2)

$ git clone https://github.com/kyclark/command-line-rust
$ cd command-line-rust/07_findr/tests/inputs
$ find .
.
./g.csv
./d
./d/b.csv
./d/e
./d/e/e.mp3
./d/d.txt
./d/d.tsv
./f
./f/f.txt
./a
./a/a.txt
./a/b
./a/b/c
./a/b/c/c.mp3
./a/b/b.csv

FreeBSD 14

$ git clone https://github.com/kyclark/command-line-rust
$ cd command-line-rust/07_findr/tests/inputs
$ find .
.
./a
./a/a.txt
./a/b
./a/b/b.csv
./a/b/c
./a/b/c/c.mp3
./d
./d/b.csv
./d/d.tsv
./d/d.txt
./d/e
./d/e/e.mp3
./f
./f/f.txt
./g.csv

Thus far I've been using tools provided by a FreeBSD 14 VM as a reference for the BSD version of each tool covered by the book. Having a BSD version of the tools to contrast against the GNU version of the tools on an Ubuntu instance has been very helpful as I've gone through the exercises.

Explanation

Based on light research, the default find sort order appears to be based on the order items are stored within the directory entries. As noted in the serverfault.com Q/A below, the sort order should ordinarily be stable for the same machine but is subject to change if the filesystem entries are modified as part of maintenance operations.

References:

Test failure output for headr is unfortunate

Putting this as an issue on this repo rather than commenting on the book because this problem isn't actually anything to do with the book text; it only exists in this repo.

I realize that because headr in chapter 3 is demonstrating how splitting UTF-8 codepoints when requesting a number of bytes results in invalid UTF-8, you can't match on Rust strings and you have to use predicate::eq(&expected.as_bytes() as &[u8]). Buuut it makes trying to figure out why a test is failing rather frustrating :(

For example:

---- multiple_files_c1 stdout ----
thread 'multiple_files_c1' panicked at 'Unexpected stdout, failed var == [61, 61, 62, 32, 46, 47, 116, 101, 115, 116, 115, 47, 105, 110, 112, 117, 116, 115, 47, 101, 109, 112, 116, 121, 46, 116, 120, 116, 32, 60, 61, 61, 10, 10, 61, 61, 62, 32, 46, 47, 116, 101, 115, 116, 115, 47, 105, 110, 112, 117, 116, 115, 47, 111, 110, 101, 46, 116, 120, 116, 32, 60, 61, 61, 10, 239, 191, 189, 10, 61, 61, 62, 32, 46, 47, 116, 101, 115, 116, 115, 47, 105, 110, 112, 117, 116, 115, 47, 116, 119, 111, 46, 116, 120, 116, 32, 60, 61, 61, 10, 84, 10, 61, 61, 62, 32, 46, 47, 116, 101, 115, 116, 115, 47, 105, 110, 112, 117, 116, 115, 47, 116, 104, 114, 101, 101, 46, 116, 120, 116, 32, 60, 61, 61, 10, 84]

command=`"/Users/carolnichols/rust/hands-on/carols-solutions/headr2/target/debug/headr" "./tests/inputs/empty.txt" "./tests/inputs/one.txt" "./tests/inputs/two.txt" "./tests/inputs/three.txt" "-c" "1"`
code=0
stdout=```"Öne line, four words.\nTwo lines.\nFour words.\nThree\r\nlines,\r\nfour words.\n"```
stderr=```""```
', /Users/carolnichols/.cargo/registry/src/github.com-1ecc6299db9ec823/assert_cmd-1.0.8/src/assert.rs:124:9

Or a different one where I am printing the invalid UTF-8 correctly (but messed up the newlines, but it's hard to tell that from this message):

---- multiple_files_c1 stdout ----
thread 'multiple_files_c1' panicked at 'Unexpected stdout, failed var == [61, 61, 62, 32, 46, 47, 116, 101, 115, 116, 115, 47, 105, 110, 112, 117, 116, 115, 47, 101, 109, 112, 116, 121, 46, 116, 120, 116, 32, 60, 61, 61, 10, 10, 61, 61, 62, 32, 46, 47, 116, 101, 115, 116, 115, 47, 105, 110, 112, 117, 116, 115, 47, 111, 110, 101, 46, 116, 120, 116, 32, 60, 61, 61, 10, 239, 191, 189, 10, 61, 61, 62, 32, 46, 47, 116, 101, 115, 116, 115, 47, 105, 110, 112, 117, 116, 115, 47, 116, 119, 111, 46, 116, 120, 116, 32, 60, 61, 61, 10, 84, 10, 61, 61, 62, 32, 46, 47, 116, 101, 115, 116, 115, 47, 105, 110, 112, 117, 116, 115, 47, 116, 104, 114, 101, 101, 46, 116, 120, 116, 32, 60, 61, 61, 10, 84]

command=`"/Users/carolnichols/rust/hands-on/carols-solutions/headr2/target/debug/headr" "./tests/inputs/empty.txt" "./tests/inputs/one.txt" "./tests/inputs/two.txt" "./tests/inputs/three.txt" "-c" "1"`
code=0
stdout=```"==> ./tests/inputs/empty.txt <==\n\n\n==> ./tests/inputs/one.txt <==\n\n�\n==> ./tests/inputs/two.txt <==\n\nT\n==> ./tests/inputs/three.txt <==\n\nT"```
stderr=```""```
', /Users/carolnichols/.cargo/registry/src/github.com-1ecc6299db9ec823/assert_cmd-1.0.8/src/assert.rs:124:9

The problem is that it prints the expected bytes but prints the actual stdout as string.

I wanted to send this as a PR rather than an issue, but I haven't found a great solution and wanted to brainstorm some ideas with you...

  • In the test run function, use try_stdout instead of stdout and, if the result is_err, do some custom printing instead of, or in addition to, what assert_cmd prints
  • Call this an upstream bug and file it with assert_cmd/predicate
  • Do the assertion some other way, losing all the nice stuff that comes with assert_cmd
  • Something else???? I thought I had other ideas but they are no longer in my brain.

What do you think?

Output formatting for 'wc'

wc command from GNU coreutils version 9.0 formats output very differently from the way shown in the book. In fact, it seems a little erratic and inconsistent in itself. It makes debugging a nightmare.

I'm currently reading an 'early version' of this book. However, judging by the code from this repository, we're still using the same code. So this is affecting everyone I guess.

A possible solution can be to post-processing the output and replace multiple whitespaces with one space and compare with that.

Examples compilation note for RL8

Hi

I had to replace "2021" with 2018 in older rust environment come with Linux distro.
This change will run "./test.sh" successfully using RL8(RockyLinux 8.5)'s default rust rpm pkgs.

 find . -type f -name "Cargo.toml" -exec perl -pi -e 's!2021!2018!'  {} \;

Chapter 2 "echor": breaking changes in clap 3 & 4 need adjustment

For everyone trying to follow along using a current (4.2.7) version of clap here's a working solution:

use clap::{Arg, ArgAction, Command};

fn main() {
    let matches = Command::new("echor")
        .version("0.1.0")
        .author("author")
        .about("echo in Rust")
        .arg(
            Arg::new("text")
                .value_name("TEXT")
                .help("input text")
                .required(true)
                .action(ArgAction::Append),
        )
        .arg(
            Arg::new("omit newline")
                .short('n')
                .help("Don't print newline character")
                .action(ArgAction::SetTrue),
        )
        .get_matches();
    let text: Vec<&str> = matches
        .get_many("text")
        .unwrap()
        .map(String::as_str)
        .collect();
    let omit_newline = matches.get_flag("omit newline");
    let ending = if omit_newline { "" } else { "\n" };
    print!("{}{}", text.join(" "), ending);
}

There have been some breaking changes in clap since 2.33 that required some work to get right. Most challenging was to collect the arguments into text.

Recommend upgrading code to use clap 3.0.0

I tried the echor example in Chapter 2 and VSCode said version 2.x of clap was outdated so I tried using version 3.0.0 and a number of changes were required in order for it to work. Interestingly without the changes it compiled but failed at runtime due to invalid utf-8 issues.

The minimally modified code I got to work was like so, though I am new to Rust and clap so I have no idea if this would be considered idiomatic or not:

use clap::{App, Arg};

fn main() {
    let matches = App::new("echor")
        .version("0.1.0")
        .author("Ken Youens-Clark <[email protected]>")
        .about("Rust echo")
        .arg(
            Arg::new("text")
                .value_name("TEXT")
                .help("Input text")
                .required(true)
                .min_values(1)
                .allow_invalid_utf8(true)
                .takes_value(true),
        )
        .arg(
            Arg::new("omit_newline")
                .short('n')
                .help("Do not print newline")
                .takes_value(false),
        )
        .get_matches();

    let text = matches.values_of_lossy("text").unwrap();
    let omit_newline = matches.is_present("omit_newline");

    print!("{}{}", text.join(" "), if omit_newline { "" } else { "\n" });
}

Chapter 3: catr implementation deviates from cat implementation for line numbers with multiple files

Hi @kyclark, the version of catr developed in chapter 03 starts the line numbers from 1 for each new file, i.e.

cargo run -q -- -n tests/inputs/fox.txt tests/inputs/spiders.txt

produces:

     1  The quick brown fox jumps over the lazy dog.
     1  Don't worry, spiders,
     2  I keep house
     3  casually.

whereas

cat -n tests/inputs/fox.txt tests/inputs/spiders.txt

produces

     1  The quick brown fox jumps over the lazy dog.
     2  Don't worry, spiders,
     3  I keep house
     4  casually.

The tests aren't failing b/c the tests/expected/all.*.out files are incorrect. This can be seen by re-running bash mk-outs.sh && cargo test.

I created a PR to fix this here: #10

Off-Topic:
Learning Rust with the book is a lot of fun! Thank you!

Errata: Chapter 1 - The Coding Challenges, para 3, sentence 2

The second sentence of the third paragraph of the 'The Coding Challenges' section has a spelling error.

While there are many flavors of Unix and so many implementations of these programs already extant

extant should be exists.

Sorry for putting this here, but the errata page link didn't work.

Also as a side note, there is already a product with the exact title "Hands-On Systems Programming with Rust". See Hands-On Systems Programming with Rust [Video] and the github page. I'm not sure if this will cause issues with the final version of your book being published.

Other than that, thank you for this book. It's exactly what I've been looking for.

Ch 3 catr/echor typo

Just a heads up that in chapter 3, echor is used some times where you meant to use catr

  • 3rd paragraph on page 50
  • Last section on page 51

There may be others as well

Chapter 13 solution no longer working

for lines in izip!(m1, m2, m3) {

This line currently fails with an error message like:

error[E0277]: `&()` is not an iterator
   --> src/lib.rs:100:40
    |
100 |                     for lines in izip!(m1, m2, m3) {
    |                                  ------^^---------
    |                                  |     |
    |                                  |     `&()` is not an iterator
    |                                  required by a bound introduced by this call
    |
    = help: the trait `Iterator` is not implemented for `&()`
    = note: required for `&()` to implement `IntoIterator`

Create tests in working repo

As part of working along, you suggest copying the tests over to our personal repo. Consider providing a cheat sheet for this as part of the documentation (i.e. a subsection in README.md).

For myself, I did:

cd command-line-rust
rsync -avz --relative  ./**/tests/** ../my_personal_repo/

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.