Version bump. Changes to .env. Changes to cli flags. Removed support for period date separator character. Better error handling for invalid files. Updated --help descriptions.

This commit is contained in:
scoobybejesus 2020-11-29 00:16:59 -05:00
parent d3c7c8c6a3
commit fafe538eac
9 changed files with 126 additions and 64 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cryptools" name = "cryptools"
version = "0.9.4" version = "0.10.0"
authors = ["scoobybejesus <scoobybejesus@users.noreply.github.com>"] authors = ["scoobybejesus <scoobybejesus@users.noreply.github.com>"]
edition = "2018" edition = "2018"
description = "Command-line utility for processing cryptocurrency transactions into 'lots' and 'movements'." description = "Command-line utility for processing cryptocurrency transactions into 'lots' and 'movements'."

View File

@ -188,8 +188,9 @@ Until "spot" funds are spent to pay off the margin loans, it's simply an [unreco
* **txDate**: As a default, this parser expects a format of `MM-dd-YY` or `MM-dd-YYYY`. * **txDate**: As a default, this parser expects a format of `MM-dd-YY` or `MM-dd-YYYY`.
The ISO 8601 date format (`YYYY-MM-dd` or `YY-MM-dd` both work) may be indicated by setting the environment variable `ISO_DATE` to `1` or `true`. The ISO 8601 date format (`YYYY-MM-dd` or `YY-MM-dd` both work) may be indicated by setting the environment variable `ISO_DATE` to `1` or `true`.
The hyphen, slash, or period delimiters (`-`, `/`, or `.`) may be indicated The hyphen date separator character (`-`) is the default. The slash date separator character (`/`) may be indicated
by setting `DATE_SEPARATOR` to `h`, `s`, or `p`, respectively (hyphen, `-`, is default). by setting the `DATE_SEPARATOR_IS_SLASH` environment variable (or in .env file) to `1` or `true`,
or by passing the `date_separator_is_slash` command line flag.
* **proceeds**: This is can be any **positive** number that will parse into a floating point 32-bit number, * **proceeds**: This is can be any **positive** number that will parse into a floating point 32-bit number,
as long as the **decimal separator** is a **period**. as long as the **decimal separator** is a **period**.

View File

@ -2,18 +2,20 @@
## ##
## If the defaults below are not suitable, copy this .env.example into a new .env file, ## If the defaults below are not suitable, copy this .env.example into a new .env file,
## uncomment the respective enviroment variable, and set the value according to your needs. ## uncomment the respective enviroment variable, and set the value according to your needs.
## Alternatively, command line flags are available for ISO_DATE and DATE_SEPARATOR_SWITCH.
## Command line flags will override enviroment variables.
# Setting to `TRUE` or `1` will cause the program to expect the `txDate` field in the `file_to_import` to use # Setting to `TRUE` or `1` will cause the program to expect the `txDate` field in the `file_to_import` to use
# the format YYYY-MM-dd or YY-MM-dd (or YYYY/MM/dd or YY/MM/dd, depending on the date-separator option) # the format YYYY-MM-dd or YY-MM-dd (or YYYY/MM/dd or YY/MM/dd, depending on the date-separator character)
# instead of the default US-style MM-dd-YYYY or MM-dd-YY (or MM/dd/YYYY or MM/dd/YY, depending on the # instead of the default US-style MM-dd-YYYY or MM-dd-YY (or MM/dd/YYYY or MM/dd/YY, depending on the
# date separator option). # date separator option).
# (bool; default is FALSE/0) # (bool; default is FALSE/0)
#ISO_DATE=0 #ISO_DATE=0
# Choose "h", "s", or "p" for hyphen, slash, or period (i.e., "-", "/", or ".") to indicate the separator # Switches the default date separator from hyphen to slash (i.e., from "-" to "/") to indicate the separator
# character used in the `file_to_import` `txDate` column (i.e. 2017/12/31, 2017-12-31, or 2017.12.31). # character used in the file_to_import txDate column (i.e. 2017-12-31 to 2017/12/31).
# (String; default is 'h') # (bool; default is FALSE/0)
#DATE_SEPARATOR=h #DATE_SEPARATOR_IS_SLASH=0
# Home currency (currency in which all resulting reports are denominated). # Home currency (currency in which all resulting reports are denominated).
# (String; default is 'USD') # (String; default is 'USD')

