use crate::compcore::CompletionState;
use crate::completion::{Completion, CompletionFlags};
use std::fs;
use std::path::PathBuf;
#[derive(Clone, Debug, Default)]
pub struct FilesOpts {
pub dirs_only: bool,
pub glob: Option<String>,
pub prefix: Option<String>,
pub suffix: Option<String>,
pub work_dir: Option<String>,
pub description: Option<String>,
pub file_patterns: Vec<String>,
pub exclude_patterns: Vec<String>,
pub show_hidden: bool,
}
impl FilesOpts {
pub fn new() -> Self {
Self::default()
}
pub fn dirs_only() -> Self {
Self {
dirs_only: true,
..Default::default()
}
}
pub fn parse(args: &[String]) -> Self {
let mut opts = Self::new();
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"-/" => opts.dirs_only = true,
"-g" => {
if i + 1 < args.len() {
opts.glob = Some(args[i + 1].clone());
i += 1;
}
}
"-P" => {
if i + 1 < args.len() {
opts.prefix = Some(args[i + 1].clone());
i += 1;
}
}
"-S" => {
if i + 1 < args.len() {
opts.suffix = Some(args[i + 1].clone());
i += 1;
}
}
"-W" => {
if i + 1 < args.len() {
opts.work_dir = Some(args[i + 1].clone());
i += 1;
}
}
"-X" => {
if i + 1 < args.len() {
opts.description = Some(args[i + 1].clone());
i += 1;
}
}
"-F" => {
if i + 1 < args.len() {
i += 1;
}
}
arg if arg.starts_with("-g") => {
opts.glob = Some(arg[2..].to_string());
}
arg if arg.starts_with("-P") => {
opts.prefix = Some(arg[2..].to_string());
}
arg if arg.starts_with("-S") => {
opts.suffix = Some(arg[2..].to_string());
}
_ => {
if !args[i].starts_with('-') {
opts.file_patterns.push(args[i].clone());
}
}
}
i += 1;
}
opts
}
}
fn matches_glob(name: &str, pattern: &str) -> bool {
let pattern_chars: Vec<char> = pattern.chars().collect();
let name_chars: Vec<char> = name.chars().collect();
fn match_helper(pattern: &[char], name: &[char]) -> bool {
match (pattern.first(), name.first()) {
(None, None) => true,
(Some('*'), _) => {
match_helper(&pattern[1..], name)
|| (!name.is_empty() && match_helper(pattern, &name[1..]))
}
(Some('?'), Some(_)) => {
match_helper(&pattern[1..], &name[1..])
}
(Some(p), Some(n)) if *p == *n => match_helper(&pattern[1..], &name[1..]),
_ => false,
}
}
match_helper(&pattern_chars, &name_chars)
}
pub fn files_execute(state: &mut CompletionState, opts: &FilesOpts) -> bool {
let prefix = &state.params.prefix;
let (base_dir, file_prefix) = if let Some(sep_pos) = prefix.rfind('/') {
let dir = &prefix[..sep_pos + 1];
let file = &prefix[sep_pos + 1..];
(PathBuf::from(dir), file.to_string())
} else {
(PathBuf::from("."), prefix.clone())
};
let search_dir = if let Some(ref wd) = opts.work_dir {
PathBuf::from(wd).join(&base_dir)
} else {
base_dir.clone()
};
let entries = match fs::read_dir(&search_dir) {
Ok(e) => e,
Err(_) => return false,
};
let group_name = if opts.dirs_only {
"directories"
} else {
"files"
};
state.begin_group(group_name, true);
if let Some(ref desc) = opts.description {
state.add_explanation(desc.clone(), Some(group_name));
}
let mut added = false;
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with('.') && !file_prefix.starts_with('.') && !opts.show_hidden {
continue;
}
if !name_str.starts_with(&file_prefix) {
continue;
}
let path = entry.path();
let is_dir = path.is_dir();
if opts.dirs_only && !is_dir {
continue;
}
if let Some(ref glob) = opts.glob {
if !is_dir && !matches_glob(&name_str, glob) {
continue;
}
}
if !opts.file_patterns.is_empty() && !is_dir {
let matches_any = opts
.file_patterns
.iter()
.any(|p| matches_glob(&name_str, p));
if !matches_any {
continue;
}
}
let mut comp_str = if base_dir == PathBuf::from(".") {
name_str.to_string()
} else {
format!("{}{}", base_dir.display(), name_str)
};
if let Some(ref pfx) = opts.prefix {
comp_str = format!("{}{}", pfx, comp_str);
}
if is_dir {
comp_str.push('/');
} else if let Some(ref sfx) = opts.suffix {
comp_str.push_str(sfx);
}
let mut comp = Completion::new(&comp_str);
if is_dir {
comp.modec = '/';
comp.flags |= CompletionFlags::NOSPACE;
} else if path.is_symlink() {
comp.modec = '@';
} else {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = path.metadata() {
if meta.permissions().mode() & 0o111 != 0 {
comp.modec = '*';
}
}
}
}
state.add_match(comp, Some(group_name));
added = true;
}
state.end_group();
added
}
pub fn directories_execute(state: &mut CompletionState) -> bool {
files_execute(state, &FilesOpts::dirs_only())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_glob_matching() {
assert!(matches_glob("foo.rs", "*.rs"));
assert!(matches_glob("foo.rs", "foo.*"));
assert!(matches_glob("foo.rs", "f?o.rs"));
assert!(matches_glob("foobar", "foo*"));
assert!(!matches_glob("foo.rs", "*.txt"));
assert!(!matches_glob("bar.rs", "foo*"));
}
#[test]
fn test_parse_opts() {
let opts = FilesOpts::parse(&["-/".to_string(), "-g".to_string(), "*.rs".to_string()]);
assert!(opts.dirs_only);
assert_eq!(opts.glob, Some("*.rs".to_string()));
}
#[test]
fn test_parse_combined_opts() {
let opts = FilesOpts::parse(&["-g*.txt".to_string(), "-Pprefix_".to_string()]);
assert_eq!(opts.glob, Some("*.txt".to_string()));
assert_eq!(opts.prefix, Some("prefix_".to_string()));
}
}