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