View File

@ -5,6 +5,7 @@ use std::error::Error;
use std::io::{self, BufRead}; use std::io::{self, BufRead};
use std::process; use std::process;
use std::path::PathBuf; use std::path::PathBuf;
use std::fs::File;
use chrono::NaiveDate; use chrono::NaiveDate;
use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::completion::{Completer, FilenameCompleter, Pair};
@ -21,7 +22,7 @@ use crptls::string_utils;
pub fn choose_file_for_import(flag_to_accept_cli_args: bool) -> Result<PathBuf, Box<dyn Error>> { pub fn choose_file_for_import(flag_to_accept_cli_args: bool) -> Result<PathBuf, Box<dyn Error>> {
if flag_to_accept_cli_args { if flag_to_accept_cli_args {
println!("WARN: Flag to 'accept args' was set, but 'file' is missing.\n"); println!("\nWARN: Flag to 'accept args' was set, but 'file_to_import' is missing.\n");
} }
println!("Please input a file (absolute or relative path) to import: "); println!("Please input a file (absolute or relative path) to import: ");
@ -30,6 +31,9 @@ pub fn choose_file_for_import(flag_to_accept_cli_args: bool) -> Result<PathBuf,
if has_tilde { if has_tilde {
choose_file_for_import(flag_to_accept_cli_args) choose_file_for_import(flag_to_accept_cli_args)
} else if File::open(&file_string).is_err() {
println!("WARN: Invalid file path.");
choose_file_for_import(false)
} else { } else {
Ok( PathBuf::from(file_string) ) Ok( PathBuf::from(file_string) )
} }
@ -125,19 +129,19 @@ pub fn choose_inventory_costing_method(cmd_line_arg: String) -> Result<Inventory
let method = _costing_method(cmd_line_arg)?; let method = _costing_method(cmd_line_arg)?;
fn _costing_method(cmd_line_arg: String) -> Result<InventoryCostingMethod, Box<dyn Error>> { fn _costing_method(env_var_arg: String) -> Result<InventoryCostingMethod, Box<dyn Error>> {
let mut input = String::new(); let mut input = String::new();
let stdin = io::stdin(); let stdin = io::stdin();
stdin.lock().read_line(&mut input).expect("Failed to read stdin"); stdin.lock().read_line(&mut input).expect("Failed to read stdin");
match input.trim() { // Without .trim(), there's a hidden \n or something preventing the match match input.trim() { // Without .trim(), there's a hidden \n or something preventing the match
"" => Ok(inv_costing_from_cmd_arg(cmd_line_arg)?), "" => Ok(inv_costing_from_cmd_arg(env_var_arg)?),
"1" => Ok(InventoryCostingMethod::LIFObyLotCreationDate), "1" => Ok(InventoryCostingMethod::LIFObyLotCreationDate),
"2" => Ok(InventoryCostingMethod::LIFObyLotBasisDate), "2" => Ok(InventoryCostingMethod::LIFObyLotBasisDate),
"3" => Ok(InventoryCostingMethod::FIFObyLotCreationDate), "3" => Ok(InventoryCostingMethod::FIFObyLotCreationDate),
"4" => Ok(InventoryCostingMethod::FIFObyLotBasisDate), "4" => Ok(InventoryCostingMethod::FIFObyLotBasisDate),
_ => { println!("Invalid choice. Please enter a valid choice."); _costing_method(cmd_line_arg) } _ => { println!("Invalid choice. Please enter a valid choice."); _costing_method(env_var_arg) }
} }
} }
@ -151,10 +155,9 @@ pub fn inv_costing_from_cmd_arg(arg: String) -> Result<InventoryCostingMethod, &
"3" => Ok(InventoryCostingMethod::FIFObyLotCreationDate), "3" => Ok(InventoryCostingMethod::FIFObyLotCreationDate),
"4" => Ok(InventoryCostingMethod::FIFObyLotBasisDate), "4" => Ok(InventoryCostingMethod::FIFObyLotBasisDate),
_ => { _ => {
eprintln!("WARN: Invalid environment variable for 'INV_COSTING_METHOD'. Using default."); println!("WARN: Invalid environment variable for 'INV_COSTING_METHOD'. Using default.");
Ok(InventoryCostingMethod::LIFObyLotCreationDate) Ok(InventoryCostingMethod::LIFObyLotCreationDate)
} }
// _ => { Err("Invalid input parameter, probably from environment variable INV_COSTING_METHOD") }
} }
} }
@ -171,7 +174,7 @@ pub(crate) fn elect_like_kind_treatment(cutoff_date_arg: &mut Option<String>) ->
second_date_try_from_user(&mut cutoff_date_arg).unwrap() second_date_try_from_user(&mut cutoff_date_arg).unwrap()
} ) ); } ) );
println!("\nUse like-kind exchange treatment through {}? [Y/n/c] ('c' to 'change') ", provided_date); println!("Use like-kind exchange treatment through {}? [Y/n/c] ('c' to 'change') ", provided_date);
let (election, date_string) = _elect_like_kind_arg(&cutoff_date_arg, provided_date)?; let (election, date_string) = _elect_like_kind_arg(&cutoff_date_arg, provided_date)?;

