BuildOn - Step #1:   TextSearch Pkg

BuildOn Project - TextSearch Package

Figure 1. TextFinder Packages
Figure 2. Step #1 Types
TextSearch is a package in the first BuildOn project, TextFinder. Figure 1 is a package diagram for Textfinder, discussed in Step #0.
TextFinder Specification

TextFinder Specification:

  1. Identify all files in a directory subtree that match a pattern and contain a specified text.
  2. Specify root path, one or more file patterns (.h, .cpp, .cs, .rs, ...), and search text on command line.
  3. Specify options /s [true|false], /v [true|false], /H [true|false] /h [true|false] for recursive directory walk, verbose output header, Hidden dirs with no match, and help message, respectively.
  4. Display file name and path, without duplication of path name, e.g., organized by directory, for files containing the search text.
  5. Interesting extensions:
    • Replace text by regular expressions for both search text and file patterns.
    • Replace sequential file searches with parallel searches to improve performance and useability.
TextSearch is provided file information by DirNav, searches for text in each file, and sends results to Display. You will find sample starter code below.

Step #1 - Build TextSearch Package

TextSearch seeks specified text and if found sends the path and file name to Display. The Display package turns the sequence of successful search completion events into useful user information. In this step we will create the TextSearch package. It contains a Finder type that:
  1. Accepts a search string in method Finder::set_txt.
  2. Accepts a directory name in DirEvent::do_dir which it stores and passes on to an instance of the GenOut type, which will move to the Display package in Step #4.
  3. Accepts a file name in DirEvent::do_file.
  4. Attempts to open the file.
  5. If successful, searches the file for the specified text.
  6. Sends a tuple of file name, boolean found value, and the search text to the GenOut type. The boolean value is true if specified text was found in the file, otherwise false.
TextSearch package defines struct Finder that provides the methods: fn do_dir(&mut self, dir: &Path)
fn do_file(&mut self, file: &Path)
fn set_txt(&mut self, srctxt: &str)
Method do_file sends its results directly to GenOut, below, rather than returning them to DirNav.
It defines, in this Step, struct GenOut to provide methods: fn set_dir(&mut self, dir: &Path)
/* rslt args, below, are: filename, found, and search_text */
fn set_file(&mut self, rslt:(&Path, bool, &str))
In this Step, GenOut is a test mock, supporting Finder testing. A completed version will migrate to Display package in Step #4.

Notes:

  1. Passing the boolean along with file name supports displaying files with the search text or files without the text, depending on how Display is configured. We might wish to use the later option to find files that do not provide comment prologues, for example.
  2. TextSearch package's Finder will create an instance of GenOut and send its events directly to GenOut instead of passing them back to Executive to forward.
  3. In a later step, after looking at generics, we will show how this structure can allow the TextFinder program to be quite flexible in how its display processing is effected.
  4. DirNav is required to find all the files matching required patterns before recursing into child directories. That ensures that TextSearch gets, and passes on to Display, the current directory name, and then supplies all of the matched files to Display before leaving the current directory.
    • You don't need DirNav for this Step. Instead, you will mock DirNav activities in the test1.rs main function, feeding the Finder instance mock values (see starter code below).
 

Tooling

The Rust ecosystem has some very well designed tools that allow you to create, build, and execute code on Windows 10, Linux, and macOS. Rust comes with a tool called cargo that creates new packages for either executables or libraries. It creates a metadata file called Cargo.toml that describes the package (Rust community calles them crates) and provides a section for describing dependencies.
Cargo Commands Create a package with the command: cargo new pkgFolder [--lib | --bin] [--n pkgname] This creates a folder pkgFolder, builds metadata for package pkgname. It also creates a /src directory that holds either lib.rs or main.rs.
After building a package, containing an executable, with the command: cargo run [arg1, ...] cargo invokes the executable it built with rustc, passing command line arguments, if any, to the executable.
After building a libray package with the command: cargo new pkgFolder --lib --n name cargo runs configured tests with the command: cargo test
If the project has an /examples sister directory to /src, any console application in that directory, say test1.rs, can be bound to the library and run with the command: cargo run --example test1
Get cargo help with the commands: cargo --help or cargo cmd --help. You can also find more details here: Cargo Book.
Rust does not come with an Integrated Development Environment (IDE), but you will find the Visual Studio Code editor, aka VSCode, to be effective; and it works on all three platforms, Windows, Linux, and macOS. For more details check out this: Tooling page.
 

