Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Features

- Add `--json` flag for JSONL format output.

## Bugfixes

Expand Down
43 changes: 43 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 10 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@ description = "fd is a simple, fast and user-friendly alternative to find."
exclude = ["/benchmarks/*"]
homepage = "https://github.com/sharkdp/fd"
documentation = "https://docs.rs/fd-find"
keywords = [
"search",
"find",
"file",
"filesystem",
"tool",
]
keywords = ["search", "find", "file", "filesystem", "tool"]
license = "MIT OR Apache-2.0"
name = "fd-find"
readme = "README.md"
Expand Down Expand Up @@ -46,6 +40,8 @@ crossbeam-channel = "0.5.15"
clap_complete = {version = "4.5.62", optional = true}
faccess = "0.2.4"
jiff = "0.2.17"
base64 = "0.22.1"
json-escape = "0.3"

[dependencies.clap]
version = "4.5.53"
Expand All @@ -57,7 +53,11 @@ default-features = false
features = ["nu-ansi-term"]

[target.'cfg(unix)'.dependencies]
nix = { version = "0.30.1", default-features = false, features = ["signal", "user", "hostname"] }
nix = { version = "0.30.1", default-features = false, features = [
"signal",
"user",
"hostname",
] }

[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
libc = "0.2"
Expand All @@ -68,13 +68,14 @@ libc = "0.2"
# This has to be kept in sync with src/main.rs where the allocator for
# the program is set.
[target.'cfg(all(not(windows), not(target_os = "android"), not(target_os = "macos"), not(target_os = "freebsd"), not(target_os = "openbsd"), not(target_os = "illumos"), not(all(target_env = "musl", target_pointer_width = "32")), not(target_arch = "riscv64")))'.dependencies]
tikv-jemallocator = {version = "0.6.0", optional = true}
tikv-jemallocator = { version = "0.6.0", optional = true }

[dev-dependencies]
diff = "0.1"
tempfile = "3.24"
filetime = "0.2"
test-case = "3.3"
serde_json = "1.0.145"

[profile.release]
lto = true
Expand Down
28 changes: 28 additions & 0 deletions doc/fd.1
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,34 @@ Maximum number of arguments to pass to the command given with -X. If the number
greater than the given size, the command given with -X is run again with remaining arguments. A
batch size of zero means there is no limit (default), but note that batching might still happen
due to OS restrictions on the maximum length of command lines.
.TP
.BI "\-\-json "
.RS
Specify JSONL (as known as NDJSON) format to use for the output.

Output fields:

- "path": An object containing the path of the file. When the path is valid UTF-8, it this contains a single "text" field
containing the path as a string. Otherwise it contains a single "bytes" field containing the base64 encoded bytes of the
path.

On windows, this may use a lossy UTF-8 encoding, since there isn't an obvious way to encode the pathname.

If a custom path separator is given, it is used in the "text" field, but not in the "bytes" field.

- "type": The file type (e.g., "file", "directory", "symlink").

- "size_bytes": The file size in bytes.

- "mode": The file permissions in octal (e.g., 644).

- "modified": The last modification time in RFC3339 (ISO 8601) format (e.g., 2000-01-01T12:00:00Z).

- "accessed": The last access time in RFC3339 format.

- "created": The creation time in RFC3339 format.
.RE
.TP
.SH PATTERN SYNTAX
The regular expression syntax used by fd is documented here:

Expand Down
10 changes: 10 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,16 @@ pub struct Opts {
)]
search_path: Vec<PathBuf>,

/// Print results in JSONL format.
#[arg(
long,
value_name = "json",
help = "Print results in JSONL format so you can pipe it to tools.",
conflicts_with_all(&["format", "list_details"]),
long_help
)]
pub json: bool,

/// By default, relative paths are prefixed with './' when -x/--exec,
/// -X/--exec-batch, or -0/--print0 are given, to reduce the risk of a
/// path starting with '-' being treated as a command line option. Use
Expand Down
13 changes: 5 additions & 8 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
use std::{path::PathBuf, sync::Arc, time::Duration};

use lscolors::LsColors;
use regex::bytes::RegexSet;

use crate::exec::CommandSet;
use crate::filetypes::FileTypes;
#[cfg(unix)]
use crate::filter::OwnerFilter;
use crate::filter::{SizeFilter, TimeFilter};
use crate::fmt::FormatTemplate;
use crate::fmt::OutputFormat;

