From 5b3771e0767572a56211c8a03b592943bfba5632 Mon Sep 17 00:00:00 2001 From: devark28 Date: Fri, 14 Nov 2025 00:30:04 +0200 Subject: [PATCH 1/9] refactor: migrate from custom CLI parsing to clap - Replace complex custom argument parsing with clap derive macros - Remove custom command types and macros in favor of clap's patterns - Maintain same command interface (lint, format, list, pick) - Add auto-generated help and standard CLI flags - Simplify codebase by removing ~200 lines of custom parsing logic --- Cargo.lock | 177 ++++++++++++++++++++++++++ Cargo.toml | 1 + src/cli/args.rs | 58 +++++++++ src/cli/cli.rs | 91 ++++++++----- src/cli/commands/cli_cmd.rs | 6 - src/cli/commands/format.rs | 51 -------- src/cli/commands/help.rs | 31 ----- src/cli/commands/lint.rs | 51 -------- src/cli/commands/list.rs | 51 -------- src/cli/commands/mod.rs | 37 ------ src/cli/commands/pick.rs | 65 ---------- src/cli/commands/version.rs | 39 ------ src/cli/macros/mod.rs | 8 -- src/cli/mod.rs | 12 +- src/main.rs | 12 +- src/parser/engine/commands/pick.rs | 6 +- src/parser/engine/commands/version.rs | 5 +- src/parser/engine/mod.rs | 8 +- src/parser/tokens/block.rs | 4 +- src/parser/tokens/line.rs | 21 --- 20 files changed, 310 insertions(+), 424 deletions(-) create mode 100644 src/cli/args.rs delete mode 100644 src/cli/commands/cli_cmd.rs delete mode 100644 src/cli/commands/format.rs delete mode 100644 src/cli/commands/help.rs delete mode 100644 src/cli/commands/lint.rs delete mode 100644 src/cli/commands/list.rs delete mode 100644 src/cli/commands/mod.rs delete mode 100644 src/cli/commands/pick.rs delete mode 100644 src/cli/commands/version.rs delete mode 100644 src/cli/macros/mod.rs diff --git a/Cargo.lock b/Cargo.lock index bf728aa..1cbbcf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,10 +2,107 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "envmn" version = "0.2.6" dependencies = [ + "clap", "indexmap", ] @@ -21,6 +118,12 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "indexmap" version = "2.12.0" @@ -30,3 +133,77 @@ dependencies = [ "equivalent", "hashbrown", ] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml index 69f9587..95a8848 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,4 @@ edition = "2024" [dependencies] indexmap = "2.12.0" +clap = { version = "4.0", features = ["derive"] } diff --git a/src/cli/args.rs b/src/cli/args.rs new file mode 100644 index 0000000..34a84e8 --- /dev/null +++ b/src/cli/args.rs @@ -0,0 +1,58 @@ +use clap::{Parser, Subcommand}; +use crate::cli::Source; +use std::io::{IsTerminal, Read, stdin}; + +#[derive(Parser)] +#[command(name = "envmn")] +#[command(about = "Environment manager for .env-style files")] +pub struct Args { + /// Display the current version + #[arg(short, long)] + pub version: bool, + + #[command(subcommand)] + pub command: Option, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Check for syntax and linting errors + Lint { + /// File to lint (defaults to .env) + file: Option, + }, + /// Pretty-format the file + Format { + /// File to format (defaults to .env) + file: Option, + }, + /// List all environment blocks in the file + List { + /// File to list blocks from (defaults to .env) + file: Option, + }, + /// Reorder the file by moving the specified block down + Pick { + /// Block name to move + block: String, + /// File to modify (defaults to .env) + file: Option, + }, +} + +impl Args { + pub fn parse_with_stdin() -> (Self, Option) { + let stdin_input = { + let mut buffer = String::new(); + if !stdin().is_terminal() { + match stdin().read_to_string(&mut buffer) { + Ok(_) => Some(Source::StdIn(buffer)), + Err(_) => None, + } + } else { + None + } + }; + (Self::parse(), stdin_input) + } +} \ No newline at end of file diff --git a/src/cli/cli.rs b/src/cli/cli.rs index 2fcd1f0..eb0f4f0 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -1,45 +1,70 @@ -use crate::cli::CliCmd; -use crate::cli::{Commands, FormatCmd, HelpCmd, LintCmd, ListCmd, PickCmd, Source, VersionCmd}; +use crate::cli::{Source, args::{Args, Commands as ArgCommands}}; +use crate::cli::constants::DEFAULT_FILE; use crate::error::{CliErrors, Error}; -use crate::try_parse_cmd; -use std::env; -use std::io::{IsTerminal, Read, stdin}; -use std::ops::Deref; #[derive(Clone, Debug)] pub struct Cli { pub input: Option, - pub command: Option, + pub command: Commands, +} + +#[derive(Clone, Debug)] +pub enum Commands { + Version { name: String, version: String }, + Lint, + Format, + List, + Pick { block_name: String }, } impl Cli { pub fn init() -> Result { - let params = env::args().skip(1).collect::>(); - let stdin_input = { - let mut buffer = String::new(); - if !stdin().is_terminal() { - match stdin().read_to_string(&mut buffer) { - Ok(_) => Some(Source::StdIn(buffer)), - Err(_) => None, - } - } else { - None - } - }; - if stdin_input.is_some() && params.is_empty() { - return Err(Error::CliError(CliErrors::NoOperationFound)); + let (args, stdin_input) = Args::parse_with_stdin(); + + if args.version { + return Ok(Cli { + input: None, + command: Commands::Version { + name: env!("CARGO_PKG_NAME").to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }, + }); } - try_parse_cmd!(VersionCmd, params.deref(), stdin_input); - try_parse_cmd!(HelpCmd, params.deref(), stdin_input); - try_parse_cmd!(LintCmd, params.deref(), stdin_input); - try_parse_cmd!(FormatCmd, params.deref(), stdin_input); - try_parse_cmd!(ListCmd, params.deref(), stdin_input); - try_parse_cmd!(PickCmd, params.deref(), stdin_input); - match params.first() { - Some(param) => Err(Error::CliError(CliErrors::UnknownCommand( - param.to_string(), - ))), - None => Err(Error::CliError(CliErrors::NoOperationFound)), + + let Some(command) = args.command else { + return Err(Error::CliError(CliErrors::NoOperationFound)); + }; + + let (command, input) = match command { + + ArgCommands::Lint { file } => ( + Commands::Lint, + Some(Self::resolve_input(file, stdin_input)) + ), + ArgCommands::Format { file } => ( + Commands::Format, + Some(Self::resolve_input(file, stdin_input)) + ), + ArgCommands::List { file } => ( + Commands::List, + Some(Self::resolve_input(file, stdin_input)) + ), + ArgCommands::Pick { block, file } => ( + Commands::Pick { block_name: block }, + Some(Self::resolve_input(file, stdin_input)) + ), + }; + + Ok(Cli { input, command }) + } + + fn resolve_input(file: Option, stdin_input: Option) -> Source { + if let Some(file_name) = file { + Source::FileName(file_name) + } else if let Some(input) = stdin_input { + input + } else { + Source::FileName(DEFAULT_FILE.to_string()) } } -} +} \ No newline at end of file diff --git a/src/cli/commands/cli_cmd.rs b/src/cli/commands/cli_cmd.rs deleted file mode 100644 index a1b1dcf..0000000 --- a/src/cli/commands/cli_cmd.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::cli::{Cli, Source}; -use crate::error::Error; - -pub trait CliCmd { - fn try_from(cmd: T, stdin_input: Option) -> Result; -} diff --git a/src/cli/commands/format.rs b/src/cli/commands/format.rs deleted file mode 100644 index 3057ab3..0000000 --- a/src/cli/commands/format.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::cli::CliCmd; -use crate::cli::constants::DEFAULT_FILE; -use crate::cli::{Cli, Commands, Source}; -use crate::error::Error; - -const COMMAND_NAME: &str = "format"; - -#[derive(Clone, Debug)] -pub struct FormatCmd { - file_name: Option, -} - -impl FormatCmd { - pub fn try_from(params: &[String]) -> Result, Error> { - let mut params_iter = params.iter(); - let cmd_token = match params_iter.next() { - Some(cmd_name) if cmd_name == COMMAND_NAME => Some(cmd_name), - _ => None, - }; - if cmd_token.is_none() { - return Ok(None); - } - let (_, file_name) = match params_iter.next() { - Some(file_name) => (cmd_token.unwrap(), Some(file_name)), - None => (cmd_token.unwrap(), None), - }; - match file_name { - Some(file_name) => Ok(Some(FormatCmd { - file_name: Some(file_name.to_string()), - })), - None => Ok(Some(FormatCmd { file_name: None })), - } - } -} - -impl CliCmd for Cli { - fn try_from(cmd: FormatCmd, stdin_input: Option) -> Result { - Ok(Cli { - input: { - if let Some(file_name) = cmd.clone().file_name { - Some(Source::FileName(file_name.to_string())) - } else if let Some(input) = stdin_input { - Some(input) - } else { - Some(Source::FileName(DEFAULT_FILE.to_string())) - } - }, - command: Some(Commands::FormatCmd), - }) - } -} diff --git a/src/cli/commands/help.rs b/src/cli/commands/help.rs deleted file mode 100644 index d9569c7..0000000 --- a/src/cli/commands/help.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::cli::CliCmd; -use crate::cli::{Cli, Commands, Source}; -use crate::error::Error; - -const COMMAND_NAME: &str = "help"; - -#[derive(Clone, Debug)] -pub struct HelpCmd; - -impl HelpCmd { - pub fn try_from(params: &[String]) -> Result, Error> { - let mut params_iter = params.iter(); - let cmd_token = match params_iter.next() { - Some(cmd_name) if cmd_name == COMMAND_NAME => Some(cmd_name), - _ => None, - }; - if cmd_token.is_none() { - return Ok(None); - } - Ok(Some(HelpCmd)) - } -} - -impl CliCmd for Cli { - fn try_from(_cmd: HelpCmd, _stdin_input: Option) -> Result { - Ok(Cli { - input: None, - command: Some(Commands::HelpCmd), - }) - } -} diff --git a/src/cli/commands/lint.rs b/src/cli/commands/lint.rs deleted file mode 100644 index cf1bf6c..0000000 --- a/src/cli/commands/lint.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::cli::CliCmd; -use crate::cli::constants::DEFAULT_FILE; -use crate::cli::{Cli, Commands, Source}; -use crate::error::Error; - -const COMMAND_NAME: &str = "lint"; - -#[derive(Clone, Debug)] -pub struct LintCmd { - file_name: Option, -} - -impl LintCmd { - pub fn try_from(params: &[String]) -> Result, Error> { - let mut params_iter = params.iter(); - let cmd_token = match params_iter.next() { - Some(cmd_name) if cmd_name == COMMAND_NAME => Some(cmd_name), - _ => None, - }; - if cmd_token.is_none() { - return Ok(None); - } - let (_, file_name) = match params_iter.next() { - Some(file_name) => (cmd_token.unwrap(), Some(file_name)), - None => (cmd_token.unwrap(), None), - }; - match file_name { - Some(file_name) => Ok(Some(LintCmd { - file_name: Some(file_name.to_string()), - })), - None => Ok(Some(LintCmd { file_name: None })), - } - } -} - -impl CliCmd for Cli { - fn try_from(cmd: LintCmd, stdin_input: Option) -> Result { - Ok(Cli { - input: { - if let Some(file_name) = cmd.clone().file_name { - Some(Source::FileName(file_name.to_string())) - } else if let Some(input) = stdin_input { - Some(input) - } else { - Some(Source::FileName(DEFAULT_FILE.to_string())) - } - }, - command: Some(Commands::LintCmd), - }) - } -} diff --git a/src/cli/commands/list.rs b/src/cli/commands/list.rs deleted file mode 100644 index 8f17639..0000000 --- a/src/cli/commands/list.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::cli::CliCmd; -use crate::cli::constants::DEFAULT_FILE; -use crate::cli::{Cli, Commands, Source}; -use crate::error::Error; - -const COMMAND_NAME: &str = "list"; - -#[derive(Clone, Debug)] -pub struct ListCmd { - file_name: Option, -} - -impl ListCmd { - pub fn try_from(params: &[String]) -> Result, Error> { - let mut params_iter = params.iter(); - let cmd_token = match params_iter.next() { - Some(cmd_name) if cmd_name == COMMAND_NAME => Some(cmd_name), - _ => None, - }; - if cmd_token.is_none() { - return Ok(None); - } - let (_, file_name) = match params_iter.next() { - Some(file_name) => (cmd_token.unwrap(), Some(file_name)), - None => (cmd_token.unwrap(), None), - }; - match file_name { - Some(file_name) => Ok(Some(ListCmd { - file_name: Some(file_name.to_string()), - })), - None => Ok(Some(ListCmd { file_name: None })), - } - } -} - -impl CliCmd for Cli { - fn try_from(cmd: ListCmd, stdin_input: Option) -> Result { - Ok(Cli { - input: { - if let Some(file_name) = cmd.clone().file_name { - Some(Source::FileName(file_name.to_string())) - } else if let Some(input) = stdin_input { - Some(input) - } else { - Some(Source::FileName(DEFAULT_FILE.to_string())) - } - }, - command: Some(Commands::ListCmd), - }) - } -} diff --git a/src/cli/commands/mod.rs b/src/cli/commands/mod.rs deleted file mode 100644 index c1b75bd..0000000 --- a/src/cli/commands/mod.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::cli::{PickCmd, VersionCmd}; - -pub mod pick; -pub mod list; -pub mod format; -pub mod lint; -pub mod version; -pub mod help; -pub mod cli_cmd; - -#[derive(Clone, Debug)] -pub enum Commands { - PickCmd(PickCmd), - FormatCmd, - ListCmd, - LintCmd, - VersionCmd(VersionCmd), - HelpCmd, -} - -/* -/************************** - CLI flags (short, long) -**************************/ -pub type FlagType = (&'static str, &'static str); - -/******************* - Flags Collection -*******************/ -pub const ALL_FLAGS: [FlagType; 2] = [FILE_FLAG, LIST_FLAG]; - -/**************** - List of Flags -****************/ -pub const FILE_FLAG: FlagType = ("f", "file"); -pub const LIST_FLAG: FlagType = ("l", "list"); -*/ diff --git a/src/cli/commands/pick.rs b/src/cli/commands/pick.rs deleted file mode 100644 index 8f3de73..0000000 --- a/src/cli/commands/pick.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::cli::CliCmd; -use crate::cli::constants::DEFAULT_FILE; -use crate::cli::{Cli, Commands, Source}; -use crate::error::{CliErrors, Error}; - -const COMMAND_NAME: &str = "pick"; - -#[derive(Clone, Debug)] -pub struct PickCmd { - pub block_name: String, - file_name: Option, -} - -impl PickCmd { - pub fn try_from(params: &[String]) -> Result, Error> { - let mut params_iter = params.iter(); - let cmd_token = match params_iter.next() { - Some(cmd_name) if cmd_name == COMMAND_NAME => Some(cmd_name), - _ => None, - }; - if cmd_token.is_none() { - return Ok(None); - } - let (_, block_name) = match params_iter.next() { - Some(block_name) => (cmd_token.unwrap(), Some(block_name)), - None => (cmd_token.unwrap(), None), - }; - if block_name.is_none() { - return Err(Error::CliError(CliErrors::FailedToParseArgs( - "Provide a block name", - ))); - } - let (block_name, file_name) = match params_iter.next() { - Some(file_name) => (block_name.unwrap(), Some(file_name)), - None => (block_name.unwrap(), None), - }; - match file_name { - Some(file_name) => Ok(Some(PickCmd { - block_name: block_name.to_string(), - file_name: Some(file_name.to_string()), - })), - None => Ok(Some(PickCmd { - block_name: block_name.to_string(), - file_name: None, - })), - } - } -} - -impl CliCmd for Cli { - fn try_from(cmd: PickCmd, stdin_input: Option) -> Result { - Ok(Cli { - input: { - if let Some(file_name) = cmd.clone().file_name { - Some(Source::FileName(file_name.to_string())) - } else if let Some(input) = stdin_input { - Some(input) - } else { - Some(Source::FileName(DEFAULT_FILE.to_string())) - } - }, - command: Some(Commands::PickCmd(cmd)), - }) - } -} diff --git a/src/cli/commands/version.rs b/src/cli/commands/version.rs deleted file mode 100644 index 93cf5c3..0000000 --- a/src/cli/commands/version.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::cli::CliCmd; -use crate::cli::{Cli, Commands, Source}; -use crate::error::Error; - -const COMMAND_NAME: &str = "version"; - -#[derive(Clone, Debug)] -pub struct VersionCmd { - pub name: String, - pub version: String, -} - -impl VersionCmd { - pub fn try_from(params: &[String]) -> Result, Error> { - let mut params_iter = params.iter(); - let cmd_token = match params_iter.next() { - Some(cmd_name) if cmd_name == COMMAND_NAME => Some(cmd_name), - _ => None, - }; - if cmd_token.is_none() { - return Ok(None); - } - let name = env!("CARGO_PKG_NAME"); - let version = env!("CARGO_PKG_VERSION"); - Ok(Some(VersionCmd { - name: name.to_string(), - version: version.to_string(), - })) - } -} - -impl CliCmd for Cli { - fn try_from(cmd: VersionCmd, _stdin_input: Option) -> Result { - Ok(Cli { - input: None, - command: Some(Commands::VersionCmd(cmd)), - }) - } -} diff --git a/src/cli/macros/mod.rs b/src/cli/macros/mod.rs deleted file mode 100644 index 84a6d7f..0000000 --- a/src/cli/macros/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[macro_export] -macro_rules! try_parse_cmd { - ($cmd_type:ty, $params:expr, $stdin_input:expr) => { - if let Some(cmd) = <$cmd_type>::try_from($params)? { - return >::try_from(cmd, $stdin_input); - } - }; -} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 8fa65dd..bb92947 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,16 +1,8 @@ mod cli; -mod commands; mod constants; -mod macros; mod source; +pub mod args; pub use cli::Cli; -pub use commands::Commands; -pub use commands::cli_cmd::CliCmd; -pub use commands::format::FormatCmd; -pub use commands::help::HelpCmd; -pub use commands::lint::LintCmd; -pub use commands::list::ListCmd; -pub use commands::pick::PickCmd; -pub use commands::version::VersionCmd; +pub use cli::Commands; pub use source::Source; diff --git a/src/main.rs b/src/main.rs index 40d733e..e37c33a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,18 +23,12 @@ fn main() { let result = match &cli { Cli { input: None, - command: Some(Commands::VersionCmd(version_cmd)), + command: Commands::Version { name, version }, } => { - Engine::process_version_cmd(version_cmd.clone()); - exit(0); - } - Cli { - input: None, - command: Some(Commands::HelpCmd), - } => { - Engine::process_help_cmd(); + Engine::process_version_cmd(name, version); exit(0); } + Cli { input: None, .. } => { eprintln!("{}", CliErrors::NoInputFound); exit(1); diff --git a/src/parser/engine/commands/pick.rs b/src/parser/engine/commands/pick.rs index 0244a56..82b5413 100644 --- a/src/parser/engine/commands/pick.rs +++ b/src/parser/engine/commands/pick.rs @@ -1,12 +1,12 @@ -use crate::cli::{PickCmd, Source}; +use crate::cli::Source; use crate::error::CliErrors; use crate::parser::engine::Engine; use std::fs; use std::process::exit; impl Engine { - pub fn process_pick_cmd(mut self, pick_cmd: PickCmd) { - match self.document.pick(pick_cmd.block_name.as_str()) { + pub fn process_pick_cmd(mut self, block_name: String) { + match self.document.pick(block_name.as_str()) { Ok(document) => { let Some(input) = &self.cli.input else { eprintln!("{}", CliErrors::NoInputFound); diff --git a/src/parser/engine/commands/version.rs b/src/parser/engine/commands/version.rs index bb9fb3c..4b03aae 100644 --- a/src/parser/engine/commands/version.rs +++ b/src/parser/engine/commands/version.rs @@ -1,8 +1,7 @@ -use crate::cli::VersionCmd; use crate::parser::engine::Engine; impl Engine { - pub fn process_version_cmd(version_cmd: VersionCmd) { - println!("{0} version {1}", version_cmd.name, version_cmd.version); + pub fn process_version_cmd(name: &str, version: &str) { + println!("{0} version {1}", name, version); } } diff --git a/src/parser/engine/mod.rs b/src/parser/engine/mod.rs index f5ff0ff..6f40c2a 100644 --- a/src/parser/engine/mod.rs +++ b/src/parser/engine/mod.rs @@ -15,10 +15,10 @@ impl Engine { } pub fn process(self) -> Result<(), Error> { match self.cli.command.clone() { - Some(Commands::LintCmd) => Ok(()), - Some(Commands::ListCmd) => Ok(self.process_list_cmd()), - Some(Commands::FormatCmd) => Ok(self.process_format_cmd()), - Some(Commands::PickCmd(pick_cmd)) => Ok(self.process_pick_cmd(pick_cmd)), + Commands::Lint => Ok(()), + Commands::List => Ok(self.process_list_cmd()), + Commands::Format => Ok(self.process_format_cmd()), + Commands::Pick { block_name } => Ok(self.process_pick_cmd(block_name)), _ => Err(Error::CliError(CliErrors::NoOperationFound)), } } diff --git a/src/parser/tokens/block.rs b/src/parser/tokens/block.rs index 45c9cd0..85949ed 100644 --- a/src/parser/tokens/block.rs +++ b/src/parser/tokens/block.rs @@ -105,7 +105,7 @@ mod tests { let mut block = Block::new("test"); block.add_variable(Variable::new("KEY", "value")).unwrap(); assert_eq!(block.lines.len(), 1); - assert!(block.lines.first().unwrap().is_variable()); + assert!(matches!(block.lines.first().unwrap(), Line::Variable(_))); } #[test] @@ -113,7 +113,7 @@ mod tests { let mut block = Block::new("test"); block.add_comment("test comment"); assert_eq!(block.lines.len(), 1); - assert!(block.lines.first().unwrap().is_comment()); + assert!(matches!(block.lines.first().unwrap(), Line::Comment(_))); } #[test] diff --git a/src/parser/tokens/line.rs b/src/parser/tokens/line.rs index 7d86952..f950614 100644 --- a/src/parser/tokens/line.rs +++ b/src/parser/tokens/line.rs @@ -7,15 +7,6 @@ pub enum Line { Variable(Variable), } -impl Line { - pub fn is_comment(&self) -> bool { - matches!(self, Line::Comment(_)) - } - pub fn is_variable(&self) -> bool { - matches!(self, Line::Variable(_)) - } -} - impl Display for Line { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -53,16 +44,4 @@ mod tests { let line2 = Line::Comment("comment".to_string()); assert_ne!(line1, line2); } - - #[test] - fn is_comment() { - let line = Line::Comment("comment".to_string()); - assert!(line.is_comment()); - } - - #[test] - fn is_variable() { - let line = Line::Variable(Variable::new("KEY", "value")); - assert!(line.is_variable()); - } } From 6bd33354d6d02f979368033e64490ce53087ffd8 Mon Sep 17 00:00:00 2001 From: devark28 Date: Fri, 14 Nov 2025 01:04:55 +0200 Subject: [PATCH 2/9] feat: improve CLI UX with enhanced help and version flag - Move version from subcommand to --version/-v flag - Add detailed help with input modes and usage examples - Make subcommands required to trigger help when no args provided - Remove custom help command in favor of clap's built-in help system --- src/cli/args.rs | 23 ++++++++++++++++++-- src/cli/cli.rs | 10 +++------ src/main.rs | 2 +- src/parser/engine/commands/help.rs | 34 ------------------------------ src/parser/engine/commands/mod.rs | 22 ++----------------- 5 files changed, 27 insertions(+), 64 deletions(-) delete mode 100644 src/parser/engine/commands/help.rs diff --git a/src/cli/args.rs b/src/cli/args.rs index 34a84e8..a442f28 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -5,13 +5,26 @@ use std::io::{IsTerminal, Read, stdin}; #[derive(Parser)] #[command(name = "envmn")] #[command(about = "Environment manager for .env-style files")] +#[command(after_help = "Input modes: + - If data is piped in, envmn reads from standard input and writes to standard output. + - If both a pipe and a file are provided, the piped input takes priority. + - If no file is provided, envmn assumes a `.env` file exists in the current directory (for convenience). + - When a file path is provided (or .env is assumed), envmn reads from (and edits, if a file was passed) the file directly. + +Examples: + cat .env | envmn lint + envmn format .env + envmn pick database_block .env > out.env + envmn --version + +For more information, visit: https://github.com/devark28/envmn")] pub struct Args { /// Display the current version #[arg(short, long)] pub version: bool, #[command(subcommand)] - pub command: Option, + pub command: Commands, } #[derive(Subcommand)] @@ -53,6 +66,12 @@ impl Args { None } }; - (Self::parse(), stdin_input) + match Self::try_parse() { + Ok(args) => (args, stdin_input), + Err(err) => { + err.print().unwrap(); + std::process::exit(1); + } + } } } \ No newline at end of file diff --git a/src/cli/cli.rs b/src/cli/cli.rs index eb0f4f0..fb27bf7 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -1,6 +1,6 @@ -use crate::cli::{Source, args::{Args, Commands as ArgCommands}}; use crate::cli::constants::DEFAULT_FILE; -use crate::error::{CliErrors, Error}; +use crate::cli::{args::{Args, Commands as ArgCommands}, Source}; +use crate::error::Error; #[derive(Clone, Debug)] pub struct Cli { @@ -31,11 +31,7 @@ impl Cli { }); } - let Some(command) = args.command else { - return Err(Error::CliError(CliErrors::NoOperationFound)); - }; - - let (command, input) = match command { + let (command, input) = match args.command { ArgCommands::Lint { file } => ( Commands::Lint, diff --git a/src/main.rs b/src/main.rs index e37c33a..6969e4f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ fn main() { let cli = match Cli::init() { Ok(cli) => cli, Err(Error::CliError(CliErrors::NoOperationFound)) => { - Engine::process_help_cmd(); + eprintln!("{}", CliErrors::NoOperationFound); exit(1); } Err(error_type) => { diff --git a/src/parser/engine/commands/help.rs b/src/parser/engine/commands/help.rs deleted file mode 100644 index 30801a2..0000000 --- a/src/parser/engine/commands/help.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::parser::engine::Engine; - -impl Engine { - pub fn process_help_cmd() { - println!(" -envmn — environment manager for .env-style files - -Usage: - envmn [options] [file] - -Commands: - help Show this help message - version Display the current version - list List all environment blocks in the file - lint Check for syntax and linting errors - format Pretty-format the file - pick Reorder the file by moving the specified block down - -Input modes: - - If data is piped in, envmn reads from standard input and writes to standard output. - - If both a pipe and a file are provided, the piped input takes priority. - - If no file is provided, envmn assumes a `.env` file exists in the current directory (for convenience). - - When a file path is provided (or .env is assumed), envmn reads from (and edits, if a file was passed) the file directly. - -Examples: - cat .env | envmn lint - envmn format .env - envmn pick database_block .env > out.env - envmn version - -For more information, visit: https://github.com/devark28/envmn -"); - } -} diff --git a/src/parser/engine/commands/mod.rs b/src/parser/engine/commands/mod.rs index 4ff18e2..36a082a 100644 --- a/src/parser/engine/commands/mod.rs +++ b/src/parser/engine/commands/mod.rs @@ -1,22 +1,4 @@ -mod pick; -mod list; mod format; +mod list; +mod pick; mod version; -mod help; -/* -/************************** - CLI flags (short, long) -**************************/ -pub type FlagType = (&'static str, &'static str); - -/******************* - Flags Collection -*******************/ -pub const ALL_FLAGS: [FlagType; 2] = [FILE_FLAG, LIST_FLAG]; - -/**************** - List of Flags -****************/ -pub const FILE_FLAG: FlagType = ("f", "file"); -pub const LIST_FLAG: FlagType = ("l", "list"); -*/ From 5e7d70dc75683bb5aba10a9713e8e1b6bcb57505 Mon Sep 17 00:00:00 2001 From: devark28 Date: Fri, 14 Nov 2025 01:31:04 +0200 Subject: [PATCH 3/9] refactor: move modules under engine/command into engine to remove unnecessary nesting. --- src/parser/engine/commands/mod.rs | 4 ---- src/parser/engine/{commands => }/format.rs | 0 src/parser/engine/{commands => }/list.rs | 0 src/parser/engine/mod.rs | 5 ++++- src/parser/engine/{commands => }/pick.rs | 0 src/parser/engine/{commands => }/version.rs | 0 src/parser/parser.rs | 2 -- 7 files changed, 4 insertions(+), 7 deletions(-) delete mode 100644 src/parser/engine/commands/mod.rs rename src/parser/engine/{commands => }/format.rs (100%) rename src/parser/engine/{commands => }/list.rs (100%) rename src/parser/engine/{commands => }/pick.rs (100%) rename src/parser/engine/{commands => }/version.rs (100%) diff --git a/src/parser/engine/commands/mod.rs b/src/parser/engine/commands/mod.rs deleted file mode 100644 index 36a082a..0000000 --- a/src/parser/engine/commands/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod format; -mod list; -mod pick; -mod version; diff --git a/src/parser/engine/commands/format.rs b/src/parser/engine/format.rs similarity index 100% rename from src/parser/engine/commands/format.rs rename to src/parser/engine/format.rs diff --git a/src/parser/engine/commands/list.rs b/src/parser/engine/list.rs similarity index 100% rename from src/parser/engine/commands/list.rs rename to src/parser/engine/list.rs diff --git a/src/parser/engine/mod.rs b/src/parser/engine/mod.rs index 6f40c2a..54e1859 100644 --- a/src/parser/engine/mod.rs +++ b/src/parser/engine/mod.rs @@ -1,4 +1,7 @@ -mod commands; +mod format; +mod list; +mod pick; +mod version; use crate::cli::{Cli, Commands}; use crate::error::{CliErrors, Error}; diff --git a/src/parser/engine/commands/pick.rs b/src/parser/engine/pick.rs similarity index 100% rename from src/parser/engine/commands/pick.rs rename to src/parser/engine/pick.rs diff --git a/src/parser/engine/commands/version.rs b/src/parser/engine/version.rs similarity index 100% rename from src/parser/engine/commands/version.rs rename to src/parser/engine/version.rs diff --git a/src/parser/parser.rs b/src/parser/parser.rs index 52a15ae..a046589 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -71,8 +71,6 @@ impl Parser { }; validate_variable_name(idx as u16, variable.key.deref())?; self.get_working_block_mut()?.add_variable(variable)?; - } else { - // TODO: handle newlines (but they are reconstructed for formatting) } } Ok(self.document) From 9a1232d0a58098cb4e2d891fc99b2cea5046a60f Mon Sep 17 00:00:00 2001 From: devark28 Date: Fri, 14 Nov 2025 02:08:17 +0200 Subject: [PATCH 4/9] refactor: remove unused code and tests related to those snippets --- src/error/access.rs | 5 ----- src/error/cli.rs | 9 --------- src/error/naming.rs | 1 - src/error/parsing.rs | 5 ----- src/parser/parser.rs | 7 ------- src/parser/tokens/document.rs | 16 +--------------- 6 files changed, 1 insertion(+), 42 deletions(-) diff --git a/src/error/access.rs b/src/error/access.rs index 8c92133..336a0cd 100644 --- a/src/error/access.rs +++ b/src/error/access.rs @@ -1,10 +1,8 @@ use std::fmt::{Display, Formatter}; -#[allow(unused)] #[derive(Debug)] pub enum AccessErrors { FileError(String, String), - VariableNotFound(String, String), BlockNotFound(String), DefaultBlockNotMovable, } @@ -12,9 +10,6 @@ pub enum AccessErrors { impl Display for AccessErrors { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - AccessErrors::VariableNotFound(variable, block_name) => { - write!(f, "Variable '{variable}' not found in block '{block_name}'") - } AccessErrors::FileError(file_path, error) => { write!(f, "Error reading file '{file_path}': {error}") } diff --git a/src/error/cli.rs b/src/error/cli.rs index de7d117..49a4000 100644 --- a/src/error/cli.rs +++ b/src/error/cli.rs @@ -1,26 +1,17 @@ use std::fmt::{Display, Formatter}; -#[allow(unused)] #[derive(Debug)] pub enum CliErrors { - UnknownCommand(String), NoOperationFound, - FailedToParseArgs(&'static str), NoInputFound, } impl Display for CliErrors { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - CliErrors::UnknownCommand(param_name) => { - write!(f, "Parameter '{param_name}' is not recognized") - } CliErrors::NoOperationFound => { write!(f, "No operation found") } - CliErrors::FailedToParseArgs(message) => { - write!(f, "{message}") - } CliErrors::NoInputFound => { write!(f, "No input found") } diff --git a/src/error/naming.rs b/src/error/naming.rs index 7f6d77e..2ddb80b 100644 --- a/src/error/naming.rs +++ b/src/error/naming.rs @@ -1,6 +1,5 @@ use std::fmt::{Display, Formatter}; -#[allow(unused)] #[derive(Debug)] pub enum NamingErrors { BlockNameEmpty, diff --git a/src/error/parsing.rs b/src/error/parsing.rs index 8a97782..1edbec3 100644 --- a/src/error/parsing.rs +++ b/src/error/parsing.rs @@ -1,6 +1,5 @@ use std::fmt::{Display, Formatter}; -#[allow(unused)] #[derive(Debug)] pub enum ParsingErrors { MissingEqSeparator(u16), @@ -8,7 +7,6 @@ pub enum ParsingErrors { EmptyInput, BlockNeverOpened(u16), ReservedWord(u16, String), - BrokenValidator(String), DuplicateBlock(String), DuplicateVariable(String, String), } @@ -39,9 +37,6 @@ impl Display for ParsingErrors { ParsingErrors::ReservedWord(line, name) => { write!(f, "Line {0}: You can not use keyword '{name}'", line + 1) } - ParsingErrors::BrokenValidator(validator_name) => { - write!(f, "Validator '{validator_name}' is broken") - } ParsingErrors::DuplicateBlock(name) => { write!(f, "Duplicate block '{name}' found") } diff --git a/src/parser/parser.rs b/src/parser/parser.rs index a046589..76ff5e3 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -87,14 +87,7 @@ impl Parser { } } -#[allow(unused)] impl Parser { - fn get_working_block(&mut self) -> Result<&Block, Error> { - match &self.current_block { - Some(block) => Ok(block), - None => self.document.get_default_block(), - } - } fn get_working_block_mut(&mut self) -> Result<&mut Block, Error> { match self.current_block.as_mut() { Some(block) => Ok(block), diff --git a/src/parser/tokens/document.rs b/src/parser/tokens/document.rs index 80266dd..d01a02c 100644 --- a/src/parser/tokens/document.rs +++ b/src/parser/tokens/document.rs @@ -44,14 +44,6 @@ impl Document { } impl Document { - pub fn get_default_block(&mut self) -> Result<&Block, Error> { - match self.blocks.first() { - Some(default_block) => Ok(default_block), - None => Err(Error::AccessError(AccessErrors::BlockNotFound( - DEFAULT_BLOCK_NAME.to_string(), - ))), - } - } pub fn get_default_block_mut(&mut self) -> Result<&mut Block, Error> { match self.blocks.get_index_mut2(0) { Some(default_block) => Ok(default_block), @@ -159,12 +151,6 @@ mod tests { index.unwrap(); } - #[test] - fn get_default_block() { - let mut doc = Document::new(); - assert_eq!(doc.get_default_block().unwrap().name, DEFAULT_BLOCK_NAME); - } - #[test] fn get_default_block_mut() { let mut doc = Document::new(); @@ -177,7 +163,7 @@ mod tests { #[test] fn default_always_exists_and_first() { let mut doc = Document::new(); - assert!(doc.get_default_block().is_ok()); + doc.get_default_block_mut().unwrap(); doc.add_block(Block::new("test")).unwrap(); doc.add_block(Block::new("test2")).unwrap(); assert_eq!(doc.blocks.first().unwrap().name, DEFAULT_BLOCK_NAME); From c70ca1d0d22239ae9f215da4918765f55f5b588d Mon Sep 17 00:00:00 2001 From: devark28 Date: Fri, 14 Nov 2025 12:20:15 +0200 Subject: [PATCH 5/9] Revert to fix "feat: improve CLI UX with enhanced help and version flag" This reverts commit 6bd33354d6d02f979368033e64490ce53087ffd8. # Conflicts: # src/parser/engine/commands/mod.rs # src/parser/engine/help.rs --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/cli/args.rs | 12 +++--------- src/cli/cli.rs | 45 +++++++++++++++++++++++++++------------------ 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1cbbcf0..ccbab3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,7 +100,7 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "envmn" -version = "0.2.6" +version = "0.2.7" dependencies = [ "clap", "indexmap", diff --git a/Cargo.toml b/Cargo.toml index 95a8848..47d2b96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "envmn" -version = "0.2.6" +version = "0.2.7" edition = "2024" [dependencies] diff --git a/src/cli/args.rs b/src/cli/args.rs index a442f28..b211b76 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -24,11 +24,11 @@ pub struct Args { pub version: bool, #[command(subcommand)] - pub command: Commands, + pub command: Option, } #[derive(Subcommand)] -pub enum Commands { +pub enum ArgCommands { /// Check for syntax and linting errors Lint { /// File to lint (defaults to .env) @@ -66,12 +66,6 @@ impl Args { None } }; - match Self::try_parse() { - Ok(args) => (args, stdin_input), - Err(err) => { - err.print().unwrap(); - std::process::exit(1); - } - } + (Self::parse(), stdin_input) } } \ No newline at end of file diff --git a/src/cli/cli.rs b/src/cli/cli.rs index fb27bf7..01dcc45 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -1,6 +1,11 @@ use crate::cli::constants::DEFAULT_FILE; -use crate::cli::{args::{Args, Commands as ArgCommands}, Source}; -use crate::error::Error; +use crate::cli::{ + Source, + args::{ArgCommands, Args}, +}; +use crate::error::{CliErrors, Error}; +use clap::CommandFactory; +use std::process::exit; #[derive(Clone, Debug)] pub struct Cli { @@ -20,7 +25,7 @@ pub enum Commands { impl Cli { pub fn init() -> Result { let (args, stdin_input) = Args::parse_with_stdin(); - + if args.version { return Ok(Cli { input: None, @@ -30,30 +35,34 @@ impl Cli { }, }); } - - let (command, input) = match args.command { - ArgCommands::Lint { file } => ( - Commands::Lint, - Some(Self::resolve_input(file, stdin_input)) - ), + let Some(command) = args.command else { + return match Args::command().print_long_help() { + Ok(_) => exit(1), + _ => Err(Error::CliError(CliErrors::NoOperationFound)), + }; + }; + + let (command, input) = match command { + ArgCommands::Lint { file } => { + (Commands::Lint, Some(Self::resolve_input(file, stdin_input))) + } ArgCommands::Format { file } => ( Commands::Format, - Some(Self::resolve_input(file, stdin_input)) - ), - ArgCommands::List { file } => ( - Commands::List, - Some(Self::resolve_input(file, stdin_input)) + Some(Self::resolve_input(file, stdin_input)), ), + ArgCommands::List { file } => { + (Commands::List, Some(Self::resolve_input(file, stdin_input))) + } ArgCommands::Pick { block, file } => ( Commands::Pick { block_name: block }, - Some(Self::resolve_input(file, stdin_input)) + Some(Self::resolve_input(file, stdin_input)), ), }; - + Ok(Cli { input, command }) } - + fn resolve_input(file: Option, stdin_input: Option) -> Source { if let Some(file_name) = file { Source::FileName(file_name) @@ -63,4 +72,4 @@ impl Cli { Source::FileName(DEFAULT_FILE.to_string()) } } -} \ No newline at end of file +} From f8c6db62ab57a0f1f79d7c24308e696e210f5e8f Mon Sep 17 00:00:00 2001 From: devark28 Date: Fri, 14 Nov 2025 15:09:23 +0200 Subject: [PATCH 6/9] feat: reimplement the version subcommand for backward compatibility --- src/cli/args.rs | 2 ++ src/cli/cli.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/cli/args.rs b/src/cli/args.rs index b211b76..696a477 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -51,6 +51,8 @@ pub enum ArgCommands { /// File to modify (defaults to .env) file: Option, }, + /// Display the current version + Version, } impl Args { diff --git a/src/cli/cli.rs b/src/cli/cli.rs index 01dcc45..abd41c4 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -58,6 +58,10 @@ impl Cli { Commands::Pick { block_name: block }, Some(Self::resolve_input(file, stdin_input)), ), + ArgCommands::Version => (Commands::Version { + name: env!("CARGO_PKG_NAME").to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }, None), }; Ok(Cli { input, command }) From d52e98f4544653c7d2316857d8d702fd44c7e892 Mon Sep 17 00:00:00 2001 From: devark28 Date: Fri, 14 Nov 2025 18:23:21 +0200 Subject: [PATCH 7/9] feat: add CLI integration tests and fix stdin priority handling - Add comprehensive integration test suite covering all CLI commands - Fix stdin input priority over file arguments in CLI parsing - Add tempfile dev dependency for test file management - Configure IntelliJ test source folder for tests directory --- .idea/envmn.iml | 1 + Cargo.lock | 106 ++++++++++++++++ Cargo.toml | 3 + src/cli/cli.rs | 20 +-- tests/cli_integration.rs | 265 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 388 insertions(+), 7 deletions(-) create mode 100644 tests/cli_integration.rs diff --git a/.idea/envmn.iml b/.idea/envmn.iml index cf84ae4..bbe0a70 100644 --- a/.idea/envmn.iml +++ b/.idea/envmn.iml @@ -3,6 +3,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index ccbab3d..9305e5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "clap" version = "4.5.51" @@ -104,6 +116,7 @@ version = "0.2.7" dependencies = [ "clap", "indexmap", + "tempfile", ] [[package]] @@ -112,6 +125,34 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "hashbrown" version = "0.16.0" @@ -140,6 +181,24 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -164,6 +223,25 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "strsim" version = "0.11.1" @@ -181,6 +259,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -193,6 +284,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -207,3 +307,9 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" diff --git a/Cargo.toml b/Cargo.toml index 47d2b96..04b128b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,6 @@ edition = "2024" [dependencies] indexmap = "2.12.0" clap = { version = "4.0", features = ["derive"] } + +[dev-dependencies] +tempfile = "3.0" diff --git a/src/cli/cli.rs b/src/cli/cli.rs index abd41c4..bf2d9eb 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -58,20 +58,26 @@ impl Cli { Commands::Pick { block_name: block }, Some(Self::resolve_input(file, stdin_input)), ), - ArgCommands::Version => (Commands::Version { - name: env!("CARGO_PKG_NAME").to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - }, None), + ArgCommands::Version => ( + Commands::Version { + name: env!("CARGO_PKG_NAME").to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }, + None, + ), }; Ok(Cli { input, command }) } fn resolve_input(file: Option, stdin_input: Option) -> Source { - if let Some(file_name) = file { + if let Some(input) = stdin_input + && let Source::StdIn(stdin) = input + && !stdin.is_empty() + { + Source::StdIn(stdin) + } else if let Some(file_name) = file { Source::FileName(file_name) - } else if let Some(input) = stdin_input { - input } else { Source::FileName(DEFAULT_FILE.to_string()) } diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs new file mode 100644 index 0000000..6af73ab --- /dev/null +++ b/tests/cli_integration.rs @@ -0,0 +1,265 @@ +use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::PathBuf; +use std::process::Command; + +/// Helper to get the compiled binary path +fn get_binary_path() -> PathBuf { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("target"); + path.push("debug"); + path.push("envmn"); + path +} + +/// Helper to create a temporary test file +fn create_test_env_file(content: &str) -> tempfile::NamedTempFile { + let mut file = tempfile::NamedTempFile::new().unwrap(); + file.write_all(content.as_bytes()).unwrap(); + file.flush().unwrap(); + file.seek(SeekFrom::Start(0)).unwrap(); + file +} + +#[test] +fn test_version_command() { + let output = Command::new(get_binary_path()) + .arg("version") + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("envmn version")); + assert!(stdout.contains("0.2.7")); +} + +#[test] +fn test_version_flag() { + let output = Command::new(get_binary_path()) + .arg("--version") + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("envmn version")); +} + +#[test] +fn test_no_command_shows_help() { + let output = Command::new(get_binary_path()) + .output() + .expect("Failed to execute command"); + + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stderr.contains("Environment manager") || stdout.contains("Environment manager")); +} + +#[test] +fn test_list_command_with_file() { + let test_content = r#"# Basic config +API_URL=https://api.example.com + +#@ database_block +DB_HOST=localhost +DB_PORT=5432 +## + +#@ api_block +API_KEY=secret +## +"#; + + let temp_file = create_test_env_file(test_content); + + let output = Command::new(get_binary_path()) + .arg("list") + .arg(temp_file.path()) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("database_block")); + assert!(stdout.contains("api_block")); +} + +#[test] +fn test_format_command_with_file() { + let test_content = r#"KEY=value + + +#@ block + +VAR=test +## + +"#; + let result_content = r#"KEY=value + +#@ block +VAR=test +## +"#; + + let mut temp_file = create_test_env_file(test_content); + + let output = Command::new(get_binary_path()) + .arg("format") + .arg(temp_file.path()) + .stdin(std::process::Stdio::null()) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let mut buffer = String::new(); + temp_file.read_to_string(&mut buffer).unwrap(); + assert_eq!(buffer, result_content); +} + +#[test] +fn test_pick_command() { + let test_content = r#"DEFAULT_VAR=value + +#@ database_block +DB_HOST=localhost +## + +#@ api_block +API_KEY=secret +## +"#; + + let temp_file = create_test_env_file(test_content); + + let output = Command::new(get_binary_path()) + .arg("pick") + .arg("database_block") + .arg(temp_file.path()) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + + // Verify that database_block appears after api_block in output (moved to bottom) + if let (Some(db_pos), Some(api_pos)) = ( + stdout.find("#@ database_block"), + stdout.find("#@ api_block"), + ) { + assert!( + db_pos > api_pos, + "database_block should come after api_block when picked" + ); + } +} + +#[test] +fn test_lint_command_success() { + let test_content = r#"KEY=value + +#@ block +VAR=test +## +"#; + + let temp_file = create_test_env_file(test_content); + + let output = Command::new(get_binary_path()) + .arg("lint") + .arg(temp_file.path()) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); +} + +#[test] +fn test_format_command_with_stdin() { + let test_content = "KEY=value\n\n#@ block\nVAR=test\n##"; + + let mut child = Command::new(get_binary_path()) + .arg("format") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .spawn() + .expect("Failed to spawn command"); + + { + let stdin = child.stdin.as_mut().expect("Failed to open stdin"); + stdin + .write_all(test_content.as_bytes()) + .expect("Failed to write to stdin"); + } + + let output = child.wait_with_output().expect("Failed to read output"); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("KEY=value")); +} + +#[test] +fn test_stdin_takes_priority_over_file() { + let file_content = "FILE_VAR=from_file"; + let stdin_content = "STDIN_VAR=from_stdin"; + + let temp_file = create_test_env_file(file_content); + + let mut child = Command::new(get_binary_path()) + .arg("format") + .arg(temp_file.path()) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .spawn() + .expect("Failed to spawn command"); + + { + let stdin = child.stdin.as_mut().expect("Failed to open stdin"); + stdin + .write_all(stdin_content.as_bytes()) + .expect("Failed to write to stdin"); + } + + let output = child.wait_with_output().expect("Failed to read output"); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("STDIN_VAR")); +} + +#[test] +fn test_invalid_command() { + let output = Command::new(get_binary_path()) + .arg("invalid_command") + .output() + .expect("Failed to execute command"); + + assert!(!output.status.success()); +} + +#[test] +fn test_pick_nonexistent_block() { + let test_content = r#"KEY=value + +#@ existing_block +VAR=test +## +"#; + + let temp_file = create_test_env_file(test_content); + + let output = Command::new(get_binary_path()) + .arg("pick") + .arg("nonexistent_block") + .arg(temp_file.path()) + .output() + .expect("Failed to execute command"); + + // Should handle gracefully (either error or no-op) + let _stderr = String::from_utf8_lossy(&output.stderr); + let _stdout = String::from_utf8_lossy(&output.stdout); + // Test passes if it doesn't crash +} From abd9cae2f380a36494c0f233cebeee744bdb6b38 Mon Sep 17 00:00:00 2001 From: devark28 Date: Fri, 14 Nov 2025 19:01:14 +0200 Subject: [PATCH 8/9] refactor: split integration tests into focused modules - Extract shared test utilities to tests/common/mod.rs - Split monolithic cli_integration.rs into command-specific files: - version_tests.rs: version and help commands - format_tests.rs: format command with file/stdin - list_tests.rs: list command - pick_tests.rs: pick command and error cases - lint_tests.rs: lint command - Improve test naming consistency (remove "test_" prefix) - Better error assertion in pick_nonexistent_block test --- tests/cli_integration.rs | 272 +-------------------------------------- tests/common/mod.rs | 32 +++++ tests/format_tests.rs | 92 +++++++++++++ tests/lint_tests.rs | 23 ++++ tests/list_tests.rs | 32 +++++ tests/pick_tests.rs | 64 +++++++++ tests/version_tests.rs | 38 ++++++ 7 files changed, 288 insertions(+), 265 deletions(-) create mode 100644 tests/common/mod.rs create mode 100644 tests/format_tests.rs create mode 100644 tests/lint_tests.rs create mode 100644 tests/list_tests.rs create mode 100644 tests/pick_tests.rs create mode 100644 tests/version_tests.rs diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs index 6af73ab..50d5396 100644 --- a/tests/cli_integration.rs +++ b/tests/cli_integration.rs @@ -1,265 +1,7 @@ -use std::io::{Read, Seek, SeekFrom, Write}; -use std::path::PathBuf; -use std::process::Command; - -/// Helper to get the compiled binary path -fn get_binary_path() -> PathBuf { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("target"); - path.push("debug"); - path.push("envmn"); - path -} - -/// Helper to create a temporary test file -fn create_test_env_file(content: &str) -> tempfile::NamedTempFile { - let mut file = tempfile::NamedTempFile::new().unwrap(); - file.write_all(content.as_bytes()).unwrap(); - file.flush().unwrap(); - file.seek(SeekFrom::Start(0)).unwrap(); - file -} - -#[test] -fn test_version_command() { - let output = Command::new(get_binary_path()) - .arg("version") - .output() - .expect("Failed to execute command"); - - assert!(output.status.success()); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("envmn version")); - assert!(stdout.contains("0.2.7")); -} - -#[test] -fn test_version_flag() { - let output = Command::new(get_binary_path()) - .arg("--version") - .output() - .expect("Failed to execute command"); - - assert!(output.status.success()); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("envmn version")); -} - -#[test] -fn test_no_command_shows_help() { - let output = Command::new(get_binary_path()) - .output() - .expect("Failed to execute command"); - - assert!(!output.status.success()); - let stderr = String::from_utf8_lossy(&output.stderr); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stderr.contains("Environment manager") || stdout.contains("Environment manager")); -} - -#[test] -fn test_list_command_with_file() { - let test_content = r#"# Basic config -API_URL=https://api.example.com - -#@ database_block -DB_HOST=localhost -DB_PORT=5432 -## - -#@ api_block -API_KEY=secret -## -"#; - - let temp_file = create_test_env_file(test_content); - - let output = Command::new(get_binary_path()) - .arg("list") - .arg(temp_file.path()) - .output() - .expect("Failed to execute command"); - - assert!(output.status.success()); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("database_block")); - assert!(stdout.contains("api_block")); -} - -#[test] -fn test_format_command_with_file() { - let test_content = r#"KEY=value - - -#@ block - -VAR=test -## - -"#; - let result_content = r#"KEY=value - -#@ block -VAR=test -## -"#; - - let mut temp_file = create_test_env_file(test_content); - - let output = Command::new(get_binary_path()) - .arg("format") - .arg(temp_file.path()) - .stdin(std::process::Stdio::null()) - .output() - .expect("Failed to execute command"); - - assert!(output.status.success()); - let mut buffer = String::new(); - temp_file.read_to_string(&mut buffer).unwrap(); - assert_eq!(buffer, result_content); -} - -#[test] -fn test_pick_command() { - let test_content = r#"DEFAULT_VAR=value - -#@ database_block -DB_HOST=localhost -## - -#@ api_block -API_KEY=secret -## -"#; - - let temp_file = create_test_env_file(test_content); - - let output = Command::new(get_binary_path()) - .arg("pick") - .arg("database_block") - .arg(temp_file.path()) - .output() - .expect("Failed to execute command"); - - assert!(output.status.success()); - let stdout = String::from_utf8_lossy(&output.stdout); - - // Verify that database_block appears after api_block in output (moved to bottom) - if let (Some(db_pos), Some(api_pos)) = ( - stdout.find("#@ database_block"), - stdout.find("#@ api_block"), - ) { - assert!( - db_pos > api_pos, - "database_block should come after api_block when picked" - ); - } -} - -#[test] -fn test_lint_command_success() { - let test_content = r#"KEY=value - -#@ block -VAR=test -## -"#; - - let temp_file = create_test_env_file(test_content); - - let output = Command::new(get_binary_path()) - .arg("lint") - .arg(temp_file.path()) - .output() - .expect("Failed to execute command"); - - assert!(output.status.success()); -} - -#[test] -fn test_format_command_with_stdin() { - let test_content = "KEY=value\n\n#@ block\nVAR=test\n##"; - - let mut child = Command::new(get_binary_path()) - .arg("format") - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .spawn() - .expect("Failed to spawn command"); - - { - let stdin = child.stdin.as_mut().expect("Failed to open stdin"); - stdin - .write_all(test_content.as_bytes()) - .expect("Failed to write to stdin"); - } - - let output = child.wait_with_output().expect("Failed to read output"); - - assert!(output.status.success()); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("KEY=value")); -} - -#[test] -fn test_stdin_takes_priority_over_file() { - let file_content = "FILE_VAR=from_file"; - let stdin_content = "STDIN_VAR=from_stdin"; - - let temp_file = create_test_env_file(file_content); - - let mut child = Command::new(get_binary_path()) - .arg("format") - .arg(temp_file.path()) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .spawn() - .expect("Failed to spawn command"); - - { - let stdin = child.stdin.as_mut().expect("Failed to open stdin"); - stdin - .write_all(stdin_content.as_bytes()) - .expect("Failed to write to stdin"); - } - - let output = child.wait_with_output().expect("Failed to read output"); - - assert!(output.status.success()); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("STDIN_VAR")); -} - -#[test] -fn test_invalid_command() { - let output = Command::new(get_binary_path()) - .arg("invalid_command") - .output() - .expect("Failed to execute command"); - - assert!(!output.status.success()); -} - -#[test] -fn test_pick_nonexistent_block() { - let test_content = r#"KEY=value - -#@ existing_block -VAR=test -## -"#; - - let temp_file = create_test_env_file(test_content); - - let output = Command::new(get_binary_path()) - .arg("pick") - .arg("nonexistent_block") - .arg(temp_file.path()) - .output() - .expect("Failed to execute command"); - - // Should handle gracefully (either error or no-op) - let _stderr = String::from_utf8_lossy(&output.stderr); - let _stdout = String::from_utf8_lossy(&output.stdout); - // Test passes if it doesn't crash -} +// Integration tests have been split into focused modules: +// - version_tests.rs: Version and help command tests +// - format_tests.rs: Format command tests +// - list_tests.rs: List command tests +// - pick_tests.rs: Pick command tests +// - lint_tests.rs: Lint command tests +// - common/mod.rs: Shared test utilities diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..b031181 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,32 @@ +use std::io::{Seek, SeekFrom, Write}; +use std::path::PathBuf; +use std::process::Command; + +/// Helper to get the compiled binary path +#[allow(unused)] +pub fn get_binary_path() -> PathBuf { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("target"); + path.push("debug"); + path.push("envmn"); + path +} + +/// Helper to create a temporary test file +#[allow(unused)] +pub fn create_test_env_file(content: &str) -> tempfile::NamedTempFile { + let mut file = tempfile::NamedTempFile::new().unwrap(); + file.write_all(content.as_bytes()).unwrap(); + file.flush().unwrap(); + file.seek(SeekFrom::Start(0)).unwrap(); + file +} + +/// Helper to run a command and return output +#[allow(unused)] +pub fn run_command(args: &[&str]) -> std::process::Output { + Command::new(get_binary_path()) + .args(args) + .output() + .expect("Failed to execute command") +} \ No newline at end of file diff --git a/tests/format_tests.rs b/tests/format_tests.rs new file mode 100644 index 0000000..723efcd --- /dev/null +++ b/tests/format_tests.rs @@ -0,0 +1,92 @@ +mod common; + +use common::{create_test_env_file, get_binary_path}; +use std::io::{Read, Write}; +use std::process::Command; + +#[test] +fn format_command_with_file() { + let test_content = r#"KEY=value + + +#@ block + +VAR=test +## + +"#; + let result_content = r#"KEY=value + +#@ block +VAR=test +## +"#; + + let mut temp_file = create_test_env_file(test_content); + + let output = Command::new(get_binary_path()) + .arg("format") + .arg(temp_file.path()) + .stdin(std::process::Stdio::null()) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let mut buffer = String::new(); + temp_file.read_to_string(&mut buffer).unwrap(); + assert_eq!(buffer, result_content); +} + +#[test] +fn format_command_with_stdin() { + let test_content = "KEY=value\n\n#@ block\nVAR=test\n##"; + + let mut child = Command::new(get_binary_path()) + .arg("format") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .spawn() + .expect("Failed to spawn command"); + + { + let stdin = child.stdin.as_mut().expect("Failed to open stdin"); + stdin + .write_all(test_content.as_bytes()) + .expect("Failed to write to stdin"); + } + + let output = child.wait_with_output().expect("Failed to read output"); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("KEY=value")); +} + +#[test] +fn stdin_takes_priority_over_file() { + let file_content = "FILE_VAR=from_file"; + let stdin_content = "STDIN_VAR=from_stdin"; + + let temp_file = create_test_env_file(file_content); + + let mut child = Command::new(get_binary_path()) + .arg("format") + .arg(temp_file.path()) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .spawn() + .expect("Failed to spawn command"); + + { + let stdin = child.stdin.as_mut().expect("Failed to open stdin"); + stdin + .write_all(stdin_content.as_bytes()) + .expect("Failed to write to stdin"); + } + + let output = child.wait_with_output().expect("Failed to read output"); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("STDIN_VAR")); +} \ No newline at end of file diff --git a/tests/lint_tests.rs b/tests/lint_tests.rs new file mode 100644 index 0000000..b3308f1 --- /dev/null +++ b/tests/lint_tests.rs @@ -0,0 +1,23 @@ +mod common; + +use common::create_test_env_file; + +#[test] +fn lint_command_with_file() { + let test_content = r#"KEY=value + +#@ block +VAR=test +## +"#; + + let temp_file = create_test_env_file(test_content); + + let output = std::process::Command::new(common::get_binary_path()) + .arg("lint") + .arg(temp_file.path()) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); +} \ No newline at end of file diff --git a/tests/list_tests.rs b/tests/list_tests.rs new file mode 100644 index 0000000..2413def --- /dev/null +++ b/tests/list_tests.rs @@ -0,0 +1,32 @@ +mod common; + +use common::{create_test_env_file}; + +#[test] +fn list_command_with_file() { + let test_content = r#"# Basic config +API_URL=https://api.example.com + +#@ database_block +DB_HOST=localhost +DB_PORT=5432 +## + +#@ api_block +API_KEY=secret +## +"#; + + let temp_file = create_test_env_file(test_content); + + let output = std::process::Command::new(common::get_binary_path()) + .arg("list") + .arg(temp_file.path()) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("database_block")); + assert!(stdout.contains("api_block")); +} \ No newline at end of file diff --git a/tests/pick_tests.rs b/tests/pick_tests.rs new file mode 100644 index 0000000..bb8bbd3 --- /dev/null +++ b/tests/pick_tests.rs @@ -0,0 +1,64 @@ +mod common; + +use common::{create_test_env_file, get_binary_path}; +use std::process::Command; + +#[test] +fn pick_command_with_file() { + let test_content = r#"DEFAULT_VAR=value + +#@ database_block +DB_HOST=localhost +## + +#@ api_block +API_KEY=secret +## +"#; + + let temp_file = create_test_env_file(test_content); + + let output = Command::new(get_binary_path()) + .arg("pick") + .arg("database_block") + .arg(temp_file.path()) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + + // Verify that database_block appears after api_block in output (moved to bottom) + if let (Some(db_pos), Some(api_pos)) = ( + stdout.find("#@ database_block"), + stdout.find("#@ api_block"), + ) { + assert!( + db_pos > api_pos, + "database_block should come after api_block when picked" + ); + } +} + +#[test] +fn pick_nonexistent_block() { + let test_content = r#"KEY=value + +#@ existing_block +VAR=test +## +"#; + + let temp_file = create_test_env_file(test_content); + + let output = Command::new(get_binary_path()) + .arg("pick") + .arg("nonexistent_block") + .arg(temp_file.path()) + .output() + .expect("Failed to execute command"); + assert!(!output.status.success()); + + let _stderr = String::from_utf8_lossy(&output.stderr); + assert!(_stderr.contains("was not found")); +} \ No newline at end of file diff --git a/tests/version_tests.rs b/tests/version_tests.rs new file mode 100644 index 0000000..f1eb76d --- /dev/null +++ b/tests/version_tests.rs @@ -0,0 +1,38 @@ +mod common; + +use common::run_command; + +#[test] +fn version_command() { + let output = run_command(&["version"]); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("envmn version")); + assert!(stdout.contains("0.2.7")); +} + +#[test] +fn version_flag() { + let output = run_command(&["--version"]); + + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("envmn version")); +} + +#[test] +fn no_command_shows_help() { + let output = run_command(&[]); + + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stderr.contains("Environment manager") || stdout.contains("Environment manager")); +} + +#[test] +fn invalid_command() { + let output = run_command(&["invalid_command"]); + assert!(!output.status.success()); +} From fb1278e205f9a1a222a0c13ab08201d32c8bd1c2 Mon Sep 17 00:00:00 2001 From: devark28 Date: Fri, 14 Nov 2025 19:16:40 +0200 Subject: [PATCH 9/9] refactor(tests): remove sample envs directory --- test-files/.env.test-1 | 25 ------------------------- test-files/.env.test-2 | 19 ------------------- test-files/.env.test-3 | 18 ------------------ test-files/.env.test-4 | 16 ---------------- test-files/.env.test-5 | 19 ------------------- 5 files changed, 97 deletions(-) delete mode 100644 test-files/.env.test-1 delete mode 100644 test-files/.env.test-2 delete mode 100644 test-files/.env.test-3 delete mode 100644 test-files/.env.test-4 delete mode 100644 test-files/.env.test-5 diff --git a/test-files/.env.test-1 b/test-files/.env.test-1 deleted file mode 100644 index 9f9d0ee..0000000 --- a/test-files/.env.test-1 +++ /dev/null @@ -1,25 +0,0 @@ -# Basic API configuration -API_URL=https://api.example.com -API_KEY=123456789abcdef -DEBUG=true - -#@ local_db -DB_HOST=localhost -DB_PORT=5432 -DB_USER=admin -DB_PASSWORD=password123 -DB_NAME=mydatabase -## - -#@ remote_db -DB_HOST=example.com -DB_PORT=5432 -DB_USER=admin -DB_PASSWORD=password123 -DB_NAME=mydatabase -## - -#@ email_block -MAILGUN_API_KEY=key-xyz123456789 -MAILGUN_DOMAIN=mg.example.com -## diff --git a/test-files/.env.test-2 b/test-files/.env.test-2 deleted file mode 100644 index c58e3b7..0000000 --- a/test-files/.env.test-2 +++ /dev/null @@ -1,19 +0,0 @@ -# App and API -APP_NAME="MyCoolApp" -API_URL="https://secureapi.example.com" -API_KEY="a1b2c3d4e5f67890" -ACCESS_TOKEN="xyz_987654321" - -#@ database_block -DB_HOST="127.0.0.1" -DB_PORT="3306" -DB_USER="db_user" -DB_PASSWORD="strongpassword" -DB_NAME="company_db" -## - -#@ server_block -SERVER_HOSTNAME="server-prod-01" -SERVER_PORT="8080" -ENVIRONMENT="production" -## diff --git a/test-files/.env.test-3 b/test-files/.env.test-3 deleted file mode 100644 index 2dc17fa..0000000 --- a/test-files/.env.test-3 +++ /dev/null @@ -1,18 +0,0 @@ -#@ secrets_block -SECRET_KEY=SK!P*2023@12345 -SESSION_SECRET="MySession!Key#123" -## - -# File paths -LOG_PATH=/var/log/myapp -UPLOAD_DIR=/home/user/uploads -TMP_DIR=/tmp/myapp - -#@ database_block -DB_CONNECTION_STRING=postgres://db_user:secretpass@localhost:5432/myappdb -## - -#@ monitoring_block -MONITORING_API_KEY="monitorKey=abc123;!@#456" -MONITORING_ENABLED=true -## diff --git a/test-files/.env.test-4 b/test-files/.env.test-4 deleted file mode 100644 index 260e69a..0000000 --- a/test-files/.env.test-4 +++ /dev/null @@ -1,16 +0,0 @@ -#@ email_block -MAIL_SERVERS="smtp.example.com,smtp2.example.com,smtp3.example.com" -MAIL_USER="user@example.com" -MAIL_PASSWORD="emailpass" -## - -#@ api_block -API_SERVICE_1=https://api.service1.com -API_SERVICE_2=https://api.service2.com -API_SERVICE_3=https://api.service3.com -## - -# Debugging -DEBUG_FLAGS="-verbose -dev -check-perf" -LOG_LEVEL=debug -ENABLE_PROFILING=true diff --git a/test-files/.env.test-5 b/test-files/.env.test-5 deleted file mode 100644 index b0399d1..0000000 --- a/test-files/.env.test-5 +++ /dev/null @@ -1,19 +0,0 @@ -#@ db_urls_block -DEV_DB_URL=http://localhost:3000/db -STAGING_DB_URL=http://staging.db.example.com -PROD_DB_URL=https://prod.db.example.com -## - -# Feature toggles -FEATURE_TOGGLE_UI=true -FEATURE_TOGGLE_RECOMMENDATION=false - -#@ time_block -TIMEZONE=UTC -EXPIRE_TIME=2025-12-31T23:59:59Z -## - -#@ meta_block -VERSION=1.4.2 -MAINTAINER="devops@example.com" -##