Starter Code

This section contains a code example showing how to:
  1. Define a struct with methods - similar in use to a C++ or C# class.
  2. Build a library that contains the struct and test its methods.
  3. Link the static library to a test console application and run that.
  4. That is done for a project structure very similar to what you will use for TextFinder - Step #1
 
Starter Code Example This is a test mock for Step #1's project structure. It gives you a good start for building Step #1 so you can focus on starting to learn the Rust programming language.
TextSearch Library Source

/////////////////////////////////////////////////////////// // BuildOnStructure::TextSearch.rs // // - demonstrate code structure for Step #1 // // Jim Fawcett, https://JimFawcett.github.io, 14 Jan 21 // /////////////////////////////////////////////////////////// use std::path::{Path, PathBuf}; /*----------------------------------------------- A trait is similar to an interface, most often used to convey a communication protocol for generic types. SearchEvent trait is not needed here, but will be in Step #4 */ pub trait SearchEvent { fn set_dir(&mut self, dir: &Path); fn set_file(&mut self, rslt: (&Path, bool, &str)); } /*----------------------------------------------- GenOut converts Finder's data into information for user. */ #[derive(Debug)] pub struct GenOut { dir: PathBuf } /*-- implement trait --------------------------*/ impl SearchEvent for GenOut { fn set_dir(&mut self, rdir: &Path) { self.dir = rdir.to_path_buf(); print!("\n {:?}", rdir); } fn set_file(&mut self, rslt: (&Path, bool, &str)) { let (file, found, text) = rslt; if found { print!("\n {:?}: {:?} found", file, text); } else { print!("\n {:?}: {:?} not found", file, text); } } } /*-- implement other member functions ---------*/ impl GenOut { pub fn new() -> GenOut { GenOut { dir: PathBuf::new() } } } /*----------------------------------------------- Trait DirEvents specifies functions that Finder provides to support DirNav calls. Not used in this step. */ pub trait DirEvent { fn do_dir(&mut self, dir: &Path); fn do_file(&mut self, file: &Path); } /*----------------------------------------------- Finder searches for text in specified file */ #[derive(Debug)] pub struct Finder { dir: PathBuf, srctxt: String, out: GenOut } /*-- implement DirEvent --*/ impl DirEvent for Finder { fn do_dir(&mut self, dir: &Path) { self.dir = dir.to_path_buf(); self.out.set_dir(dir); } fn do_file(&mut self, file: &Path) { /* Pretending to search for text in file. Function should: 1. append flnm to dir 2. attempt to open file 3. search for text 4. send result to out */ if self.srctxt == "BuildOn" { self.out.set_file((file, true, &self.srctxt)); } else { self.out.set_file((file, false, &self.srctxt)); } } } /*-- implement Finder methods -----------------*/ impl Finder { pub fn new() -> Finder { Finder { dir: PathBuf::new(), srctxt: String::new(), out: GenOut::new() } } pub fn set_txt(&mut self, txt: &str) { self.srctxt = txt.to_string(); } } #[cfg(test)] mod tests { use super::*; #[test] fn construct_genout() { let g = GenOut::new(); assert_eq!(g.dir , PathBuf::from("")); } #[test] fn construct_finder() { let f = Finder::new(); assert_eq!(f.dir, PathBuf::from("")); } }
TextSearch Test Results


C:\su\temp\BuildOnStructure\Step1\TextSearch> cargo test Compiling text_search v0.1.0 (C:\su\temp\BuildOnStructure\Step1\TextSearch) Finished test [unoptimized + debuginfo] target(s) in 0.49s Running target\debug\deps\text_search-ebf377eb2c5e40d1.exe running 2 tests test tests::construct_finder ... ok test tests::construct_genout ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests text_search running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Test1 Example Source

