feat(output): implement JSON output#1800
feat(output): implement JSON output#1800Dustin-Jiang wants to merge 39 commits intosharkdp:masterfrom
Conversation
|
I think i would really prefer json output. There are more tools that would support that as input. As for the streaming problem, I think there are a couple of ways to handle that:
|
tmccombs
left a comment
There was a problem hiding this comment.
Should also have some tests, and the man page should be updated.
As well as the things I commented on above.
src/output.rs
Outdated
| // Try to convert to UTF-8 first | ||
| match std::str::from_utf8(bytes) { | ||
| Ok(utf8_str) => { | ||
| let escaped: String = utf8_str.escape_default().collect(); |
There was a problem hiding this comment.
I don't think collecting into an intermediate string is strictly necessary, if PathEncoding included a lifetime based on the path.
There was a problem hiding this comment.
collecting into an intermediate string
I'm a little bit confused, since the escape_default always allocates new memory.
In my understanding, to avoid an intermediate string, I should borrow the utf8_str and store in the PathEncoding as Cow<'a, str>, and escape it when write!().
But the PathEncoding is created per file and released after the certain file is printed (or not?), and it is a must (or not?) to .escape_default().collet() into a String when escaping, so I can't see differences between.
But I'm just a newbie to Rust and I'm pretty sure you are right :), so looking forward to your further suggestion.
There was a problem hiding this comment.
So, you would define PathEncoding like
enum<'a> PathEncoding<'a> {
Utf8(EscapeDefault<'a>),
Bytes(&'a [u8]),
}Then this would be defined like:
fn encode_path(path: &Path) -> PathEncoding<'_> {
match path.to_str() {
Some(utf8) => PathEncoding::Utf8(utf8.escape_default()),
None => PathEncoding::Bytes(path.as_os_str().as_encoded_bytes()),
}
}And FileDetail would need a lifetime parameter as well.
There was a problem hiding this comment.
I would strongly encourage y'all to look at how ripgrep encodes paths. And in particular, you really really really should not be using WTF-8 in your output format. From the WTF-8 spec:
Any WTF-8 data must be converted to a Unicode encoding at the system’s boundary before being emitted. UTF-8 is recommended. WTF-8 must not be used to represent text in a file format or for transmission over the Internet.
You'll also want to check how PathEncoding serializes. Sometimes just serializing a &[u8] leads to something sub-optimal (like an array of integers or something). This is why ripgrep base64 encodes data that isn't valid UTF-8.
|
Just want to throw out a reference to libxo as my favourite way for command line programs to support structured output formats. Not sure there's something like that in the Rust ecosystem. |
tmccombs
left a comment
There was a problem hiding this comment.
I few minor suggestions, but I think it's close.
src/output.rs
Outdated
| write!(self.stdout, "}}") | ||
| } | ||
|
|
||
| fn print_entry_detail(&mut self, format: OutputFormat, entry: &DirEntry) -> io::Result<()> { |
There was a problem hiding this comment.
nit: We could potentially combine this with the print_entry_json_obj method now, since it is only used for jsonl.
OTOH, it may be useful in the future if we add a yaml format later, or maybe a tabular format?
|
It would be useful if there was a distinction between fd --json invocations that do stat calls vs ones that don't. Because for large directories, the stat calls are what dominates runtime, and fd can run e.g. 10x faster without them. Compare for example with The second call is much slower, because the For JSON output, there's no reason to not output all the info we have - but we don't have the stat info by default. For example the param could be called basic |
|
There isn't much we can output without a full stat call. Just the filename, the file type, and the inode number (on unix). |
|
@sharkdp since this is a pretty significant change, I'd like to confirm you are ok with this change before merging it. |
Thank you for asking. This looks like a great feature to have! And thank you for your contribution, @Dustin-Jiang! While I'm here… the format that we introduce here is something that users will depend on, so it is worth investing some time to come up with a first version of this format that we can hopefully depend on for a long time:
|
I'm kind of split on this. Decimal is a pretty bad format for human consumption. But if this json is then consumed programatically, it is probably more convenient to have it as a number than a string. Perhaps using the octal encoding as a string could be a good middle graound? |
|
@Dustin-Jiang, if you want, I could help make those changes to this PR. |
Yes, exactly. The problem with human consumption is that, even if unlikely, it is technically possible for a mode to be I agree that we should probably optimize for programmatic consumption, though. But even then, the string seems more practical to me? Because I will probably want to split the number by owner/group/others, and that is much easier if it's already in this string notation. Converting a single digit from a character to an integer should be an operation that is easily available in every programming language. It gets more tricky actually if we consider stricky/setgid/setuid where the mode can be something like
How is that different from what I am proposing? Something like "0644" or "4777" would be the octal encoding as a (fixed length) string, right? Or would you suggest "0o0644" / "0o4777"? |
Sorry, I meant as opposed to something like "rwxr-x---", which would probably be the most uset friendly but not very computer friendly in most cases. |
This also addresses feedback from json PR: - mode is output as a string, using the octal representation - path uses the same format as the ripgrep output - use "size_bytes" instead of "size" to make the unit more clear Also, I fixed an issue where the mode included high bytes that are actually used to encode the filetype (at least on Linux).
| // as_encoded_bytes() isn't necessarily stable between rust versions | ||
| // so the best we can really do is a lossy string | ||
| #[cfg(not(unix))] | ||
| write!(out, r#""path":{{"text":{:?}}}"#, path.to_string_lossy())?; |
There was a problem hiding this comment.
This is what ripgrep does. Although I'm not sure if it would be better to do one of the following:
- Use
OsStr::as_encoded_bytes. Currently, on windows, I think this uses WTF-8, but the documentation makes it clear this isn't guaranteed, and could change in a future version of rust. - Output as (invalid) UTF-16, base64 encoded on windows. Probably not what most people would expect, but at least doesn't lose data.
- Explicitly output using WTF-8 by converting the OsStr to
[u16](or an iterater over wide chars), then back to wtf8. Possibly using the "wtf8" crate. It seems wasteful, and not very performant, but at least we aren't reliant on rust's current implementation.
There was a problem hiding this comment.
Yes, ripgrep has been doing this lossy encoding for years. So far there hasn't been a single complaint. I think non-UTF-16 paths are extremely rare on Windows. Much rarer than non-UTF-8 paths on Unix.
2. Output as (invalid) UTF-16, base64 encoded on windows. Probably not what most people would expect, but at least doesn't lose data.
If you want to avoid lossiness, I think this is the best option. ripgrep also does this for invalid UTF-8. (It looks like this PR does it too.) Namely, this avoids making it too easy to spread WTF-8 as an interchange format:
WTF-8 must not be used to represent text in a file format or for transmission over the Internet.
This also addresses feedback from json PR: - mode is output as a string, using the octal representation - path uses the same format as the ripgrep output - use "size_bytes" instead of "size" to make the unit more clear Also, I fixed an issue where the mode included high bytes that are actually used to encode the filetype (at least on Linux).
Unless we use binary output

Implement
--yamlswitch for YAML format output, which allows usingyqor nushell to interact with the result.Actually I was initially working on #1765, but could not reach to a perfect state. JSON afraids trailing commas, making it not so easy to statelessly stream the result without a buffer. Considering a large number of potiential results, it would be memory-consuming to store all lines and print them at last.
On the other side, the YAML format, while infamous for its complexity on parsing, is friendly for streaming output. No need for serializing or extra dependencies, the simple and fast
write!is all you need.There are tools like
yqthat work just like belovedjq, and nushell supports YAML as well, so I suggest this PR might be able to close #1765.