View File

@ -95,7 +95,7 @@ pub fn import_and_process_final(
if settings.lk_treatment_enabled { if settings.lk_treatment_enabled {
println!(" Applying like-kind treatment for cut-off date: {}.", settings.lk_cutoff_date); println!(" Applying like-kind treatment through cut-off date: {}.", settings.lk_cutoff_date);
import_cost_proceeds_etc::apply_like_kind_treatment( import_cost_proceeds_etc::apply_like_kind_treatment(
&settings, &settings,

View File

@ -26,7 +26,17 @@ pub(crate) fn import_from_csv(
transactions_map: &mut HashMap<u32, Transaction>, transactions_map: &mut HashMap<u32, Transaction>,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let file = File::open(import_file_path)?; println!("\nCSV ledger file opened successfully.\n"); let file = match File::open(import_file_path) {
Ok(x) => {
println!("\nCSV ledger file opened successfully.\n");
x
},
Err(e) => {
println!("Invalid import_file_path");
eprintln!("System error: {}", e);
std::process::exit(1);
}
};
let mut rdr = csv::ReaderBuilder::new() let mut rdr = csv::ReaderBuilder::new()
.has_headers(true) .has_headers(true)
@ -184,6 +194,7 @@ fn import_transactions(
let acct_idx = ind - 2; // acct_num and acct_key would be idx + 1, so subtract 2 from ind to get 1 let acct_idx = ind - 2; // acct_num and acct_key would be idx + 1, so subtract 2 from ind to get 1
let account_key = acct_idx as u16; let account_key = acct_idx as u16;
// TODO: implement conversion for negative numbers surrounded in parentheses
let amount_str = field.replace(",", ""); let amount_str = field.replace(",", "");
let amount = amount_str.parse::<d128>().unwrap(); let amount = amount_str.parse::<d128>().unwrap();
let amount_rounded = round_d128_1e8(&amount); let amount_rounded = round_d128_1e8(&amount);

View File

@ -31,24 +31,24 @@ pub struct Cli {
/// User is instructing the program to skip the data entry wizard. /// User is instructing the program to skip the data entry wizard.
/// When set, default settings will be assumed if they are not set by /// When set, default settings will be assumed if they are not set by
/// environment variables (or .env file). /// environment variables (or .env file) or certain command line flags.
#[structopt(name = "accept args", short = "a", long = "accept")] #[structopt(name = "accept args", short = "a", long = "accept")]
accept_args: bool, accept_args: bool,
/// This flag will suppress the printing of "all" reports, except that it *will* trigger the /// Suppresses the printing of "all" reports, except that it *will* trigger the
/// exporting of a txt file containing an accounting journal entry for every transaction. /// exporting of a txt file containing an accounting journal entry for every transaction.
/// Individual account and transaction reports may still be printed via the print_menu /// Individual account and transaction reports may still be printed via the print_menu
/// with the -p flag. Note: the journal entries are not suitable for like-kind transactions. /// with the -p flag. Note: the journal entries are not suitable for like-kind transactions.
#[structopt(name = "journal entries", short, long = "journal-entries")] #[structopt(name = "journal entries", short, long = "journal-entries")]
journal_entries_only: bool, journal_entries_only: bool,
/// Once the import file has been fully processed, the user will be presented /// Once the file_to_import has been fully processed, the user will be presented
/// with a menu for manually selecting which reports to print/export. If this flag is not /// with a menu for manually selecting which reports to print/export. If this flag is not
/// set, the program will print/export all available reports. /// set, the program will print/export all available reports.
#[structopt(name = "print menu", short, long = "print-menu")] #[structopt(name = "print menu", short, long = "print-menu")]
print_menu: bool, print_menu: bool,
/// This will prevent the program from writing reports to files. /// Prevents the program from writing reports to files.
/// This will be ignored if -a is not set (the wizard will always ask to output). /// This will be ignored if -a is not set (the wizard will always ask to output).
#[structopt(name = "suppress reports", short, long = "suppress")] #[structopt(name = "suppress reports", short, long = "suppress")]
suppress_reports: bool, suppress_reports: bool,
@ -57,10 +57,26 @@ pub struct Cli {
#[structopt(name = "output directory", short, long = "output", default_value = ".", parse(from_os_str))] #[structopt(name = "output directory", short, long = "output", default_value = ".", parse(from_os_str))]
output_dir_path: PathBuf, output_dir_path: PathBuf,
/// File to be imported. By default, the program expects the `txDate` column to be formatted as %m/%d/%y. /// Causes the program to expect the `txDate` field in the file_to_import to use the format YYYY-MM-dd
/// You may alter this with ISO_DATE and DATE_SEPARATOR environment variables. See .env.example for /// or YY-MM-dd (or YYYY/MM/dd or YY/MM/dd) instead of the default US-style MM-dd-YYYY or MM-dd-YY
/// further details. /// (or MM/dd/YYYY or MM/dd/YY).
#[structopt(name = "file", parse(from_os_str))] /// NOTE: this flag overrides the ISO_DATE environment variable, including if set in the .env file.
#[structopt(name = "imported file uses ISO 8601 date format", short = "i", long = "iso")]
iso_date: bool,
/// Tells the program a non-default date separator (instead of a hyphen "-", a slash "/") was used
/// in the file_to_import `txDate` column (i.e. 2017-12-31 instead of 2017/12/31).
/// NOTE: this flag overrides the DATE_SEPARATOR_IS_SLASH environment variable, including if set in the .env file.
#[structopt(name = "date separator character is slash", short = "d", long = "date-separator-is-slash")]
date_separator_is_slash: bool,
/// File to be imported. Some notes on the columns: (a) by default, the program expects the `txDate` column to
/// be formatted as %m-%d-%y. You may alter this with ISO_DATE and DATE_SEPARATOR_IS_SLASH flags or environment
/// variables; (b) the `proceeds` column and any values in transactions must have a period (".") as the decimal
/// separator; and (c) any transactions with negative values must not be wrapped in parentheses (use the python
/// script for sanitizing/converting negative values).
/// See .env.example for further details on environment variables.
#[structopt(name = "file_to_import", parse(from_os_str))]
file_to_import: Option<PathBuf>, file_to_import: Option<PathBuf>,
} }
@ -72,10 +88,9 @@ pub struct Cfg {
/// The default value is `false`, meaning the program will expect default US-style MM-dd-YYYY or MM-dd-YY (or MM/dd/YYYY /// The default value is `false`, meaning the program will expect default US-style MM-dd-YYYY or MM-dd-YY (or MM/dd/YYYY
/// or MM/dd/YY, depending on the date separator option). /// or MM/dd/YY, depending on the date separator option).
iso_date: bool, iso_date: bool,
/// Set the corresponding environment variable to "h", "s", or "p" for hyphen, slash, or period (i.e., "-", "/", or ".") /// Switches the default date separator from hyphen to slash (i.e., from "-" to "/") to indicate the separator
/// to indicate the separator character used in the `Cli::file_to_import` `txDate` column (i.e. 2017/12/31, 2017-12-31, or 2017.12.31). /// character used in the file_to_import txDate column (i.e. 2017-12-31 to 2017/12/31).
/// The default is `h`. date_separator_is_slash: bool,
date_separator: String,
/// Home currency (currency from the `proceeds` column of the `Cli::file_to_import` and in which all resulting reports are denominated). /// Home currency (currency from the `proceeds` column of the `Cli::file_to_import` and in which all resulting reports are denominated).
/// Default is `USD`. /// Default is `USD`.
home_currency: String, home_currency: String,
@ -109,7 +124,7 @@ change default program behavior.
Note: The software is designed to import a full history. Gains and losses may be incorrect otherwise. Note: The software is designed to import a full history. Gains and losses may be incorrect otherwise.
"); ");
let cfg = setup::get_env()?; let cfg = setup::get_env(&args)?;
let (input_file_path, settings) = setup::run_setup(args, cfg)?; let (input_file_path, settings) = setup::run_setup(args, cfg)?;

View File

@ -3,8 +3,8 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::error::Error; use std::error::Error;
use std::process;
use std::env; use std::env;
use std::fs::File;
use chrono::NaiveDate; use chrono::NaiveDate;
use dotenv; use dotenv;
@ -17,31 +17,53 @@ use crate::skip_wizard;
use crate::wizard; use crate::wizard;
pub fn get_env() -> Result<super::Cfg, Box<dyn Error>> { pub fn get_env(cmd_args: &super::Cli) -> Result<super::Cfg, Box<dyn Error>> {
match dotenv::dotenv() { match dotenv::dotenv() {
Ok(_path) => {println!("Setting environment variables from .env file.")}, Ok(_path) => { println!("Setting environment variables from .env file.") },
Err(_e) => println!("Did not find .env file.") Err(_e) => println!("Did not find .env file.")
} }
let iso_date: bool = match env::var("ISO_DATE") { let iso_date: bool = if cmd_args.iso_date {
Ok(val) => { println!(" Command line flag for ISO_DATE was set. Using YY-mm-dd or YY/mm/dd.");
if val == "1" || val.to_lowercase() == "true" { true
true } else {
} else { match env::var("ISO_DATE") {
Ok(val) => {
if val == "1" || val.to_lowercase() == "true" {
println!(" Found ISO_DATE env var: {}. Using YY-mm-dd or YY/mm/dd.", val);
true
} else {
println!(" Found ISO_DATE env var: {} (not 1 or true). Using MM-dd-YY or MM/dd/YY.", val);
false
}
},
Err(_e) => {
println!(" Using default dating convention (MM-dd-YY or MM/dd/YY).");
false false
} },
}, }
Err(_e) => false,
}; };
let date_separator: String = match env::var("DATE_SEPARATOR") { let date_separator_is_slash: bool = if cmd_args.date_separator_is_slash {
Ok(val) => { println!(" Command line flag for DATE_SEPARATOR_IS_SLASH was set. Date separator set to slash (\"/\").");
println!(" Found DATE_SEPARATOR env var: {}", val); true
val.to_lowercase()}, } else {
Err(_e) => { match env::var("DATE_SEPARATOR_IS_SLASH") {
println!(" Using default date separator (hyphen)."); Ok(val) => {
"h".to_string()}, if val == "1" || val.to_ascii_lowercase() == "true" {
println!(" Found DATE_SEPARATOR_IS_SLASH env var: {}. Date separator set to slash (\"/\").", val);
true
} else {
println!(" Found DATE_SEPARATOR_IS_SLASH env var: {} (not 1 or true). Date separator set to hyphen (\"-\").", val);
false
}
}
Err(_e) => {
println!(" Using default date separator, hyphen (\"-\").");
false
},
}
}; };
let home_currency = match env::var("HOME_CURRENCY") { let home_currency = match env::var("HOME_CURRENCY") {
@ -71,7 +93,7 @@ pub fn get_env() -> Result<super::Cfg, Box<dyn Error>> {
let cfg = super::Cfg { let cfg = super::Cfg {
iso_date, iso_date,
date_separator, date_separator_is_slash,
home_currency, home_currency,
lk_cutoff_date, lk_cutoff_date,
inv_costing_method, inv_costing_method,
@ -90,19 +112,26 @@ pub struct ArgsForImportVarsTBD {
pub (crate) fn run_setup(cmd_args: super::Cli, cfg: super::Cfg) -> Result<(PathBuf, ImportProcessParameters), Box<dyn Error>> { pub (crate) fn run_setup(cmd_args: super::Cli, cfg: super::Cfg) -> Result<(PathBuf, ImportProcessParameters), Box<dyn Error>> {
let date_separator = match cfg.date_separator.as_str() { let date_separator = match cfg.date_separator_is_slash {
"h" => { "-" } false => { "-" } // Default
"s" => { "/" } true => { "/" } // Overridden by env var or cmd line flag
"p" => { "." }
_ => {
println!("\nFATAL: ENV: The date-separator arg requires either an 'h', an 's', or a 'p'.\n");
process::exit(1)
}
}; };
let input_file_path = match cmd_args.file_to_import { let input_file_path = match cmd_args.file_to_import {
Some(file) => file, Some(file) => {
None => cli_user_choices::choose_file_for_import(cmd_args.accept_args)? if File::open(&file).is_ok() {
file
} else {
cli_user_choices::choose_file_for_import(cmd_args.accept_args)?
}
},
None => {
if !cmd_args.accept_args {
wizard::shall_we_proceed()?;
println!("Note: No file was provided as a command line arg, or the provided file wasn't found.\n");
}
cli_user_choices::choose_file_for_import(cmd_args.accept_args)?
}
}; };
let wizard_or_not_args = ArgsForImportVarsTBD { let wizard_or_not_args = ArgsForImportVarsTBD {

View File

@ -20,7 +20,8 @@ pub(crate) fn wizard(args: ArgsForImportVarsTBD) -> Result<(
PathBuf, PathBuf,
), Box<dyn Error>> { ), Box<dyn Error>> {
shall_we_proceed()?; println!("\nThe following wizard will guide you through your choices:\n");
// shall_we_proceed()?;
let costing_method_choice = cli_user_choices::choose_inventory_costing_method(args.inv_costing_method_arg)?; let costing_method_choice = cli_user_choices::choose_inventory_costing_method(args.inv_costing_method_arg)?;
@ -39,9 +40,9 @@ pub(crate) fn wizard(args: ArgsForImportVarsTBD) -> Result<(
Ok((costing_method_choice, like_kind_election, like_kind_cutoff_date_string, should_export, output_dir_path.to_path_buf())) Ok((costing_method_choice, like_kind_election, like_kind_cutoff_date_string, should_export, output_dir_path.to_path_buf()))
} }
fn shall_we_proceed() -> Result<(), Box<dyn Error>> { pub fn shall_we_proceed() -> Result<(), Box<dyn Error>> {
println!("Shall we proceed? [Y/n] "); println!("\n Shall we proceed? [Y/n] ");
_proceed()?; _proceed()?;
@ -64,7 +65,7 @@ fn shall_we_proceed() -> Result<(), Box<dyn Error>> {
fn export_reports_to_output_dir(output_dir_path: PathBuf) -> Result<(bool, PathBuf), Box<dyn Error>> { fn export_reports_to_output_dir(output_dir_path: PathBuf) -> Result<(bool, PathBuf), Box<dyn Error>> {
println!("\nThe directory currently selected for exporting reports is: {}", output_dir_path.to_str().unwrap()); println!("The directory currently selected for exporting reports is: {}", output_dir_path.to_str().unwrap());
if output_dir_path.to_str().unwrap() == "." { if output_dir_path.to_str().unwrap() == "." {
println!(" (A 'dot' denotes the default value: current working directory.)"); println!(" (A 'dot' denotes the default value: current working directory.)");