Skip to content

Commit f32bc2f

Browse files
committed
✨ Add -O/--show-fflag option to list subcommand
1 parent d6da306 commit f32bc2f

4 files changed

Lines changed: 318 additions & 80 deletions

File tree

cli/src/command/list.rs

Lines changed: 142 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ pub(crate) struct ListCommand {
7070
help = "Display ACLs in a table (unstable)"
7171
)]
7272
show_acl: bool,
73+
#[arg(
74+
short = 'O',
75+
long = "show-fflags",
76+
help = "Display file flags (uchg, nodump, hidden, etc.)"
77+
)]
78+
show_fflags: bool,
7379
#[arg(
7480
long = "private",
7581
requires = "unstable",
@@ -310,6 +316,7 @@ struct TableRow {
310316
xattrs: Vec<ExtendedAttribute>,
311317
acl: HashMap<chunk::AcePlatform, Vec<chunk::Ace>>,
312318
privates: Vec<RawChunk>,
319+
fflags: Vec<String>,
313320
}
314321

315322
impl TableRow {
@@ -383,6 +390,7 @@ where
383390
.filter(|it| it.ty() != chunk::faCe && it.ty() != chunk::faCl)
384391
.map(|it| (*it).clone().into())
385392
.collect::<Vec<_>>(),
393+
fflags: entry.fflags(),
386394
})
387395
}
388396
}
@@ -408,6 +416,7 @@ fn list_archive(args: ListCommand, color: ColorChoice) -> anyhow::Result<()> {
408416
solid: args.solid,
409417
show_xattr: args.show_xattr,
410418
show_acl: args.show_acl,
419+
show_fflags: args.show_fflags,
411420
show_private: args.show_private,
412421
time_format: if args.long_time {
413422
TimeFormat::Long
@@ -494,6 +503,7 @@ pub(crate) struct ListOptions {
494503
pub(crate) solid: bool,
495504
pub(crate) show_xattr: bool,
496505
pub(crate) show_acl: bool,
506+
pub(crate) show_fflags: bool,
497507
pub(crate) show_private: bool,
498508
pub(crate) time_format: TimeFormat,
499509
pub(crate) time_field: TimeField,
@@ -601,7 +611,7 @@ fn print_formatted_entries(
601611
) -> anyhow::Result<()> {
602612
match options.format {
603613
Some(Format::Line) => simple_list_entries_to(entries, options, out)?,
604-
Some(Format::JsonL) => json_line_entries_to(entries, out)?,
614+
Some(Format::JsonL) => json_line_entries_to(entries, options, out)?,
605615
Some(Format::Table) => detail_list_entries_to(entries, options, out)?,
606616
Some(Format::Tree) => tree_entries_to(entries, options, out)?,
607617
Some(Format::BsdTar) => bsd_tar_list_entries_to(entries, options, out)?,
@@ -712,23 +722,24 @@ fn detail_list_entries_to(
712722
) -> io::Result<()> {
713723
let underline = Color::new("\x1B[4m", "\x1B[0m");
714724
let reset = Color::new("\x1B[8m", "\x1B[0m");
715-
let header = [
716-
"Encryption",
717-
"Compression",
718-
"Permissions",
719-
"Raw Size",
720-
"Compressed Size",
721-
"User",
722-
"Group",
723-
options.time_field.as_str(),
724-
"Name",
725-
];
726725
let mut acl_rows = Vec::new();
727726
let mut xattr_rows = Vec::new();
728727
let mut builder = TableBuilder::new();
729728
builder.set_empty(String::new());
730729
if options.header {
731-
builder.push_record(header);
730+
let header = [
731+
Some("Encryption"),
732+
Some("Compression"),
733+
Some("Permissions"),
734+
options.show_fflags.then_some("Fflags"),
735+
Some("Raw Size"),
736+
Some("Compressed Size"),
737+
Some("User"),
738+
Some("Group"),
739+
Some(options.time_field.as_str()),
740+
Some("Name"),
741+
];
742+
builder.push_record(header.into_iter().flatten());
732743
}
733744
for content in entries {
734745
let has_acl = !content.acl.is_empty();
@@ -742,40 +753,44 @@ fn detail_list_entries_to(
742753
|| "-".into(),
743754
|it| it.group_display(options.numeric_owner).to_string(),
744755
);
745-
builder.push_record([
746-
content.encryption,
747-
content.compression,
748-
paint_permission(&content.entry_type, permission_mode, has_xattr, has_acl),
749-
content
750-
.raw_size
751-
.map_or_else(|| "-".into(), |size| size.to_string()),
752-
content.compressed_size.to_string(),
753-
user,
754-
group,
755-
match options.time_field {
756-
TimeField::Created => content.created,
757-
TimeField::Modified => content.modified,
758-
TimeField::Accessed => content.accessed,
759-
}
760-
.map_or_else(|| "-".into(), |d| datetime(options.time_format, d)),
761-
{
762-
let name = match content.entry_type {
763-
EntryType::Directory(path) if options.classify => format!("{path}/"),
764-
EntryType::SymbolicLink(name, link_to) if options.classify => {
765-
format!("{name}@ -> {link_to}")
756+
builder.push_record(
757+
[
758+
Some(content.encryption),
759+
Some(content.compression),
760+
Some(paint_permission(
761+
&content.entry_type,
762+
permission_mode,
763+
has_xattr,
764+
has_acl,
765+
)),
766+
options.show_fflags.then(|| {
767+
if content.fflags.is_empty() {
768+
"-".into()
769+
} else {
770+
content.fflags.join(",")
766771
}
767-
EntryType::File(path) | EntryType::Directory(path) => path,
768-
EntryType::SymbolicLink(path, link_to) | EntryType::HardLink(path, link_to) => {
769-
format!("{path} -> {link_to}")
772+
}),
773+
Some(
774+
content
775+
.raw_size
776+
.map_or_else(|| "-".into(), |size| size.to_string()),
777+
),
778+
Some(content.compressed_size.to_string()),
779+
Some(user),
780+
Some(group),
781+
Some(
782+
match options.time_field {
783+
TimeField::Created => content.created,
784+
TimeField::Modified => content.modified,
785+
TimeField::Accessed => content.accessed,
770786
}
771-
};
772-
if options.hide_control_chars {
773-
hide_control_chars(&name).to_string()
774-
} else {
775-
name
776-
}
777-
},
778-
]);
787+
.map_or_else(|| "-".into(), |d| datetime(options.time_format, d)),
788+
),
789+
Some(detailed_format_name(content.entry_type, options)),
790+
]
791+
.into_iter()
792+
.flatten(),
793+
);
779794
if options.show_acl {
780795
let acl = content.acl.into_iter().flat_map(|(platform, ace)| {
781796
ace.into_iter().map(move |it| chunk::AceWithPlatform {
@@ -811,21 +826,30 @@ fn detail_list_entries_to(
811826
}
812827
}
813828
let mut table = builder.build();
829+
// Determine size columns for right alignment
830+
let size_cols_start = if options.show_fflags { 4 } else { 3 };
831+
let size_cols_end = size_cols_start + 1;
814832
table
815833
.with(TableStyle::empty())
816-
.with(Colorization::columns([
817-
Color::FG_MAGENTA,
818-
Color::FG_BLUE,
819-
Color::empty(),
820-
Color::FG_GREEN,
821-
Color::FG_GREEN,
822-
Color::FG_CYAN,
823-
Color::FG_CYAN,
824-
Color::FG_CYAN,
825-
Color::FG_CYAN,
826-
Color::empty(),
827-
]))
828-
.with(Modify::new(Segment::new(.., 3..=4)).with(Alignment::right()));
834+
.with(Colorization::columns(
835+
[
836+
Some(Color::FG_MAGENTA), // Encryption
837+
Some(Color::FG_BLUE), // Compression
838+
Some(Color::empty()), // Permissions
839+
options.show_fflags.then_some(Color::FG_YELLOW), // Fflags
840+
Some(Color::FG_GREEN), // Raw Size
841+
Some(Color::FG_GREEN), // Compressed Size
842+
Some(Color::FG_CYAN), // User
843+
Some(Color::FG_CYAN), // Group
844+
Some(Color::FG_CYAN), // Time
845+
Some(Color::empty()), // Name
846+
]
847+
.into_iter()
848+
.flatten(),
849+
))
850+
.with(
851+
Modify::new(Segment::new(.., size_cols_start..=size_cols_end)).with(Alignment::right()),
852+
);
829853
if options.header {
830854
table.with(Colorization::exact([underline], Rows::first()));
831855
}
@@ -838,6 +862,24 @@ fn detail_list_entries_to(
838862
writeln!(out, "{table}")
839863
}
840864

865+
fn detailed_format_name(entry: EntryType, options: &ListOptions) -> String {
866+
let name = match entry {
867+
EntryType::Directory(path) if options.classify => format!("{path}/"),
868+
EntryType::SymbolicLink(name, link_to) if options.classify => {
869+
format!("{name}@ -> {link_to}")
870+
}
871+
EntryType::File(path) | EntryType::Directory(path) => path,
872+
EntryType::SymbolicLink(path, link_to) | EntryType::HardLink(path, link_to) => {
873+
format!("{path} -> {link_to}")
874+
}
875+
};
876+
if options.hide_control_chars {
877+
hide_control_chars(&name).to_string()
878+
} else {
879+
name
880+
}
881+
}
882+
841883
const DURATION_SIX_MONTH: Duration = Duration::from_secs(60 * 60 * 24 * 30 * 6);
842884

843885
fn within_six_months(now: SystemTime, x: SystemTime) -> bool {
@@ -999,6 +1041,8 @@ struct FileInfo<'a> {
9991041
created: String,
10001042
modified: String,
10011043
accessed: String,
1044+
#[serde(skip_serializing_if = "Option::is_none")]
1045+
fflags: Option<&'a [String]>,
10021046
acl: Vec<AclEntry>,
10031047
xattr: Vec<XAttr<'a>>,
10041048
}
@@ -1015,7 +1059,12 @@ struct XAttr<'a> {
10151059
value: String,
10161060
}
10171061

1018-
fn json_line_entries_to(entries: Vec<TableRow>, mut out: impl Write) -> anyhow::Result<()> {
1062+
fn json_line_entries_to(
1063+
entries: Vec<TableRow>,
1064+
options: &ListOptions,
1065+
mut out: impl Write,
1066+
) -> anyhow::Result<()> {
1067+
let show_fflags = options.show_fflags;
10191068
let entries = entries
10201069
.par_iter()
10211070
.map(|it| {
@@ -1051,6 +1100,11 @@ fn json_line_entries_to(entries: Vec<TableRow>, mut out: impl Write) -> anyhow::
10511100
accessed: it
10521101
.accessed
10531102
.map_or_else(String::new, |d| datetime(TimeFormat::Long, d)),
1103+
fflags: if show_fflags {
1104+
Some(it.fflags.as_slice())
1105+
} else {
1106+
None
1107+
},
10541108
acl: it
10551109
.acl
10561110
.iter()
@@ -1103,17 +1157,22 @@ fn delimited_entries_to(
11031157
options: &ListOptions,
11041158
mut wtr: csv::Writer<impl Write>,
11051159
) -> io::Result<()> {
1106-
wtr.write_record([
1107-
"filename",
1108-
"permissions",
1109-
"owner",
1110-
"group",
1111-
"raw_size",
1112-
"compressed_size",
1113-
"encryption",
1114-
"compression",
1115-
options.time_field.as_str(),
1116-
])?;
1160+
wtr.write_record(
1161+
[
1162+
Some("filename"),
1163+
Some("permissions"),
1164+
Some("owner"),
1165+
Some("group"),
1166+
Some("raw_size"),
1167+
Some("compressed_size"),
1168+
Some("encryption"),
1169+
Some("compression"),
1170+
options.show_fflags.then_some("fflags"),
1171+
Some(options.time_field.as_str()),
1172+
]
1173+
.into_iter()
1174+
.flatten(),
1175+
)?;
11171176

11181177
let rows = entries
11191178
.par_iter()
@@ -1133,21 +1192,24 @@ fn delimited_entries_to(
11331192
.map_or_else(String::new, |d| datetime(TimeFormat::Long, d));
11341193

11351194
[
1136-
row.entry_type.name().to_string(),
1137-
permission_string(
1195+
Some(row.entry_type.name().to_string()),
1196+
Some(permission_string(
11381197
&row.entry_type,
11391198
permission_mode,
11401199
!row.xattrs.is_empty(),
11411200
!row.acl.is_empty(),
1142-
),
1143-
owner,
1144-
group,
1145-
row.raw_size.unwrap_or(0).to_string(),
1146-
row.compressed_size.to_string(),
1147-
row.encryption.clone(),
1148-
row.compression.clone(),
1149-
time,
1201+
)),
1202+
Some(owner),
1203+
Some(group),
1204+
Some(row.raw_size.unwrap_or(0).to_string()),
1205+
Some(row.compressed_size.to_string()),
1206+
Some(row.encryption.clone()),
1207+
Some(row.compression.clone()),
1208+
options.show_fflags.then(|| row.fflags.join(",")),
1209+
Some(time),
11501210
]
1211+
.into_iter()
1212+
.flatten()
11511213
})
11521214
.collect::<Vec<_>>();
11531215

cli/src/command/stdio.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,7 @@ fn run_list_archive(args: StdioCommand) -> anyhow::Result<()> {
824824
solid: true,
825825
show_xattr: false,
826826
show_acl: false,
827+
show_fflags: false,
827828
show_private: false,
828829
time_format: TimeFormat::Auto(SystemTime::now()),
829830
time_field: TimeField::default(),

cli/tests/cli/list.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ mod option_format_tree;
1717
mod option_format_tsv;
1818
#[cfg(not(target_family = "wasm"))]
1919
mod option_no_recursive;
20+
#[cfg(not(target_family = "wasm"))]
21+
mod option_show_fflags;

0 commit comments

Comments
 (0)