Skip to content
Merged
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 Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ timeago = { version = "0.6.0", default-features = false }
unicode-width = "0.2"
ansi-width = "0.1.0"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0"
dirs = "6.0.0"
serde_norway = "0.9"
backtrace = "0.3"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ If you want to pass arguments this way, use e.g. `nix run github:fxrdhan/lsr --
- **-R**, **--recurse**: recurse into directories
- **-T**, **--tree**: recurse into directories as a tree
- **--code[=MODE]**: print lines-of-code summary by language (Odin, Rust, C/C++, Python, Go, etc.)
- **--json**: output file listing and metadata as structured JSON
- **-x**, **--across**: sort the grid across, rather than downwards
- **-F**, **--classify[=(when)]**: display type indicator by file names (always, auto, never)
- **--colo[u]r=(when)**: when to use terminal colours (always, auto, never)
Expand Down
3 changes: 3 additions & 0 deletions man/eza.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ When used without a value, defaults to ‘`automatic`’.

: The given paths (or the current directory) are walked recursively, honouring a git repository’s `.gitignore` when one is present, and each recognised language (including Odin, Rust, C/C++, Python, Go, and 100+ others) is reported with its file, line, code, comment, and blank counts, plus a bar visualising its share of the code. Valid modes are ‘`lines`’, ‘`percent`’, and ‘`both`’ (the default).

`--json`
: Output file listing and metadata as structured JSON for easy parsing and scripting.

`--follow-symlinks`
: Drill down into symbolic links that point to directories.

Expand Down
3 changes: 3 additions & 0 deletions man/lsr.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ When used without a value, defaults to ‘`automatic`’.

: The given paths (or the current directory) are walked recursively, honouring a git repository’s `.gitignore` when one is present, and each recognised language (including Odin, Rust, C/C++, Python, Go, and 100+ others) is reported with its file, line, code, comment, and blank counts, plus a bar visualising its share of the code. Valid modes are ‘`lines`’, ‘`percent`’, and ‘`both`’ (the default).

`--json`
: Output file listing and metadata as structured JSON for easy parsing and scripting.

`--follow-symlinks`
: Drill down into symbolic links that point to directories.

Expand Down
1 change: 1 addition & 0 deletions src/fs/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::fs::File;
/// This object gets passed to the Files themselves, in order for them to
/// check the existence of surrounding files, then highlight themselves
/// accordingly. (See `File#get_source_files`)
#[derive(Debug)]
pub struct Dir {
/// A vector of the files that have been read from this directory.
contents: Vec<DirEntry>,
Expand Down
41 changes: 41 additions & 0 deletions src/fs/feature/xattr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,10 @@ const ATTRIBUTE_DISPLAYS: &[AttributeDisplay] = &[
attribute: "com.apple.macl",
display: display_macl,
},
AttributeDisplay {
attribute: "com.apple.ResourceFork",
display: display_resourcefork,
},
];

#[cfg(not(target_os = "macos"))]
Expand All @@ -608,6 +612,43 @@ fn display_lastuseddate(attribute: &Attribute) -> Option<String> {
})
}

// Decode Classic Mac OS Resource Fork headers
#[cfg(target_os = "macos")]
fn display_resourcefork(attribute: &Attribute) -> Option<String> {
let value = attribute.value.as_deref()?;
if value.len() < 16 {
return None;
}
let map_offset = u32::from_be_bytes(value[4..8].try_into().ok()?) as usize;
let map_len = u32::from_be_bytes(value[12..16].try_into().ok()?) as usize;
if value.len() < map_offset.checked_add(map_len)? || map_len < 28 {
return None;
}
let map = &value[map_offset..map_offset + map_len];
let type_list_offset = u16::from_be_bytes(map[24..26].try_into().ok()?) as usize;
if map.len() < type_list_offset.checked_add(2)? {
return None;
}
let type_list = &map[type_list_offset..];
let num_types_minus_1 = u16::from_be_bytes(type_list[0..2].try_into().ok()?);
let num_types = num_types_minus_1 as usize + 1;
if type_list.len() < 2 + num_types * 8 {
return None;
}
let mut resources = Vec::new();
for i in 0..num_types {
let entry = &type_list[2 + i * 8..2 + (i + 1) * 8];
let type_code = &entry[0..4];
let count = u16::from_be_bytes(entry[4..6].try_into().ok()?) + 1;
let type_str = match std::str::from_utf8(type_code) {
Ok(s) => s.trim().to_string(),
Err(_) => format!("{type_code:02x?}"),
};
resources.push(format!("{type_str}: {count}"));
}
Some(format!("[{}]", resources.join(", ")))
}

