Rust Story Code
Chapter 4. - Rust Structures
Example libraries, tests, and documentation
4.0 Prologue
Previous chapters have provided a lot of technical details about the Rust programming language. In this chapter we will bring all that
together by presenting a few libraries: Logger, CmdLineParser, and DirNavigator.
For the first library, Logger we will explore library structure, its implementation, testing, and documentation. The Rust tool cargo,
in concert with the rustc compiler, provide an excellent environment for building libraries and applications, with detailed testing and
documentation.
This chapter is a good place to start exploring that.
4.1 Logger Library
Fig 1. Rust Logger Output
The Logger Library provides a light-weight logger, useful for debugging and demonstration.
Library development started with specifications:
-
Support simultaneous writes to console and a file.
-
Provide file open and close operations and enable or disable
console output.
-
Write text messages.
-
Optionally prepend a message with a date-time stamp, using local system time.
Implementation starts with a struct:
Logger Struct
pub struct Logger {
fl:Option<File>,
console:bool,
}
Logger has two data members: fl and console. fl is an Option enumeration that either holds a File
instance as Some(f:File) or None. When you create a logger with Logger::new(),
fl is None, so the logger is not attached to a file.
The member console is a boolean that determines whether a write sends its output to the console. Logger::new() sets
console to true, so the logger will, by default, write to the console. To do anything useful, we need some functions
that operate on that member data. Note
that the Logger structure is public, but its data members are not, so only Logger methods can alter their state.
Logger has nine methods:
new(), init(f:File, con:bool),
console(con:bool), file(f:file), opt(f:Option<File>),
open(file_name:&str), ts_write(s:&str), write(s:&str),
and close()
new and init are creational methods, console, file, and opt mutate the logger state,
open and close manage the state of logger's file member, and write and ts_write handle
logger's main business, writing to the console and/or file.
Logger Methods and Functions:
Logger Methods:
#[allow(dead_code)]
impl Logger {
/*-- create default logger --*/
pub fn new() -> Self {
Self { fl:None, console:true, }
}
/*-- create initialized logger --*/
pub fn init(f:File, con:bool) -> Self {
Self { fl:Some(f), console:con, }
}
/*-- enable | disable writing to console --*/
pub fn console(&mut self, con:bool) {
self.console = con
}
/*-- attach file --*/
pub fn file(&mut self, f:File) {
self.fl = Some(f);
}
/*-- attach Option<File> --*/
pub fn opt(&mut self, f:Option<File>) {
self.fl = f;
}
/*-- attempt to open logger file --*/
pub fn open(&mut self, s:&str) -> bool {
use std::fs::OpenOptions;
self.fl = OpenOptions::new()
.write(true)
.create(true)
.append(true)
.open(s).ok();
if self.fl.is_some() {
return true;
}
false
}
/*-- write log entry with date-time stamp */
/*-- can be chained */
pub fn ts_write(&mut self, s:&str)
-> &mut Self {
let now: DateTime<Local> = Local::now();
/* format DateTime string */
let mut now_str =
format!("\n {}", now.to_rfc2822());
/* remove trailing -0400 */
now_str.truncate(now_str.len() - 6);
let _ = Logger::write(self, &now_str);
let rslt = Logger::write(self, s);
rslt
}
/*-- write log entry, can be chained --*/
pub fn write(&mut self, s:&str)
-> &mut Self {
if self.console {
print!("{}", s);
}
if let Some(ref mut f) = self.fl {
let rslt = f.write(s.as_bytes());
match rslt {
Ok(_) => {},
Err(_) =>
print!("\n file write failed\n"),
}
}
self
}
/*-- close log file */
pub fn close(&mut self) {
self.fl = None;
}
}
#[derive(PartialEq)]
#[allow(dead_code)]
pub enum OpenMode { Truncate, Append }
#[allow(dead_code)]
/*-----------------------------------------
Helper functions
*/
/*-- attempt to open file by name --*/
pub fn open_file(s:&str, mode:OpenMode)
-> Option<File> {
let fl:Option<File>;
use std::fs::OpenOptions;
if mode == OpenMode::Truncate {
fl = OpenOptions::new()
.write(true)
.truncate(true)
.open(s).ok();
}
else {
fl = OpenOptions::new()
.write(true)
.create(true)
.append(true)
.open(s).ok();
}
fl
}
/*-- attempt to remove file by name --*/
pub fn remove_file(s:&str) -> bool {
let rslt = std::fs::remove_file(s);
rslt.is_ok()
}
/*-- does file contain a string? --*/
pub fn file_contains(fl:&str, ts:&str)
-> bool {
let contents = std::fs::read_to_string(fl);
let mut s = "".to_string();
if contents.is_ok() {
s = contents.unwrap();
}
s.contains(ts)
}
/*-- display contents of named file --*/
pub fn file_contents(fl:&str) {
let contents = std::fs::read_to_string(fl);
if contents.is_ok() {
let s = contents.unwrap();
print!("{}", s);
}
else {
print!("\n no contents");
}
}
/*-- does file exist? --*/
pub fn file_exists(s:&str) -> bool {
let path = Path::new(s);
return path.exists();
}
Explanations:
Methods are defined in one or more impl blocks.
Each block contains method definitions, as you see in the right panel.
The type Self is the type of the Structure, e.g., Logger. The variable self is a reference
to the instance on which the method was called.
The method new() returns an instance of the type Self, e.g., the logger we are creating with
this method. It only has one line: Self { fl:None, console:true, }. Since there
is no semicolon following the closing brace in this line, this is an expression, and any scope has the value
of its last expression, so we are returning Logger { fl:None, console:true }.
Many of the methods are relatively simple, but the write method has some peculiar code, e.g.:
if let Some(ref mut f) = self.fl { ... }
The "=" operator is not an assignment. It is a matching operator. The term "Some(ref mut f)
is the Option<T>::Some(t:T) holding a mutable reference to a File instance. So, we are matching
self.fl with a pattern that requires the Some option rather than None.
If the match is satisfied, e.g., the logger has a file reference, then we write to the file.
The remainder of the function
is a test to see if the user elected to use the console, and if so, writes to the console. So, if both conditions match, the
logger writes to both the console and the log file.
The ts_write method uses code from the DateTime crate to generate the local system date and time, format it, and then write
using the write method. It then repeats with its string argument.
Both write and ts_write return &mut Self, so they can be chained, e.g.,
l.write("first).write(", second");
Logger also contains functions:
open_file(file_name, open_mode), remove_file(file_name),
file_contains(file_name, search_text), file_contents(file_name),
and file_exists(file_name).
These are helper files used during logger testing. They will probably be useful for testing other projects.
4.1.1 Library Unit Tests
when cargo creates a library project with cargo new --lib, it creates a src directory containing a lib.rs
file. At the end of the file you will find a #[cfg(test)] directive that defines one or more tests to run.
Each test starts with a #[test] directive containing a single test function. The test function is expected to have one or more
assertions. The test passes if all assertions are satisfied, otherwise it fails.
You run unit tests with the cargo command:
cargo test
The details dropdown, below, lists all of the unit tests for the Logger library.
Logger Unit Tests:
Unit Test Code:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_open_file() {
let stest = "test_open.txt";
let mut l = Logger::new();
l.open(stest);
open_file(stest, OpenMode::Truncate);
assert_eq!(file_exists(stest),true);
remove_file(stest);
assert_eq!(file_exists(stest),false);
}
#[test]
fn test_file_contains() {
let stest = "test_contains.txt";
let mut l = Logger::new();
l.open(stest);
assert_eq!(l.fl.is_some(), true);
l.write("test contents with a short string");
l.close();
assert_eq!(file_exists(stest), true);
assert_eq!(file_contains(stest, "a short"), true);
remove_file(stest);
assert_eq!(file_exists(stest),false);
}
#[test]
fn test_remove_file() {
let stest = "test_remove";
open_file(stest, OpenMode::Truncate);
remove_file(stest);
assert_eq!(file_exists(stest),false);
}
#[test]
fn test_file_exists() {
assert_eq!(file_exists("foobar.fee"),false);
}
#[test]
fn test_new() {
let stest = "test_new";
let mut l = Logger::new();
l.open(stest);
assert_eq!(file_exists(stest), true);
remove_file(stest);
assert_eq!(file_exists(stest), false);
}
#[test]
fn test_init() {
let stest = "test_new";
let opt = open_file(stest, OpenMode::Append);
let mut l = Logger::init(opt.unwrap(), false);
l.open(stest);
assert_eq!(file_exists(stest), true);
remove_file(stest);
assert_eq!(file_exists(stest), false);
}
#[test]
fn test_console() {
let mut l = Logger::new();
assert_eq!(l.console == true, true);
l.console(false);
assert_eq!(l.console == false, true);
}
#[test]
fn test_file() {
let mut l = Logger::new();
let stest = "test_file";
l.file(open_file(stest, OpenMode::Append).unwrap());
assert_eq!(file_exists(stest), true);
remove_file(stest);
assert_eq!(file_exists(stest), false);
}
#[test]
fn test_opt() {
let mut l = Logger::new();
let stest = "test_file";
let file_opt = open_file(stest, OpenMode::Append);
l.opt(file_opt);
assert_eq!(file_exists(stest), true);
remove_file(stest);
assert_eq!(file_exists(stest), false);
}
#[test]
fn test_open() {
let mut l = Logger::new();
let stest = "test_open";
l.open(stest);
assert_eq!(file_exists(stest), true);
remove_file(stest);
assert_eq!(file_exists(stest), false);
}
#[test]
fn test_write() {
let mut l = Logger::new();
let stest = "test_write";
l.open(stest);
let stxt = "abc 012 xyz 789";
let _ = l.write(stxt);
assert_eq!(file_contains(stest, stxt), true);
remove_file(stest);
assert_eq!(file_exists(stest), false);
}
#[test]
fn test_ts_write() {
let mut l = Logger::new();
let stest = "test_ts_write";
l.open(stest);
let sdt = "2020"; // change if year != 2020
let stxt = "abc 012 xyz 789";
let _ = l.ts_write(stxt);
assert_eq!(file_contains(stest,sdt), true);
assert_eq!(file_contains(stest, stxt), true);
remove_file(stest);
assert_eq!(file_exists(stest), false);
}
#[test]
fn test_close() {
let mut l = Logger::new();
let stest = "test_close.txt";
l.open(stest);
assert_eq!(l.fl.is_some(), true);
l.close();
assert_eq!(l.fl.is_none(), true);
remove_file(stest);
assert_eq!(file_exists(stest), false);
}
}
Output:
C:\github\JimFawcett\RustBasicDemos\logger>
cargo test
// compiler output elided
running 13 tests
test tests::test_console ... ok
test tests::test_file_exists ... ok
test tests::test_new ... ok
test tests::test_file ... ok
test tests::test_close ... ok
test tests::test_open ... ok
test tests::test_remove_file ... ok
test tests::test_file_contains ... ok
test tests::test_open_file ... ok
test tests::test_opt ... ok
test tests::test_init ... ok
test tests::test_write ... ok
test tests::test_ts_write ... ok
test result: ok.
13 passed;
0 failed;
0 ignored;
0 measured;
0 filtered out
Since cargo sets up the unit test configuration code for each library crate that it creates,
it is quite easy to write unit tests as we write functions. When development is finished we
have a robust test sequence that can be run any time maintenance changes are made to the library.
4.1.2 Library Examples
If you create an examples directory as a sibling of src, then you can put code there with a main function that
exercises your library, perhaps to demonstrate its operations for new users. If you do that then
cargo run --example test1
will trigger cargo to attempt to build a binary crate from
test1.rs and run its executable.
Examples:
Examples/test1.rs
Demonstrating Logger
======================
deleted previous log file
writing to "Log.txt"
----------------------
Thu, 19 Mar 2020 18:18:59
starting log
first entry, second entry, third entry
contents of log.txt
---------------------
Thu, 19 Mar 2020 18:18:59
starting log
first entry, second entry, third entry
closing then reopening log
----------------------------
Thu, 19 Mar 2020 18:18:59
reopening log
after reopen
found "first entry" in "log.txt"
That's all Folks!
Output:
>cargo run --example test1
// compiler output elided
Demonstrating Logger
======================
deleted previous log file
writing to "Log.txt"
----------------------
Wed, 18 Mar 2020 14:37:40
starting log
first entry, second entry, third entry
Wed, 18 Mar 2020 14:37:40
reopening log
after reopen
contents =
Wed, 18 Mar 2020 14:37:40
starting log
first entry, second entry, third entry
Wed, 18 Mar 2020 14:37:40
reopening log
after reopen
found first entry in log.txt
That's all Folks!
You can have any number of example files in the examples directory, each with a main function that demonstrates some
particular aspect of your library. You can also supply examples suitable for developer's with different levels
of expertise with Rust.
4.1.3 Library Documentation
Figure 1. Logger Documentation
You can endow your own libraries with the same kind of documentation provided for the std libraries.
If you put comments above each method or function with the format:
/// ```
/// let mut logr = Logger::new();
///
/// sets fl:None, console:true;
/// ```
pub fn new() -> Self {
Self { fl:None, console:true, }
}
That places your comments in the document below the function, as shown in Figure 1.
To create documentation, simply issue the cargo command:
cargo doc --document-private-items
If you don't want to display private items, then leave off that option.
The doc command builds documentation and deposites it in your Target folder.
This creates documentation, not only for your code, but also for code it depends upon.
Be aware that if you issue a cargo clean command that will delete all the
documentation because clean deletes the Target folder.
One more thing: if you put documentation comments in your code, you need to set
doctest = false in a [lib] section of your cargo.toml file.
If you don't do this, the compiler attempts to compile your comments and emits a lot of
errors and warnings.
Cargo.toml
The [lib] section needs to come before [dependencies]
Logger Cargo File
[package]
name = "logger"
version = "0.1.0"
authors = ["James W. Fawcett <jfawcett@twcny.rr.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# prevents rustc from attempting to compile doc comments
doctest = false
[dependencies]
chrono = "0.2.16"
display = { path = "../display" }
This completes our discussion of the Logger project. You can find the code and discussion
in RustLogger repository.
4.2 CmdLine Parser Library
Fig 1. CmdLineParser Output
RustCmdLine is a facility for parsing command line arguments. Here's a sample:
/P "." /p "rs,txt" /s /H /r "abc"
where:
/P "." - path
/p "rs,txt" - file patterns
/s - recurse through directory tree rooted at path
/H - hide directories that don't contain target files
/r "abc" - regular expression
The intent is that a program creates an instance of CmdLineParser, uses that to parse its command line, then
passes it to any code that needs to know about a pattern or an option.
Design:
This library contains a single user-defined type: CmdLineParser.
CmdLineParser Struct
pub struct CmdLineParse {
opt_map : Options,
patterns : CmdLinePatterns,
help_str : String,
}
The opt_map field is a HashMap<K, V> of key-value pairs, where each key is a command line
option; that is, a character from the command line that is preceeded by a '/' character.
If an option, o, is succeeded by a non-option string, that becomes the value. If there is no value,
then the opt_map contains "true" for the value of the key o.
The field, patterns, is a vector of Strings where each string is expected to be a file extension,
e.g., rs. Note that using *.rs will not work as expected for applications like RustTextFinder.
It is expected that applications using CmdLineParser will process only files with the specified
extensions. If none are supplied, then the application processes all files it encounters.
The field, help_str, is a String containing information to supply to the user in response to an
option /h. In this library, the string contains the defaults, cited above, but an application
can change the string with replace_help(hs&str).
4.2.1 CmdLine Parser Methods
CmdLineParser implements the following methods and functions:
-
new() -> Self
Create new CmdLineParser which has an options hashmap, patterns vector, and help string.
-
parse(&self)
Builds internal options hashmap and patterns vector.
-
path(&self) -> String
Return value of relative root path, held in options map.
-
abs_path(&self) -> String
Return value of absolute root path, from canonicalized relative path.
-
set_path(&mut self, p:&str)
Replaces value of root path, held in options map.
-
set_regex(&mut self, re:&str)
Replaces value of regex string, held in options map.
-
get_regex(&mut self) -> &str
Retrieves value of regex string, held in options map.
-
default_options(&mut self)
Sets values of some of the options in options map.
-
contains_option(&self, opt:char) -> bool
returns true if options map contains key opt, else returns false.
-
add_option(&mut self, opt:char, val:&str)
Inserts option in options hashmap, adding key if it does not exist, else overriding previous value.
-
value(&self, opt:char) -> &str
Inserts option in options hashmap, adding key if it does not exist, else overriding previous value.
-
add_pattern(&mut self, patt:&str) -> &mut self
Inserts patt into patterns vector. Method can be chained.
-
patterns(&self) -> &CmdLinePatterns
Returns reference to vector of patterns.
-
options(&self) -> &Options
Returns reference to hashmap of options.
-
help(&self) -> &str
Returns default help string.
-
replace_help(&mut self, hs:&str)
Replace internal help string.
Code for CmdLineParser library and a demonstration program are provided in the dropdown, below.
CmdLineParser Implementation
CmdLineParser lib.rs
/////////////////////////////////////////////////////////////
// rust_cmd_line::lib.rs //
// //
// Jim Fawcett, https://JimFawcett.github.io, 19 Apr 2020 //
/////////////////////////////////////////////////////////////
use std::env::{args};
use std::collections::HashMap;
use std::fs::*;
/////////////////////////////////////////////////////////////
// sample command line with options
//-----------------------------------------------------------
// /P "." /p "rs,txt" /s [true] /r "abc" /h [true] /H [true]
//
// P - path in either absolute or relative form
// p - pattern, a file extension indicating file to process
// s - recurse directory tree rooted at P
// r - regular expression
// H - hide directories that don't contain any target files
// h - help: display this message
// custom option:
// /x [v] - x is application specific option which may
// have value, v
// Note:
// Any attribute that has no value on command line will
// have value "true" in option map
/////////////////////////////////////////////////////////////
// CmdLineParse methods
//-----------------------------------------------------------
// new() -> CmdLineParse
// parse(&self)
// contains_option(&self, opt: char) -> bool
// value(&self, char opt) -> &str
// options(&self) -> &HashMap<char, String>
// - defaults are created with default_options():
// - /P "." - root search path is current directory
// - /s "true" - recurse
// - /r "." - match all text
// - /H "true" - hide unused directories
// - no patterns are equivalent to all files
// path(&self) -> String
// abs_path(&self) -> String
// set_path(&self, p:&str)
// patterns(&self) -> &Vec<String>
// add_pattern(&mut self, p:&str) -> &mut self
// set_regex(&mut self, re:&str)
// get_regex(&self) -> &str
// help(&self) -> String
// replace_help(&mut self, &str) -> String
/// display command line arguments
pub fn show_cmd_line() {
print!("\n {:?}\n ", args().next());
for arg in args().skip(1) {
print!("{:?} ", arg)
}
}
pub type Options = HashMap<char, String>;
pub type CmdLinePatterns = Vec<String>;
/// Parses command line into options and patterns
#[derive(Debug, Default)]
pub struct CmdLineParse {
opt_map : Options,
patterns : CmdLinePatterns,
help_str : String,
}
impl CmdLineParse {
/// create new instance of parser
pub fn new() -> Self {
let help = CmdLineParse::help_txt();
Self {
opt_map: Options::default(),
patterns: CmdLinePatterns::new(),
help_str: help,
}
}
/// returns string with command line arguments example
fn help_txt() -> String {
let mut str =
"\n Help:\n Options: /P . /p \"rs,txt\"".to_string();
str.push_str(" /s /r \"abc\" /H /h");
str
}
/// does the command line argument start with '/'
fn is_opt(&self, s:&str) -> bool {
let bytes = s.as_bytes();
bytes[0] as char == '/'
}
/// returns path string with default value "."
pub fn path(&self) -> String {
if self.contains_option('P') {
self.opt_map[&'P'].clone()
}
else {
".".to_string()
}
}
/// replace Win path separator "\\" with Linux "/"
/// - use only with absolute paths for Windows
fn replace_sep(path: &str) -> String {
let mut rtn = path.to_string();
if rtn.contains("\\") {
rtn = rtn.replace("\\", "/");
rtn = rtn.chars().skip(4).collect();
}
rtn
}
/// convert relative to absolute path
pub fn abs_path(&self) -> String {
let abs = std::path::PathBuf::from(self.path());
let rslt = canonicalize(&abs);
match rslt {
Ok(path_buf) => {
// print!("\n--path_buf = {:?}",path_buf);
let ap = path_buf.to_string_lossy().to_string();
let ap = CmdLineParse::replace_sep(&ap);
// print!("\n--abs_path = {:?}",ap);
ap
}
Err(error) => error.to_string()
}
}
/// set new root path
pub fn set_path(&mut self, p:&str) {
self.opt_map.insert('P', p.to_string());
}
/// set new regex string for matching
pub fn set_regex(&mut self, re:&str) {
self.opt_map.insert('r', re.to_string());
}
/// return current regex string
pub fn get_regex(&self) -> &str {
let re_opt = self.opt_map.get(&'r');
match re_opt {
Some(value) => value,
None => ".",
}
}
/// commonly used default options
pub fn default_options(&mut self) {
self.opt_map.insert('P', ".".to_string()); // root is curr dir
self.opt_map.insert('s', "true".to_string()); // recurse
self.opt_map.insert('r', ".".to_string()); // regex always matches
self.opt_map.insert('H', "true".to_string()); // hide unused dirs
}
/// does options contain opt char?
pub fn contains_option(&self, opt:char) -> bool {
self.opt_map.contains_key(&opt)
}
/// insert {o,v} if o key doesn't exist, else overwrite v
pub fn add_option(&mut self, o:char, v:&str) {
self.opt_map.insert(o, v.to_string());
}
/// return option value
pub fn value(&self, opt:char) -> &str {
&self.opt_map[&opt]
}
/// add file ext (with no "*.")
pub fn add_pattern(&mut self, p:&str) -> &mut Self {
let s = String::from(p);
if !self.patterns.contains(&s) {
self.patterns.push(s);
}
self
}
/// returns non-mutable reference to patterns
pub fn patterns(&self) -> &CmdLinePatterns {
&self.patterns
}
/// returns non-mutable reference to options
pub fn options(& self) -> &Options {
&self.opt_map
}
/// return help string
pub fn help(&self) -> &str {
&self.help_str
}
/// replace help string
pub fn replace_help(&mut self, s:&str) {
self.help_str = s.to_string();
}
/// parse command line arguments, provided by env()
pub fn parse(&mut self) {
let cl_args:Vec<String> = args().collect();
let end = cl_args.len();
for i in 1..end {
if self.is_opt(&cl_args[i]) {
let bytes = cl_args[i].as_bytes();
let key = bytes[1] as char;
if i < end - 1 {
if !self.is_opt(&cl_args[i+1]) {
self.opt_map.insert(key,cl_args[i+1].to_string());
}
else {
self.opt_map.insert(key, "true".to_string());
}
}
else {
self.opt_map.insert(key, "true".to_string());
}
}
}
/*-- build patterns --*/
if self.contains_option('p') {
let pat_str = self.value('p').to_string();
let split_iter = pat_str.split(',');
for patt in split_iter {
self.add_pattern(patt);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cl_args() {
let _mock_args = vec!["/P", ".", "/p", "rs,txt", "/s"];
print!("\n cl args: ");
for arg in args() {
print!("{:?} ", arg);
}
let mut parser = CmdLineParse::new();
parser.parse();
for arg in args() {
let bytes = arg.as_bytes();
if '/' == (bytes[0] as char) {
assert!(parser.opt_map.contains_key(&(bytes[1] as char)));
}
}
}
}
Example program
/////////////////////////////////////////////////////////////
// rust_cmd_line::test1.rs //
// //
// Jim Fawcett, https://JimFawcett.github.io, 19 Apr 2020 //
/////////////////////////////////////////////////////////////
use rust_cmd_line::*;
fn main() {
print!("\n Command line arguments:");
show_cmd_line();
let mut parser = CmdLineParse::new();
// let mut parser = CmdLineParse::default();
parser.default_options();
print!("\n {}\n",parser.help());
parser.parse();
print!("\n path = {:?}", parser.path());
print!("\n abspath = {:?}", parser.abs_path());
let new_path = "C:/github/foo";
parser.set_path(new_path);
print!("\n setting path to {:?}", new_path);
print!("\n path = {:?}", parser.path());
parser.add_pattern("rs");
parser.add_pattern("rs"); // is not repeated
parser.add_pattern("exe");
let patts = parser.patterns();
print!("\n patts = {:?}", patts);
print!("\n regex = {:?}", parser.get_regex());
let opts = parser.options();
print!("\n opts = {:#?}", opts);
print!("\n\n adding option {{x,false}}");
parser.add_option('x', "false");
let opts = parser.options();
print!("\n opts = {:#?}", opts);
print!("\n\n adding option {{x,true}}");
parser.add_option('x', "true");
let opts = parser.options();
print!("\n opts = {:#?}", opts);
print!("\n\n That's all Folks!\n\n")
}
Output:
Command line arguments:
Help:
Options: /P . /p "rs,txt" /s /r "abc" /H /h
path = "."
abspath = "C:/github/JimFawcett/RustCmdLine"
setting path to "C:/github/foo"
path = "C:/github/foo"
patts = ["rs", "exe"]
regex = "."
opts = {
'r': ".",
'H': "true",
's': "true",
'P': "C:/github/foo",
}
adding option {x,false}
opts = {
'x': "false",
'r': ".",
'H': "true",
's': "true",
'P': "C:/github/foo",
}
adding option {x,true}
opts = {
'x': "true",
'r': ".",
'H': "true",
's': "true",
'P': "C:/github/foo",
}
That's all Folks!
The CmdLineParser library is used in the RustTextFinder application.
I expect to use it for many more tools.
4.3 Directory Navigator Library
Fig 1. Directory Navigator Types
Fig 1. Directory Navigator Output
RustDirNav is a facility for Depth-First-Search (DFS) of a specified directory tree. It uses generic parameter
App to provide application specific do_dir and do_file operations.
Design:
There is one struct, DirNav<App>, with methods and functions implementing this design:
Methods:
-
new() -> Self
Create new DirNav which has visit method for recursive DFS.
-
add_pat(&mut self, s&str) -> Self
Add pattern to match file extension. Can be chained.
-
visit(&mut self, p:&Path)
Walks directory tree rooted at path p, looking for files matching pattern(s).
-
recurse(&mut self, p:bool)
Sets or resets option to recurse directory tree.
-
hide(&mut self, p:bool)
Sets or resets option to hide directories with no target contents.
-
get_app(&mut self) -> &mut app
Retrieves reference to embedded application, set with generic parameter.
-
get_dirs(&self) -> usize
Retrieves the number of directories entered
-
get_files(&self) -> usize
Retrieves the number of files processed.
-
get_patts(&self) -> &SearchPatterns
Retrieves vector of patterns.
-
clear(&self)
Returns DirNav<app> to its initial state.
The DirEvent trait constrains the App generic parameter to supply do_dir and do_file methods that
define what the application will do when the navigator encounters a new directory or file.
pub trait DirEvent {
fn do_dir(&mut self, d: &str);
fn do_file(&mut self, f: &str);
}
The DirNav package provides a demonstration of the library in its /examples/test1.rs file. For the
demo, test1 simply displays each directory once, and all of the files in that directory. The App
type is defined by implementing the DirEvent trait, which may contain data members.
Directory Navigator Implementation
lib.rs
/////////////////////////////////////////////////////////////
// rust_dir_nav::lib1.rs //
// //
// Jim Fawcett, https://JimFawcett.github.io, 12 Apr 2020 //
/////////////////////////////////////////////////////////////
/*
DirNav<App> is a directory navigator that uses the generic
parameter App to define how files and directories are
handled.
- displays only paths that have file targets by default
- hide(false) will show all directories traversed
- recurses directory tree at specified root by default
- recurse(false) examines only specified path.
*/
use std::fs::{self, DirEntry};
use std::io;
use std::io::{Error, ErrorKind};
#[allow(unused_imports)]
use std::path::{Path, PathBuf};
/// trait required of the App generic parameter type
pub trait DirEvent {
fn do_dir(&mut self, d: &str);
fn do_file(&mut self, f: &str);
}
//---------------------------------------
// Sample implementation of DirNav param
// --------------------------------------
// #[derive(Debug, Default)]
// pub struct Appl;
// impl DirEvent for Appl {
// fn do_dir(&mut self, d:&str) {
// print!("\n {:?}", d);
// }
// fn do_file(&mut self, f:&str) {
// print!("\n {:?}", f);
// }
// }
/////////////////////////////////////////////////
// Patterns are a collection of extension strings
// used to identify files as search targets
type SearchPatterns = Vec<std::ffi::OsString>;
/// Directory Navigator Structure
#[allow(dead_code)]
#[derive(Debug, Default)]
pub struct DirNav<App: DirEvent> {
/// file extensions to look for
pats: SearchPatterns,
/// instance of App : DirEvent,
/// requires do_file and do_dir methods
app: App,
/// number of files processed
num_file: usize,
/// number of dirs processed
num_dir: usize,
/// recurse ?
recurse : bool,
/// hide dirs with no targets ?
hide: bool,
}
impl<App: DirEvent + Default> DirNav<App> {
pub fn new() -> Self
where
App: DirEvent + Default,
{
Self {
pats: SearchPatterns::new(),
app: App::default(),
num_file: 0,
num_dir: 0,
recurse: true,
hide: true,
}
}
/// do recursive visit?
pub fn recurse(&mut self, p:bool) {
self.recurse = p;
}
/// hide dirs with no targets?
pub fn hide(&mut self, p:bool) {
self.hide = p;
}
/// return reference to App to get results, if any
pub fn get_app(&mut self) -> &mut App {
&mut self.app
}
/// return number of dirs processed
pub fn get_dirs(&self) -> usize {
self.num_dir
}
/// return number of files processed
pub fn get_files(&self) -> usize {
self.num_file
}
/// return patterns, e.g., file extensions to look for
pub fn get_patts(&self) -> &SearchPatterns {
&self.pats
}
/// add extention to search for
/// - takes either String or &str
pub fn add_pat<S: Into<String>>(&mut self, p: S)
-> &mut DirNav<App> {
let mut t = std::ffi::OsString::new();
t.push(p.into());
self.pats.push(t);
self
}
/// reset to default state
pub fn clear(&mut self) {
self.pats.clear();
self.num_dir = 0;
self.num_file = 0;
self.app = App::default();
}
/// Depth First Search for file extentions starting at
/// path dir<br />
/// Displays only directories with files matching pattern
pub fn visit(&mut self, dir: &Path) -> io::Result<()>
where App: DirEvent
{
self.num_dir += 1;
let dir_name: String =
self.replace_sep(dir).to_string_lossy().to_string();
let mut files = Vec::<std::ffi::OsString>::new();
let mut sub_dirs = Vec::<std::ffi::OsString>::new();
if dir.is_dir() {
/* search local directory */
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let cd = self.replace_sep(&path);
sub_dirs.push(cd);
} else {
self.num_file += 1;
if self.in_patterns(&entry) |
self.pats.is_empty() {
files.push(entry.file_name());
}
}
}
/*-- display only dirs with found files --*/
if !files.is_empty() || !self.hide {
self.app.do_dir(&dir_name);
}
for fl in files {
let flnm = fl.to_string_lossy().to_string();
self.app.do_file(&flnm);
}
/*-- recurse into subdirectories --*/
for sub in sub_dirs {
let mut pb = std::path::PathBuf::new();
pb.push(sub);
if self.recurse {
self.visit(&pb)?;
}
}
return Ok(()); // normal return
}
Err(Error::new(ErrorKind::Other, "not a directory"))
}
/// replace Win directory separator with Linux separator
pub fn replace_sep(&self, path: &Path)
-> std::ffi::OsString {
let rtn = path.to_string_lossy();
let mod_path = rtn.replace("\\", "/");
let mut os_str: std::ffi::OsString =
std::ffi::OsString::new();
os_str.push(mod_path);
os_str
}
/// does store contain d.path().extension() ?
pub fn in_patterns(&self, d: &DirEntry) -> bool {
let p = d.path();
let ext = p.extension();
match ext {
Some(extn) => self.pats.contains(
&(extn.to_os_string())
),
None => false,
}
}
}
#[cfg(test)]
mod tests {
// test_setup() should run first. To ensure that:
// use cargo -- --test-threads=1
// to see console output:
// use cargo test -- --show-output --test-threads=1
use super::*;
#[derive(Debug)]
struct ApplTest {
rslt_store: Vec<String>,
}
impl DirEvent for ApplTest {
fn do_dir(&mut self, _d: &str) {
//print!("\n {:?}", d);
}
fn do_file(&mut self, f: &str) {
//print!("\n {:?}", f);
self.rslt_store.push((*f).to_string());
}
}
impl Default for ApplTest {
fn default() -> Self {
ApplTest {
rslt_store: Vec::<String>::new(),
}
}
}
#[test]
fn test_setup() {
let _ = std::fs::create_dir("./test_dir");
let _ = std::fs::create_dir("./test_dir/test_sub1_dir");
let _ = std::fs::create_dir("./test_dir/test_sub2_dir");
let _ = std::fs::File::create("./test_dir/test_file.rs");
let _ = std::fs::File::create(
"./test_dir/test_sub1_dir/test_file1.rs"
);
let _ = std::fs::File::create(
"./test_dir/test_sub1_dir/test_file2.exe"
);
let _ = std::fs::File::create(
"./test_dir/test_sub2_dir/test_file3.txt"
);
}
#[test]
fn test_walk() {
let mut dn = DirNav::<ApplTest>::new();
dn.add_pat("rs").add_pat("exe")
.add_pat("txt");
let mut pb = PathBuf::new();
pb.push("./test_dir".to_string());
let _ = dn.visit(&pb);
let rl = &dn.get_app().rslt_store;
/*
run exe in target/debug with --nocapture option
to see output of print statement below.
*/
print!("\n {:?}", rl);
// test for found files
let l = |s: &str| -> String { s.to_string() };
assert!(rl.contains(&l("test_file.rs")));
assert!(rl.contains(&l("test_file1.rs")));
assert!(rl.contains(&l("test_file2.exe")));
assert!(rl.contains(&l("test_file3.txt")));
/*
uncomment line below to make test fail
*/
//assert!(rl.contains(&l("foobar")));
}
#[test]
fn test_patts() {
let mut dn = DirNav::<ApplTest>::new();
dn.add_pat("foo").add_pat("bar");
assert_eq!(dn.get_patts().len(), 2);
let pats = dn.get_patts();
let mut foo_str = std::ffi::OsString::new();
foo_str.push("foo");
assert!(pats.contains(&foo_str));
let mut bar_str = std::ffi::OsString::new();
bar_str.push("bar");
assert!(pats.contains(&bar_str));
dn.clear();
assert_eq!(dn.get_patts().len(), 0);
}
}
test1.rs
/////////////////////////////////////////////////////////////
// rust_dir_nav::test1.rs //
// //
// Jim Fawcett, https://JimFawcett.github.io, 12 Apr 2020 //
/////////////////////////////////////////////////////////////
use rust_dir_nav::*;
#[allow(unused_imports)]
use std::env::current_dir;
use std::io;
struct Appl;
impl DirEvent for Appl {
fn do_dir(&mut self, d: &str) {
print!("\n {}", d);
}
fn do_file(&mut self, f: &str) {
print!("\n {}", f);
}
}
impl Default for Appl {
fn default() -> Self {
Appl
}
}
fn main() -> io::Result<()> {
let mut dn = DirNav::<Appl>::new();
/*-- takes a variety of formats --*/
let _pat1: String = "rs".to_string();
let _pat4: String = "rlib".to_string();
dn.add_pat(&_pat1);
dn.add_pat("toml".to_string());
dn.add_pat("txt");
dn.add_pat(_pat4);
dn.add_pat(&"exe".to_string());
//dn.hide(false);
let path = current_dir()?;
print!("\n Searching path {:?}\n", &path);
let _rslt = dn.visit(&path)?;
print!(
"\n\n processed {} files and {} dirs",
dn.get_files(),
dn.get_dirs()
);
print!("\n");
dn.clear();
dn.add_pat("rs").add_pat("toml")
.add_pat("exe").add_pat("txt");
let mut path = std::path::PathBuf::new();
path.push("./test_dir");
print!("\n Searching path {:?}\n", &path);
let _rslt = dn.visit(&path)?;
print!(
"\n\n processed {} files in {} dirs",
dn.get_files(),
dn.get_dirs()
);
///////////////////////////////////////////////
// uncomment lines below to see error return
//---------------------------------------------
// print!("\n");
// path.pop();
// path.push("foobar");
// dn.visit(&path)?;
print!("\n\n");
Ok(())
}
The lib.rs file defines all of the DirNav processing and, in its tests section, defines an ApplTest
type that implements DirEvent and contains a vector that will hold each of the files found during test,
so we can verify its operation.
4.4 Epilogue
The three libraries discussed here: Logger, CmdLineParser, and DirNav are good examples to study while learning
the Rust programming language. They illustrate common Rust coding idioms and the great support for testing
and documentation provided by cargo, rustc, and rustdoc. I've used the
clippy tool, e.g., cargo clippy,
to find non-idiomatic or inefficient code, before including here.
4.5 References
Reference Link |
Description |
A half-hour to learn Rust
|
Code fragments with commentary that cover most of the Rust ideas.
|
intorust.com
|
A few short screen casts - Nicholas Matsakis, ...
|
readrust.net/getting-started
|
Nice set of "first-look" posts.
|
rust-learning repository
|
Extensive list of resources: books, videos, podcasts, ...
|
A Gentle Introduction To Rust
|
A good walkthrough for most of the Rust ideas.
|
The Rust Programming Language
|
Long but gentle walkthrough of the Rust Language
|
An alternative introduction to Rust
|
Steve Klabnik
|
Rust pain-points
|
Steve Donovan
|
Rust by Example
|
Rust docs - walkthrough of syntax
|
Getting Started with Rust on Windows and Visual Studio Code
|
Install Rust, Verify, Configure Visual Studio Code, Create Hello World, Create Build Task,
Configuring Unit Tests, Configure Debugging,
|
rust-lang.org home page
|
Links to download and documentation
|
rust cheat sheet
|
Quite extensive list of cheats and helpers.
|
rust cargo book
|
Very clear, simple, description of the cargo facilities and commands. |
More References
|
Basic, intermediate, and advanced references |