mirror of https://github.com/sharkdp/fd.git
Re-structure code, add unit tests, use LS_COLORS
This commit is contained in:
parent
d29115b4da
commit
c6209471a2
|
@ -3,6 +3,14 @@ name = "fd"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
authors = ["David Peter <mail@david-peter.de>"]
|
authors = ["David Peter <mail@david-peter.de>"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fd"
|
||||||
|
path = "src/bin/main.rs"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "fd"
|
||||||
|
path = "src/fd.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ansi_term = "0.9"
|
ansi_term = "0.9"
|
||||||
getopts = "0.2"
|
getopts = "0.2"
|
||||||
|
|
|
@ -3,24 +3,22 @@ extern crate getopts;
|
||||||
extern crate isatty;
|
extern crate isatty;
|
||||||
extern crate regex;
|
extern crate regex;
|
||||||
extern crate ignore;
|
extern crate ignore;
|
||||||
|
extern crate fd;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fs::File;
|
use std::io::Write;
|
||||||
use std::io::{Write, BufReader,BufRead};
|
|
||||||
use std::path::{Path, Component};
|
use std::path::{Path, Component};
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
use ansi_term::{Style, Colour};
|
use ansi_term::Style;
|
||||||
use getopts::Options;
|
use getopts::Options;
|
||||||
use isatty::stdout_isatty;
|
use isatty::stdout_isatty;
|
||||||
use regex::{Regex, RegexBuilder};
|
use regex::{Regex, RegexBuilder};
|
||||||
use ignore::WalkBuilder;
|
use ignore::WalkBuilder;
|
||||||
|
|
||||||
/// Maps file extensions to ANSI colors / styles.
|
use fd::lscolors::LsColors;
|
||||||
type ExtensionStyles = HashMap<String, ansi_term::Style>;
|
|
||||||
|
|
||||||
/// Configuration options for *fd*.
|
/// Configuration options for *fd*.
|
||||||
struct FdOptions {
|
struct FdOptions {
|
||||||
|
@ -31,7 +29,7 @@ struct FdOptions {
|
||||||
follow_links: bool,
|
follow_links: bool,
|
||||||
max_depth: Option<usize>,
|
max_depth: Option<usize>,
|
||||||
colored: bool,
|
colored: bool,
|
||||||
extension_styles: Option<ExtensionStyles>
|
ls_colors: LsColors
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print a search result to the console.
|
/// Print a search result to the console.
|
||||||
|
@ -61,21 +59,16 @@ fn print_entry(path_root: &Path, path_entry: &Path, config: &FdOptions) {
|
||||||
if component_path.symlink_metadata()
|
if component_path.symlink_metadata()
|
||||||
.map(|md| md.file_type().is_symlink())
|
.map(|md| md.file_type().is_symlink())
|
||||||
.unwrap_or(false) {
|
.unwrap_or(false) {
|
||||||
Colour::Cyan.normal()
|
config.ls_colors.symlink
|
||||||
} else if component_path.is_dir() {
|
} else if component_path.is_dir() {
|
||||||
Colour::Blue.bold()
|
config.ls_colors.directory
|
||||||
} else {
|
} else {
|
||||||
// Loop up file extension
|
// Loop up file extension
|
||||||
if let Some(ref ext_styles) = config.extension_styles {
|
component_path.extension()
|
||||||
component_path.extension()
|
.and_then(|e| e.to_str())
|
||||||
.and_then(|e| e.to_str())
|
.and_then(|e| config.ls_colors.extensions.get(e))
|
||||||
.and_then(|e| ext_styles.get(e))
|
.map(|r| r.clone())
|
||||||
.map(|r| r.clone())
|
.unwrap_or(Style::new())
|
||||||
.unwrap_or(Style::new())
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Style::new()
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
print!("{}", style.paint(comp_str.to_str().unwrap()));
|
print!("{}", style.paint(comp_str.to_str().unwrap()));
|
||||||
|
@ -132,61 +125,6 @@ fn error(message: &str) -> ! {
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse `dircolors` file.
|
|
||||||
fn parse_dircolors(path: &Path) -> std::io::Result<ExtensionStyles> {
|
|
||||||
let file = File::open(path)?;
|
|
||||||
let mut ext_styles = HashMap::new();
|
|
||||||
|
|
||||||
let pattern_ansi_256 =
|
|
||||||
Regex::new(r"^\.([A-Za-z0-9]+)\s*(?:00;)?38;5;([0-9]+)\b")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let pattern_ansi =
|
|
||||||
Regex::new(r"^\.([A-Za-z0-9]+)\s*(?:([0-9]+);)?([0-9][0-9])\b")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
for line in BufReader::new(file).lines() {
|
|
||||||
let line_s = line.unwrap();
|
|
||||||
if let Some(caps) = pattern_ansi_256.captures(line_s.as_str()) {
|
|
||||||
if let Some(ext) = caps.get(1).map(|m| m.as_str()) {
|
|
||||||
let fg = caps.get(2)
|
|
||||||
.map(|m| m.as_str())
|
|
||||||
.and_then(|n| u8::from_str_radix(n, 10).ok())
|
|
||||||
.unwrap_or(7); // white
|
|
||||||
ext_styles.insert(String::from(ext),
|
|
||||||
Colour::Fixed(fg).normal());
|
|
||||||
}
|
|
||||||
} else if let Some(caps) = pattern_ansi.captures(line_s.as_str()) {
|
|
||||||
if let Some(ext) = caps.get(1).map(|m| m.as_str()) {
|
|
||||||
let color_s = caps.get(3)
|
|
||||||
.map_or("", |m| m.as_str());
|
|
||||||
let color = match color_s {
|
|
||||||
"31" => Colour::Red,
|
|
||||||
"32" => Colour::Green,
|
|
||||||
"33" => Colour::Yellow,
|
|
||||||
"34" => Colour::Blue,
|
|
||||||
"35" => Colour::Purple,
|
|
||||||
"36" => Colour::Cyan,
|
|
||||||
_ => Colour::White
|
|
||||||
};
|
|
||||||
let style_s = caps.get(2)
|
|
||||||
.map_or("", |m| m.as_str());
|
|
||||||
let style = match style_s {
|
|
||||||
"1" => color.bold(),
|
|
||||||
"01" => color.bold(),
|
|
||||||
"3" => color.italic(),
|
|
||||||
"03" => color.italic(),
|
|
||||||
"4" => color.underline(),
|
|
||||||
"04" => color.underline(),
|
|
||||||
_ => color.normal()
|
|
||||||
};
|
|
||||||
ext_styles.insert(String::from(ext), style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(ext_styles)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
||||||
|
@ -231,14 +169,11 @@ fn main() {
|
||||||
let colored_output = !matches.opt_present("no-color") &&
|
let colored_output = !matches.opt_present("no-color") &&
|
||||||
stdout_isatty();
|
stdout_isatty();
|
||||||
|
|
||||||
let ext_styles =
|
let ls_colors =
|
||||||
if colored_output {
|
env::var("LS_COLORS")
|
||||||
env::home_dir()
|
.ok()
|
||||||
.map(|h| h.join(".dir_colors"))
|
.map(|val| LsColors::from_string(&val))
|
||||||
.and_then(|path| parse_dircolors(&path).ok())
|
.unwrap_or(LsColors::default());
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let config = FdOptions {
|
let config = FdOptions {
|
||||||
// The search will be case-sensitive if the command line flag is set or
|
// The search will be case-sensitive if the command line flag is set or
|
||||||
|
@ -253,7 +188,7 @@ fn main() {
|
||||||
matches.opt_str("max-depth")
|
matches.opt_str("max-depth")
|
||||||
.and_then(|ds| usize::from_str_radix(&ds, 10).ok()),
|
.and_then(|ds| usize::from_str_radix(&ds, 10).ok()),
|
||||||
colored: colored_output,
|
colored: colored_output,
|
||||||
extension_styles: ext_styles
|
ls_colors: ls_colors
|
||||||
};
|
};
|
||||||
|
|
||||||
match RegexBuilder::new(pattern)
|
match RegexBuilder::new(pattern)
|
|
@ -0,0 +1,7 @@
|
||||||
|
extern crate ansi_term;
|
||||||
|
extern crate getopts;
|
||||||
|
extern crate isatty;
|
||||||
|
extern crate regex;
|
||||||
|
extern crate ignore;
|
||||||
|
|
||||||
|
pub mod lscolors;
|
|
@ -0,0 +1,161 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use ansi_term::{Style, Colour};
|
||||||
|
|
||||||
|
/// Maps file extensions to ANSI colors / styles.
|
||||||
|
pub type ExtensionStyles = HashMap<String, Style>;
|
||||||
|
|
||||||
|
/// Maps filenames to ANSI colors / styles.
|
||||||
|
pub type FilenameStyles = HashMap<String, Style>;
|
||||||
|
|
||||||
|
const LS_CODES: &'static [&'static str] =
|
||||||
|
&["no", "no", "fi", "rs", "di", "ln", "ln", "ln", "or", "mi", "pi", "pi",
|
||||||
|
"so", "bd", "bd", "cd", "cd", "do", "ex", "lc", "lc", "rc", "rc", "ec",
|
||||||
|
"ec", "su", "su", "sg", "sg", "st", "ow", "ow", "tw", "tw", "ca", "mh",
|
||||||
|
"cl"];
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct LsColors {
|
||||||
|
pub directory: Style,
|
||||||
|
pub symlink: Style,
|
||||||
|
pub extensions: ExtensionStyles,
|
||||||
|
pub filenames: FilenameStyles,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LsColors {
|
||||||
|
/// Get a default LsColors structure.
|
||||||
|
pub fn default() -> LsColors {
|
||||||
|
LsColors {
|
||||||
|
directory: Colour::Blue.bold(),
|
||||||
|
symlink: Colour::Cyan.normal(),
|
||||||
|
extensions: HashMap::new(),
|
||||||
|
filenames: HashMap::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a single ANSI style like `38;5;10`.
|
||||||
|
fn parse_style(code: &str) -> Option<Style> {
|
||||||
|
let mut split = code.split(";");
|
||||||
|
|
||||||
|
if let Some(first) = split.next() {
|
||||||
|
let second = split.next();
|
||||||
|
let third = split.next();
|
||||||
|
|
||||||
|
let style =
|
||||||
|
if first == "38" && second == Some("5") {
|
||||||
|
let n_white = 7;
|
||||||
|
let n = if let Some(num) = third {
|
||||||
|
u8::from_str_radix(num, 10).unwrap_or(n_white)
|
||||||
|
} else {
|
||||||
|
n_white
|
||||||
|
};
|
||||||
|
|
||||||
|
Colour::Fixed(n).normal()
|
||||||
|
} else {
|
||||||
|
let style_s = if second.is_some() { first } else { "" };
|
||||||
|
let color_s = second.unwrap_or(first);
|
||||||
|
|
||||||
|
let color = match color_s {
|
||||||
|
"30" => Colour::Black,
|
||||||
|
"31" => Colour::Red,
|
||||||
|
"32" => Colour::Green,
|
||||||
|
"33" => Colour::Yellow,
|
||||||
|
"34" => Colour::Blue,
|
||||||
|
"35" => Colour::Purple,
|
||||||
|
"36" => Colour::Cyan,
|
||||||
|
_ => Colour::White
|
||||||
|
};
|
||||||
|
|
||||||
|
match style_s {
|
||||||
|
"1" => color.bold(),
|
||||||
|
"01" => color.bold(),
|
||||||
|
"3" => color.italic(),
|
||||||
|
"03" => color.italic(),
|
||||||
|
"4" => color.underline(),
|
||||||
|
"04" => color.underline(),
|
||||||
|
_ => color.normal()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(style)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new LS_COLORS entry
|
||||||
|
fn add_entry(&mut self, input: &str) {
|
||||||
|
let mut parts = input.trim().split("=");
|
||||||
|
if let Some(pattern) = parts.next() {
|
||||||
|
if let Some(style_code) = parts.next() {
|
||||||
|
// Ensure that the input was split into exactly two parts:
|
||||||
|
if !parts.next().is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(style) = LsColors::parse_style(style_code) {
|
||||||
|
// Try to match against one of the known codes
|
||||||
|
let res = LS_CODES.iter().find(|&&c| c == pattern);
|
||||||
|
|
||||||
|
if let Some(code) = res {
|
||||||
|
match code.as_ref() {
|
||||||
|
"di" => self.directory = style,
|
||||||
|
"ln" => self.symlink = style,
|
||||||
|
_ => return
|
||||||
|
}
|
||||||
|
} else if pattern.starts_with("*.") {
|
||||||
|
let extension = String::from(pattern).split_off(2);
|
||||||
|
self.extensions.insert(extension, style);
|
||||||
|
}
|
||||||
|
else if pattern.starts_with("*") {
|
||||||
|
let filename = String::from(pattern).split_off(1);
|
||||||
|
self.extensions.insert(filename, style);
|
||||||
|
} else {
|
||||||
|
// Unknown/corrupt pattern
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a `LsColors` structure from a string.
|
||||||
|
pub fn from_string(input: &String) -> LsColors {
|
||||||
|
let mut lscolors = LsColors::default();
|
||||||
|
|
||||||
|
for s in input.split(":") {
|
||||||
|
lscolors.add_entry(&s);
|
||||||
|
}
|
||||||
|
|
||||||
|
lscolors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_style() {
|
||||||
|
assert_eq!(Some(Colour::Red.normal()),
|
||||||
|
LsColors::parse_style("31"));
|
||||||
|
|
||||||
|
assert_eq!(Some(Colour::Red.normal()),
|
||||||
|
LsColors::parse_style("00;31"));
|
||||||
|
|
||||||
|
assert_eq!(Some(Colour::Blue.italic()),
|
||||||
|
LsColors::parse_style("03;34"));
|
||||||
|
|
||||||
|
assert_eq!(Some(Colour::Cyan.bold()),
|
||||||
|
LsColors::parse_style("01;36"));
|
||||||
|
|
||||||
|
assert_eq!(Some(Colour::Fixed(115).normal()),
|
||||||
|
LsColors::parse_style("38;5;115"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lscolors() {
|
||||||
|
assert_eq!(LsColors::default(), LsColors::from_string(&String::new()));
|
||||||
|
|
||||||
|
let result = LsColors::from_string(
|
||||||
|
&String::from("rs=0:di=03;34:ln=01;36:*.foo=01;31:"));
|
||||||
|
|
||||||
|
assert_eq!(Colour::Blue.italic(), result.directory);
|
||||||
|
assert_eq!(Colour::Cyan.bold(), result.symlink);
|
||||||
|
}
|
Loading…
Reference in New Issue