From 026c4384992218560dc95c0be160855a8d66df2b Mon Sep 17 00:00:00 2001 From: SARDONYX-sard <68905624+SARDONYX-sard@users.noreply.github.com> Date: Wed, 17 Jan 2024 00:03:49 +0900 Subject: [PATCH] feat(cli): implement cli sub commands BREAKING CHANGE: Subcommands have been added, so command usage has changed. - Implement `unhide-dar` command - Implement `remove-oar` command - Color CLI(Implemented by downgrading clap to v3.) --- dar2oar_cli/Cargo.toml | 4 +- dar2oar_cli/src/cli/commands.rs | 59 +++++++++++++ dar2oar_cli/src/cli/mod.rs | 45 ++++++++++ dar2oar_cli/src/convert.rs | 70 +++++++++++++++ dar2oar_cli/src/logger.rs | 37 ++++++++ dar2oar_cli/src/main.rs | 68 ++------------- dar2oar_core/src/fs/converter/support_cmd.rs | 10 +-- test/sample_scripts/multi-mod-conversion.ps1 | 90 ++++++++++++++------ 8 files changed, 291 insertions(+), 92 deletions(-) create mode 100644 dar2oar_cli/src/cli/commands.rs create mode 100644 dar2oar_cli/src/cli/mod.rs create mode 100644 dar2oar_cli/src/convert.rs create mode 100644 dar2oar_cli/src/logger.rs diff --git a/dar2oar_cli/Cargo.toml b/dar2oar_cli/Cargo.toml index 2872775..1a80330 100644 --- a/dar2oar_cli/Cargo.toml +++ b/dar2oar_cli/Cargo.toml @@ -16,7 +16,8 @@ path = "./src/main.rs" [dependencies] anyhow = { version = "1.0.75", features = ["backtrace"] } -clap = { version = "4.4.4", features = ["derive"] } # For CLI +# NOTE: clap uses v3.2.23, the last successfully built version, because color mode was erased in v4 +clap = { version = "3.2.23", features = ["derive"] } # For CLI dar2oar_core = { path = "../dar2oar_core" } tokio = { version = "1.33.0", features = [ "fs", @@ -25,6 +26,7 @@ tokio = { version = "1.33.0", features = [ "macros", ] } tracing = "0.1.40" # Logger +tracing-appender = "0.2.2" tracing-subscriber = "0.3.17" [dev-dependencies] diff --git a/dar2oar_cli/src/cli/commands.rs b/dar2oar_cli/src/cli/commands.rs new file mode 100644 index 0000000..3c7df7b --- /dev/null +++ b/dar2oar_cli/src/cli/commands.rs @@ -0,0 +1,59 @@ +use crate::convert::Convert; +use clap::Args; + +#[derive(Debug, clap::Parser)] +#[clap(version, about)] +pub(crate) enum Commands { + /// Convert DAR to OAR + #[clap(arg_required_else_help = true)] + Convert(Convert), + + #[clap(arg_required_else_help = true)] + /// Unhide all files in the `DynamicAnimationReplacer` directory + /// by removing the `mohidden` extension + UnhideDar(UnhideDarOption), + + #[clap(arg_required_else_help = true)] + /// Find and delete `OpenAnimationReplacer` directory + RemoveOar(RemoveOarOption), +} + +#[derive(Debug, Args)] +pub(super) struct UnhideDarOption { + #[clap(value_parser)] + /// DAR directory containing files with ".mohidden" extension + pub dar_dir: String, + + // ---logger + #[clap(long)] + /// Log output to stdout as well + pub stdout: bool, + #[clap(long, default_value = "error")] + /// Log level + /// + /// trace | debug | info | warn | error + pub log_level: String, + #[clap(long, default_value = "./convert.log")] + /// Output path of log file + pub log_path: String, +} + +#[derive(Debug, Args)] +pub(super) struct RemoveOarOption { + #[clap(value_parser)] + /// Path containing the "OpenAnimationReplacer" directory + pub target_path: String, + + // ---logger + #[clap(long)] + /// Log output to stdout as well + pub stdout: bool, + #[clap(long, default_value = "error")] + /// Log level + /// + /// trace | debug | info | warn | error + pub log_level: String, + #[clap(long, default_value = "./convert.log")] + /// Output path of log file + pub log_path: String, +} diff --git a/dar2oar_cli/src/cli/mod.rs b/dar2oar_cli/src/cli/mod.rs new file mode 100644 index 0000000..2ce3b0a --- /dev/null +++ b/dar2oar_cli/src/cli/mod.rs @@ -0,0 +1,45 @@ +mod commands; + +use crate::cli::commands::Commands; +use crate::convert::dar2oar; +use crate::init_tracing; +use dar2oar_core::{remove_oar, unhide_dar, Closure}; +use std::str::FromStr; +use tracing::Level; + +/// Converter CLI version +#[derive(Debug, clap::Parser)] +#[clap(name = "dar2oar", about)] +pub(crate) struct Cli { + #[clap(subcommand)] + command: Commands, +} + +macro_rules! init_logger { + ($args:ident) => { + init_tracing( + &$args.log_path, + Level::from_str(&$args.log_level).unwrap_or(Level::ERROR), + $args.stdout, + )?; + }; +} + +pub(crate) async fn run_cli(args: Cli) -> anyhow::Result<()> { + match args.command { + Commands::Convert(args) => { + init_logger!(args); + dar2oar(args).await?; + } + Commands::UnhideDar(args) => { + init_logger!(args); + unhide_dar(args.dar_dir, Closure::default).await?; + } + Commands::RemoveOar(args) => { + init_logger!(args); + remove_oar(args.target_path, Closure::default).await?; + } + } + + Ok(()) +} diff --git a/dar2oar_cli/src/convert.rs b/dar2oar_cli/src/convert.rs new file mode 100644 index 0000000..0f0fdf7 --- /dev/null +++ b/dar2oar_cli/src/convert.rs @@ -0,0 +1,70 @@ +use dar2oar_core::{convert_dar_to_oar, get_mapping_table, Closure, ConvertOptions}; + +pub(crate) async fn dar2oar(args: Convert) -> anyhow::Result<()> { + let config = ConvertOptions { + dar_dir: args.src, + oar_dir: args.dist, + mod_name: args.name, + author: args.author, + section_table: get_mapping_table(args.mapping_file).await, + section_1person_table: get_mapping_table(args.mapping_1person_file).await, + run_parallel: args.run_parallel, + hide_dar: args.hide_dar, + }; + match convert_dar_to_oar(config, Closure::default).await { + Ok(()) => Ok(()), + Err(err) => { + tracing::error!("{}", err); + anyhow::bail!("{}", err) + } + } +} + +#[derive(Debug, clap::Args)] +pub(crate) struct Convert { + #[clap(value_parser)] + /// DAR source dir path + src: String, + #[clap(long)] + /// OAR destination dir path(If not, it is inferred from DAR path) + dist: Option, + #[clap(long)] + /// Mod name in config.json & directory name(If not, it is inferred from DAR path) + name: Option, + #[clap(long)] + /// Mod author in config.json + author: Option, + #[clap(long)] + /// Path to section name table + /// + /// - See more details + /// https://github.com/SARDONYX-sard/dar-to-oar/wiki#what-is-the-mapping-file + mapping_file: Option, + #[clap(long)] + /// Path to section name table(For _1st_person) + mapping_1person_file: Option, + #[clap(long)] + /// Use multi thread + /// + /// [Note] + /// More than twice the processing speed can be expected, + /// but the concurrent processing results in thread termination timings being out of order, + /// so log writes will be out of order as well, greatly reducing readability of the logs. + run_parallel: bool, + #[clap(long)] + /// After conversion, add ".mohidden" to all DAR files to hide them(For MO2 user) + hide_dar: bool, + + // ---logger + #[clap(long)] + /// Log output to stdout as well + pub stdout: bool, + #[clap(long, default_value = "error")] + /// Log level + /// + /// trace | debug | info | warn | error + pub log_level: String, + #[clap(long, default_value = "./convert.log")] + /// Output path of log file + pub log_path: String, +} diff --git a/dar2oar_cli/src/logger.rs b/dar2oar_cli/src/logger.rs new file mode 100644 index 0000000..f0cd490 --- /dev/null +++ b/dar2oar_cli/src/logger.rs @@ -0,0 +1,37 @@ +use std::fs::File; +use std::path::Path; +use tracing::level_filters::LevelFilter; + +/// Init logger. +pub(crate) fn init_tracing( + log_path: impl AsRef, + filter: impl Into, + with_stdout: bool, +) -> anyhow::Result<()> { + use tracing_subscriber::{fmt, layer::SubscriberExt}; + let log_path = log_path.as_ref(); + if let Some(log_path) = log_path.parent() { + std::fs::create_dir_all(log_path)?; + }; + + match with_stdout { + true => tracing::subscriber::set_global_default( + fmt::Subscriber::builder() + .with_max_level(filter) + .finish() + .with( + fmt::Layer::default() + .with_writer(File::create(log_path)?) + .with_line_number(true) + .with_ansi(false), + ), + )?, + false => tracing_subscriber::fmt() + .with_ansi(false) + .with_writer(File::create(log_path)?) + .with_max_level(filter) + .init(), + } + + Ok(()) +} diff --git a/dar2oar_cli/src/main.rs b/dar2oar_cli/src/main.rs index 18ca3bc..fcfd41e 100644 --- a/dar2oar_cli/src/main.rs +++ b/dar2oar_cli/src/main.rs @@ -1,69 +1,17 @@ -use clap::{arg, Parser}; -use dar2oar_core::{convert_dar_to_oar, get_mapping_table, Closure, ConvertOptions}; -use std::fs::File; -use std::str::FromStr; -use tokio::time::Instant; -use tracing::Level; +mod cli; +mod convert; +mod logger; -/// dar2oar --src "DAR path" --dist "OAR path" -#[derive(Debug, Parser)] -#[command(version, about)] -pub struct Args { - /// DAR source dir path - #[arg(long)] - src: String, - /// OAR destination dir path(If not, it is inferred from src) - #[arg(long)] - dist: Option, - /// mod name in config.json & directory name(If not, it is inferred from src) - #[arg(long)] - name: Option, - /// mod author in config.json - #[arg(long)] - author: Option, - /// path to section name table - #[arg(long)] - mapping_file: Option, - /// path to section name table(For _1st_person) - #[arg(long)] - mapping_1person_file: Option, - /// log_level trace | debug | info | warn | error - #[arg(long, default_value = "error")] - log_level: String, - /// Output path of log file - #[arg(long, default_value = "./convert.log")] - log_path: String, - /// use multi thread(More than twice the processing speed can be expected, but the logs are difficult to read and may fail due to unexpected bugs.) - #[arg(long)] - run_parallel: bool, - #[arg(long)] - /// After converting to OAR, add mohidden to the DAR directory before conversion to treat it as a hidden directory. (for MO2 users) - /// NOTE: It appears to work on the MO2 Tree view, but it is doubtful that it works in the author's actual experience. - hide_dar: bool, -} +use crate::cli::{run_cli, Cli}; +use crate::logger::init_tracing; +use clap::Parser; +use tokio::time::Instant; #[tokio::main] async fn main() -> anyhow::Result<()> { let start = Instant::now(); - let args = Args::parse(); - tracing_subscriber::fmt() - .with_ansi(false) - .with_writer(File::create(&args.log_path)?) - .with_max_level(Level::from_str(&args.log_level).unwrap_or(Level::ERROR)) - .init(); - - let config = ConvertOptions { - dar_dir: args.src, - oar_dir: args.dist, - mod_name: args.name, - author: args.author, - section_table: get_mapping_table(args.mapping_file).await, - section_1person_table: get_mapping_table(args.mapping_1person_file).await, - run_parallel: args.run_parallel, - hide_dar: args.hide_dar, - }; - match convert_dar_to_oar(config, Closure::default).await { + match run_cli(Cli::parse()).await { Ok(()) => { let elapsed = start.elapsed(); tracing::info!( diff --git a/dar2oar_core/src/fs/converter/support_cmd.rs b/dar2oar_core/src/fs/converter/support_cmd.rs index 87823da..2048549 100644 --- a/dar2oar_core/src/fs/converter/support_cmd.rs +++ b/dar2oar_core/src/fs/converter/support_cmd.rs @@ -6,11 +6,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tokio::fs; -/// # Returns -/// Report which dirs have been shown -/// -/// # NOTE -/// It is currently used only in GUI, but is implemented in Core as an API. +/// A parallel search will find the `DynamicAnimationReplacer` directory in the path passed as the argument +/// and remove only the `mohidden` extension names from the files in that directory. pub async fn unhide_dar( dar_dir: impl AsRef, mut progress_fn: impl FnMut(usize), @@ -58,8 +55,7 @@ pub async fn unhide_dar( } } -/// # NOTE -/// It is currently used only in GUI, but is implemented in Core as an API. +/// A parallel search will find and remove the `OpenAnimationReplacer` directory from the path passed as the argument. pub async fn remove_oar( search_dir: impl AsRef, mut progress_fn: impl FnMut(usize), diff --git a/test/sample_scripts/multi-mod-conversion.ps1 b/test/sample_scripts/multi-mod-conversion.ps1 index 4b4bfe2..b10fe33 100644 --- a/test/sample_scripts/multi-mod-conversion.ps1 +++ b/test/sample_scripts/multi-mod-conversion.ps1 @@ -1,35 +1,77 @@ -<# - -Example dir status - +function Convert-Mods($base, $mods_dir, $log_level) { + <# Example dir status D:/Programming/rust/dar-to-oar ├─── test │ └─── data │ ├─── Modern Female Sitting Animations Overhaul │ └─── UNDERDOG Animations └─── logs - #> -# Convert target base directory -$base_dir = "D:/Programming/rust/dar-to-oar" -# Create log dir if it doesn't exist. -if (!$(Test-Path "$base_dir/logs")) { - New-Item -ItemType Directory "$base_dir/logs" + # Create log dir if it doesn't exist. + if (!$(Test-Path "$base_dir/logs")) { + New-Item -ItemType Directory "$base_dir/logs" + } + + Get-ChildItem $mods_dir -Directory | + ForEach-Object { + # The following values are expected for `$_.FullName`. + # - D:/Programming/rust/dar-to-oar/test/data/Modern Female Sitting Animations Overhaul + # - D:/Programming/rust/dar-to-oar/test/data/UNDERDOG Animations + + # The following values are expected for `$_.Name`. + # - Modern Female Sitting Animations Overhaul + # - UNDERDOG Animations + cargo run --release -- ` + convert $_.FullName ` + --run-parallel ` + --stdout ` + --log-level $log_level ` + --log-path "$base_dir/logs/convert-$($_.Name).log" + Write-Host "" + } } -Get-ChildItem "$base_dir/test/data" | -ForEach-Object { - # The following values are expected for `$_.FullName`. - # - D:/Programming/rust/dar-to-oar/test/data/Modern Female Sitting Animations Overhaul - # - D:/Programming/rust/dar-to-oar/test/data/UNDERDOG Animations - - # The following values are expected for `$_.Name`. - # - Modern Female Sitting Animations Overhaul - # - UNDERDOG Animations - cargo run --release -- ` - --src $_.FullName ` - --run-parallel ` - --log-level "info" ` - --log-path "$base_dir/logs/convert-$($_.Name).log" +function Show-Dar($base, $mods_dir, $log_level) { + if (!$(Test-Path "$base_dir/logs")) { + New-Item -ItemType Directory "$base_dir/logs" + } + + Get-ChildItem $mods_dir -Directory | + ForEach-Object { + cargo run --release -- ` + unhide-dar $_.FullName ` + --stdout ` + --log-level $log_level ` + --log-path "$base_dir/logs/convert-$($_.Name).log" + Write-Host "" + } } + +function Remove-Oar($base, $mods_dir, $log_level) { + if (!$(Test-Path "$base_dir/logs")) { + New-Item -ItemType Directory "$base_dir/logs" + } + + Get-ChildItem $mods_dir -Directory | + ForEach-Object { + cargo run --release -- ` + remove-oar $_.FullName ` + --stdout ` + --log-level $log_level ` + --log-path "$base_dir/logs/convert-$($_.Name).log" + Write-Host "" + } +} +function Get-Help() { + cargo run --release -- --help + cargo run --release -- convert --help + cargo run --release -- remove-oar --help + cargo run --release -- unhide-dar --help +} + +$base_dir = "D:/Programming/rust/dar-to-oar" # Convert target base directory +# Convert-Mods $base_dir "$base_dir/test/data" "debug" +Remove-Oar $base_dir "$base_dir/test/data" "debug" +Show-Dar $base_dir "$base_dir/test/data" "debug" +# Get-Help