Skip to content

Commit 1800215

Browse files
author
Shelvacu
committed
add --json-lines option to print output as lines of json
1 parent 1cf582b commit 1800215

File tree

1 file changed

+108
-49
lines changed

1 file changed

+108
-49
lines changed

src/bin/nix-locate.rs

Lines changed: 108 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
//! Tool for searching for files in nixpkgs packages
2+
use std::borrow::Cow;
23
use std::collections::HashSet;
34
use std::path::PathBuf;
45
use std::process;
56
use std::result;
67
use std::str;
78
use std::str::FromStr;
89

9-
use clap::{value_parser, Parser};
10+
use clap::{value_parser, Args, Parser};
1011
use error_chain::error_chain;
1112
use nix_index::database;
1213
use nix_index::files::{self, FileTreeEntry, FileType};
1314
use owo_colors::{OwoColorize, Stream};
1415
use regex::bytes::Regex;
1516
use separator::Separatable;
17+
use serde::Serialize;
1618

1719
error_chain! {
1820
errors {
@@ -29,8 +31,14 @@ error_chain! {
2931
}
3032
}
3133

34+
enum OutputFormat {
35+
Full,
36+
Minimal,
37+
JsonLines,
38+
}
39+
3240
/// The struct holding the parsed arguments for searching
33-
struct Args {
41+
struct LocateArgs {
3442
/// Path of the nix-index database.
3543
database: PathBuf,
3644
/// The pattern to search for. This is always in regex syntax.
@@ -41,11 +49,11 @@ struct Args {
4149
file_type: Vec<FileType>,
4250
only_toplevel: bool,
4351
color: bool,
44-
minimal: bool,
52+
format: OutputFormat,
4553
}
4654

4755
/// The main function of this module: searches with the given options in the database.
48-
fn locate(args: &Args) -> Result<()> {
56+
fn locate(args: &LocateArgs) -> Result<()> {
4957
// Build the regular expression matcher
5058
let pattern = Regex::new(&args.pattern).chain_err(|| ErrorKind::Grep(args.pattern.clone()))?;
5159
let package_pattern = if let Some(ref pat) = args.package_pattern {
@@ -105,42 +113,77 @@ fn locate(args: &Args) -> Result<()> {
105113
attr = format!("({})", attr);
106114
}
107115

108-
if args.minimal {
109-
// only print each package once, even if there are multiple matches
110-
if printed_attrs.insert(attr.clone()) {
111-
println!("{}", attr);
116+
match args.format {
117+
OutputFormat::JsonLines => {
118+
#[derive(Debug, Serialize)]
119+
struct Info<'a> {
120+
attr: &'a str,
121+
output_name: &'a str,
122+
toplevel: bool,
123+
full_attr: String,
124+
r#type: &'static str,
125+
size: u64,
126+
store_path: Cow<'a, str>,
127+
path: Cow<'a, str>,
128+
}
129+
130+
let o = store_path.origin();
131+
132+
let info = Info {
133+
attr: o.attr.as_str(),
134+
output_name: o.output.as_str(),
135+
toplevel: o.toplevel,
136+
full_attr: attr,
137+
r#type: typ,
138+
size,
139+
store_path: store_path.as_str(),
140+
path: String::from_utf8_lossy(&path),
141+
};
142+
143+
println!(
144+
"{}",
145+
serde_json::to_string(&info)
146+
.expect("serialization should never fail, this is a bug")
147+
);
112148
}
113-
} else {
114-
print!(
115-
"{:<40} {:>14} {:>1} {}",
116-
attr,
117-
size.separated_string(),
118-
typ,
119-
store_path.as_str()
120-
);
121-
122-
let path = String::from_utf8_lossy(&path);
123-
124-
if args.color {
125-
let mut prev = 0;
126-
for mat in pattern.find_iter(path.as_bytes()) {
127-
// if the match is empty, we need to make sure we don't use string
128-
// indexing because the match may be "inside" a single multibyte character
129-
// in that case (for example, the pattern may match the second byte of a multibyte character)
130-
if mat.start() == mat.end() {
131-
continue;
149+
OutputFormat::Minimal => {
150+
// only print each package once, even if there are multiple matches
151+
if printed_attrs.insert(attr.clone()) {
152+
println!("{}", attr);
153+
}
154+
}
155+
OutputFormat::Full => {
156+
print!(
157+
"{:<40} {:>14} {:>1} {}",
158+
attr,
159+
size.separated_string(),
160+
typ,
161+
store_path.as_str()
162+
);
163+
164+
let path = String::from_utf8_lossy(&path);
165+
166+
if args.color {
167+
let mut prev = 0;
168+
for mat in pattern.find_iter(path.as_bytes()) {
169+
// if the match is empty, we need to make sure we don't use string
170+
// indexing because the match may be "inside" a single multibyte character
171+
// in that case (for example, the pattern may match the second byte of a multibyte character)
172+
if mat.start() == mat.end() {
173+
continue;
174+
}
175+
print!(
176+
"{}{}",
177+
&path[prev..mat.start()],
178+
(&path[mat.start()..mat.end()])
179+
.if_supports_color(Stream::Stdout, |txt| txt.red()),
180+
);
181+
prev = mat.end();
132182
}
133-
print!(
134-
"{}{}",
135-
&path[prev..mat.start()],
136-
(&path[mat.start()..mat.end()])
137-
.if_supports_color(Stream::Stdout, |txt| txt.red()),
138-
);
139-
prev = mat.end();
183+
println!("{}", &path[prev..]);
184+
} else {
185+
println!("{}", path);
140186
}
141-
println!("{}", &path[prev..]);
142-
} else {
143-
println!("{}", path);
144187
}
145188
}
146189
}
@@ -151,7 +194,7 @@ fn locate(args: &Args) -> Result<()> {
151194
/// Extract the parsed arguments for clap's arg matches.
152195
///
153196
/// Handles parsing the values of more complex arguments.
154-
fn process_args(matches: Opts) -> result::Result<Args, clap::Error> {
197+
fn process_args(matches: Opts) -> result::Result<LocateArgs, clap::Error> {
155198
let pattern_arg = matches.pattern;
156199
let package_arg = matches.package;
157200

@@ -177,7 +220,7 @@ fn process_args(matches: Opts) -> result::Result<Args, clap::Error> {
177220
Color::Never => false,
178221
};
179222

180-
let args = Args {
223+
let args = LocateArgs {
181224
database: matches.database,
182225
group: !matches.no_group,
183226
pattern: make_pattern(&pattern_arg, true),
@@ -188,7 +231,7 @@ fn process_args(matches: Opts) -> result::Result<Args, clap::Error> {
188231
.unwrap_or_else(|| files::ALL_FILE_TYPES.to_vec()),
189232
only_toplevel: matches.top_level,
190233
color,
191-
minimal: matches.minimal,
234+
format: matches.format.into_enum(),
192235
};
193236
Ok(args)
194237
}
@@ -223,11 +266,29 @@ Limitations
223266
but we know that `xmonad-with-packages.out` requires it.
224267
"#;
225268

226-
fn cache_dir() -> &'static OsStr {
227-
let base = xdg::BaseDirectories::with_prefix("nix-index").unwrap();
228-
let cache_dir = Box::new(base.get_cache_home());
229-
let cache_dir = Box::leak(cache_dir);
230-
cache_dir.as_os_str()
269+
#[derive(Copy, Clone, Debug, Args)]
270+
#[group(multiple = false)]
271+
struct FormatArgs {
272+
/// Only print attribute names of found files or directories. Other details such as size or
273+
/// store path are omitted. This is useful for scripts that use the output of nix-locate.
274+
#[clap(long)]
275+
minimal: bool,
276+
277+
/// Print matches as json objects separated by newlines
278+
#[clap(long)]
279+
json_lines: bool,
280+
}
281+
282+
impl FormatArgs {
283+
fn into_enum(self) -> OutputFormat {
284+
if self.json_lines {
285+
OutputFormat::JsonLines
286+
} else if self.minimal {
287+
OutputFormat::Minimal
288+
} else {
289+
OutputFormat::Full
290+
}
291+
}
231292
}
232293

233294
/// Quickly finds the derivation providing a certain file
@@ -288,10 +349,8 @@ struct Opts {
288349
#[clap(long)]
289350
at_root: bool,
290351

291-
/// Only print attribute names of found files or directories. Other details such as size or
292-
/// store path are omitted. This is useful for scripts that use the output of nix-locate.
293-
#[clap(long)]
294-
minimal: bool,
352+
#[clap(flatten)]
353+
format: FormatArgs,
295354
}
296355

297356
#[derive(clap::ValueEnum, Clone, Copy, Debug)]

0 commit comments

Comments
 (0)