4.0 Prologue
Previous chapters cover the technical details of the Rust programming language. This chapter ties those details together by
presenting three libraries: Logger, CmdLineParser, and DirNavigator.
The first library, Logger, illustrates library structure, implementation, testing, and documentation. Cargo and the
rustc compiler together provide an excellent environment for building libraries and applications, with strong support for
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 lightweight logger, useful for debugging and demonstration.
Development starts 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. The member fl is an Option enumeration
that holds either a File instance as Some(f:File) or None. Logger::new() sets
fl to None, so the new logger has no attached file.
The member console is a boolean that controls whether a write sends output to the console. Logger::new() sets
console to true, so the logger writes to the console by default. The Logger structure is public, but its
data members are not, so only Logger methods can alter their state.
Logger provides 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 logger state.
open and close manage the state of the file member, and write and ts_write handle
the 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:
Method definitions live in one or more impl blocks. Each block contains method definitions,
as shown 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, i.e., the logger being created. Its body
has one line: Self { fl:None, console:true, }. No semicolon follows the closing brace,
so this is an expression. A scope takes the value of its last expression, so new() returns
Logger { fl:None, console:true }.
Most methods are relatively simple, but the write method contains one peculiar line:
if let Some(ref mut f) = self.fl { ... }
The "=" is not an assignment - it is a matching operator. The term Some(ref mut f)
is the Option<T>::Some(t:T) variant holding a mutable reference to a File instance.
The match binds self.fl against a pattern that requires the Some variant rather than
None. When the match succeeds, the logger has a file reference, and write() writes to that file.
The rest of the function checks whether the caller enabled the console, and if so, writes to the console.
When both conditions hold, the logger writes to both the console and the log file.
The ts_write method uses the DateTime crate to generate the local system date and time, format it,
then write it using the write method. It then repeats with its string argument.
Both write and ts_write return &mut Self, so callers can chain them, e.g.,
l.write("first).write(", second");
Logger also provides free 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 helpers support logger testing and will likely help when 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 that file, a #[cfg(test)] directive defines one or more tests to run.
Each test starts with a #[test] directive containing a single test function. The test function contains one or more
assertions. The test passes when all assertions succeed, otherwise it fails.
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
Cargo sets up unit test configuration code for each library crate it creates, making it easy
to write unit tests as you write functions. When development finishes, a robust test sequence
remains and can run any time maintenance changes touch the library.
4.1.2 Library Examples
Create an examples directory as a sibling of src, and put code there with a main function that exercises the library,
perhaps to demonstrate its operations for new users. With that in place,
cargo run --example test1
tells cargo 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!
The examples directory can hold any number of example files, each with a main function that demonstrates a particular
aspect of the library. Examples can also target developers with different levels of expertise with Rust.
4.1.3 Library Documentation
Figure 1. Logger Documentation
Your own libraries can carry the same kind of documentation the std libraries provide.
Place comments above each method or function using this format:
/// ```
/// let mut logr = Logger::new();
///
/// sets fl:None, console:true;
/// ```
pub fn new() -> Self {
Self { fl:None, console:true, }
}
Rustdoc places those comments in the document below the function, as shown in Figure 1.
Build documentation with the cargo command:
cargo doc --document-private-items
Drop the option to exclude private items.
The doc command builds documentation and deposits it in the Target folder.
This produces documentation for your code and for the code it depends upon.
A cargo clean command deletes all of that documentation, because clean removes the
Target folder.
One more thing: doc comments in your code require doctest = false in a [lib]
section of your cargo.toml file. Without that setting, the compiler tries to compile
the comments and emits many 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" }
That completes the discussion of the Logger project. The code and discussion live in the
RustLogger repository.
4.2 CmdLine Parser Library
Fig 1. CmdLineParser Output
RustCmdLine parses command line arguments. 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
A program creates an instance of CmdLineParser, uses it to parse its command line, then passes it to any code
that needs 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 -
a character from the command line preceded by a '/'. If a non-option string follows an option o, that
string becomes the value. If no value follows, opt_map stores "true" for key o.
The patterns field is a vector of Strings, each expected to hold a file extension, e.g., rs. Using
*.rs will not work as expected for applications like RustTextFinder.
Applications that use CmdLineParser process only files with the specified extensions. When none are
supplied, the application processes every file it encounters.
The help_str field is a String containing information to display to the user in response to the /h
option. In this library the string contains the defaults cited above, and an application can replace
it 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.
The dropdown below holds the CmdLineParser library code and a demonstration program.
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 RustTextFinder application uses the CmdLineParser library, and
I expect to reuse it in many more tools.
4.3 Directory Navigator Library
Fig 1. Directory Navigator Types
Fig 1. Directory Navigator Output
RustDirNav performs a Depth-First-Search (DFS) of a specified directory tree. It uses the generic parameter
App to supply application-specific do_dir and do_file operations.
Design:
The library provides one struct, DirNav<App>, with methods and functions that implement 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. These methods
define what the application does 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 demonstrates the library in its /examples/test1.rs file. In this demo, test1 displays
each directory once, along with every file in that directory. The App type comes from an implementation of
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 DirNav processing. Its tests section defines an ApplTest type that implements
DirEvent and holds a vector for every file found during the test, so verification is straightforward.
4.4 Epilogue
The three libraries discussed here - Logger, CmdLineParser, and DirNav - offer good examples to study while
learning the Rust programming language. They illustrate common Rust idioms and the strong support for testing
and documentation that cargo, rustc, and rustdoc provide. I used the clippy
tool, e.g., cargo clippy, to find non-idiomatic or inefficient code before including it 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 |