Skip to content

Instantly share code, notes, and snippets.

@glinesbdev
Last active January 15, 2023 03:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save glinesbdev/b55741f39c7e011c968fac5b1a6b1f4b to your computer and use it in GitHub Desktop.
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)
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);
}
}
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(())
}
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(())
}
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