ecformat 0.1.1

command line tool to keep files correct in respect of your EditorConfig
Documentation
// SPDX-FileCopyrightText: Contributors to ecformat project <https://codeberg.org/BaumiCoder/ecformat>
//
// SPDX-License-Identifier: BlueOak-1.0.0

//! Structs for the Command Line Interface commands and options,
//! which are also necessary as parameters for using ecformat as library.

use std::{
    env,
    path::{Path, PathBuf},
    slice, vec,
};

use anyhow::{self, Context};
use clap::{Args, Parser, Subcommand, ValueEnum};
use itertools::Itertools;

/// Parser for the CLI with clap.
#[derive(Parser)]
#[command(version, about, long_about = None)]
#[command(propagate_version = true, max_term_width = 100)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Commands,
}

/// All commands of ecformat including some which are only available in the bin crate.
#[derive(Subcommand)]
pub enum Commands {
    #[command(flatten)]
    Lib(LibCommands),
    /// Show license information of ecformat and its dependencies.
    License(LicenseArgs),
}

/// Commands which are also available when using ecformat as a lib crate.
#[derive(Subcommand)]
pub enum LibCommands {
    /// Checks that the EditorConfig properties are respected in the files.
    /// If a property is violated in a file, the checks for this file are stopped,
    /// as some violations (e.g., incorrect charset), may lead to subsequence errors.
    /// However, the checks on the other files continue.
    Check(CommandArgs),
    /// Fixes violations against the EditorConfig properties in the files.
    Fix(CommandArgs),
}

impl LibCommands {
    /// Returns reference to the [`CommandArgs`] inside the [`LibCommands`] instance.
    pub fn get_args(&self) -> &CommandArgs {
        match self {
            Self::Check(args) => args,
            Self::Fix(args) => args,
        }
    }
}

/// CLI arguments for both lib commands
#[derive(Args)]
#[command(next_line_help = true)]
pub struct CommandArgs {
    /// The target directories or files to process on. Default is the current working directory.
    targets: Option<Vec<PathBuf>>,
    #[command(flatten)]
    verbosity: clap_verbosity_flag::Verbosity,
    #[command(flatten)]
    pub editorconfig_args: EditorConfigArgs,
    #[command(flatten)]
    pub ignore_args: IgnoreArgs,
}

/// Arguments specific for EditorConfig properties
#[derive(Args)]
#[command(next_help_heading = "Disable EditorConfig properties")]
pub struct EditorConfigArgs {
    /// Consider the `charset` property
    #[arg(short = 'c', long = "disable-charset", action = clap::ArgAction::SetFalse, help =
    "Disables that the charset property of EditorConfig is considered for any file. \
    Determining the actual charset of files can be faulty. Use this option for such cases.")]
    pub charset: bool,
    #[arg(short = 'e', long = "disable-end-of-line", action = clap::ArgAction::SetFalse, help =
    "Disables that the end_of_line property of EditorConfig is considered for any file.")]
    pub end_of_line: bool,
}

/// Arguments for ignoring files
#[derive(Args)]
#[command(next_help_heading = "Control which files are ignored")]
pub struct IgnoreArgs {
    /// Include hidden files and directories (but not .git directory)
    #[arg(short = 'd', long = "ignore-hidden", action = clap::ArgAction::SetFalse, help =
    "Ignore hidden files and directories (starting with a dot). Default is not to ignore them, \
    except for the .git directory with the meta data of a git repository.")]
    pub hidden: bool,
    /// Respect git settings about ignored files.
    #[arg(short = 'g', long = "no-git-settings", action = clap::ArgAction::SetFalse, help =
    "Ignore all git settings about ignored files. Default is that in a git repository \
    (i.e., root contains .git directory) the git settings are respected \
    (i.e., .gitignore and .git/info/exclude files, the git config option core.excludesFile \
    and ignoring the .git directory even if hidden directories are not ignored).")]
    pub git_settings: bool,
    /// One file name of additional ignore files (e.g., `.ecignore` or `.ignore`).
    #[arg(short = 'f', long)]
    pub ignore_file: Option<PathBuf>,
}

impl CommandArgs {
    /// Creates a new `CommandArgs` for using ecformat as lib crate
    /// with a single optional target (default is the current working directory).
    /// See the command helps in the bin crate (e.g. `ecformat help check`) for details.
    ///
    /// No support for the verbosity (`-v` / `-q`) here, as this is only used in the bin crate,
    /// and you can use your own logger (for the [log crate](https://crates.io/crates/log))
    /// when using ecformat as a library.
    pub fn new<P: AsRef<Path>>(
        target: Option<P>,
        editorconfig_args: EditorConfigArgs,
        ignore_args: IgnoreArgs,
    ) -> CommandArgs {
        let targets = target.as_ref().map(|p| slice::from_ref(p));
        CommandArgs::new_internal(targets, editorconfig_args, ignore_args)
    }

    /// Creates a new `CommandArgs` for using ecformat as lib crate
    /// with multiple targets, if the given slice of paths is not empty.
    ///
    /// For more details see the [CommandArgs::new] function which is for a single target.
    pub fn new_multi_target<P: AsRef<Path>>(
        targets: &[P],
        editorconfig_args: EditorConfigArgs,
        ignore_args: IgnoreArgs,
    ) -> Option<CommandArgs> {
        if targets.is_empty() {
            None
        } else {
            Some(CommandArgs::new_internal(
                Some(targets),
                editorconfig_args,
                ignore_args,
            ))
        }
    }

    fn new_internal<P: AsRef<Path>>(
        targets: Option<&[P]>,
        editorconfig_args: EditorConfigArgs,
        ignore_args: IgnoreArgs,
    ) -> CommandArgs {
        CommandArgs {
            targets: targets.map(|s| s.iter().map(|p| p.as_ref().to_path_buf()).collect_vec()),
            // Verbosity is only needed in the `main.rs` (bin crate)
            // and therefore the value is not relevant for lib usage.
            verbosity: clap_verbosity_flag::Verbosity::new(0, 0),
            editorconfig_args,
            ignore_args,
        }
    }

    pub(crate) fn targets(&self) -> anyhow::Result<Vec<PathBuf>> {
        match self.targets.as_ref() {
            Some(t) => Ok(t.iter().map(|p| p.to_path_buf()).collect_vec()),
            None => env::current_dir()
                .with_context(|| "Current working directory no accessible!")
                .map(|p| vec![p]),
        }
    }

    pub fn verbosity(&self) -> clap_verbosity_flag::Verbosity {
        self.verbosity
    }
}

/// Arguments for the license command
#[derive(Args)]
#[command(next_line_help = true)]
pub struct LicenseArgs {
    /// Output format of the license information
    #[arg(short, long, default_value_t = MarkdownFormat::Auto, value_enum)]
    pub format: MarkdownFormat,
}

/// Formats for markdown output
#[derive(Clone, ValueEnum)]
pub enum MarkdownFormat {
    /// If stdout is a terminal, paginate is used and otherwise plain
    Auto,
    /// Shows the rendered Markdown code in a pagination view
    Paginate,
    /// Prints the rendered Markdown code without pagination
    Pretty,
    /// Prints the Markdown code in plain text
    Plain,
}