Last active
January 15, 2023 03:08
-
-
Save glinesbdev/b55741f39c7e011c968fac5b1a6b1f4b to your computer and use it in GitHub Desktop.
Sample program to open and write to a file conditionally with arguments (https://github.com/glinesbdev/file_reader_writer)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::path::Path; | |
use crate::types::Result; | |
/// Struct to store command line arguments | |
pub struct Args { | |
filepath: String, | |
contents: String, | |
append: bool, | |
truncate: bool, | |
print_contents: bool, | |
} | |
impl Default for Args { | |
fn default() -> Self { | |
Self { | |
filepath: "".into(), | |
contents: "".into(), | |
append: false, | |
truncate: false, | |
print_contents: true, | |
} | |
} | |
} | |
impl Args { | |
/// Filepath from args | |
pub fn filepath(&self) -> &String { | |
&self.filepath | |
} | |
/// Contents from args | |
pub fn contents(&self) -> &String { | |
&self.contents | |
} | |
/// Flag given to the program i.e. `--append`. | |
/// If present, it appends `contents` to the file in the `filepath`. | |
/// If not present, it overwrites the contents of the file in `filepath`. | |
pub fn appendable(&self) -> bool { | |
self.append | |
} | |
/// Flag given to the program i.e. `--truncate`. | |
/// If present, truncates the file in `filepath` before writing `contents` to it. | |
pub fn truncatable(&self) -> bool { | |
self.truncate | |
} | |
/// Flag given to the program i.e. `--no-print`. | |
/// If present, it doesn't print the output of the written file to stdout. | |
pub fn print_contents(&self) -> bool { | |
self.print_contents | |
} | |
/// Collects command line arguments. Takes full ownership of the `args` argument; | |
/// | |
/// # Examples | |
/// | |
/// ``` | |
/// use std::env; | |
/// use file_reader_writer::args::Args; | |
/// let args: Vec<String> = vec![ | |
/// "target/debug/file_reader_writer".into(), | |
/// "./tmp/test.txt".into(), | |
/// "Some content".into() | |
/// ]; // = env::args().collect(); | |
/// let args = Args::from_env(args)?; | |
/// # Ok::<(), Box<dyn std::error::Error>>(()) | |
/// ``` | |
pub fn from_env(args: Vec<String>) -> Result<Self> { | |
let (flags, args): (Vec<String>, Vec<String>) = | |
args.into_iter().skip(1).partition(|s| s.starts_with("-")); | |
let mut result = Self::default(); | |
result.parse_flags(&flags[..]); | |
result.filepath = args.get(0).ok_or_else(|| "Filepath required")?.to_string(); | |
if !Path::is_file(Path::new(&result.filepath)) { | |
return Err("Filepath arg must be a file".into()); | |
} | |
if let Some(contents) = args.get(1) { | |
result.contents = contents.into(); | |
} | |
Ok(result) | |
} | |
fn parse_flags(&mut self, flags: &[String]) { | |
for arg in &flags[..] { | |
match arg.as_str() { | |
"--append" | "-a" => self.append = true, | |
"--truncate" | "-t" => self.truncate = true, | |
"--no-print" | "-np" => self.print_contents = false, | |
"--help" | "-h" => Self::print_help(), | |
_ => (), | |
} | |
} | |
} | |
fn print_help() { | |
let version = env!("CARGO_PKG_VERSION"); | |
println!( | |
r#" | |
File Reader Writer - {version} | |
Usage: file_reader_writer [filepath] [contents] [OPTIONS] | |
Example: file_reader_writer ./tmp/my_file.txt "Here is some content." -t | |
Options: | |
-a, --append Appends new [contents] to the [filepath] file. | |
-t, --truncate Truncates the [filepath] file before writing [contents] to it. | |
Ignores --append option when used. | |
-np, --no-print Doesn't print the output of the file after writing. | |
-h, --help Shows this help screen. | |
"# | |
); | |
std::process::exit(0); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::{ | |
fs::File, | |
io::{ErrorKind, Write}, | |
}; | |
pub mod args; | |
mod types; | |
use args::Args; | |
use types::Result; | |
/// Opens or creates a file from args and returns an open file handle in write and append mode. | |
/// | |
/// # Examples | |
/// | |
/// ``` | |
/// use std::env; | |
/// use file_reader_writer::*; | |
/// use args::Args; | |
/// let args: Vec<String> = vec![ | |
/// "target/debug/file_reader_writer".into(), | |
/// "./tmp/test.txt".into(), | |
/// "Some content".into() | |
/// ]; // = env::args().collect(); | |
/// let args = Args::from_env(args)?; | |
/// open_or_create_file(&args)?; | |
/// # Ok::<(), Box<dyn std::error::Error>>(()) | |
/// ``` | |
pub fn open_or_create_file(args: &Args) -> Result<File> { | |
match File::options() | |
.write(true) | |
.append(args.appendable()) | |
.truncate(args.truncatable()) | |
.open(args.filepath()) | |
{ | |
Ok(f) => Ok(f), | |
Err(err) => match err.kind() { | |
ErrorKind::NotFound => Ok(File::create(args.filepath())?), | |
_ => Err("Could not open file".into()), | |
}, | |
} | |
} | |
/// Writes to an open file handle | |
/// | |
/// # Examples | |
/// | |
/// ``` | |
/// use std::env; | |
/// use file_reader_writer::*; | |
/// use args::Args; | |
/// let args: Vec<String> = vec![ | |
/// "target/debug/file_reader_writer".into(), | |
/// "./tmp/test.txt".into(), | |
/// "Some content".into() | |
/// ]; // = env::args().collect(); | |
/// let args = Args::from_env(args)?; | |
/// let mut file = open_or_create_file(&args)?; | |
/// let contents = "My awesome file text"; | |
/// | |
/// write_to_file(&mut file, contents)?; | |
/// # Ok::<(), Box<dyn std::error::Error>>(()) | |
/// ``` | |
pub fn write_to_file(file: &mut File, contents: &str) -> Result<()> { | |
let mut permissions = file.metadata()?.permissions(); | |
permissions.set_readonly(false); | |
file.set_permissions(permissions)?; | |
file.write_all(contents.as_bytes())?; | |
Ok(()) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use file_reader_writer::{*, args::Args}; | |
use types::Result; | |
use std::{env, fs}; | |
mod types; | |
fn main() -> Result<()> { | |
let args: Vec<String> = env::args().collect(); | |
let args = Args::from_env(args)?; | |
let mut file = open_or_create_file(&args)?; | |
write_to_file(&mut file, args.contents())?; | |
if args.print_contents() { | |
let result = fs::read_to_string(args.filepath())?; | |
println!("The contents of {} is\n{result}", args.filepath()); | |
} | |
Ok(()) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use assert_cmd::prelude::*; | |
use assert_fs::prelude::FileWriteStr; | |
use predicates::prelude::*; | |
use std::process::Command; | |
const BIN_NAME: &str = env!("CARGO_PKG_NAME"); | |
type TestResult<T> = Result<T, Box<dyn std::error::Error>>; | |
#[test] | |
fn creates_file_no_content() -> TestResult<()> { | |
let mut cmd = Command::cargo_bin(BIN_NAME)?; | |
cmd.arg("./tmp/test.txt"); | |
cmd.assert().success().stdout(predicate::str::contains( | |
"The contents of ./tmp/test.txt is\n", | |
)); | |
Ok(()) | |
} | |
#[test] | |
fn creates_file_with_content() -> TestResult<()> { | |
let mut cmd = Command::cargo_bin(BIN_NAME)?; | |
cmd.arg("./tmp/test.txt").arg("Some contents"); | |
cmd.assert() | |
.success() | |
.stdout(predicate::str::contains("Some contents")); | |
Ok(()) | |
} | |
#[test] | |
fn writes_to_open_file() -> TestResult<()> { | |
let file = assert_fs::NamedTempFile::new("existing_file.txt")?; | |
file.write_str("Some existing data")?; | |
let mut cmd = Command::cargo_bin(BIN_NAME)?; | |
cmd.arg(file.path()).arg("Some new contents"); | |
cmd.assert() | |
.success() | |
.stdout(predicate::str::contains("Some new contents")); | |
Ok(()) | |
} | |
#[test] | |
fn error_missing_filename() -> TestResult<()> { | |
let mut cmd = Command::cargo_bin(BIN_NAME)?; | |
cmd.assert() | |
.failure() | |
.stderr(predicate::str::contains("Filepath required")); | |
Ok(()) | |
} | |
#[test] | |
fn error_filepath_must_be_file() -> TestResult<()> { | |
let mut cmd = Command::cargo_bin(BIN_NAME)?; | |
cmd.arg("./tmp").arg("Some contents."); | |
cmd.assert() | |
.failure() | |
.stderr(predicate::str::contains("Filepath arg must be a file")); | |
Ok(()) | |
} | |
#[test] | |
fn using_append_flags() -> TestResult<()> { | |
let file = assert_fs::NamedTempFile::new("existing_file.txt")?; | |
file.write_str("File with content.")?; | |
let mut cmd = Command::cargo_bin(BIN_NAME)?; | |
cmd.arg(file.path()) | |
.arg(" Some other content.") | |
.arg("--append"); | |
cmd.assert().success().stdout(predicate::str::contains( | |
"File with content. Some other content.", | |
)); | |
let mut cmd = Command::cargo_bin(BIN_NAME)?; | |
cmd.arg(file.path()).arg(" Even more content!").arg("-a"); | |
cmd.assert() | |
.success() | |
.stdout(predicate::str::contains("Some other content.")) | |
.stdout(predicate::str::contains("Even more content!")); | |
Ok(()) | |
} | |
#[test] | |
fn using_truncate_flags() -> TestResult<()> { | |
let file = assert_fs::NamedTempFile::new("existing_file.txt")?; | |
file.write_str("File with content.")?; | |
let mut cmd = Command::cargo_bin(BIN_NAME)?; | |
cmd.arg(file.path()) | |
.arg("Some new content.") | |
.arg("--truncate"); | |
cmd.assert() | |
.success() | |
.stdout(predicate::str::contains("File with content.").not()) | |
.stdout(predicate::str::contains("Some new content")); | |
let mut cmd = Command::cargo_bin(BIN_NAME)?; | |
cmd.arg(file.path()).arg("Replaced content.").arg("-t"); | |
cmd.assert() | |
.success() | |
.stdout(predicate::str::contains("Some new content").not()) | |
.stdout(predicate::str::contains("Replaced content.")); | |
Ok(()) | |
} | |
#[test] | |
fn prints_help_menu() -> TestResult<()> { | |
let mut cmd = Command::cargo_bin(BIN_NAME)?; | |
let version = env!("CARGO_PKG_VERSION"); | |
cmd.arg("-h"); | |
cmd.assert() | |
.success() | |
.stdout(predicate::str::contains(format!( | |
"File Reader Writer - {version}" | |
))) | |
.stdout(predicate::str::contains( | |
"Usage: file_reader_writer [filepath] [contents] [OPTIONS]", | |
)); | |
let mut cmd = Command::cargo_bin(BIN_NAME)?; | |
cmd.arg("--help"); | |
cmd.assert() | |
.success() | |
.stdout(predicate::str::contains(format!( | |
"File Reader Writer - {version}" | |
))) | |
.stdout(predicate::str::contains( | |
"Usage: file_reader_writer [filepath] [contents] [OPTIONS]", | |
)); | |
Ok(()) | |
} | |
#[test] | |
fn using_no_print_flags() -> TestResult<()> { | |
let mut cmd = Command::cargo_bin(BIN_NAME)?; | |
cmd.arg("./tmp/new_file.txt") | |
.arg("Some content") | |
.arg("--no-print"); | |
cmd.assert().success().stdout(predicate::str::is_empty()); | |
let mut cmd = Command::cargo_bin(BIN_NAME)?; | |
cmd.arg("./tmp/new_file.txt").arg("Some content").arg("-np"); | |
cmd.assert().success().stdout(predicate::str::is_empty()); | |
Ok(()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment