1#![warn(clippy::pedantic)]
23#![allow(clippy::module_name_repetitions)]
24#![allow(clippy::doc_markdown)]
25#![allow(clippy::struct_excessive_bools)]
26#![cfg_attr(
29 test,
30 allow(
31 clippy::cast_lossless,
32 clippy::cast_possible_truncation,
33 clippy::cast_possible_wrap,
34 clippy::cast_sign_loss,
35 clippy::identity_op,
36 clippy::match_wildcard_for_single_variants,
37 clippy::semicolon_if_nothing_returned,
38 clippy::uninlined_format_args,
39 clippy::unreadable_literal,
40 )
41)]
42
43use anyhow::Result;
44use clap::{ArgAction, Parser, ValueEnum};
45
46mod balance;
47mod check;
48mod device;
49mod filesystem;
50mod inspect;
51mod property;
52mod qgroup;
53mod quota;
54mod receive;
55mod reflink;
56mod replace;
57mod rescue;
58mod restore;
59mod scrub;
60mod send;
61mod subvolume;
62mod util;
63
64pub use crate::{
65 balance::*, check::*, device::*, filesystem::*, inspect::*, property::*,
66 qgroup::*, quota::*, receive::*, reflink::*, replace::*, rescue::*,
67 restore::*, scrub::*, send::*, subvolume::*,
68};
69
70#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
72pub enum Format {
73 #[default]
74 Text,
75 Json,
76 Modern,
77}
78
79impl std::str::FromStr for Format {
80 type Err = String;
81
82 fn from_str(s: &str) -> Result<Self, Self::Err> {
83 <Self as clap::ValueEnum>::from_str(s, true).map_err(|e| e.clone())
84 }
85}
86
87#[derive(
89 Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum,
90)]
91pub enum Level {
92 Debug,
93 #[default]
94 Info,
95 Warn,
96 Error,
97}
98
99#[derive(Parser, Debug)]
109#[allow(clippy::doc_markdown)]
110#[clap(
111 version,
112 infer_subcommands = true,
113 arg_required_else_help = true,
114 max_term_width = 100
115)]
116pub struct Arguments {
117 #[clap(flatten)]
118 pub global: GlobalOptions,
119
120 #[clap(subcommand)]
121 pub command: Command,
122}
123
124const GLOBAL_OPTIONS: &str = "Global options";
125
126#[derive(Parser, Debug)]
128pub struct GlobalOptions {
129 #[clap(global = true, short, long, action = ArgAction::Count, help_heading = GLOBAL_OPTIONS)]
131 pub verbose: u8,
132
133 #[clap(global = true, short, long, help_heading = GLOBAL_OPTIONS)]
135 pub quiet: bool,
136
137 #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
139 pub dry_run: bool,
140
141 #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
143 pub log: Option<Level>,
144
145 #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
147 pub format: Option<Format>,
148}
149
150pub struct RunContext {
152 pub format: Format,
154 pub dry_run: bool,
156 pub quiet: bool,
158}
159
160pub trait Runnable {
162 fn run(&self, ctx: &RunContext) -> Result<()>;
168
169 fn supported_formats(&self) -> &[Format] {
174 &[Format::Text, Format::Modern]
175 }
176
177 fn supports_dry_run(&self) -> bool {
183 false
184 }
185}
186
187pub trait CommandGroup {
193 fn leaf(&self) -> &dyn Runnable;
194}
195
196impl<T: CommandGroup> Runnable for T {
197 fn run(&self, ctx: &RunContext) -> Result<()> {
198 self.leaf().run(ctx)
199 }
200
201 fn supported_formats(&self) -> &[Format] {
202 self.leaf().supported_formats()
203 }
204
205 fn supports_dry_run(&self) -> bool {
206 self.leaf().supports_dry_run()
207 }
208}
209
210#[derive(Parser, Debug)]
211pub enum Command {
212 Balance(BalanceCommand),
213 Check(CheckCommand),
214 Device(DeviceCommand),
215 Filesystem(FilesystemCommand),
216 #[cfg(feature = "fuse")]
217 Fuse(btrfs_fuse::args::MountArgs),
218 #[command(alias = "inspect-internal")]
219 Inspect(InspectCommand),
220 #[cfg(feature = "mkfs")]
221 Mkfs(btrfs_mkfs::args::Arguments),
222 Property(PropertyCommand),
223 Qgroup(QgroupCommand),
224 Quota(QuotaCommand),
225 Receive(ReceiveCommand),
226 Reflink(ReflinkCommand),
227 Replace(ReplaceCommand),
228 Rescue(RescueCommand),
229 Restore(RestoreCommand),
230 Scrub(ScrubCommand),
231 Send(SendCommand),
232 Subvolume(SubvolumeCommand),
233 #[cfg(feature = "tune")]
234 Tune(btrfs_tune::args::Arguments),
235}
236
237#[cfg(feature = "fuse")]
238impl Runnable for btrfs_fuse::args::MountArgs {
239 fn run(&self, _ctx: &RunContext) -> Result<()> {
240 btrfs_fuse::run::run_mount(self)
241 }
242}
243
244#[cfg(feature = "mkfs")]
245impl Runnable for btrfs_mkfs::args::Arguments {
246 fn run(&self, _ctx: &RunContext) -> Result<()> {
247 btrfs_mkfs::run::run(self)
248 }
249}
250
251#[cfg(feature = "tune")]
252impl Runnable for btrfs_tune::args::Arguments {
253 fn run(&self, _ctx: &RunContext) -> Result<()> {
254 btrfs_tune::run::run(self)
255 }
256}
257
258impl CommandGroup for Command {
259 fn leaf(&self) -> &dyn Runnable {
260 match self {
261 Command::Balance(cmd) => cmd,
262 Command::Check(cmd) => cmd,
263 Command::Device(cmd) => cmd,
264 Command::Filesystem(cmd) => cmd,
265 #[cfg(feature = "fuse")]
266 Command::Fuse(cmd) => cmd,
267 Command::Inspect(cmd) => cmd,
268 #[cfg(feature = "mkfs")]
269 Command::Mkfs(cmd) => cmd,
270 Command::Property(cmd) => cmd,
271 Command::Qgroup(cmd) => cmd,
272 Command::Quota(cmd) => cmd,
273 Command::Receive(cmd) => cmd,
274 Command::Reflink(cmd) => cmd,
275 Command::Replace(cmd) => cmd,
276 Command::Rescue(cmd) => cmd,
277 Command::Restore(cmd) => cmd,
278 Command::Scrub(cmd) => cmd,
279 Command::Send(cmd) => cmd,
280 Command::Subvolume(cmd) => cmd,
281 #[cfg(feature = "tune")]
282 Command::Tune(cmd) => cmd,
283 }
284 }
285}
286
287impl Arguments {
288 pub fn run(&self) -> Result<()> {
294 let level = if let Some(explicit) = self.global.log {
295 match explicit {
296 Level::Debug => log::LevelFilter::Debug,
297 Level::Info => log::LevelFilter::Info,
298 Level::Warn => log::LevelFilter::Warn,
299 Level::Error => log::LevelFilter::Error,
300 }
301 } else if self.global.quiet {
302 log::LevelFilter::Error
303 } else {
304 match self.global.verbose {
305 0 => log::LevelFilter::Warn,
306 1 => log::LevelFilter::Info,
307 2 => log::LevelFilter::Debug,
308 _ => log::LevelFilter::Trace,
309 }
310 };
311 env_logger::Builder::new().filter_level(level).init();
312
313 if self.global.dry_run && !self.command.supports_dry_run() {
314 anyhow::bail!(
315 "the --dry-run option is not supported by this command"
316 );
317 }
318
319 let format = self
320 .global
321 .format
322 .or_else(|| {
327 std::env::var("BTRFS_OUTPUT_FORMAT")
328 .ok()
329 .and_then(|s| s.parse().ok())
330 })
331 .unwrap_or_default();
332 if !self.command.supported_formats().contains(&format) {
333 anyhow::bail!(
334 "the --format {format:?} option is not supported by this command",
335 );
336 }
337
338 let ctx = RunContext {
339 format,
340 dry_run: self.global.dry_run,
341 quiet: self.global.quiet,
342 };
343 self.command.run(&ctx)
344 }
345}