From ad9bfcef5b3ee3247f26ed501ae724d6f0129615 Mon Sep 17 00:00:00 2001 From: Knight Date: Tue, 10 May 2016 23:33:29 +0800 Subject: [PATCH] dircolors: implemented TODO: More test case --- src/dircolors/colors.rs | 192 +++++++++++++++++++ src/dircolors/dircolors.rs | 380 +++++++++++++++++++++++++++++++++++++ 2 files changed, 572 insertions(+) create mode 100644 src/dircolors/colors.rs create mode 100644 src/dircolors/dircolors.rs diff --git a/src/dircolors/colors.rs b/src/dircolors/colors.rs new file mode 100644 index 000000000..8338dd6f3 --- /dev/null +++ b/src/dircolors/colors.rs @@ -0,0 +1,192 @@ +pub const INTERNAL_DB: &'static str = + r#"# Configuration file for dircolors, a utility to help you set the +# LS_COLORS environment variable used by GNU ls with the --color option. +# Copyright (C) 1996-2016 Free Software Foundation, Inc. +# Copying and distribution of this file, with or without modification, +# are permitted provided the copyright notice and this notice are preserved. +# The keywords COLOR, OPTIONS, and EIGHTBIT (honored by the +# slackware version of dircolors) are recognized but ignored. +# Below are TERM entries, which can be a glob patterns, to match +# against the TERM environment variable to determine if it is colorizable. +TERM Eterm +TERM ansi +TERM color-xterm +TERM con[0-9]*x[0-9]* +TERM cons25 +TERM console +TERM cygwin +TERM dtterm +TERM eterm-color +TERM gnome +TERM gnome-256color +TERM hurd +TERM jfbterm +TERM konsole +TERM kterm +TERM linux +TERM linux-c +TERM mach-color +TERM mach-gnu-color +TERM mlterm +TERM putty +TERM putty-256color +TERM rxvt* +TERM screen* +TERM st +TERM st-256color +TERM terminator +TERM tmux* +TERM vt100 +TERM xterm* +# Below are the color init strings for the basic file types. A color init +# string consists of one or more of the following numeric codes: +# Attribute codes: +# 00=none 01=bold 04=underscore 05=blink 07=reverse 08=concealed +# Text color codes: +# 30=black 31=red 32=green 33=yellow 34=blue 35=magenta 36=cyan 37=white +# Background color codes: +# 40=black 41=red 42=green 43=yellow 44=blue 45=magenta 46=cyan 47=white +#NORMAL 00 # no color code at all +#FILE 00 # regular file: use no color at all +RESET 0 # reset to "normal" color +DIR 01;34 # directory +LINK 01;36 # symbolic link. (If you set this to 'target' instead of a + # numerical value, the color is as for the file pointed to.) +MULTIHARDLINK 00 # regular file with more than one link +FIFO 40;33 # pipe +SOCK 01;35 # socket +DOOR 01;35 # door +BLK 40;33;01 # block device driver +CHR 40;33;01 # character device driver +ORPHAN 40;31;01 # symlink to nonexistent file, or non-stat'able file ... +MISSING 00 # ... and the files they point to +SETUID 37;41 # file that is setuid (u+s) +SETGID 30;43 # file that is setgid (g+s) +CAPABILITY 30;41 # file with capability +STICKY_OTHER_WRITABLE 30;42 # dir that is sticky and other-writable (+t,o+w) +OTHER_WRITABLE 34;42 # dir that is other-writable (o+w) and not sticky +STICKY 37;44 # dir with the sticky bit set (+t) and not other-writable +# This is for files with execute permission: +EXEC 01;32 +# List any file extensions like '.gz' or '.tar' that you would like ls +# to colorize below. Put the extension, a space, and the color init string. +# (and any comments you want to add after a '#') +# If you use DOS-style suffixes, you may want to uncomment the following: +#.cmd 01;32 # executables (bright green) +#.exe 01;32 +#.com 01;32 +#.btm 01;32 +#.bat 01;32 +# Or if you want to colorize scripts even if they do not have the +# executable bit actually set. +#.sh 01;32 +#.csh 01;32 + # archives or compressed (bright red) +.tar 01;31 +.tgz 01;31 +.arc 01;31 +.arj 01;31 +.taz 01;31 +.lha 01;31 +.lz4 01;31 +.lzh 01;31 +.lzma 01;31 +.tlz 01;31 +.txz 01;31 +.tzo 01;31 +.t7z 01;31 +.zip 01;31 +.z 01;31 +.Z 01;31 +.dz 01;31 +.gz 01;31 +.lrz 01;31 +.lz 01;31 +.lzo 01;31 +.xz 01;31 +.bz2 01;31 +.bz 01;31 +.tbz 01;31 +.tbz2 01;31 +.tz 01;31 +.deb 01;31 +.rpm 01;31 +.jar 01;31 +.war 01;31 +.ear 01;31 +.sar 01;31 +.rar 01;31 +.alz 01;31 +.ace 01;31 +.zoo 01;31 +.cpio 01;31 +.7z 01;31 +.rz 01;31 +.cab 01;31 +# image formats +.jpg 01;35 +.jpeg 01;35 +.gif 01;35 +.bmp 01;35 +.pbm 01;35 +.pgm 01;35 +.ppm 01;35 +.tga 01;35 +.xbm 01;35 +.xpm 01;35 +.tif 01;35 +.tiff 01;35 +.png 01;35 +.svg 01;35 +.svgz 01;35 +.mng 01;35 +.pcx 01;35 +.mov 01;35 +.mpg 01;35 +.mpeg 01;35 +.m2v 01;35 +.mkv 01;35 +.webm 01;35 +.ogm 01;35 +.mp4 01;35 +.m4v 01;35 +.mp4v 01;35 +.vob 01;35 +.qt 01;35 +.nuv 01;35 +.wmv 01;35 +.asf 01;35 +.rm 01;35 +.rmvb 01;35 +.flc 01;35 +.avi 01;35 +.fli 01;35 +.flv 01;35 +.gl 01;35 +.dl 01;35 +.xcf 01;35 +.xwd 01;35 +.yuv 01;35 +.cgm 01;35 +.emf 01;35 +# http://wiki.xiph.org/index.php/MIME_Types_and_File_Extensions +.ogv 01;35 +.ogx 01;35 +# audio formats +.aac 00;36 +.au 00;36 +.flac 00;36 +.m4a 00;36 +.mid 00;36 +.midi 00;36 +.mka 00;36 +.mp3 00;36 +.mpc 00;36 +.ogg 00;36 +.ra 00;36 +.wav 00;36 +# http://wiki.xiph.org/index.php/MIME_Types_and_File_Extensions +.oga 00;36 +.opus 00;36 +.spx 00;36 +.xspf 00;36"#; diff --git a/src/dircolors/dircolors.rs b/src/dircolors/dircolors.rs new file mode 100644 index 000000000..f6224a36a --- /dev/null +++ b/src/dircolors/dircolors.rs @@ -0,0 +1,380 @@ +#![crate_name = "uu_dircolors"] + +// This file is part of the uutils coreutils package. +// +// (c) Jian Zeng +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// + +extern crate libc; +extern crate glob; +extern crate getopts; + +#[macro_use] +extern crate uucore; + +use getopts::Options; + +use std::fs::File; +use std::io::{BufRead, BufReader, Write}; +use std::borrow::Borrow; +use std::env; + +static NAME: &'static str = "dircolors"; +static VERSION: &'static str = env!("CARGO_PKG_VERSION"); + +mod colors; +use colors::INTERNAL_DB; + +#[derive(PartialEq, Debug)] +enum OutputFmt { + Shell, + CShell, + Unknown, +} + +macro_rules! disp_err { + ($($args:tt)+) => ({ + pipe_write!(&mut ::std::io::stderr(), "{}: ", NAME); + pipe_writeln!(&mut ::std::io::stderr(), $($args)+); + pipe_writeln!(&mut ::std::io::stderr(), "Try '{} --help' for more information.", NAME); + }) +} + +fn guess_syntax() -> OutputFmt { + match env::var("SHELL") { + Ok(s) => { + if s.is_empty() { + return OutputFmt::Unknown; + } + if let Some(last) = s.rsplit('/').next() { + if last == "csh" || last == "tcsh" { + OutputFmt::CShell + } else { + OutputFmt::Shell + } + } else { + OutputFmt::Shell + } + } + Err(_) => OutputFmt::Unknown, + } +} + +pub fn uumain(args: Vec) -> i32 { + let mut opts = Options::new(); + + opts.optflag("b", "sh", "output Bourne shell code to set LS_COLORS"); + opts.optflag("", + "bourne-shell", + "output Bourne shell code to set LS_COLORS"); + opts.optflag("c", "csh", "output C shell code to set LS_COLORS"); + opts.optflag("", "c-shell", "output C shell code to set LS_COLORS"); + opts.optflag("p", "print-database", "print the byte counts"); + + opts.optflag("h", "help", "display this help and exit"); + opts.optflag("", "version", "output version information and exit"); + + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => crash!(1, "Invalid options\n{}", f), + }; + + if matches.opt_present("help") { + println!("Usage: {} [OPTION]... [FILE] +Output commands to set the LS_COLORS environment variable. + +Determine format of output: + -b, --sh, --bourne-shell output Bourne shell code to set LS_COLORS + -c, --csh, --c-shell output C shell code to set LS_COLORS + -p, --print-database output defaults + --help display this help and exit + --version output version information and exit + +If FILE is specified, read it to determine which colors to use for which +file types and extensions. Otherwise, a precompiled database is used. +For details on the format of these files, run 'dircolors --print-database'.", + NAME); + return 0; + } + + if matches.opt_present("version") { + println!("{} {}", NAME, VERSION); + return 0; + } + + if (matches.opt_present("csh") || matches.opt_present("c-shell") || + matches.opt_present("sh") || matches.opt_present("bourne-shell")) && + matches.opt_present("print-database") { + disp_err!("the options to output dircolors' internal database and\nto select a shell \ + syntax are mutually exclusive"); + return 1; + } + + if matches.opt_present("print-database") { + if !matches.free.is_empty() { + disp_err!("extra operand ‘{}’\nfile operands cannot be combined with \ + --print-database (-p)", + matches.free[0]); + return 1; + } + println!("{}", INTERNAL_DB); + return 0; + } + + let mut out_format = OutputFmt::Unknown; + if matches.opt_present("csh") || matches.opt_present("c-shell") { + out_format = OutputFmt::CShell; + } else if matches.opt_present("sh") || matches.opt_present("bourne-shell") { + out_format = OutputFmt::Shell; + } + + if out_format == OutputFmt::Unknown { + match guess_syntax() { + OutputFmt::Unknown => { + show_info!("no SHELL environment variable, and no shell type option given"); + return 1; + } + fmt => out_format = fmt, + } + } + + let result; + if matches.free.is_empty() { + result = parse(INTERNAL_DB.lines(), out_format, "") + } else { + if matches.free.len() > 1 { + disp_err!("extra operand ‘{}’", matches.free[1]); + return 1; + } + match File::open(matches.free[0].as_str()) { + Ok(f) => { + let fin = BufReader::new(f); + result = parse(fin.lines().filter_map(|l| l.ok()), + out_format, + matches.free[0].as_str()) + } + Err(e) => { + show_info!("{}: {}", matches.free[0], e); + return 1; + } + } + } + match result { + Ok(s) => { + println!("{}", s); + 0 + } + Err(s) => { + show_info!("{}", s); + 1 + } + } +} + +trait StrUtils { + fn purify(&self) -> &Self; + fn split_two(&self) -> (&str, &str); + fn fnmatch(&self, pattern: &str) -> bool; +} + +impl StrUtils for str { + /// Remove comments and trim whitespaces + fn purify(&self) -> &Self { + let mut line = self; + for (n, c) in self.chars().enumerate() { + if c != '#' { + continue; + } + + // Ignore if '#' is at the beginning of line + if n == 0 { + line = &self[..0]; + break; + } + + // Ignore the content after '#' + // only if it is preceded by at least one whitespace + if self.chars().nth(n - 1).unwrap().is_whitespace() { + line = &self[..n]; + } + } + line.trim() + } + + /// Like split_whitespace() but only produce 2 components + fn split_two(&self) -> (&str, &str) { + if let Some(b) = self.find(char::is_whitespace) { + let key = &self[..b]; + if let Some(e) = self[b..].find(|c: char| !c.is_whitespace()) { + (key, &self[b + e..]) + } else { + (key, "") + } + } else { + ("", "") + } + } + + fn fnmatch(&self, pat: &str) -> bool { + pat.parse::().unwrap().matches(self) + } +} + +#[derive(PartialEq)] +enum ParseState { + Global, + Matched, + Continue, + Pass, +} +use std::collections::HashMap; +fn parse(lines: T, fmt: OutputFmt, fp: &str) -> Result + where T: IntoIterator, + T::Item: Borrow +{ + // 1440 > $(dircolors | wc -m) + let mut result = String::with_capacity(1440); + match fmt { + OutputFmt::Shell => result.push_str("LS_COLORS='"), + OutputFmt::CShell => result.push_str("setenv LS_COLORS '"), + _ => unreachable!(), + } + + let mut table: HashMap<&str, &str> = HashMap::with_capacity(48); + table.insert("normal", "no"); + table.insert("norm", "no"); + table.insert("file", "fi"); + table.insert("reset", "rs"); + table.insert("dir", "di"); + table.insert("lnk", "ln"); + table.insert("link", "ln"); + table.insert("symlink", "ln"); + table.insert("orphan", "or"); + table.insert("missing", "mi"); + table.insert("fifo", "pi"); + table.insert("pipe", "pi"); + table.insert("sock", "so"); + table.insert("blk", "bd"); + table.insert("block", "bd"); + table.insert("chr", "cd"); + table.insert("char", "cd"); + table.insert("door", "do"); + table.insert("exec", "ex"); + table.insert("left", "lc"); + table.insert("leftcode", "lc"); + table.insert("right", "rc"); + table.insert("rightcode", "rc"); + table.insert("end", "ec"); + table.insert("endcode", "ec"); + table.insert("suid", "su"); + table.insert("setuid", "su"); + table.insert("sgid", "sg"); + table.insert("setgid", "sg"); + table.insert("sticky", "st"); + table.insert("other_writable", "ow"); + table.insert("owr", "ow"); + table.insert("sticky_other_writable", "tw"); + table.insert("owt", "tw"); + table.insert("capability", "ca"); + table.insert("multihardlink", "mh"); + table.insert("clrtoeol", "cl"); + + let term = env::var("TERM").unwrap_or("none".to_owned()); + let term = term.as_str(); + + let mut state = ParseState::Global; + + for (num, line) in lines.into_iter().enumerate() { + let num = num + 1; + let line = line.borrow().purify(); + if line.is_empty() { + continue; + } + + let (key, val) = line.split_two(); + if val.is_empty() { + return Err(format!("{}:{}: invalid line; missing second token", fp, num)); + } + let lower = key.to_lowercase(); + + if lower == "term" { + if term.fnmatch(val) { + state = ParseState::Matched; + } else if state != ParseState::Matched { + state = ParseState::Pass; + } + } else { + if state == ParseState::Matched { + // prevent subsequent mismatched TERM from + // cancelling the input + state = ParseState::Continue; + } + if state != ParseState::Pass { + if key.starts_with(".") { + result.push_str(format!("*{}={}:", key, val).as_str()); + } else if key.starts_with("*") { + result.push_str(format!("{}={}:", key, val).as_str()); + } else if lower == "options" || lower == "color" || lower == "eightbit" { + // Slackware only. Ignore + } else { + if let Some(s) = table.get(lower.as_str()) { + result.push_str(format!("{}={}:", s, val).as_str()); + } else { + return Err(format!("{}:{}: unrecognized keyword {}", fp, num, key)); + } + } + } + } + } + + match fmt { + OutputFmt::Shell => result.push_str("';\nexport LS_COLORS"), + OutputFmt::CShell => result.push('\''), + _ => unreachable!(), + } + + Ok(result) +} + +#[test] +fn test_shell_syntax() { + use std::env; + let last = env!("SHELL"); + env::set_var("SHELL", "/path/csh"); + assert_eq!(OutputFmt::CShell, guess_syntax()); + env::set_var("SHELL", "csh"); + assert_eq!(OutputFmt::CShell, guess_syntax()); + env::set_var("SHELL", "/path/bash"); + assert_eq!(OutputFmt::Shell, guess_syntax()); + env::set_var("SHELL", "bash"); + assert_eq!(OutputFmt::Shell, guess_syntax()); + env::set_var("SHELL", "bash"); + assert_eq!(OutputFmt::Shell, guess_syntax()); + env::set_var("SHELL", "/asd/bar"); + assert_eq!(OutputFmt::Shell, guess_syntax()); + env::set_var("SHELL", "foo"); + assert_eq!(OutputFmt::Shell, guess_syntax()); + env::set_var("SHELL", ""); + assert_eq!(OutputFmt::Unknown, guess_syntax()); + env::remove_var("SHELL"); + assert_eq!(OutputFmt::Unknown, guess_syntax()); + + env::set_var("SHELL", last); +} + +#[test] +fn test_strutils() { + let s = " asd#zcv #hk\t\n "; + assert_eq!("asd#zcv", s.purify()); + + let s = "con256asd"; + assert!(s.fnmatch("*[2][3-6][5-9]?sd")); + + let s = "zxc \t\nqwe jlk hjl"; + let (k, v) = s.split_two(); + assert_eq!("zxc", k); + assert_eq!("qwe jlk hjl", v); +}