#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::struct_excessive_bools)]
#![cfg_attr(
test,
allow(
clippy::cast_lossless,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::identity_op,
clippy::match_wildcard_for_single_variants,
clippy::semicolon_if_nothing_returned,
clippy::uninlined_format_args,
clippy::unreadable_literal,
)
)]
use anyhow::Result;
use clap::{ArgAction, Parser, ValueEnum};
mod balance;
mod check;
mod device;
mod filesystem;
mod inspect;
mod property;
mod qgroup;
mod quota;
mod receive;
mod reflink;
mod replace;
mod rescue;
mod restore;
mod scrub;
mod send;
mod subvolume;
mod util;
pub use crate::{
balance::*, check::*, device::*, filesystem::*, inspect::*, property::*,
qgroup::*, quota::*, receive::*, reflink::*, replace::*, rescue::*,
restore::*, scrub::*, send::*, subvolume::*,
};
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Format {
#[default]
Text,
Json,
Modern,
}
impl std::str::FromStr for Format {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
<Self as clap::ValueEnum>::from_str(s, true).map_err(|e| e.clone())
}
}
#[derive(
Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum,
)]
pub enum Level {
Debug,
#[default]
Info,
Warn,
Error,
}
#[derive(Parser, Debug)]
#[allow(clippy::doc_markdown)]
#[clap(
version,
infer_subcommands = true,
arg_required_else_help = true,
max_term_width = 100
)]
pub struct Arguments {
#[clap(flatten)]
pub global: GlobalOptions,
#[clap(subcommand)]
pub command: Command,
}
const GLOBAL_OPTIONS: &str = "Global options";
#[derive(Parser, Debug)]
pub struct GlobalOptions {
#[clap(global = true, short, long, action = ArgAction::Count, help_heading = GLOBAL_OPTIONS)]
pub verbose: u8,
#[clap(global = true, short, long, help_heading = GLOBAL_OPTIONS)]
pub quiet: bool,
#[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
pub dry_run: bool,
#[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
pub log: Option<Level>,
#[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
pub format: Option<Format>,
}
pub struct RunContext {
pub format: Format,
pub dry_run: bool,
pub quiet: bool,
}
pub trait Runnable {
fn run(&self, ctx: &RunContext) -> Result<()>;
fn supported_formats(&self) -> &[Format] {
&[Format::Text, Format::Modern]
}
fn supports_dry_run(&self) -> bool {
false
}
}
pub trait CommandGroup {
fn leaf(&self) -> &dyn Runnable;
}
impl<T: CommandGroup> Runnable for T {
fn run(&self, ctx: &RunContext) -> Result<()> {
self.leaf().run(ctx)
}
fn supported_formats(&self) -> &[Format] {
self.leaf().supported_formats()
}
fn supports_dry_run(&self) -> bool {
self.leaf().supports_dry_run()
}
}
#[derive(Parser, Debug)]
pub enum Command {
Balance(BalanceCommand),
Check(CheckCommand),
Device(DeviceCommand),
Filesystem(FilesystemCommand),
#[cfg(feature = "fuse")]
Fuse(btrfs_fuse::args::MountArgs),
#[command(alias = "inspect-internal")]
Inspect(InspectCommand),
#[cfg(feature = "mkfs")]
Mkfs(btrfs_mkfs::args::Arguments),
Property(PropertyCommand),
Qgroup(QgroupCommand),
Quota(QuotaCommand),
Receive(ReceiveCommand),
Reflink(ReflinkCommand),
Replace(ReplaceCommand),
Rescue(RescueCommand),
Restore(RestoreCommand),
Scrub(ScrubCommand),
Send(SendCommand),
Subvolume(SubvolumeCommand),
#[cfg(feature = "tune")]
Tune(btrfs_tune::args::Arguments),
}
#[cfg(feature = "fuse")]
impl Runnable for btrfs_fuse::args::MountArgs {
fn run(&self, _ctx: &RunContext) -> Result<()> {
btrfs_fuse::run::run_mount(self)
}
}
#[cfg(feature = "mkfs")]
impl Runnable for btrfs_mkfs::args::Arguments {
fn run(&self, _ctx: &RunContext) -> Result<()> {
btrfs_mkfs::run::run(self)
}
}
#[cfg(feature = "tune")]
impl Runnable for btrfs_tune::args::Arguments {
fn run(&self, _ctx: &RunContext) -> Result<()> {
btrfs_tune::run::run(self)
}
}
impl CommandGroup for Command {
fn leaf(&self) -> &dyn Runnable {
match self {
Command::Balance(cmd) => cmd,
Command::Check(cmd) => cmd,
Command::Device(cmd) => cmd,
Command::Filesystem(cmd) => cmd,
#[cfg(feature = "fuse")]
Command::Fuse(cmd) => cmd,
Command::Inspect(cmd) => cmd,
#[cfg(feature = "mkfs")]
Command::Mkfs(cmd) => cmd,
Command::Property(cmd) => cmd,
Command::Qgroup(cmd) => cmd,
Command::Quota(cmd) => cmd,
Command::Receive(cmd) => cmd,
Command::Reflink(cmd) => cmd,
Command::Replace(cmd) => cmd,
Command::Rescue(cmd) => cmd,
Command::Restore(cmd) => cmd,
Command::Scrub(cmd) => cmd,
Command::Send(cmd) => cmd,
Command::Subvolume(cmd) => cmd,
#[cfg(feature = "tune")]
Command::Tune(cmd) => cmd,
}
}
}
impl Arguments {
pub fn run(&self) -> Result<()> {
let level = if let Some(explicit) = self.global.log {
match explicit {
Level::Debug => log::LevelFilter::Debug,
Level::Info => log::LevelFilter::Info,
Level::Warn => log::LevelFilter::Warn,
Level::Error => log::LevelFilter::Error,
}
} else if self.global.quiet {
log::LevelFilter::Error
} else {
match self.global.verbose {
0 => log::LevelFilter::Warn,
1 => log::LevelFilter::Info,
2 => log::LevelFilter::Debug,
_ => log::LevelFilter::Trace,
}
};
env_logger::Builder::new().filter_level(level).init();
if self.global.dry_run && !self.command.supports_dry_run() {
anyhow::bail!(
"the --dry-run option is not supported by this command"
);
}
let format = self
.global
.format
.or_else(|| {
std::env::var("BTRFS_OUTPUT_FORMAT")
.ok()
.and_then(|s| s.parse().ok())
})
.unwrap_or_default();
if !self.command.supported_formats().contains(&format) {
anyhow::bail!(
"the --format {format:?} option is not supported by this command",
);
}
let ctx = RunContext {
format,
dry_run: self.global.dry_run,
quiet: self.global.quiet,
};
self.command.run(&ctx)
}
}