// com.apple.macl is a two byte flag followed by a uuid for the application
#[cfg(target_os = "macos")]
fn format_macl(value: &[u8]) -> String {
Expand Down
20 changes: 20 additions & 0 deletions src/fs/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,26 @@ impl<'dir> File<'dir> {
pub fn flags(&self) -> f::Flags {
f::Flags(0)
}

#[cfg(unix)]
pub fn permissions_plus(&self, xattrs: bool) -> Option<f::PermissionsPlus> {
self.permissions().map(|p| f::PermissionsPlus {
file_type: self.type_char(),
permissions: p,
xattrs,
})
}

#[allow(clippy::unnecessary_wraps)] // Needs to match Unix function
#[cfg(windows)]
pub fn permissions_plus(&self, xattrs: bool) -> Option<f::PermissionsPlus> {
Some(f::PermissionsPlus {
file_type: self.type_char(),
#[cfg(windows)]
attributes: self.attributes()?,
xattrs,
})
}
}

impl<'a> AsRef<File<'a>> for File<'a> {
Expand Down
29 changes: 28 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ use crate::fs::filter::{FileFilterFlags::OnlyFiles, GitIgnore};
use crate::fs::{Dir, File};
use crate::options::stdin::FilesInput;
use crate::options::{Options, Vars, vars};
use crate::output::{Mode, View, code, details, escape, file_name, grid, grid_details, lines};
use crate::output::{
Mode, View, code, details, escape, file_name, grid, grid_details, json, lines,
};
use crate::theme::Theme;
use log::*;

Expand Down Expand Up @@ -265,6 +267,14 @@ fn git_repos(options: &Options, args: &[&OsStr]) -> bool {
..
},
..
})
| Mode::Json(json::Options {
details:
Some(details::Options {
table: Some(ref table),
..
}),
..
}) => table.columns.subdir_git_repos || table.columns.subdir_git_repos_no_stat,
_ => false,
};
Expand Down Expand Up @@ -367,6 +377,21 @@ impl Exa<'_> {
let no_files = files.is_empty();
let is_only_dir = dirs.len() == 1 && no_files;

// Separate json mode as there is special cases for multi directories cases
if let Mode::Json(opts) = &self.options.view.mode {
let r = json::Render::new(
self.git.as_ref(),
self.options.filter.dot_filter,
opts,
self.options.filter.git_ignore == GitIgnore::CheckAndIgnore,
self.git_repos,
&self.options,
);

r.render(files, dirs, &mut self.writer)?;
return Ok(exit_status);
}

self.options.filter.filter_argument_files(&mut files);
self.print_files(None, files)?;

Expand Down Expand Up @@ -625,6 +650,8 @@ impl Exa<'_> {
// The code summary never lists files; it’s handled up front in
// `run` before we ever get here.
(Mode::Code(_), _) => unreachable!("--code is handled in Exa::run"),

(Mode::Json(_), _) => unreachable!("--json is handled in Exa::run"),
};
result?;

Expand Down
2 changes: 2 additions & 0 deletions src/options/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ pub struct UiStylesOverride {

pub filenames: Option<HashMap<String, FileNameStyleOverride>>,
pub extensions: Option<HashMap<String, FileNameStyleOverride>>,
pub directorynames: Option<HashMap<String, FileNameStyleOverride>>,
}

