This blog post is the fourth in a series that describes the structure of the wink command line program that I have written in rust and that I use to invoke other Windows and Linux programs from bash and Windows shells under Windows Subsystem for Linux. This post describes the HelpError struct used to indicate why the wink program should render usage information and exit, defined in the /src/helperror.rs file and hence crate::helperror:HelpError.
/src/helperror.rs: Defines and implements the HelpError struct used to control rendering of usage information for the wink command.
In some cases when the wink program should render usage information and exit, it creates an instance of the HelpError struct to indicate why.
The first line implements features that support formatting debugging messages for the type.
#[derive(Debug)]
The structure contains a single string field for a helpful message and an implementation that includes a new() function for use as a constructor, a fmt() function that formats the value of the struct for debugging purposes, and a description() function that enables use of this type as something like a derivative of std::error::Error.
This blog post is the third in a series that describes the structure of the wink command line program that I have written in rust and that I use to invoke other Windows and Linux programs from bash and Windows shells under Windows Subsystem for Linux. This post describes the components defined in the /src/lib.rs file. This file contains implementations relevant to the contents of the /src/main.rs file, specifically its main() function.
This file contains far to much code to list here. The following are important statements:
pub mod wsl;
This effectively parses the contents of /src/wsl.rs here, bringing its components into scope. To avoid a potential namespace conflict, it is important that this statement not also appear in another crate in the package, such as /src/main.rs.
The following statements define rust modules at this path (the crate root) from the corresponding files, where modules contain the crate::helperror::HelpError and crate::winkconfig::WinkConfig structs and the crate::wsl library. Again, keeping the HelpError and WinkConfig type definitions in their own files seems to be against convention, although there are easy techniques to make it easier to shorten the namespace references.
pub mod helperror; // /src/helperror.rs defines helperror::HelpError
pub mod winkconfig; // /src/winkconfig.rs defines winkconfig::WinkConfig
pub mod wsl; // /wsl.rs defines the contents of wsl::
The following statements allow use of tokens such as InvocableCategory without the long path prefixes.
use crate::wsl::inv::invocablecategory::InvocableCategory; // /src/wsl/inv/invocablecategory.rs
use crate::wsl::inv::invocablecategorylist::InvocableCategoryList; // /src/wsl/inv/invocablecategorylist.rs
use crate::wsl::inv::invoker::Invoker; // /src/wsl/inv/invoker.rs
The run() function in /src/lib.rs contains the body of the program invoked by the main() function in /src/main.rs. It accepts a WinkConfig (/src/winkconfig.rs) named config and an InvocableCategoryList (/src/wsl/inv/invocablecategorylist.rs) named category_list and returns an integer to use as an operating system exit code for the program.
The run function passes the command code entered on the command line to the get_invocable method of the InvocableCategoryList named category_list.
if let Some(invocable) = category_list.get_invocable(&config.command_code) {
The get_invocable() function iterates through the Invocables in its InvocableCategories to find one that matches the command code specified on the command line. The get_invocable() function returns an Option, which is a rust enumeration that contains of either Some or None. Some contains a value; None does not. If the get_invocable() function returns Some, then it contains the Invocable to invoke (or, depending on command line parameters, export as JSON, or for which to output the command line it would run to stdout), and the inner block runs.
if config.export {
if config.pretty_print {
println!("{}", serde_json::to_string_pretty(&invocable).unwrap());
} else {
println!("{}", serde_json::to_string(&invocable).unwrap());
}
}
let invoker = Invoker {};
invoker.invoke(invocable, config.dry_run, config.verbose, config.cmd_args);
return 0;
} else if config.export && config.command_code.is_empty() {
if config.pretty_print {
println!("{}", serde_json::to_string_pretty(&category_list).unwrap());
} else {
println!("{}", serde_json::to_string(&category_list).unwrap());
}
return 0;
} else if (config.command_code.is_empty() || !config.export) && config.dry_run {
return 0;
}
help(
&format!("Command not recognized: {0}", config.command_code),
config,
category_list.categories,
)
}
If a matching Invocable exists, and if command line arguments specify, the run() function serializes the Invocable as JSON to stdout.
Next, run() creates an Invoker (/src/wsl/inv/invoker.rs) and passes the Invocable retrieved from match and configuration data retrieved earlier to the invoke() function of that Invoker, runs calls the operating system to run the command. The run() function then returns 0 as the value to return to the operating system to indicate successful program execution. If no Invoker matches the command code specified on the command line, then the run() function calls the help() function also defined in /src/lib.wsl to display usage information.
The help() function does not warrant much description. It mainly uses the print! and println!() macros to write to stdout and the color() function defined in /src/lib.wsl to vary some parts of its output. It iterates the categories of Invocable and displays their contents.
This blog post is the second in a series that describes the structure of the wink command line program that I have written in rust and that I use to invoke other Windows and Linux programs from bash and Windows shells under Windows Subsystem for Linux. This post describes the main() function in the /src/main.rs file. The main function typically passes any configuration from the environment, the command line, and elsewhere to a function that implements the program.
/src/main.rs: Defines the wink program, a binary (executable) crate in the wink package.
Comments that begin with //! will appear in documentation generated by cargo. Code comments begin with // or we can use /*comment*/.
The main() function does not accept any parameters or return any value. It can use the args() function under the std::env path (part of rust standard libraries) to retrieve command line arguments passed to the wink command. It can use the std::process::exit() function to return a value to the operating system and exit the program.
This example uses a wink::wsl::inv::invocablecategorylist::InvocableCategoryList named category_list defined by the InvocableCategoryList struct in the /src/wsl/invocablecategorylist.rs file to retrieve some configuration data hard-coded elsewhere in the program and from a JSON file, if that file exists. This custom InvocableCategoryList struct contains a list of categories, each of which has a name and contains a list of Invocables that each contain metadata about a command that the system can invoke.
Sometimes I prefer to be explicit, especially for demonstrative purposes, but to avoid repeating long paths in code, this code could begin with a statement such as the following line.
use wink::wsl::inv::invocablecategorylist::InvocableCategoryList;
The main() function then uses the crate::winkconfig::WinkConfig::new() function to instantiate a crate::winkconfig::WinkConfig struct defined in /src/winkconfig.rs from the command line arguments.
fn main() {
// get a list of categories containing invocable commands
// defined in /wsl/inv/invocablecategorylist.rs
let category_list = wink::wsl::inv::invocablecategorylist::InvocableCategoryList::get();
let config = wink::winkconfig::WinkConfig::new(std::env::args().collect());
The std::process::exit() function returns an exit code value to the operating system and ends the program. That value is the result of a match operation against the WinkConfig structure. In fact, calling WinkConfig::new(), where new is just a naming convention for a function associated with the WinkConfig struct, did not return a WinkConfig struct. It returned a Result, which is an enumeration, where enumerations can contain values. The compiler just hides a bit of these details for us. In the case of Result, there are two members of the enumeration: Ok, which the run() function returns in the case of success, and Err(), which it returns if an error occurs.
std::process::exit(match config {
Ok(config) => wink::run(config, category_list), // what if this raises an error
Err((config, e)) => wink::help(&e.to_string(), config, category_list.categories),
});
}
The Ok() and Err() enumerations each contain values determined by the WinkConfig::new() function. The Ok() enumeration contains the WinkConfig that you might have expected the new() function to return, but which the compiler places in Ok(). The Err() enumeration contains a tuple (I think) that contains the same WinkConfig, but with an std::error::Error, which holds data about an error. I generally avoid tuples in favor of explicit types, but they can be useful for binding things together, instrumentation, and error management at a high level, and can avoid unnecessary simple type definitions at any level. Incidentally, I also avoid functions that return multiple values and think that wrapping errors and even multiple values in an enumeration may be an optimal solution.
It is worth noting that because this program does not expect the wink::run() function in /src/lib.rs to panic (crash the program), it does not trap its potential errors, for which the operating system should provide error handling superior to any that this program could perform. Under memory constraints and other system problem conditions, the program should terminate rather than launching more programs or otherwise consuming resources.
The if the Result returned by WinkConfig::new() is the Ok() enumeration, then it contains the WinkConfig. The => operator passes that WinkConfig to wink::run() as the config argument, along with the category_list retrieved previously. The comma ends that condition of the match expression.
Along with the WinkConfig and the list of categories from category_list, the Err() enumeration passes the string value of the std::error::Error that it contains, which should be an instance wink::helperror::HelpError struct defined in /src/helperror.rs, to the wink::help function defined in /lib.rs.
Even though /src/main.rs is the root of a binary (executable) crate and /src/lib.rs is the root of a library crate in the same package, at least for command line tools and background processes rather than libraries, I think that it is appropriate to think them together as defining a single type, where main() has minimal responsibilities, primarily for retrieving data from the environment including the command line.
This series of blog posts describe the structure of the wink command line program that I have written in rust and that I use to invoke other Windows and Linux programs from bash and Windows shells under Windows Subsystem for Linux. Especially as rust is well-documented and uses constructs much like many other languages, my goal is to describe the major structure of the program rather than every coding detail, including some elements specific to rust, but with minimal distracting links to documentation. This post introduces the project and its files.
I should start by repeating that I am not a programmer, and I am new to rust. I just started this learning project a few weeks ago. The following post from one of my other blogs provides a bit of background.
/src/wsl/inv.rs: Groups the types defined in the files under the /src/wsl/inv directory under the wsl::inv path.
/wince: Unix sh script uses cargo to manage building wink for Windows and Unix.
I seem to be working against rust conventions by storing each type in a separate file, but because I am storing configuration in code and have configured over 450 things, one of my types has hundreds of lines (more than a thousand lines, depending on line length limits enforced by rustfmt). Either I am working against convention or rust uses constructs unfamiliar to me that merge the contents of multiple files into a single rust path (namespace).
If crates in packages other thank my wink program could use them, then /src/wsl.rs as well as the /src/wsl directory and all of its contents could belong in a separate library crate in a separate library package, typically created using cargo.
In the wink package (which is really a file system directory containing a .toml file and a /src directory with either a /src/main.rs file or a /src/lib.rs file or both):
/Cargo.toml: Defines the wink package, which contains one or more rust crates (cargo manages devops for rust).
I used the cargo new command to create the wink package. Under [package], name is important, as it determines the root name component of the path to library and binary crates in the package. Rust paths (namespaces) typically correspond to file system paths under the /src directory but also include paths in crates from external packages.
Under [dependencies], this package depends on crates for regular expressions, JSON serialization and deserialization, and coloring console output.
[package]
name = "wink"
version = "0.1.0"
authors = ["jw"]
edition = "2018"
include = ["src/**/*", "Cargo.toml"]
[dependencies]
regex = "1"
derive-new = "0.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
term = "*"
By convention, the rust compiler starts at /src/main.rs, which places an executable binary crate in the wink package, and includes /src/lib.rs, which places a library crate in the same package. These pieces of source code need to use rust path syntax with or without the use and mod keywords to reference any external crates, other crates and modules in this package, and even additional source code files in this package.
Running this package will call the main() function in /src/main.rs.
This blog post contains some notes about mutable and immutable variables in the rust programming language.
Rust supports both mutable and immutable variables. Mutable variables are typical of all programming languages: they hold values that can change. Variables are immutable by default. Immutable variables can hold values that cannot change, but they are not the same as constants. Constants always have a single, hard-coded value. Immutable variables accept dynamic assignment, but only once, after which their values cannot change.
The following block of code initializes a mutable integer named result and then updates its value.
pub mod wsl;
use crate::wsl::inv::invocablecategory::InvocableCategory;
use crate::wsl::inv::invocablecategorylist::InvocableCategoryList;
use crate::wsl::inv::invoker::Invoker;
pub fn run(config: WinkConfig) -> Result<u8, Box<dyn std::error::Error>> {
// categories contain lists of invocables that map command codes to commands
let category_list = InvocableCategoryList::get();
let mut result: u8 = 0;
//TODO: convert to Err()
if !config.help_msg.is_empty() {
help(
&config.help_msg.to_string(),
config,
category_list.categories,
);
result = 1;
} else if !wsl::is_windows_or_wsl() {
help("Runs only under Windows and Windows Subsystem for Linux (WSL). Define WSL_DISTRO_NAME environment variable to override.", config, category_list.categories);
result = 2;
} else if config.pretty_print && !config.export {
help("-p invalid without -e", config, category_list.categories);
result = 3;
} else if config.command_code.is_empty() && !(config.export || config.dry_run) {
help("No command specified", config, category_list.categories);
result = 4;
} else if let Some(invocable) = category_list.get_invocable(&config.command_code) {
if config.export {
if config.pretty_print {
println!("{}", serde_json::to_string_pretty(&invocable).unwrap());
} else {
println!("{}", serde_json::to_string(&invocable).unwrap());
}
}
Invoker {}.invoke(invocable, config.dry_run, config.verbose, config.cmd_args);
result = 0;
} else if config.export && config.command_code.is_empty() {
if config.pretty_print {
println!("{}", serde_json::to_string_pretty(&category_list).unwrap());
} else {
println!("{}", serde_json::to_string(&category_list).unwrap());
}
result = 0;
} else if (config.command_code.is_empty() || !config.export) && config.dry_run {
result = 0;
} else {
help(
&format!("Command not recognized: {0}", config.command_code),
config,
category_list.categories,
);
result = 5;
}
Ok(result)
}
Note that the integer values assigned to result are actually constants; they just do not have names. These are the return codes from the program that are not used, meaningful, or important enough to have names assigned to them.
One note about integers in rust (though I may have things wrong): I think that I read somewhere that overflow (not sure about underflow) panics test builds but just overflows in release builds, which might be appropriate in some cases to prevent panic/crashes in production, but differs from other languages, could lead to defects, and will not get caught without comprehensive test coverage of relatively unexpected conditions or even cases that are logically impossible in production. This seems like a strange feature for a language that is otherwise relatively strict. The return value in this code does not change, so it cannot overflow.
The code above gives result a default value, which may be considered safe in some cases, but for certainty and code clarity, each of the conditions afterwards overrides that value, even with that default value. This might function properly, but generates a warning, because why assign the zero and never use it? Is the developer aware of what is happening here? Does the compiler have a better suggestion?
warning: value assigned to `result` is never read
--> src/lib.rs:12:13
|
12 | let mut result: u8 = 0;
| ^^^^^^
|
= note: `#[warn(unused_assignments)]` on by default
= help: maybe it is overwritten before being read?
warning: 1 warning emitted
If we remove the lines that override the default value of result with itself (result = 0;) from the code above, then this code compiles without warnings, because with that change the compiler can determine that under some conditions, the Ok() statement returns that original default value of result.
Alternatively, and in this case preferably because each condition sets the variable but only once, we can just use an immutable variable.
pub fn run(config: WinkConfig) -> Result<u8, Box<dyn std::error::Error>> {
// categories contain lists of invocables that map command codes to commands
let category_list = InvocableCategoryList::get();
let result: u8;
//TODO: convert to Err()
if !config.help_msg.is_empty() {
help(
&config.help_msg.to_string(),
config,
category_list.categories,
);
result = 1;
} else if !wsl::is_windows_or_wsl() {
help("Runs only under Windows and Windows Subsystem for Linux (WSL). Define WSL_DISTRO_NAME environment variable to override.", config, category_list.categories);
result = 2;
} else if config.pretty_print && !config.export {
help("-p invalid without -e", config, category_list.categories);
result = 3;
} else if config.command_code.is_empty() && !(config.export || config.dry_run) {
help("No command specified", config, category_list.categories);
result = 4;
} else if let Some(invocable) = category_list.get_invocable(&config.command_code) {
if config.export {
if config.pretty_print {
println!("{}", serde_json::to_string_pretty(&invocable).unwrap());
} else {
println!("{}", serde_json::to_string(&invocable).unwrap());
}
}
Invoker {}.invoke(invocable, config.dry_run, config.verbose, config.cmd_args);
result = 0;
} else if config.export && config.command_code.is_empty() {
if config.pretty_print {
println!("{}", serde_json::to_string_pretty(&category_list).unwrap());
} else {
println!("{}", serde_json::to_string(&category_list).unwrap());
}
result = 0;
} else if (config.command_code.is_empty() || !config.export) && config.dry_run {
result = 0;
} else {
help(
&format!("Command not recognized: {0}", config.command_code),
config,
category_list.categories,
);
result = 5;
}
Ok(result)
}
My project structure is not very complex (/main.rs, /lib.rs, /wsl.rs, /wsl/inv.rs, /wsl/inv/<4 types>.rs), but it is important to reference everything properly, and apparently only once.
I had this in /main.rs.
mod wsl; use wsl::inv::invoker::Invoker; use wsl::inv::invocablecategorylist::InvocableCategoryList;
I had this in /lib.rs.
mod wsl;
use crate::wsl::inv::invocablecategory::InvocableCategory;
Do you see the issue? I obviously did not. Both /main.rs and /lib.rs contained the following line.
mod wsl;
This causes both the binary crate (the executable defined by /main.rs) and the library crate (defined by /lib.rs) to do something with the contents of /wsl.rs in both places, which leads to the mismatch.
The solution was to remove mod wsl; from /main.rs and update the corresponding line in /lib.rs with the pub modifier to make it available to other crates that use the library, such as the binary crate.
pub mod wsl;
I may have had to update references in /main.rs to include an absolute path for to access things in wsl, where wink is the name of my project.
if !wink::wsl::is_windows_or_wsl() {
I intend to write a more complete post about project/file/path structure, as I faced some other challenges, especially trying to use separate files for types that I wanted to appear in a single path (rust equivalent of C# namespace).
This blog post describes an issue that you may face using cargo to test rust programs that parse command line arguments.
I used something like the following command line to invoke rusts tests for some logic that parses command line arguments.
cargo test -v --workspace --all-features --target-dir $linbld -- --nocapture -- -epdv word a b c
I would break this into some parts:
cargo test -v --workspace --all-features --target-dir $linbld
I think that this tells cargo to build the test binary in the $linbld directory and run all tests in the project (possibly excluding code samples in documentation comments) in verbose mode.
--
After this, the remaining arguments should go to the cargo test infrastructure.
--nocapture
Tell the cargo test infrastructure not to capture and hence prevent stdout and stderr from appearing in the output of running the tests, so as to render println! macros that running tests would normally omit.
--
Tell the cargo test infrastructure to pass the remaining arguments to my program.
-epdv word a b c
These should be command line arguments to my program.
My test program failed testing. The reason is that, to run tests, cargo builds a binary that uses a main() function that is *not* the main() function in your /main.rs file, but something that invokes the tests. This executable test binary has a unique name, such as /tmp/wink.build/debug/deps/wink-442d9986badfd864 in one case, and must contain some logic that parses command line arguments such as –show-output (two hyphens) and sets some global flags (or something) to enable the tests to write to the console (tests generally assert and panic rather than generating output). The test binary apparently ignores command line arguments such as — (two hyphens), which my program also ignores. When the tests run, the program name is not the name in my package, as the tests expected, and the arguments include those passed to the binary that runs the tests, which the tests did not expect.
My program/tests actually include some infrastructure to show the command line used to run the tests and the the parameters passed to them. With this command line:
cargo test -v --workspace --all-features --target-dir /tmp/wink.build -- --nocapture -- -epdv word a b c
The build script that runs this command to invoke the tests generate the following output, showing the binary name, –nocapture, and — arguments that I had not expected.
cargo test -v --workspace --all-features --target-dir /tmp/wink.build -- --nocapture -- -epdv word a b c
+ tee /dev/fd/2
Fresh unicode-xid v0.2.2
...
Fresh wink v0.1.0 (/mnt/c/temp/wink)
Finished test [unoptimized + debuginfo] target(s) in 0.06s
Running `/tmp/wink.build/debug/deps/wink-2c659bbd243ff8ff --nocapture -- -epdv word a b c`
running 2 tests
thread 'tests::it_has_a_path' panicked at 'it_has_a_path intentional panic to render path to parent of this tests module.', src/lib.rs:116:9
stack backtrace:
it_gets_from_command_line_args: {
"cmd_name": "wink-2c659bbd243ff8ff",
"verbose": true,
"dry_run": true,
"command_code": "word",
"export": true,
"pretty_print": true,
"all_args": [
"/tmp/wink.build/debug/deps/wink-2c659bbd243ff8ff",
"--nocapture",
"--",
"-epdv",
"word",
"a",
"b",
"c"
],
"cmd_args": [
"a",
"b",
"c"
],
"help_msg": ""
}
test tests::it_gets_from_command_line_args ... ok
0: std::panicking::begin_panic
at /rustc/53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b/library/std/src/panicking.rs:519:12
1: wink::tests::it_has_a_path
at ./src/lib.rs:116:9
2: wink::tests::it_has_a_path::{{closure}}
at ./src/lib.rs:115:5
...
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
This issue could cause challenges or issues for testing and logic that depends on the command name or uses the same command line parameter tokens that the cargo test infrastructure uses, but there do not seem to many arguments used by the test executables, so this seems unlikely. It also require logic in the tests such as that bolded in the example below, which may require additions over time. There are some ways that this could complicate or invalidate testing, such if you *want* see how your code *would* respond to the test infrastructure arguments, or if your function needs to parse command line arguments directly from std::env::args() rather than accepting them as a parameter.
/// The WinkConfig struct represents command line options passed
/// to the wink command.
#[derive(serde::Serialize)]
pub struct WinkConfig {
/// The name of the command without the path, such as wink or wink.exe.
pub cmd_name: String,
/// Verbose: true if the -v command line option is present. Generates more output.
pub verbose: bool,
/// DryRun: true if the -d command line option is present. Do not run the command.
pub dry_run: bool,
/// the command code entered by the user, such as EXP or CMD.
pub command_code: String,
/// Export: true if the -e command line option is present. Export JSON configuration.
pub export: bool,
/// PrettyPrint: true if the -p command line option is present. Pretty-print JSON exports.
pub pretty_print: bool,
/// all of the arguments on the command line, including cmd_name
pub all_args: Vec<String>,
/// unprocessed command line arguments to pass to the command that wink will invoke.
pub cmd_args: Vec<String>,
/// empty unless there is a problem parsing command line arguments
pub help_msg: String,
}
/// Implement the Display trait for WinkConfig to render the struct as JSON.
impl std::fmt::Display for WinkConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.pretty_print {
write!(f, "{}", serde_json::to_string_pretty(&self).unwrap())
} else {
write!(f, "{}", serde_json::to_string(&self).unwrap())
}
}
}
impl WinkConfig {
/// The get_from_cmd_line_args function return a WinkConfig
/// created from parsing the command line.
pub fn new(args: Vec<String>) -> WinkConfig {
let mut dry_run: bool = false; // -d command line option
let mut verbose: bool = false; // -v command line option
let mut export: bool = false; // -e command line option
let mut pretty_print: bool = false; // -p command line option
let mut first_arg_index = 1; // number of processed command line arguments (first is command name, such as wink)
let mut help_msg = String::new();
for arg in args.iter().skip(first_arg_index) {
if arg.to_lowercase() == "help" {
help_msg = format!("Help requested by {0}", arg);
break;
}
let prefix: char = arg.chars().next().unwrap();
// if the argument is not help and does not start with a slash or a dash, then it should be a command code
if prefix != '/' && prefix != '-' {
break;
}
for char in arg.chars() {
match char {
'/' | '-' => continue,
'v' => verbose = true,
'd' => dry_run = true,
'p' => pretty_print = true,
'e' => export = true,
'h' | '?' => {
help_msg = format!("Help requested by {0}", arg);
break;
}
_ => {
help_msg = format!("Unrecognized command line option: {0}", arg);
break;
}
}
}
first_arg_index += 1; // just to offend C++ programmers
}
let mut command_code = String::new();
if first_arg_index < args.len() {
command_code = args[first_arg_index].to_owned();
first_arg_index += 1;
}
WinkConfig {
cmd_name: regex::Regex::new(r".*[\\/](?P<name>[^\\/]+$)")
.unwrap()
.replace_all(args[0].as_str(), "$name")
.to_string(),
verbose,
dry_run,
command_code,
export,
pretty_print,
cmd_args: (args[first_arg_index..]).to_vec(),
all_args: args,
help_msg,
}
}
}
#[cfg(test)]
mod tests {
#[test]
/// to pass, run tests like this:
/// cargo test -v --all-features --all=targets --target-dir $linbld -- --show-output -epdv word a b c
fn it_gets_from_command_line_args() {
let mut args: Vec<String> = std::env::args().collect();
if let Some(pos) = args
.iter()
.position(|x| *x == "--show-output" || *x == "--nocapture")
{
args.remove(pos);
}
let wink_config = crate::WinkConfig::new(args);
println!("it_gets_from_command_line_args: {0}", wink_config);
assert!(
wink_config.cmd_name.starts_with("wink"), // cargo test adds a suffix
"{0} !starts_with({1})",
wink_config,
"wink"
);
assert!(wink_config.verbose);
assert!(wink_config.dry_run);
assert!(wink_config.export);
assert!(wink_config.pretty_print);
assert_eq!(wink_config.command_code, "word");
assert!(wink_config.help_msg.is_empty(), "{}", wink_config.help_msg);
}
#[test]
#[should_panic]
/// this test renders the path to the parent of this tests module
fn it_has_a_path() {
panic!("it_has_a_path intentional panic to render path to parent of this tests module.")
}
}
Coincidentally, tests in my rust program revealed what I consider to be a defect, or at least an issue, with the wslpath command that converts between Windows and Unix file system paths under Windows Subsystem for Linux. When converting C:\ from a Windows path to a Unix path, the wslpath command outputs /mnt/c/ (I guess the trailing slash is logical). When converting C: to a Unix path (without the trailing backslash), wslpath returns C: without any slashes or backslashes where I would expect /mnt/c.
Sometimes we just want a simple technique to list the values of an object for logging, diagnostic, debugging, or other purposes. Serializing to JSON with the Display trait may not be comprehensive or consider all cases (for example, some values may require greater security), but could be one of the easiest possible approaches for developers learning rust, and can certainly be useful during coding and debugging.
To use serde to serialize structs to JSON, we can add something like this to the [dependencies] section in cargo.toml.
[dependencies]
derive-new = "0.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
The following a struct implements the Display trait to render itself as JSON.
There is probably a way to implement the function more generically, but WinkConfig just happens to support a pretty_print parameter used for controlling serialization of something else.
I was refactoring some code to move command-line parsing logic from /main.rs into /lib.rs and I ran into a compilation error that has a very solution that demonstrates some of the concepts and issues with ownership and borrowing in rust and how easy it can be to work around some of them, though not always. It shows that with rust, we always need to think about memory allocation, ownership, lifetime, and scope, where we must maintain ownership in scope to keep data valid.
Here was my first attempt at a struct with a method that parses the command line to create an instance of itself.
pub struct WinkConfig {
cmd_name: String,
verbose: bool,
dry_run: bool,
command_code: String,
export: bool,
pretty_print: bool,
all_args: Vec<String>,
cmd_args: Vec<String>,
help_msg: String
}
impl WinkConfig {
pub fn get_from_cmd_line_args() -> WinkConfig {
let args: Vec<String> = std::env::args().collect();
let mut dry_run: bool = false; // -d command line option
let mut verbose: bool = false; // -v command line option
let mut export: bool = false; // -e command line option
let mut pretty_print: bool = false; // -p command line option
let mut first_arg_index = 1; // number of processed command line arguments (first is command name)
let mut help_msg = String::new();
for arg in args.iter().skip(first_arg_index) {
if arg == "help" {
help_msg = format!("Help requested by {0}", arg);
break;
}
let prefix: char = arg.to_lowercase().chars().next().unwrap();
// if the argument is not help and does not start with a slash or a dash, then it should be a command code
if prefix != '/' && prefix != '-' {
break;
}
for char in arg.chars() {
match char {
'/' | '-' => continue,
'v' => verbose = true,
'd' => dry_run = true,
'p' => pretty_print = true,
'e' => export = true,
'h' | '?' => {
help_msg = format!("Help requested by {0}", arg);
break;
},
_ => {
help_msg = format!("Unrecognized command line option: {0}", arg);
break;
}
}
}
first_arg_index + first_arg_index + 1;
}
let mut command_code = String::new();
if first_arg_index < args.len() {
command_code = args[first_arg_index].to_owned();
first_arg_index = first_arg_index + 1;
}
WinkConfig {
cmd_name: args[0].to_owned(),
verbose: verbose,
dry_run: dry_run,
command_code: command_code,
export: export,
pretty_print: pretty_print,
all_args: args,
cmd_args: (&args[first_arg_index..]).to_vec(),
help_msg: help_msg,
}
}
}
This may not be the rustiest code, but it should function. The last block might look unfamiliar to developers coming from languages such as C#. Instead of returning an expression with a return statement and a semicolon, rust prefers raw expressions without semicolons to end functions.
The compilation error is in this last block that creates the WinkConfig to return. Can you see the defect? Trust me, I would not have noticed or even partially understood this a few days ago.
error[E0382]: borrow of moved value: `args`
--> src/lib.rs:72:25
|
15 | let args: Vec<String> = std::env::args().collect();
| ---- move occurs because `args` has type `Vec<String>`, which does not implement the `Copy` trait
...
71 | all_args: args,
| ---- value moved here
72 | cmd_args: (&args[first_arg_index..]).to_vec().to_owned(),
| ^^^^ value borrowed here after move
The first line sets the all_args field of the struct to the args vector returned earlier from parsing the command line parameters. If I understand correctly, this takes ownership of the vector from the thing named args and gives it to the thing named all_args. At this point, args is no longer valid. If all_args is not in scope, then memory holding the vector may have been freed, and even otherwise, something might have changed its values through all_args, leaving args in an unreliable state.
The next clause tries to set a different field based on a slice of args, which is now invalid.
cmd_args: (&args[first_arg_index..]).to_vec(),
When I get something like this wrong, which is constantly, it is usually more complicated than this.
There is a chance that some syntax will let cmd_args initialize from all_args within WinkConfig, and the compiler even seems to have some intelligence about this, but I could not figure it out. Things like this give various exceptions.
In this case, a different solution is incredibly simple: create the slice before changing ownership, which means just reversing the order of these two statements so that args is still valid when creating the slice, before giving ownership of the vector to all_args.
We can create a similar compilation error by removing to_owned() from some of the other expressions, such as at the start of that last block.
cmd_name: args[0], // removed to_owned()
This will cause an exception because after exiting the function, args still has ownership of the data, but is out of scope and therefore invalid. I cannot explain why cmd_args gets ownership in the first case but cmd_name does not get ownership in this case; I assume it has something to do with args[0] being owned by the vector even after retrieving its value or maybe one case involves a reference and the other doesn’t or one piece of data is on the stack rather than the heap or something along those lines. By adding to_owned(), we give cmd_name ownership of that data, or make a copy, or something, because args[] also still seems to be able to use it.
error[E0507]: cannot move out of index of `Vec<String>`
--> src/lib.rs:65:23
|
65 | cmd_name: args[0],
| ^^^^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait
For more information about this error, try `rustc --explain E0507`.
error[E0507]: cannot move out of index of `Vec<String>`
--> src/lib.rs:60:28
|
60 | command_code = args[first_arg_index];
| ^^^^^^^^^^^^^^^^^^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait
For more information about this error, try `rustc --explain E0507`.
Despite warnings due to the program not yet using this code, after re-ordering those two clauses and restoring the to_owned() function calls, this code now compiles, but has not been tested in the slightest. I am particularly concerned about the following line,s which probably use the wrong values and may cause panics or something if there are no such elements in the source vector.
if first_arg_index < args.len()
...
cmd_args: (&args[first_arg_index..]).to_vec(),
This blog post intends to provide guidance that may assist developers familiar with common web programming technologies such as ASP.NET and JavaScript to learn the rust programming language. I am going to try to list the little things that I had to learn before I could do anything correctly with rust. This post does not explain how to use rust but lists key concepts including significant differences from other languages.
I need to explain that I am not a professional programmer, and I am just learning rust. My background is more with C#, as I have learned to avoid the risks of C (depending on perspective and preference) compounded by the incredible complexity of C++ and the (compared to .NET) relatively ridiculous development environments and toolchains for both. I certainly prefer Rust to either of those languages.
I also need to be clear that I do not know rust terminology. I understand some of the concepts, but I might use the wrong terms. Maybe it doesn’t matter; the compiler cares about our symbols, not what we call them. For me, it’s more important to use terms that convey my meaning.
It seems like everyone using rust is supposed to read the same book and program the same way. The rustfmt command reformats *overwrites* our code files, generally adjusting whitespace and other aspects without changing any tokens. The cargo-clippy command provides a variety of suggestions for modifying code to meet rust conventions. The community, which generally seems to come from a C/C++/Linux background, strongly encourages use of those conventions. Though in some cases it seems almost unavoidable, working against these conventions is working against ourselves and everyone else in the community. To maximize productivity in rust, we must understand and follow the conventions whenever possible rather than trying to apply coding styles from other platforms to rust.
In terms of coding symbols, general code structure, and in some cases exact or almost-exact syntax duplication, rust appears much like other languages that eventually derive from things like C. Assumptions based on this similarity can lead to confusion. It is relatively easy to get the rust compiler to accept code without understanding exactly how that code functions.
There are a few significant rust constructs to understand. Maybe the key is enums. In other languages, enums may be simple things, like hard-coded lists of values. In rust, an enum is still simple and part of a static list of values, but those values can contain other values.
Enums are not the same concept in rust. They can contain things. Honestly, we need to understand how enums work before we do any coding with rust.
It seems like the main way to create custom data types is with structs, which can have methods that can be members, so they’re somewhat like classes with both static and member methods in C#.
It seems strange, but once we get past some of the confusion, we embrace these things, even the conventions that may be completely dissimilar to our own.
Rust does not have a garbage collection. Resources free when they go out of scope. We do not need to allocate memory manually – we can simply declare things – but we need to be aware of the impact of this approach to memory management. Sometimes the compiler knows things about potential resource lifetime limitations that we may not have considered; sometimes we know things about resource lifetimes that the compiler does not. Sometimes we can tell the compiler about those things; sometimes we need to change our approach to satisfy the compiler.
For performance, we prefer to allocate things on the stack rather than the heap, although the heap may be faster for large data structures and other cases. While the relative performance impact often seems almost theoretical, it is important to understand the difference. The first thing to understand is ownership and then probably lifetimes.
Because other languages can provide such a relatively high level of abstraction, working with strings in rust can be challenging, confusing, and even frustrating at first, but are actually a good way to learn about ownership and lifetimes. It is important to understand the difference between the owned String type (I think generally on the heap, works like a reference in other languages) and str slices (I think generally on the stack). Rust has a few other string types that are less important to understand.
Beware that rust seems to like to keep all types for a path/scope/namespace in a single source code file. Working against this presents some challenges that involve the module system. For relatively small programs, we can use rust without much understanding of its crate and module system. When we want to start separating our programs into multiple files, we need to understand how packages, crates, and modules work, especially how they correspond to files and paths (namespaces). Otherwise, our expectations based on things like namespaces may lead us in the wrong directions.
We can do relatively simple things with just an .rs source code file and the rust compiler on the command line, but really you should start with cargo, which is developer tooling that comes with rust itself. I have found Microsoft Visual Studio Code to be an excellent development environment for working with rust.
The ! token does not always mean not. At the end of what looks like a function/method name, it means macro. Any rust program likely uses macros constantly but I have not even read about what they are.
It seems that there is no ++ operator to increment an integer by one. This is a minor inconvenience that is highly worthwhile just to offend C++ programmers.