/// Configuration options for *fd*.
pub struct Config {
Expand Down Expand Up @@ -70,10 +69,6 @@ pub struct Config {
/// `max_buffer_time`.
pub max_buffer_time: Option<Duration>,

/// `None` if the output should not be colorized. Otherwise, a `LsColors` instance that defines
/// how to style different filetypes.
pub ls_colors: Option<LsColors>,

/// Whether or not we are writing to an interactive terminal
#[cfg_attr(not(unix), allow(unused))]
pub interactive_terminal: bool,
Expand All @@ -87,8 +82,10 @@ pub struct Config {
/// The value (if present) will be a lowercase string without leading dots.
pub extensions: Option<RegexSet>,

/// A format string to use to format results, similarly to exec
pub format: Option<FormatTemplate>,
/// The format to use for the output
///
/// determined by multiple options
pub format: OutputFormat,

/// If a value is supplied, each item found will be used to generate and execute commands.
pub command: Option<Arc<CommandSet>>,
Expand Down
97 changes: 97 additions & 0 deletions src/fmt/json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use std::borrow::Cow;
use std::fs::{FileType, Metadata};
use std::io::Write;
#[cfg(unix)]
use std::os::unix::{ffi::OsStrExt, fs::MetadataExt};
use std::path::{MAIN_SEPARATOR, Path};
use std::time::SystemTime;

#[cfg(unix)]
use base64::{Engine as _, prelude::BASE64_STANDARD};
use jiff::Timestamp;
use json_escape::escape_str;

pub fn output_json<W: Write>(
out: &mut W,
path: &Path,
filetype: Option<FileType>,
metadata: Option<&Metadata>,
path_separator: &Option<String>,
) -> std::io::Result<()> {
out.write_all(b"{")?;

// Print the path as an object that either has a "text" key containing the
// utf8 path, or a "bytes" key with the base64 encoded bytes of the path
#[cfg(unix)]
match path.to_str() {
Some(text) => {
let final_path: Cow<str> = if let Some(sep) = path_separator {
text.replace(MAIN_SEPARATOR, sep).into()
} else {
text.into()
};
// NB: This assumes that rust's debug output for a string
// is a valid JSON string. At time of writing this is the case
// but it is possible, though unlikely, that this could change
// in the future.
write!(out, r#""path":{{"text":"{}"}}"#, escape_str(&final_path))?;
}
None => {
let encoded_bytes = BASE64_STANDARD.encode(path.as_os_str().as_bytes());
write!(out, r#""path":{{"bytes":"{}"}}"#, encoded_bytes)?;
}
};

// On non-unix platforms, if the path isn't valid utf-8,
// we don't know what kind of encoding was used, and
// as_encoded_bytes() isn't necessarily stable between rust versions
// so the best we can really do is a lossy string
#[cfg(not(unix))]
{
let mut path = path.to_string_lossy();
if let Some(sep) = path_separator {
path = path.replace(MAIN_SEPARATOR, sep).into();
}
write!(out, r#""path":{{"text":"{}"}}"#, escape_str(&path))?;
}

// print the type of file
let ft = match filetype {
Some(ft) if ft.is_dir() => "directory",
Some(ft) if ft.is_file() => "file",
Some(ft) if ft.is_symlink() => "symlink",
_ => "unknown",
};
write!(out, r#","type":"{}""#, ft)?;

if let Some(meta) = metadata {
// Output the mode as octal
// We also need to mask it to just include the permission
// bits and not the file type bits (that is handled by "type" above)
#[cfg(unix)]
write!(out, r#","mode":"{:o}""#, meta.mode() & 0x7777)?;

write!(out, r#","size_bytes":{}"#, meta.len())?;

// would it be better to do these with os-specific functions?
if let Ok(modified) = meta.modified().map(json_timestamp) {
write!(out, r#","modified":"{}""#, modified)?;
}
if let Ok(accessed) = meta.accessed().map(json_timestamp) {
write!(out, r#","modified":"{}""#, accessed)?;
}
if let Ok(created) = meta.created().map(json_timestamp) {
write!(out, r#","modified":"{}""#, created)?;
}
}

out.write_all(b"}")
}

fn json_timestamp(time: SystemTime) -> Timestamp {
// System timestamps should always be valid, so assume that we can
// unwrap it
// If we ever do want to handle an error here, maybe convert to either the MAX or MIN
// timestamp depending on which side of the epoch the SystemTime is?
Timestamp::try_from(time).expect("Invalid timestamp")
}
Loading