Skip to content

Commit

Permalink
feat(cli): sed syntax support
Browse files Browse the repository at this point in the history
  • Loading branch information
vic1707 committed Jun 12, 2024
1 parent 6704b16 commit 39bb5ed
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 12 deletions.
43 changes: 35 additions & 8 deletions rens-cli/src/cli/renaming.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/* Modules */
pub mod options;
/* Crate imports */
use self::options::Options;
use self::options::{Options, PatternOpt};
/* Dependencies */
use clap::Subcommand;
use regex::{Regex, RegexBuilder};
use rens_common::Strategy;
use rens_common::{SedPattern, Strategy};

#[derive(Debug, Subcommand)]
pub enum Mode {
Expand All @@ -17,6 +17,8 @@ pub enum Mode {
/// The string you with to replace it with.
with: String,
#[command(flatten)]
pattern_opt: PatternOpt,
#[command(flatten)]
options: Options,
},
/// Perform renaming from a regex pattern.
Expand All @@ -29,6 +31,24 @@ pub enum Mode {
/// The string you with to replace it with.
with: String,
#[command(flatten)]
pattern_opt: PatternOpt,
#[command(flatten)]
options: Options,
},
/// Perform renaming from a sed pattern.
Sed {
/// The sed pattern used to rename.
/// Follows the pattern /regex/string/options.
/// [supported options: g, i, I, x, U, <number>]
///
/// Notes:
/// - `g` flag is enabled by default (pass any number to restrict).
/// - You can use anything as a separator.
/// - The regex must comply with `regex` crate syntax.
/// - You can escape the separator (any other escape sequence will be kept as is).
#[arg(verbatim_doc_comment)]
sed_pattern: SedPattern,
#[command(flatten)]
options: Options,
},
}
Expand All @@ -39,11 +59,11 @@ impl Mode {
Self::Regex {
mut pattern,
with,
pattern_opt,
options,
} => {
let limit =
options.pattern_opt.occurence.map_or(0, usize::from);
if options.pattern_opt.case_insensitive {
let limit = pattern_opt.occurence.map_or(0, usize::from);
if pattern_opt.case_insensitive {
pattern = to_regex_case_insensitive(&pattern);
}

Expand All @@ -52,21 +72,28 @@ impl Mode {
Self::String {
pattern,
with,
pattern_opt,
options,
} => {
let limit =
options.pattern_opt.occurence.map_or(0, usize::from);
let limit = pattern_opt.occurence.map_or(0, usize::from);
// safety guarenteed by [`regex::escape`]
#[allow(clippy::expect_used)]
let mut regex_pattern = Regex::new(&regex::escape(&pattern))
.expect("Unable to build regex.");

if options.pattern_opt.case_insensitive {
if pattern_opt.case_insensitive {
regex_pattern = to_regex_case_insensitive(&regex_pattern);
}

(Strategy::new(regex_pattern, with, limit), options)
},
Self::Sed {
sed_pattern,
options,
} => {
let (pattern, with, limit) = sed_pattern.export();
(Strategy::new(pattern, with, limit), options)
},
}
}
}
Expand Down
3 changes: 0 additions & 3 deletions rens-cli/src/cli/renaming/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@ pub struct Options {
#[command(flatten)]
pub paths_opt: PathsOpt,

#[command(flatten)]
pub pattern_opt: PatternOpt,

#[command(flatten)]
pub recursion: Recursion,
}
Expand Down
1 change: 0 additions & 1 deletion rens-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ fn main() -> anyhow::Result<()> {
recursion,
target,
paths,
pattern_opt: _,
},
) = mode.get_strategy_and_options();

Expand Down
2 changes: 2 additions & 0 deletions rens-common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* Modules */
mod file;
mod sed_pattern;
pub mod traits;
/* Dependencies */
use derive_more::{Constructor, Display};
use regex::Regex;
/* Re-exports */
pub use file::{File, RenameTarget};
pub use sed_pattern::SedPattern;

#[derive(Debug, Display, Constructor)]
#[display("{pattern}\n{with}\n{limit}")]
Expand Down
106 changes: 106 additions & 0 deletions rens-common/src/sed_pattern/flag.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/* Built-in imports */
use core::{iter::Peekable, num::ParseIntError, str::Chars};

/// A single regex flag.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Flag {
/// Regex should be case insensitive. Corresponds to `i`.
CaseInsensitive,
/// Regex should be case sensitive. Corresponds to `I`.
CaseSensitive,
/// Regex is run for every match in a string. Corresponds to `g`.
Global,
/// Regex is run N times in a string.
Numbered(usize),
/// "Greedy swap" flag. Corresponds to `U`.
GreedySwap,
/// Ignore whitespaces. Corresponds to `x`.
IgnoreWhitespaces,
}

impl Flag {
pub fn list_from_chars(
mut chars: Peekable<Chars>,
) -> Result<Box<[Self]>, Error> {
let mut flags = Vec::new();

while let Some(ch) = chars.next() {
if ch.is_numeric() {
let mut num_str = ch.to_string();

while let Some(&next_c) = chars.peek() {
if next_c.is_numeric() {
num_str.push(next_c);
chars.next();
} else {
break;
}
}

flags.push(Self::Numbered(num_str.parse::<usize>()?));
} else {
flags.push(Self::try_from(ch)?);
}
}

Ok(flags.into_boxed_slice())
}
}

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum Error {
#[error("Unknown sed flag: {0}")]
UnknownFlag(char),
#[error("{0}")]
ParseIntError(#[from] ParseIntError),
}

impl TryFrom<char> for Flag {
type Error = Error;

fn try_from(ch: char) -> Result<Self, Self::Error> {
match ch {
'i' => Ok(Self::CaseInsensitive),
'I' => Ok(Self::CaseSensitive),
'g' => Ok(Self::Global),
'U' => Ok(Self::GreedySwap),
'x' => Ok(Self::IgnoreWhitespaces),
_ => Err(Self::Error::UnknownFlag(ch)),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_mixed_flags() {
let chars: Peekable<Chars> = "i12g34U".chars().peekable();
let flags = Flag::list_from_chars(chars).unwrap();
assert_eq!(
flags.as_ref(),
vec![
Flag::CaseInsensitive,
Flag::Numbered(12),
Flag::Global,
Flag::Numbered(34),
Flag::GreedySwap,
]
);
}

#[test]
fn test_invalid_flag() {
let chars: Peekable<Chars> = "iX".chars().peekable();
let result = Flag::list_from_chars(chars);
assert_eq!(result.unwrap_err(), Error::UnknownFlag('X'));
}

#[test]
fn test_empty_input() {
let chars = "".chars().peekable();
let flags = Flag::list_from_chars(chars).unwrap();
assert!(flags.is_empty());
}
}
Loading

0 comments on commit 39bb5ed

Please sign in to comment.