mod color;
mod config;
mod exit;
mod filter;
mod format;
mod format_meta;
mod tree;
mod walker;
use anyhow::{Context, Result};
use clap::Parser;
use color::{ColorMode, Colorizer};
use config::{OutputFormat, SortKey};
use filter::Filter;
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use tree::RenderOptions;
#[derive(Parser, Debug)]
#[command(
name = "bush",
version,
about = "A tree command substitute that respects .*ignore files"
)]
struct Cli {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(short = 'I', long = "ignore-file", value_name = "NAME")]
ignore_file: Vec<String>,
#[arg(long)]
no_ignore: bool,
#[arg(short = 'H', long = "hidden", visible_alias = "include-hidden")]
hidden: bool,
#[arg(short = 'L', long = "max-depth", value_name = "N")]
max_depth: Option<usize>,
#[arg(short, long, value_name = "FILE")]
output: Option<PathBuf>,
#[arg(long, conflicts_with = "output")]
stdout: bool,
#[arg(short = 'd', long = "dirs-only")]
dirs_only: bool,
#[arg(long)]
follow_symlinks: bool,
#[arg(long, value_name = "FILE")]
config: Option<PathBuf>,
#[arg(long)]
no_config: bool,
#[arg(long, value_enum)]
color: Option<ColorMode>,
#[arg(short = 's', long = "show-sizes")]
show_sizes: bool,
#[arg(short = 'D', long = "show-mtime")]
show_mtime: bool,
#[arg(long, value_enum)]
sort: Option<SortKey>,
#[arg(short = 'r', long)]
reverse: bool,
#[arg(long = "include", value_name = "GLOB")]
include: Vec<String>,
#[arg(long = "exclude", value_name = "GLOB")]
exclude: Vec<String>,
#[arg(long = "noreport")]
no_report: bool,
#[arg(short = 'l', long = "show-symlink-target")]
show_symlink_target: bool,
#[arg(short = 'p', long = "show-permissions")]
show_permissions: bool,
#[arg(long, value_enum)]
format: Option<OutputFormat>,
}
fn main() {
if let Err(err) = run() {
eprintln!("Error: {err:#}");
std::process::exit(exit::ERROR);
}
std::process::exit(exit::SUCCESS);
}
fn run() -> Result<()> {
let _ = ctrlc::set_handler(|| std::process::exit(exit::INTERRUPTED));
let cli = Cli::parse();
let search_dir = if cli.path.is_file() {
cli.path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| cli.path.clone())
} else {
cli.path.clone()
};
let home = std::env::var_os("HOME")
.map(PathBuf::from)
.map(|p| p.canonicalize().unwrap_or(p));
let xdg_config = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.filter(|p| !p.as_os_str().is_empty());
let bush_config_env = std::env::var_os("BUSH_CONFIG").map(PathBuf::from);
let explicit_config = cli.config.clone().or(bush_config_env);
let mut config = config::load_layered(
explicit_config.as_deref(),
cli.no_config,
&search_dir,
home.as_deref(),
xdg_config.as_deref(),
)?;
if cli.no_ignore {
config.use_ignore = false;
}
for name in &cli.ignore_file {
if !config.ignore_files.contains(name) {
config.ignore_files.push(name.clone());
}
}
if cli.hidden {
config.include_hidden = true;
}
if let Some(d) = cli.max_depth {
config.max_depth = Some(d);
}
if let Some(o) = cli.output {
config.output = Some(o);
}
if cli.stdout {
config.output = None;
}
if cli.dirs_only {
config.directories_only = true;
}
if cli.follow_symlinks {
config.follow_symlinks = true;
}
if let Some(c) = cli.color {
config.color = c;
}
if cli.show_sizes {
config.show_sizes = true;
}
if cli.show_mtime {
config.show_mtime = true;
}
if let Some(s) = cli.sort {
config.sort = s;
}
if cli.reverse {
config.reverse = true;
}
if !cli.include.is_empty() {
config.include = cli.include;
}
if !cli.exclude.is_empty() {
config.exclude = cli.exclude;
}
if cli.no_report {
config.no_report = true;
}
if cli.show_symlink_target {
config.show_symlink_target = true;
}
if cli.show_permissions {
config.show_permissions = true;
}
if let Some(f) = cli.format {
config.format = f;
}
let filter = Filter::new(&config.include, &config.exclude)?;
let prune_empty_dirs = filter.has_include();
let entries = walker::walk(&cli.path, &config, &filter)?;
let writing_to_file = config.output.is_some();
let is_tty =
!writing_to_file && matches!(config.format, OutputFormat::Tree) && color::stdout_is_tty();
let opts = RenderOptions {
colorizer: Colorizer::new(config.color, is_tty),
show_sizes: config.show_sizes,
show_mtime: config.show_mtime,
show_symlink_target: config.show_symlink_target,
show_permissions: config.show_permissions,
sort: config.sort,
reverse: config.reverse,
prune_empty_dirs,
no_report: config.no_report,
};
match config.output.as_ref() {
Some(p) => {
let file = std::fs::File::create(p)
.with_context(|| format!("creating output file {}", p.display()))?;
let mut out = BufWriter::new(file);
dispatch(&cli.path, &entries, &opts, config.format, &mut out)?;
out.flush()?;
}
None => {
let stdout = std::io::stdout();
let mut out = BufWriter::new(stdout.lock());
dispatch(&cli.path, &entries, &opts, config.format, &mut out)?;
}
}
Ok(())
}
fn dispatch<W: Write>(
root: &std::path::Path,
entries: &[walker::Entry],
opts: &RenderOptions,
fmt: OutputFormat,
out: &mut W,
) -> Result<()> {
match fmt {
OutputFormat::Tree => tree::render(root, entries, opts, out)?,
OutputFormat::Json => {
let prepared = tree::prepare(root, entries, opts);
format::render_json(root, &prepared, out)?;
}
OutputFormat::Html => {
let prepared = tree::prepare(root, entries, opts);
format::render_html(root, &prepared, out)?;
}
OutputFormat::Xml => {
let prepared = tree::prepare(root, entries, opts);
format::render_xml(root, &prepared, out)?;
}
}
Ok(())
}