/////////////////////////////////////////////////////////// // BuildOnStructure::Step1::test1.rs // // - demonstrate code structure for Step #1 // // Jim Fawcett, https://JimFawcett.github.io, 14 Jan 21 // /////////////////////////////////////////////////////////// use text_search::*; use std::path::{Path}; fn main() { print!("\n -- demonstrate structure for Step#1 --\n"); let mut finder = Finder::new(); finder.set_txt("BuildOn"); finder.do_dir(&Path::new("Dir1")); finder.do_file(&Path::new("file1.txt")); finder.set_txt("BuildOff"); finder.do_file(&Path::new("file2.txt")); finder.do_dir(&Path::new("Dir2")); finder.set_txt("abc"); finder.do_file(&Path::new("file3.txt")); finder.set_txt("123"); finder.do_file(&Path::new("file4.txt")); print!("\n\n That's all Folks!\n\n"); }
Test1 Example Output

C:\su\temp\BuildOnStructure\Step1\TextSearch> cargo run --example test1 Finished dev [unoptimized + debuginfo] target(s) in 0.01s Running `target\debug\examples\test1.exe` -- demonstrate structure for Step#1 -- Dir1 file1.txt: "BuildOn" found file2.txt: "BuildOff" not found Dir2 file3.txt: "abc" not found file4.txt: "123" not found That's all Folks! C:\su\temp\BuildOnStructure\Step1\TextSearch> cargo clean
Project Structure

Figure 1. Project Structure - Step #1
cargo.toml

[package] name = "text_search" version = "0.1.0" authors = ["James W. Fawcett "] edition = "2018" # See more keys and their definitions at # https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies]
You may wish to open this code in Visual Studio Code. To do that, clone the BuildOn Repository. Then navigate to the cloned directory in a command window, cd into BuildOnStructure/Step1/TextSearch, and emit the command code ., e.g.: code . That will bring up VSCode with the library and test1 code loaded. You can do the same thing with the RustTextFinder code. Clone the repository, then navigate into it in a command Widnow, and emit the command: code . You then select the directory of the package you want to examine and build.
 

Step #1 References

The table below provides references relevant for Step #1 : TextSearch. The first links refer to specific regions of the Rust Story, from this site. Other links go to Rust documentation. You can look at the Rust Story by going to the Home page (Home link in the menu), clicking on the Stories menu item (at the top) and selecting the Rust Story link.
 

Table 1. - Step #1 References

Topic Description Link
Look here first Starts with quick to intermediate tutorials on the Rust language. Getting Started
Tooling The Rust environment comes with some great tools: cargo, clippy, doc. You will want to add Visual Studio Code. Tooling
Cargo Book
Visual Studio Code
File System Rust has a well engineered facility for accessing files and directories.
Some key types in std::fs are: DirEntry, File, OpenOptions, ReadDir, ...
Rust story File System
std::fs
std::path  
Error Handling Rust error handling is based on use of the enumeration: enum Result<T,E> { Ok(T), Err(E), } where T is the type of the returned value, E is the type of the expected error. Rust enums are unique in that each of the enumertion items may be a wrapper for a specified type, like Ok and Err. RustBites Enums & Err Hndlg
RustStory Enums
RustStory Error Handling
Gentle Introduction to Rust std::Result
Data Operations Rust data operations copy, move, and clone. These operations combined with Rust's ownership rules (discussed next time) are the defining characteristics of the language. They are responsible for Rust's data safety and performance. Hello Data
Data types, copy, and move
Rust Story Data
Trait Copy
keyword move
Trait Clone
Strings Rust strings come in two flavors: String and str, representing string objects and literal strings. Each contains utf-8 characters. Rust Story Strings
std::String
Primitive Type str
struct Rust structs serve the same role as classes do in C++ and C#. Struct methods are defined inside impl StructName {} blocks. Rust Story structs
std::Stuct
keyword impl
You won't need all these references. Scan them for each category and decide which work best for you.
 
 toggle menu