pathmut 0.8.0

Command line utility for manipulating path strings
Documentation
use clap::builder::ValueParser;
use clap::{crate_version, value_parser, Arg, ArgAction, Command};

use crate::command::PathKind;
use crate::command::Question;
use crate::path::Component;

pub fn build() -> Command {
    Command::new("pathmut")
        .version(crate_version!())
        .about("Mutate path strings")
        .args([parse_as_unix_arg(), parse_as_win_arg(), parse_as_url_arg()])
        .subcommands([
            get_command(),
            remove_command(),
            replace_command(),
            set_command(),
            has_command(),
            is_command(),
            convert_command(),
            info_command(),
            depth_command(),
        ])
        .dont_delimit_trailing_values(true)
        .arg_required_else_help(true)
        .subcommand_value_name("COMMAND or COMPONENT")
        .allow_external_subcommands(true)
        .after_help(components_help_section())
}

fn parse_as_win_arg() -> Arg {
    Arg::new("as-windows")
        .global(true)
        .short('w')
        .long("as-windows")
        .action(ArgAction::SetTrue)
        .conflicts_with_all(["as-unix", "as-url"])
        .help("Parse paths as windows paths")
}

fn parse_as_unix_arg() -> Arg {
    Arg::new("as-unix")
        .global(true)
        .short('x')
        .long("as-unix")
        .action(ArgAction::SetTrue)
        .conflicts_with_all(["as-windows", "as-url"])
        .help("Parse paths as unix paths")
}

fn parse_as_url_arg() -> Arg {
    Arg::new("as-url")
        .global(true)
        .short('u')
        .long("as-url")
        .action(ArgAction::SetTrue)
        .conflicts_with_all(["as-windows", "as-unix"])
        .help("Parse paths as URLs")
}

fn components_help_section() -> &'static str {
    "\x1B[4;1mFile Components:\x1B[0m\n\
    \x20 \x1B[1mext\x1B[0m       File extension\n\
    \x20 \x1B[1mstem\x1B[0m      File stem\n\
    \x20 \x1B[1mprefix\x1B[0m    File prefix (before first dot)\n\
    \x20 \x1B[1mname\x1B[0m      File name\n\
    \x20 \x1B[1mdisk\x1B[0m      Disk of a windows path\n\
    \x20 \x1B[1mwinprefix\x1B[0m Windows path prefix\n\n\
    \x1B[4;1mURL Components:\x1B[0m\n\
    \x20 \x1B[1mscheme\x1B[0m    URL scheme (http, https, etc.)\n\
    \x20 \x1B[1mhost\x1B[0m      Hostname\n\
    \x20 \x1B[1mport\x1B[0m      Port number\n\
    \x20 \x1B[1mpath\x1B[0m      URL path\n\
    \x20 \x1B[1mquery\x1B[0m     Query string\n\
    \x20 \x1B[1mfrag\x1B[0m      Fragment identifier\n\
    \x20 \x1B[1muser\x1B[0m      Username\n\
    \x20 \x1B[1mpass\x1B[0m      Password\n\
    \x20 \x1B[1morigin\x1B[0m    scheme://host:port\n\
    \x20 \x1B[1mtld\x1B[0m       Top-level domain\n"
}

fn questions_help_section() -> &'static str {
    "\x1B[4;1mQuestions:\x1B[0m\n\
    \x20 \x1B[1mabsolute\x1B[0m\n\
    \x20 \x1B[1mrelative\x1B[0m\n\
    \x20 \x1B[1munix\x1B[0m\n\
    \x20 \x1B[1mwindows\x1B[0m\n\
    \x20 \x1B[1murl\x1B[0m\n"
}

fn component_arg() -> Arg {
    Arg::new("component")
        .required(true)
        .value_parser(|s: &str| Component::try_from(s))
        .help("Path component")
}

fn paths_arg() -> Arg {
    Arg::new("path")
        .required(true)
        .action(ArgAction::Append)
        .help("Path strings")
        //.value_parser(value_parser!(TypedPathBuf))
        .value_parser(ValueParser::os_string())
}

fn question_arg() -> Arg {
    Arg::new("question")
        .required(true)
        .help("Question to ask")
        .value_parser(value_parser!(Question))
}

pub fn get_command() -> Command {
    Command::new("get")
        .about("Read a path component [default]")
        .arg_required_else_help(true)
        .args([component_arg(), paths_arg()])
        .after_help(components_help_section())
}

pub fn has_command() -> Command {
    Command::new("has")
        .about("Check if a path component exists")
        .arg_required_else_help(true)
        .args(true_false_args())
        .args([component_arg(), paths_arg()])
        .after_help(components_help_section())
}

fn remove_command() -> Command {
    Command::new("delete")
        .about("Remove a path component")
        .arg_required_else_help(true)
        .args([component_arg(), paths_arg()])
        .after_help(components_help_section())
}

fn replace_command() -> Command {
    // todo: fix this, it works funny since arg component without str works
    Command::new("replace")
        .about("Replace an existing path component")
        .arg_required_else_help(true)
        .args([Arg::new("str")
            .required(true)
            .value_parser(ValueParser::os_string())])
        .args([component_arg(), paths_arg()])
        .after_help(components_help_section())
}

fn set_command() -> Command {
    // todo: fix this, it works funny since arg component without str works
    Command::new("set")
        .about("Set a path component")
        .arg_required_else_help(true)
        .args([Arg::new("str")
            .required(true)
            .value_parser(ValueParser::os_string())])
        .args([component_arg(), paths_arg()])
        .after_help(components_help_section())
}

fn true_false_args() -> [Arg; 3] {
    let any = Arg::new("any")
        .help("[default] True if one path succeeds")
        .long("any")
        .action(ArgAction::SetTrue);
    let all = Arg::new("all")
        .help("True only if all paths succeed")
        .long("all")
        .action(ArgAction::SetTrue)
        .conflicts_with("any");
    let print = Arg::new("print")
        .help("Print 'true' or 'false' to stdout instead of exit code")
        .short('p')
        .long("print")
        .action(ArgAction::SetTrue);
    [any, all, print]
}

fn is_command() -> Command {
    Command::new("is")
        .about("Ask questions about a file path")
        .arg_required_else_help(true)
        .args(true_false_args())
        .args([question_arg(), paths_arg()])
        .after_help(questions_help_section())
}

fn path_type_arg() -> Arg {
    Arg::new("type")
        .help("Type of path")
        .required(true)
        .value_parser(value_parser!(PathKind))
}

fn convert_command() -> Command {
    Command::new("convert")
        .about("Convert between unix and windows paths")
        .arg_required_else_help(true)
        .args([path_type_arg(), paths_arg()])
}

fn info_command() -> Command {
    Command::new("info")
        .about("Print information about paths")
        .arg_required_else_help(true)
        .args([
            paths_arg(),
            Arg::new("json")
                .long("json")
                .help("Output as JSON instead of YAML")
                .action(ArgAction::SetTrue),
        ])
}

fn depth_command() -> Command {
    Command::new("depth")
        .about("Number of components before the last component.")
        .arg_required_else_help(true)
        .args([paths_arg()])
}