1#![warn(clippy::pedantic)]
23#![allow(clippy::module_name_repetitions)]
24#![allow(clippy::doc_markdown)]
25#![allow(clippy::struct_excessive_bools)]
26
27use anyhow::Result;
28use clap::{ArgAction, Parser, ValueEnum};
29
30mod balance;
31mod check;
32mod device;
33mod filesystem;
34mod inspect;
35mod property;
36mod qgroup;
37mod quota;
38mod receive;
39mod replace;
40mod rescue;
41mod restore;
42mod scrub;
43mod send;
44mod subvolume;
45mod util;
46
47pub use crate::{
48 balance::*, check::*, device::*, filesystem::*, inspect::*, property::*,
49 qgroup::*, quota::*, receive::*, replace::*, rescue::*, restore::*,
50 scrub::*, send::*, subvolume::*,
51};
52
53#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
55pub enum Format {
56 #[default]
57 Text,
58 Json,
59 Modern,
60}
61
62impl std::str::FromStr for Format {
63 type Err = String;
64
65 fn from_str(s: &str) -> Result<Self, Self::Err> {
66 <Self as clap::ValueEnum>::from_str(s, true).map_err(|e| e.clone())
67 }
68}
69
70#[derive(
72 Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum,
73)]
74pub enum Level {
75 Debug,
76 #[default]
77 Info,
78 Warn,
79 Error,
80}
81
82#[derive(Parser, Debug)]
92#[allow(clippy::doc_markdown)]
93#[clap(version, infer_subcommands = true, arg_required_else_help = true)]
94pub struct Arguments {
95 #[clap(flatten)]
96 pub global: GlobalOptions,
97
98 #[clap(subcommand)]
99 pub command: Command,
100}
101
102const GLOBAL_OPTIONS: &str = "Global options";
103
104#[derive(Parser, Debug)]
106pub struct GlobalOptions {
107 #[clap(global = true, short, long, action = ArgAction::Count, help_heading = GLOBAL_OPTIONS)]
109 pub verbose: u8,
110
111 #[clap(global = true, short, long, help_heading = GLOBAL_OPTIONS)]
113 pub quiet: bool,
114
115 #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
117 pub dry_run: bool,
118
119 #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
121 pub log: Option<Level>,
122
123 #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
125 pub format: Option<Format>,
126}
127
128pub struct RunContext {
130 pub format: Format,
132 pub dry_run: bool,
134 pub quiet: bool,
136}
137
138pub trait Runnable {
140 fn run(&self, ctx: &RunContext) -> Result<()>;
146
147 fn supported_formats(&self) -> &[Format] {
152 &[Format::Text, Format::Modern]
153 }
154
155 fn supports_dry_run(&self) -> bool {
161 false
162 }
163}
164
165pub trait CommandGroup {
171 fn leaf(&self) -> &dyn Runnable;
172}
173
174impl<T: CommandGroup> Runnable for T {
175 fn run(&self, ctx: &RunContext) -> Result<()> {
176 self.leaf().run(ctx)
177 }
178
179 fn supported_formats(&self) -> &[Format] {
180 self.leaf().supported_formats()
181 }
182
183 fn supports_dry_run(&self) -> bool {
184 self.leaf().supports_dry_run()
185 }
186}
187
188#[derive(Parser, Debug)]
189pub enum Command {
190 Balance(BalanceCommand),
191 Check(CheckCommand),
192 Device(DeviceCommand),
193 Filesystem(FilesystemCommand),
194 #[command(alias = "inspect-internal")]
195 Inspect(InspectCommand),
196 #[cfg(feature = "mkfs")]
197 Mkfs(btrfs_mkfs::args::Arguments),
198 Property(PropertyCommand),
199 Qgroup(QgroupCommand),
200 Quota(QuotaCommand),
201 Receive(ReceiveCommand),
202 Replace(ReplaceCommand),
203 Rescue(RescueCommand),
204 Restore(RestoreCommand),
205 Scrub(ScrubCommand),
206 Send(SendCommand),
207 Subvolume(SubvolumeCommand),
208 #[cfg(feature = "tune")]
209 Tune(btrfs_tune::args::Arguments),
210}
211
212#[cfg(feature = "mkfs")]
213impl Runnable for btrfs_mkfs::args::Arguments {
214 fn run(&self, _ctx: &RunContext) -> Result<()> {
215 btrfs_mkfs::run::run(self)
216 }
217}
218
219#[cfg(feature = "tune")]
220impl Runnable for btrfs_tune::args::Arguments {
221 fn run(&self, _ctx: &RunContext) -> Result<()> {
222 btrfs_tune::run::run(self)
223 }
224}
225
226impl CommandGroup for Command {
227 fn leaf(&self) -> &dyn Runnable {
228 match self {
229 Command::Balance(cmd) => cmd,
230 Command::Check(cmd) => cmd,
231 Command::Device(cmd) => cmd,
232 Command::Filesystem(cmd) => cmd,
233 Command::Inspect(cmd) => cmd,
234 #[cfg(feature = "mkfs")]
235 Command::Mkfs(cmd) => cmd,
236 Command::Property(cmd) => cmd,
237 Command::Qgroup(cmd) => cmd,
238 Command::Quota(cmd) => cmd,
239 Command::Receive(cmd) => cmd,
240 Command::Replace(cmd) => cmd,
241 Command::Rescue(cmd) => cmd,
242 Command::Restore(cmd) => cmd,
243 Command::Scrub(cmd) => cmd,
244 Command::Send(cmd) => cmd,
245 Command::Subvolume(cmd) => cmd,
246 #[cfg(feature = "tune")]
247 Command::Tune(cmd) => cmd,
248 }
249 }
250}
251
252impl Arguments {
253 pub fn run(&self) -> Result<()> {
259 let level = if let Some(explicit) = self.global.log {
260 match explicit {
261 Level::Debug => log::LevelFilter::Debug,
262 Level::Info => log::LevelFilter::Info,
263 Level::Warn => log::LevelFilter::Warn,
264 Level::Error => log::LevelFilter::Error,
265 }
266 } else if self.global.quiet {
267 log::LevelFilter::Error
268 } else {
269 match self.global.verbose {
270 0 => log::LevelFilter::Warn,
271 1 => log::LevelFilter::Info,
272 2 => log::LevelFilter::Debug,
273 _ => log::LevelFilter::Trace,
274 }
275 };
276 env_logger::Builder::new().filter_level(level).init();
277
278 if self.global.dry_run && !self.command.supports_dry_run() {
279 anyhow::bail!(
280 "the --dry-run option is not supported by this command"
281 );
282 }
283
284 let format = self
285 .global
286 .format
287 .or_else(|| {
292 std::env::var("BTRFS_OUTPUT_FORMAT")
293 .ok()
294 .and_then(|s| s.parse().ok())
295 })
296 .unwrap_or_default();
297 if !self.command.supported_formats().contains(&format) {
298 anyhow::bail!(
299 "the --format {format:?} option is not supported by this command",
300 );
301 }
302
303 let ctx = RunContext {
304 format,
305 dry_run: self.global.dry_run,
306 quiet: self.global.quiet,
307 };
308 self.command.run(&ctx)
309 }
310}