use clap::{Arg, Command, ValueHint};
use clap_complete::Shell;
use core::panic;
use std::path::PathBuf;
pub fn build() -> Command {
Command::new("tagctl")
.about(
"Adds or removes tags to given paths inside square brackets or to extended attributes",
)
.version(env!("CARGO_PKG_VERSION"))
.after_help("Note: More help information can be found by using --help")
.after_long_help(
"Warning:
If you wish to input file names from the right hand side from an external
command, i.e. `tagctl -t tag $(ls)`, file names with characters which need
to be escaped i.e. \"this file.txt\" may appear as multiple files unless
the external command is surrounded by quotes, as shown below. tagctl
splits all stdin and right hand side arguments by newline to accommodate
that.
Examples:
Add tag \"example\" to current directory using file names:
tagctl -t example \"$(ls)\"
ls | tagctl --input --tag example
Remove tag \"example\" from current directory using file names:
tagctl -r --tag=example \"$(ls)\"
ls | tagctl --remove -it example
Add tag \"example\" to current directory using extended attributes:
tagctl -xt example \"$(ls)\"
ls | tagctl --xattr --input --tag example
Remove tag \"example\" from current directory using extended attributes:
tagctl -xr --tag=example \"$(ls)\"
ls | tagctl --xattr --remove -it example
Add tag \"example\" to two sets of inputs using file names:
find /home/user/Documents | tagctl -it \"example\" \"$(ls)\"
",
)
.arg(
Arg::new("tag")
.short('t')
.long("tag")
.required_unless_present_any(["generate_autocomplete", "remove_all"])
.help(
"Tag to add/remove to selected files. \"%p\" uses the parent directory \
name, \"%y\" uses the modified year, \"%m\" uses modified month, \"%d\" uses \
modified day, and \"%w\" uses modified weekday",
),
)
.arg(
Arg::new("delimiter")
.short('d')
.long("delimiter")
.default_value(",")
.help("Separator for multiple tags"),
)
.arg(
Arg::new("rhs_in")
.value_hint(ValueHint::AnyPath)
.action(clap::ArgAction::Append)
.help("Files provided from right hand side of program to add/remove tags to"),
)
.arg(
Arg::new("input")
.short('i')
.long("input")
.action(clap::ArgAction::SetTrue)
.help("Accepts input from stdin"),
)
.arg(
Arg::new("xattr")
.short('x')
.long("xattr")
.conflicts_with("delimiter")
.action(clap::ArgAction::SetTrue)
.help("Adds/removes tags via xattr under 'user.xdg.tags'"),
)
.arg(
Arg::new("remove")
.short('r')
.long("remove")
.conflicts_with("remove_all")
.action(clap::ArgAction::SetTrue)
.help("Removes tag instead of adding"),
)
.arg(
Arg::new("remove_all")
.short('R')
.long("remove_all")
.action(clap::ArgAction::SetTrue)
.conflicts_with_all(["remove", "tag"])
.help("Removes all tags"),
)
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.action(clap::ArgAction::SetTrue)
.help("Increases verbosity of output"),
)
.arg(
Arg::new("generate_autocomplete")
.short('g')
.long("generate_autocomplete")
.num_args(1)
.ignore_case(true)
.exclusive(true)
.value_parser(["bash", "elvish", "fish", "zsh"])
.help("The shell to generate auto-completion for")
.required(false),
)
}
#[derive(Eq, PartialEq)]
pub enum DesiredAction {
RemoveTag,
RemoveAllTag,
AddTag,
RemoveName,
RemoveAllName,
AddName,
GenerateAutocomplete,
}
pub struct Args {
pub action: DesiredAction,
pub input: bool,
pub verbose: bool,
pub delimiter: String,
pub tag: String,
pub tag_varies: bool,
pub rhs_in: Vec<String>,
pub generate_autocomplete: Option<String>,
}
impl Args {
pub fn get_paths(&self) -> Vec<PathBuf> {
let mut stdin_paths = vec![];
if self.input {
let stdin: Vec<String> = get_stdin();
let stdin_split = split_to_vec(&stdin);
for each_split in to_paths(&stdin_split) {
stdin_paths.push(each_split);
}
}
let strings = split_to_vec(&self.rhs_in);
let mut paths = to_paths(&strings);
if !stdin_paths.is_empty() {
for each_path in stdin_paths {
paths.push(each_path);
}
}
paths
}
pub fn autocomplete_created(&self) -> Option<Shell> {
match &self.generate_autocomplete {
Some(shell) => match shell.as_ref() {
"bash" => Some(Shell::Bash),
"elvish" => Some(Shell::Elvish),
"fish" => Some(Shell::Fish),
"zsh" => Some(Shell::Zsh),
_ => {
panic!("Uncaught incorrect shell")
}
},
None => None,
}
}
}
pub fn tag_is_variable(tag: &str) -> bool {
let special_tags: Vec<&str> = vec!["%p", "%y", "%m", "%d", "%w"];
for each_tag in special_tags {
if tag.contains(each_tag) {
return true;
}
}
false
}
impl From<clap::ArgMatches> for Args {
fn from(matched_args: clap::ArgMatches) -> Self {
let mut matched_rhs_in: Vec<String> = Vec::new();
if matched_args.get_many::<String>("rhs_in").is_some() {
for each_arg in matched_args.get_many::<String>("rhs_in").unwrap() {
matched_rhs_in.push(each_arg.to_owned());
}
}
let desired_action = if matched_args.get_flag("xattr") {
if matched_args.get_flag("remove") {
DesiredAction::RemoveTag
} else if matched_args.get_flag("remove_all") {
DesiredAction::RemoveAllTag
} else {
DesiredAction::AddTag
}
} else if matched_args
.get_one::<String>("generate_autocomplete")
.is_some()
{
DesiredAction::GenerateAutocomplete
} else if matched_args.get_flag("remove") {
DesiredAction::RemoveName
} else if matched_args.get_flag("remove_all") {
DesiredAction::RemoveAllName
} else {
DesiredAction::AddName
};
let tag = matched_args
.get_one::<String>("tag")
.unwrap_or(&String::new())
.to_owned();
let args = Args {
action: desired_action,
input: matched_args.get_flag("input"),
verbose: matched_args.get_flag("verbose"),
delimiter: matched_args
.get_one::<String>("delimiter")
.unwrap()
.to_owned(),
tag: tag.clone(),
tag_varies: tag_is_variable(&tag),
rhs_in: matched_rhs_in,
generate_autocomplete: matched_args
.get_one::<String>("generate_autocomplete")
.cloned(),
};
args
}
}
fn split_to_vec(string_list: &[String]) -> Vec<String> {
let mut split_string_list = vec![];
for each_string in string_list {
for each_split in each_string.split('\n') {
split_string_list.push(each_split.to_owned());
}
}
split_string_list
}
fn get_stdin() -> Vec<String> {
let mut stdin_vec = vec![];
loop {
let mut input = String::new();
match std::io::stdin().read_line(&mut input) {
Ok(len) => {
if len == 0 {
break;
}
stdin_vec.push(input);
}
Err(error) => {
eprintln!("error: {error}");
break;
}
}
}
stdin_vec
}
fn to_paths(string_list: &[String]) -> Vec<PathBuf> {
let mut path_list = vec![];
for each_string in string_list {
let each_path = PathBuf::from(each_string)
.canonicalize()
.unwrap_or_else(|_| PathBuf::from(""));
if each_path.is_file() {
path_list.push(each_path);
} else if each_path.to_str().unwrap() != "" {
eprintln!(
"Couldn't find expected file: \"{}\"",
each_path.to_str().unwrap()
);
}
}
path_list
}