#![allow(clippy::unwrap_used)]
use async_trait::async_trait;
use std::ffi::OsString;
use std::path::Path;
use crate::builtins::clap_env::apply_env_defaults;
use crate::builtins::generated::ls_args::{LS_ENV_DEFAULTS, ls_command};
use crate::builtins::{Builtin, Context, resolve_path};
use crate::error::Result;
use crate::fs::FileType;
use crate::interpreter::ExecResult;
const LS_SUPPORTED_IDS: &[&str] = &[
"long", "all", "human-readable", "1", "recursive", "t", "classify", "C", "paths",
"help",
];
pub(super) struct LsOptions {
pub(super) long: bool,
pub(super) all: bool,
pub(super) human: bool,
pub(super) one_per_line: bool,
pub(super) recursive: bool,
pub(super) sort_by_time: bool,
pub(super) classify: bool,
pub(super) columns: bool,
}
pub struct Ls;
#[async_trait]
impl Builtin for Ls {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
let argv: Vec<OsString> = std::iter::once(OsString::from("ls"))
.chain(ctx.args.iter().map(OsString::from))
.collect();
let argv = apply_env_defaults(argv, LS_ENV_DEFAULTS, ctx.env);
let cmd = ls_command().help_template("Usage: {usage}\n{about}\n\n{all-args}\n");
let matches = match cmd.try_get_matches_from(argv) {
Ok(m) => m,
Err(e) => {
let kind = e.kind();
let rendered = e.render().to_string();
if matches!(
kind,
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion
) {
return Ok(ExecResult::ok(rendered));
}
return Ok(ExecResult::err(rendered, 2));
}
};
let unsupported: Vec<String> = matches
.ids()
.filter(|id| {
let name = id.as_str();
!LS_SUPPORTED_IDS.contains(&name)
&& matches.value_source(name) != Some(clap::parser::ValueSource::DefaultValue)
})
.map(|id| id.as_str().to_string())
.collect();
if !unsupported.is_empty() {
return Ok(ExecResult::err(
format!(
"ls: option(s) not yet implemented in bashkit: {}\n",
unsupported.join(", ")
),
2,
));
}
let classify = matches.contains_id("classify")
&& matches
.get_one::<String>("classify")
.map(|v| v != "never")
.unwrap_or(true);
let opts = LsOptions {
long: matches.get_flag("long"),
all: matches.get_flag("all"),
human: matches.get_flag("human-readable"),
one_per_line: matches.get_flag("1"),
recursive: matches.get_flag("recursive"),
sort_by_time: matches.get_flag("t"),
classify,
columns: matches.get_flag("C"),
};
let paths_owned: Vec<String> = matches
.get_many::<OsString>("paths")
.map(|vs| vs.map(|v| v.to_string_lossy().into_owned()).collect())
.unwrap_or_default();
let mut paths: Vec<&str> = paths_owned.iter().map(String::as_str).collect();
if paths.is_empty() {
paths.push(".");
}
let mut output = String::new();
let multiple_paths = paths.len() > 1 || opts.recursive;
let mut file_args: Vec<(&str, crate::fs::Metadata)> = Vec::new();
let mut dir_args: Vec<(usize, &str, std::path::PathBuf)> = Vec::new();
for (i, path_str) in paths.iter().enumerate() {
let path = resolve_path(ctx.cwd, path_str);
if !ctx.fs.exists(&path).await.unwrap_or(false) {
return Ok(ExecResult::err(
format!(
"ls: cannot access '{}': No such file or directory\n",
path_str
),
2,
));
}
let metadata = ctx.fs.stat(&path).await?;
if metadata.file_type.is_file() {
file_args.push((path_str, metadata));
} else {
dir_args.push((i, path_str, path));
}
}
if opts.sort_by_time {
file_args.sort_by_key(|entry| std::cmp::Reverse(entry.1.modified));
}
if opts.long {
for (path_str, metadata) in &file_args {
let mut entry = format_long_entry(path_str, metadata, opts.human);
if opts.classify {
let suffix = classify_suffix(metadata);
if !suffix.is_empty() {
entry.insert_str(entry.len() - 1, suffix);
}
}
output.push_str(&entry);
}
} else if !file_args.is_empty() {
let names: Vec<String> = file_args
.iter()
.map(|(path_str, metadata)| {
let mut name = (*path_str).to_string();
if opts.classify {
name.push_str(classify_suffix(metadata));
}
name
})
.collect();
if opts.columns && !opts.one_per_line {
output.push_str(&format_columns(&names, 80));
} else {
for name in &names {
output.push_str(name);
output.push('\n');
}
}
}
for (i, path_str, path) in &dir_args {
if let Err(e) = list_directory(
&ctx,
path,
path_str,
&mut output,
&opts,
multiple_paths,
*i > 0 || !file_args.is_empty(),
)
.await
{
return Ok(ExecResult::err(format!("ls: {}\n", e), 2));
}
}
Ok(ExecResult::ok(output))
}
}
async fn list_directory(
ctx: &Context<'_>,
path: &Path,
display_path: &str,
output: &mut String,
opts: &LsOptions,
show_header: bool,
add_newline: bool,
) -> std::result::Result<(), String> {
if add_newline {
output.push('\n');
}
if show_header {
output.push_str(&format!("{}:\n", display_path));
}
let entries = ctx
.fs
.read_dir(path)
.await
.map_err(|e| format!("cannot open directory '{}': {}", display_path, e))?;
let mut sorted_entries = entries;
if opts.sort_by_time {
sorted_entries.sort_by_key(|entry| std::cmp::Reverse(entry.metadata.modified));
} else {
sorted_entries.sort_by(|a, b| a.name.cmp(&b.name));
}
let filtered: Vec<_> = sorted_entries
.iter()
.filter(|e| opts.all || !e.name.starts_with('.'))
.collect();
let mut subdirs: Vec<(std::path::PathBuf, String)> = Vec::new();
if opts.long {
for entry in &filtered {
let mut line = format_long_entry(&entry.name, &entry.metadata, opts.human);
if opts.classify {
let suffix = classify_suffix(&entry.metadata);
if !suffix.is_empty() {
line.insert_str(line.len() - 1, suffix);
}
}
output.push_str(&line);
if opts.recursive && entry.metadata.file_type.is_dir() {
subdirs.push((
path.join(&entry.name),
format!("{}/{}", display_path, entry.name),
));
}
}
} else {
let mut names: Vec<String> = Vec::new();
for entry in &filtered {
let mut name = entry.name.clone();
if opts.classify {
name.push_str(classify_suffix(&entry.metadata));
}
names.push(name);
if opts.recursive && entry.metadata.file_type.is_dir() {
subdirs.push((
path.join(&entry.name),
format!("{}/{}", display_path, entry.name),
));
}
}
if opts.columns && !opts.one_per_line {
output.push_str(&format_columns(&names, 80));
} else {
for name in &names {
output.push_str(name);
output.push('\n');
}
}
}
if opts.recursive {
for (subpath, display) in subdirs {
Box::pin(list_directory(
ctx, &subpath, &display, output, opts, true, true,
))
.await?;
}
}
Ok(())
}
pub(super) fn classify_suffix(metadata: &crate::fs::Metadata) -> &'static str {
match metadata.file_type {
FileType::Directory => "/",
FileType::Symlink => "@",
FileType::Fifo => "|",
FileType::File => {
if metadata.mode & 0o111 != 0 { "*" } else { "" }
}
}
}
pub(super) fn format_columns(entries: &[String], terminal_width: usize) -> String {
if entries.is_empty() {
return String::new();
}
let max_width = entries.iter().map(|e| e.len()).max().unwrap_or(0);
let max_possible_cols = (terminal_width / (max_width.min(1) + 2)).max(1);
let mut num_cols = 1;
let mut col_widths: Vec<usize> = vec![0];
let mut num_rows = entries.len();
for try_cols in 2..=max_possible_cols.min(entries.len()) {
let try_rows = entries.len().div_ceil(try_cols);
let mut widths = vec![0usize; try_cols];
for (i, entry) in entries.iter().enumerate() {
let col = i / try_rows;
if col < try_cols {
widths[col] = widths[col].max(entry.len());
}
}
let total: usize = widths.iter().sum::<usize>() + (try_cols - 1) * 2;
if total <= terminal_width {
num_cols = try_cols;
col_widths = widths;
num_rows = try_rows;
}
}
let mut output = String::new();
for row in 0..num_rows {
for (col, col_w) in col_widths.iter().enumerate() {
let idx = col * num_rows + row;
if idx < entries.len() {
let is_last = col == num_cols - 1 || idx + num_rows >= entries.len();
if is_last {
output.push_str(&entries[idx]);
} else {
let width = col_w + 2; output.push_str(&format!("{:<width$}", entries[idx], width = width));
}
}
}
output.push('\n');
}
output
}
pub(super) fn format_long_entry(name: &str, metadata: &crate::fs::Metadata, human: bool) -> String {
let file_type = match metadata.file_type {
FileType::Directory => 'd',
FileType::Symlink => 'l',
FileType::Fifo => 'p',
FileType::File => '-',
};
let mode = metadata.mode;
let perms = format!(
"{}{}{}{}{}{}{}{}{}",
if mode & 0o400 != 0 { 'r' } else { '-' },
if mode & 0o200 != 0 { 'w' } else { '-' },
if mode & 0o100 != 0 { 'x' } else { '-' },
if mode & 0o040 != 0 { 'r' } else { '-' },
if mode & 0o020 != 0 { 'w' } else { '-' },
if mode & 0o010 != 0 { 'x' } else { '-' },
if mode & 0o004 != 0 { 'r' } else { '-' },
if mode & 0o002 != 0 { 'w' } else { '-' },
if mode & 0o001 != 0 { 'x' } else { '-' },
);
let size = if human {
human_readable_size(metadata.size)
} else {
format!("{:>8}", metadata.size)
};
let modified = metadata
.modified
.duration_since(std::time::UNIX_EPOCH)
.map(|d| {
let secs = d.as_secs();
let days = secs / 86400;
let hours = (secs % 86400) / 3600;
let mins = (secs % 3600) / 60;
let years = 1970 + (days / 365);
let remaining_days = days % 365;
let month = remaining_days / 30 + 1;
let day = remaining_days % 30 + 1;
format!(
"{:04}-{:02}-{:02} {:02}:{:02}",
years, month, day, hours, mins
)
})
.unwrap_or_else(|_| "????-??-?? ??:??".to_string());
format!("{}{} {} {} {}\n", file_type, perms, size, modified, name)
}
fn human_readable_size(size: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = 1024 * KB;
const GB: u64 = 1024 * MB;
if size >= GB {
format!("{:>5.1}G", size as f64 / GB as f64)
} else if size >= MB {
format!("{:>5.1}M", size as f64 / MB as f64)
} else if size >= KB {
format!("{:>5.1}K", size as f64 / KB as f64)
} else {
format!("{:>6}", size)
}
}