use std::collections::HashSet;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::process;
use std::result;
use std::str;
use std::str::FromStr;
use clap::{value_parser, Parser};
use nix_index::database;
use nix_index::files::{self, FileTreeEntry, FileType};
use owo_colors::{OwoColorize, Stream};
use regex::bytes::Regex;
use separator::Separatable;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("reading from the database at '{database}' failed: {source}.\n\
This may be caused by a corrupt or missing database, try (re)running `nix-index` to generate the database. \n\
If the error persists please file a bug report at https://github.com/nix-community/nix-index.")]
ReadDatabase {
database: PathBuf,
#[source]
source: database::Error,
},
#[error("constructing the regular expression from the pattern '{pattern}' failed: {source}")]
Grep {
pattern: String,
#[source]
source: regex::Error,
},
#[error("searching the database at '{database}' failed: {source}")]
SearchDatabase {
database: PathBuf,
#[source]
source: database::Error,
},
}
pub type Result<T> = std::result::Result<T, Error>;
struct Args {
database: PathBuf,
pattern: String,
group: bool,
hash: Option<String>,
package_pattern: Option<String>,
file_type: Vec<FileType>,
only_toplevel: bool,
color: bool,
minimal: bool,
}
fn locate(args: &Args) -> Result<()> {
let pattern = Regex::new(&args.pattern).map_err(|e| Error::Grep {
pattern: args.pattern.clone(),
source: e,
})?;
let package_pattern = if let Some(ref pat) = args.package_pattern {
Some(Regex::new(pat).map_err(|e| Error::Grep {
pattern: pat.clone(),
source: e,
})?)
} else {
None
};
let index_file = args.database.join("files");
let db = database::Reader::open(&index_file).map_err(|e| Error::ReadDatabase {
database: index_file.clone(),
source: e,
})?;
let results = db
.query(&pattern)
.package_pattern(package_pattern.as_ref())
.hash(args.hash.clone())
.run()
.map_err(|e| Error::SearchDatabase {
database: index_file.clone(),
source: e,
})?
.filter(|v| {
v.as_ref().ok().map_or_else(
|| true,
|v| {
let &(ref store_path, FileTreeEntry { ref path, ref node }) = v;
let m = pattern
.find_iter(path)
.last()
.expect("path should match the pattern");
let conditions = [
!args.group || !path[m.end()..].contains(&b'/'),
!args.only_toplevel || store_path.origin().toplevel,
args.file_type.iter().any(|t| &node.get_type() == t),
];
conditions.iter().all(|c| *c)
},
)
});
let mut printed_attrs = HashSet::new();
for v in results {
let (store_path, FileTreeEntry { path, node }) = v.map_err(|e| Error::ReadDatabase {
database: index_file.clone(),
source: e,
})?;
use crate::files::FileNode::*;
let (typ, size) = match node {
Regular { executable, size } => (if executable { "x" } else { "r" }, size),
Directory { size, contents: () } => ("d", size),
Symlink { .. } => ("s", 0),
};
let mut attr = format!(
"{}.{}",
store_path.origin().attr,
store_path.origin().output
);
if !store_path.origin().toplevel {
attr = format!("({})", attr);
}
if args.minimal {
if printed_attrs.insert(attr.clone()) {
println!("{}", attr);
}
} else {
print!(
"{:<40} {:>14} {:>1} {}",
attr,
size.separated_string(),
typ,
store_path.as_str()
);
let path = String::from_utf8_lossy(&path);
if args.color {
let mut prev = 0;
for mat in pattern.find_iter(path.as_bytes()) {
if mat.start() == mat.end() {
continue;
}
print!(
"{}{}",
&path[prev..mat.start()],
(&path[mat.start()..mat.end()])
.if_supports_color(Stream::Stdout, |txt| txt.red()),
);
prev = mat.end();
}
println!("{}", &path[prev..]);
} else {
println!("{}", path);
}
}
}
Ok(())
}
fn process_args(matches: Opts) -> result::Result<Args, clap::Error> {
let pattern_arg = matches.pattern;
let package_arg = matches.package;
let start_anchor = if matches.at_root { "^" } else { "" };
let end_anchor = if matches.whole_name { "$" } else { "" };
let make_pattern = |s: &str, wrap: bool| {
let regex = if matches.regex {
s.to_string()
} else {
regex::escape(s)
};
if wrap {
format!("{}{}{}", start_anchor, regex, end_anchor)
} else {
regex
}
};
let color = match matches.color {
Color::Auto => atty::is(atty::Stream::Stdout),
Color::Always => true,
Color::Never => false,
};
let args = Args {
database: matches.database,
group: !matches.no_group,
pattern: make_pattern(&pattern_arg, true),
package_pattern: package_arg.as_deref().map(|p| make_pattern(p, false)),
hash: matches.hash,
file_type: matches
.r#type
.unwrap_or_else(|| files::ALL_FILE_TYPES.to_vec()),
only_toplevel: !matches.all,
color,
minimal: matches.minimal,
};
Ok(args)
}
const LONG_USAGE: &str = r#"
How to use
==========
In the simplest case, just run `nix-locate part/of/file/path` to search for all packages that contain
a file matching that path:
$ nix-locate 'bin/firefox'
...all packages containing a file named 'bin/firefox'
Before using this tool, you first need to generate a nix-index database.
Use the `nix-index` tool to do that.
Limitations
===========
* this tool can only find packages which are built by hydra, because only those packages
will have file listings that are indexed by nix-index
* we can't know the precise attribute path for every package, so if you see the syntax `(attr)`
in the output, that means that `attr` is not the target package but that it
depends (perhaps indirectly) on the package that contains the searched file. Example:
$ nix-locate 'bin/xmonad'
(xmonad-with-packages.out) 0 s /nix/store/nl581g5kv3m2xnmmfgb678n91d7ll4vv-ghc-8.0.2-with-packages/bin/xmonad
This means that we don't know what nixpkgs attribute produces /nix/store/nl581g5kv3m2xnmmfgb678n91d7ll4vv-ghc-8.0.2-with-packages,
but we know that `xmonad-with-packages.out` requires it.
"#;
fn cache_dir() -> &'static OsStr {
let base = xdg::BaseDirectories::with_prefix("nix-index");
let cache_dir = Box::new(base.get_cache_home().unwrap());
let cache_dir = Box::leak(cache_dir);
cache_dir.as_os_str()
}
#[derive(Debug, Parser)]
#[clap(author, about, version, after_help = LONG_USAGE)]
struct Opts {
pattern: String,
#[clap(short, long = "db", default_value_os = cache_dir(), env = "NIX_INDEX_DATABASE")]
database: PathBuf,
#[clap(short, long)]
regex: bool,
#[clap(short, long)]
package: Option<String>,
#[clap(long, name = "HASH")]
hash: Option<String>,
#[clap(long)]
all: bool,
#[clap(short, long, value_parser=value_parser!(FileType))]
r#type: Option<Vec<FileType>>,
#[clap(long)]
no_group: bool,
#[clap(long, value_enum, default_value = "auto")]
color: Color,
#[clap(short, long)]
whole_name: bool,
#[clap(long)]
at_root: bool,
#[clap(long)]
minimal: bool,
}
#[derive(clap::ValueEnum, Clone, Copy, Debug)]
enum Color {
Always,
Never,
Auto,
}
impl FromStr for Color {
type Err = &'static str;
fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
match s {
"always" => Ok(Color::Always),
"never" => Ok(Color::Never),
"auto" => Ok(Color::Auto),
_ => Err(""),
}
}
}
fn main() {
let args = Opts::parse();
let args = process_args(args).unwrap_or_else(|e| e.exit());
if let Err(e) = locate(&args) {
eprintln!("error: {}", e);
process::exit(2);
}
}