impl FromOverride<UiStylesOverride> for UiStyles {
Expand Down Expand Up @@ -613,6 +614,7 @@ impl FromOverride<UiStylesOverride> for UiStyles {

filenames: FromOverride::from(value.filenames, default.filenames),
extensions: FromOverride::from(value.extensions, default.extensions),
directorynames: FromOverride::from(value.directorynames, default.directorynames),
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion src/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ use clap::ArgMatches;
use crate::fs::dir_action::DirAction;
use crate::fs::filter::{FileFilter, GitIgnore};
use crate::options::stdin::FilesInput;
use crate::output::{Mode, View, details, grid_details};
use crate::output::{Mode, View, details, grid_details, json};
use crate::theme::Options as ThemeOptions;

mod dir_action;
Expand Down Expand Up @@ -144,6 +144,14 @@ impl Options {
..
},
..
})
| Mode::Json(json::Options {
details:
Some(details::Options {
table: Some(ref table),
..
}),
..
}) => table.columns.git,
_ => false,
}
Expand Down
1 change: 1 addition & 0 deletions src/options/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub fn get_command() -> clap::Command {
.arg(arg!(--"follow-symlinks" "drill down into symbolic links that point to directories"))
.arg(arg!(-w --width <COLS> "set screen width in columns")
.value_parser(value_parser!(usize)))
.arg(arg!(--json "display as a json object"))

.next_help_heading("DISPLAY OPTIONS")
.arg(arg!(-F --classify [WHEN] "display type indicator by file names")
Expand Down
72 changes: 71 additions & 1 deletion src/options/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::output::table::{
Columns, FlagsFormat, GroupFormat, Options as TableOptions, SizeFormat, TimeTypes, UserFormat,
};
use crate::output::time::TimeFormat;
use crate::output::{Mode, TerminalWidth, View, code, details, grid};
use crate::output::{Mode, TerminalWidth, View, code, details, grid, json};

use super::parser::{ColorScaleArgs, TimeArgs};

Expand Down Expand Up @@ -75,6 +75,12 @@ impl Mode {
let oneline = matches.get_flag("oneline");
let grid = matches.get_flag("grid");
let tree = matches.get_flag("tree");
let json = matches.get_flag("json");

if json {
let json = json::Options::deduce(matches, vars, long)?;
return Ok(Self::Json(json));
}

if !long && strict {
Self::strict_check_long_flags(matches)?;
Expand Down Expand Up @@ -165,6 +171,18 @@ impl grid::Options {
}
}

impl json::Options {
fn deduce<V: Vars>(matches: &ArgMatches, vars: &V, long: bool) -> Result<Self, OptionsError> {
let details = if long {
Some(details::Options::deduce_json(matches, vars)?)
} else {
None
};

Ok(json::Options { details })
}
}

impl details::Options {
fn deduce_tree<V: Vars>(matches: &ArgMatches, vars: &V) -> Self {
details::Options {
Expand All @@ -178,6 +196,18 @@ impl details::Options {
}
}

fn deduce_json<V: Vars>(matches: &ArgMatches, vars: &V) -> Result<Self, OptionsError> {
Ok(details::Options {
table: Some(TableOptions::deduce(matches, vars)?),
header: false,
xattr: xattr::ENABLED && matches.get_flag("extended"),
secattr: xattr::ENABLED && matches.get_flag("security-context"),
mounts: matches.get_flag("mounts"),
color_scale: ColorScaleOptions::default(),
follow_links: matches.get_flag("follow-symlinks"),
})
}

fn deduce_long<V: Vars>(
matches: &ArgMatches,
vars: &V,
Expand Down Expand Up @@ -1407,4 +1437,44 @@ mod tests {
let view = View::deduce(&matches, &MockVars::default(), false).unwrap();
assert!(!view.total_entries);
}

#[test]
fn test_deduce_json_short() {
let matches = mock_cli(vec!["--json"]);
let mode = Mode::deduce(&matches, &MockVars::default(), false, false).unwrap();
match mode {
Mode::Json(opts) => {
assert!(opts.details.is_none());
}
_ => panic!("Expected Mode::Json"),
}
}

#[test]
fn test_deduce_json_long() {
let matches = mock_cli(vec!["--long", "--json"]);
let mode = Mode::deduce(&matches, &MockVars::default(), false, false).unwrap();
match mode {
Mode::Json(opts) => {
assert!(opts.details.is_some());
}
_ => panic!("Expected Mode::Json with details"),
}
}

#[test]
fn test_deduce_json_columns() {
let matches = mock_cli(vec!["--long", "--octal-permissions", "--bytes", "--json"]);
let mode = Mode::deduce(&matches, &MockVars::default(), false, false).unwrap();
match mode {
Mode::Json(opts) => {
let details = opts.details.expect("details must be Some");
let table = details.table.expect("table must be Some");
#[cfg(unix)]
assert!(table.columns.octal);
assert_eq!(table.size_format, SizeFormat::JustBytes);
}
_ => panic!("Expected Mode::Json"),
}
}
}
36 changes: 20 additions & 16 deletions src/output/details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,19 +250,6 @@ impl<'a> Render<'a> {
}
}

/// Whether to show the extended attribute hint
pub fn show_xattr_hint(&self, file: &File<'_>) -> bool {
// Do not show the hint '@' if the only extended attribute is the security
// attribute and the security attribute column is active.
let xattr_count = file.extended_attributes().len();
let selinux_ctx_shown = self.opts.secattr
&& match file.security_context().context {
SecurityContextType::SELinux(_) => true,
SecurityContextType::None => false,
};
xattr_count > 1 || (xattr_count == 1 && !selinux_ctx_shown)
}

/// Adds files to the table, possibly recursively. This is easily
/// parallelisable, and uses a pool of threads.
fn add_files_to_table<'dir>(
Expand Down Expand Up @@ -308,9 +295,13 @@ impl<'a> Render<'a> {
&[]
};

let table_row = table
.as_ref()
.map(|t| t.row_for_file(file, self.show_xattr_hint(file), color_scale_info));
let table_row = table.as_ref().map(|t| {
t.row_for_file(
file,
show_xattr_hint(self.opts.secattr, file),
color_scale_info,
)
});

let mut dir = None;
let follow_links = self.opts.follow_links;
Expand Down Expand Up @@ -555,3 +546,16 @@ impl Iterator for Iter {
})
}
}

/// Whether to show the extended attribute hint
pub fn show_xattr_hint(secattr: bool, file: &File<'_>) -> bool {
// Do not show the hint '@' if the only extended attribute is the security
// attribute and the security attribute column is active.
let xattr_count = file.extended_attributes().len();
let selinux_ctx_shown = secattr
&& match file.security_context().context {
SecurityContextType::SELinux(_) => true,
SecurityContextType::None => false,
};
xattr_count > 1 || (xattr_count == 1 && !selinux_ctx_shown)
}
Loading