Part 2. Head (10 points)
In the next two parts of the lab, you’re going to implement the head
command
line utility in Rust. This will be divided into two tasks: command line
argument parsing (this part) and the implementation of head
itself (Part
3).
Before you begin, read the man page for head
. While you’re
reading it, pay attention to the command line arguments and where head
gets
its input (from files vs. from stdin
) in which cases.
There are many versions of head
. You’ll be implementing the FreeBSD version
which is the version described in the man page above. The GNU project has
another version which is used in most Linux distributions. In general, the GNU
versions of command-line utilities tend to have more options. If you run
$ man head
on the lab machines, you’ll get a manual for the GNU version of
head
.
We’re going to use the Command Line Argument Parsing library, `clap`` to handle command-line argument parsing for us. As we’ll see, Rust makes supporting command-line arguments, including a nicely formatted help message, very easy.
Let’s look at an example.
extern crate clap; use std::path::PathBuf; use clap::Parser; #[derive(Parser, Debug)] #[command(author, version, about = "Example command line parsing", long_about = None)] struct Config { /// Use colored output #[arg(short, long)] color: bool, /// Use at most COUNT values #[arg(short = 'C', long)] count: Option<i32>, /// Specify optional log file #[arg(short = 'f', long, conflicts_with = "count")] log_file: Option<PathBuf>, /// Input files #[arg(default_value = "-")] files: Vec<PathBuf>, } fn main() { let config = Config::parse(); println!("{config:#?}"); }
There’s a lot going on here! At the top of the code, there are two use
statements which inform the compiler about PathBuf
from the standard
library and Parser
from clap
.
Next we have a struct
named Config
.
This structure defines the supported command line arguments of this
example program. The main
function calls the function Config::parse()
which parses all of the command line arguments and crucially,
returns an instance of Config
with the structure’s members. We can access
the members as config.color
, config.count
, config.log_file
, and config.files
.
The println!("{config:#?}")
line will pretty-print the debug representation of config
.
The format specifier {config:?}
(with the colon followed by a question mark) means to
print the debug representation and the #
in {config:#?}
means to pretty-print it
with new lines.
All good command line utilities have a --help
option or similar. Before we look
at this code in detail, here’s the output of running this program with the --help
option which just prints out the help message and then exits the program.
$ cargo run -- --help
Example command line parsing
Usage: ex [OPTIONS] [FILES]...
Arguments:
[FILES]... Input files [default: -]
Options:
-c, --color Use colored output
-C, --count <COUNT> Use at most COUNT values
-f, --log-file <LOG_FILE> Specify optional log file
-h, --help Print help
-V, --version Print version
This code defines three options, each of which has a short and a long form. Two of
the options take arguments and two of the options conflict with one another meaning
at most one of those options can be specified at a time. It also
takes a list of file paths. Each file path is represented by a PathBuf
in Rust.
Because the final line of code prints out the Config
instance that was returned from
Config::parse()
, we can run the program a few times with different options to see
the results. Note that in what follows I’m passing the --quiet
option to cargo run
because I don’t want cargo
to print out any output like “Created binary …” As usual,
the argument before the --
go to cargo run
, the arguments after the --
go to
the program itself.
$ cargo run --quiet --
Config {
color: false,
count: None,
log_file: None,
files: [
"-",
],
}
$ cargo run --quiet -- --color -f log.txt input1.txt input2.txt
Config {
color: true,
count: None,
log_file: Some(
"log.txt",
),
files: [
"input1.txt",
"input2.txt",
],
}
$ cargo run --quiet -- --version
ex 0.1.0
$ cargo run --quiet -- -f log.txt -C 10
error: the argument '--log-file <LOG_FILE>' cannot be used with '--count <COUNT>'
Usage: ex --log-file <LOG_FILE> [FILES]...
For more information, try '--help'.
The definition of Config
starts by deriving the clap::Parser
and Debug
traits.
The Parser
trait is what provides the parse()
function which reads the command-line
arguments and returns an instance of Config
.
The next line
#[command(author, version, about = "Example command line parsing", long_about = None)]
is new. It is attaching metadata to the Config
struct that the clap
library will
use to do all of the argument parsing and producing usage and error messages. The about = "..."
lets us specify a short about message to appear in the --help
message.
Each of the arguments consists of three parts.
/// Use colored output
#[arg(short, long)]
color: bool,
The comment with three slashes is a doc comment. Normally, such a comment is used to write documentation. (Running cargo doc
produces HTML documentation of your code. The doc comments are where the documentation comes from.)
Clap is repurposing that doc comment to construct a --help
message.
The next line, #[arg(short, long)]
, is attaching some more
metadata for Clap. It says this should have a short option -c
and a long option --color
. It took both
option names
from the name of the variable color
itself. Note that we can be explict about what the option names are.
There are a variety of other things we can do including making options conflict. If we try to pass both -f
and -C
,
we’ll get an error from Clap.
The third line is the variable and its type, of course. Because the type of color
is
bool
, this acts as an on/off flag. For other types, using an Option<...>
indicates
that the argument is optional. If you want to allow 0 or more, you use a
Vec
rather than an Option
as I did for files
.
That’s a lot of information to take in! Fortunately, I’ve got a simple recipe to follow for parsing command line arguments:
Start by defining a new struct
that derives Debug
and Parser
.
#[derive(Parser, Debug)]
#[command(author, version, about = "Short description", long_about = None)]
struct Config {
}
Now, for each option, modify one of the examples below and remember, you can
change the short or long options by using short = 'c'
or long = "blah"
, for example.
- If you want a boolean flag, use a
bool
/// Boolean flag, defaults to false #[arg(short, long)] flag: bool,
- If you want an option that takes an argument, use an
Option
/// Option that takes a String argument #[arg(short, long)] name: Option<String>,
- If you want a path to a file, use an
Option<PathBuf>
/// Option that takes a file path #[arg(short, long)] path: Option<PathBuf>,
- If you want a list of input files, don’t use
short
orlong
but do useVec<PathBuf>
; if you want to specify a default value, usedefault_value
/// Input files but if none are specified, use - #[arg(default_value = "-")] files: Vec<PathBuf>,
- If you want to specify that two options conflict, mark one of them (it doesn’t matter
which) as
conflicts_with
the other/// An option that takes a path #[arg(short, long)] log: Option<PathBuf>, /// A flag that conflicts with option `--log` #[arg(short, long, conflicts_with = "log")] output: bool,
Finally, add a main
function that prints out the debug representation. (Click the Run
button to run this or the Show button to show the whole example.)
// You don't need the `extern crate clap` line. That's just to make this runnable on the web. extern crate clap; use std::path::PathBuf; use clap::Parser; #[derive(Parser, Debug)] #[command(author, version, about = "Short description", long_about = None)] struct Config { /// Boolean flag, defaults to false #[arg(short, long)] flag: bool, /// Option that takes a String argument #[arg(short, long)] name: Option<String>, /// Option that takes a file path #[arg(short, long)] path: Option<PathBuf>, /// Input files but if none are specified, use - #[arg(default_value = "-")] files: Vec<PathBuf>, /// An option that takes a path #[arg(short, long)] log: Option<PathBuf>, /// A flag that conflicts with option `--log` #[arg(short, long, conflicts_with = "log")] output: bool, } fn main() { let config = Config::parse(); println!("{config:#?}"); }
The Clap tutorial has a lot more information.
Your task
Before you start, you need to create a new project inside your assignment
repository and add the clap
library.
$ cargo new head
$ cd head
$ cargo add clap --features derive
Your task is to implement parsing the head
command line arguments and
printing out a --help
message. Your head
will support the following
options.
-c
/--bytes
;-n
/--lines
;-h
/--help
; and-V
/--version
.
The --help
and --version
options are handled by clap
automatically. Use
clap
’s conflicts_with
mechanism for marking --bytes
and --lines
as
conflicting. See the tip above.
Additionally, head
takes 0 or more files as input. Configure clap
such
that if no files are passed, then it defaults to -
. See the tip above for
how to set default values.
Many command line utilities use -
in place of a file path to denote reading
from stdin
instead of the file or writing to stdout
instead of writing to
the file. Our head
will support that convention.
Once you have this working, you should be able to run head --help
and see
this.
$ cargo run --quiet -- --help
Display the first few lines of a file
Usage: head [OPTIONS] [FILES]...
Arguments:
[FILES]... Input files or - for stdin [default: -]
Options:
-n, --lines <LINES> Print LINES lines of each of the specified files
-c, --bytes <BYTES> Print BYTES bytes of each of the specified files
-h, --help Print help
-V, --version Print version
The version number printed by --version
can be set by editing the version
field in Cargo.toml
. Go ahead and set the version number to 1.2.3 so that you get
this output.
$ cargo run --quiet -- --version
head 1.2.3