Skip to main content

btrfs_cli/
lib.rs

1//! # btrfs-cli: the btrfs command-line tool
2//!
3//! This crate provides the `btrfs` command-line binary, an alternative
4//! implementation of btrfs-progs written in Rust. It is built on top of `btrfs-uapi` for kernel communication,
5//! `btrfs-disk` for direct on-disk structure parsing, and `btrfs-stream` for
6//! send/receive stream processing.
7//!
8//! Not all commands from btrfs-progs are implemented yet. Run `btrfs help` to
9//! see what is available. Most commands require root privileges or
10//! `CAP_SYS_ADMIN`.
11
12#![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/// Output format for commands that support structured output.
44#[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/// Log verbosity level, ordered from most to least verbose.
61#[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/// User-space command-line tool for managing Btrfs filesystems.
73///
74/// btrfs is a modern copy-on-write filesystem for Linux that provides advanced features
75/// including subvolumes, snapshots, RAID support, compression, quotas, and checksumming.
76/// This tool allows you to create and manage filesystems, devices, subvolumes, snapshots,
77/// quotas, and perform various maintenance operations.
78///
79/// Most operations require CAP_SYS_ADMIN (root privileges) or special permissions for
80/// the specific filesystem.
81#[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/// Flags shared across all subcommands (verbosity, dry-run, output format).
95#[derive(Parser, Debug)]
96pub struct GlobalOptions {
97    /// Increase verbosity (repeat for more: -v, -vv, -vvv)
98    #[clap(global = true, short, long, action = ArgAction::Count, help_heading = GLOBAL_OPTIONS)]
99    pub verbose: u8,
100
101    /// Print only errors
102    #[clap(global = true, short, long, help_heading = GLOBAL_OPTIONS)]
103    pub quiet: bool,
104
105    /// If supported, do not do any active/changing actions
106    #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
107    pub dry_run: bool,
108
109    /// Set log level
110    #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
111    pub log: Option<Level>,
112
113    /// If supported, print subcommand output in that format. [env: BTRFS_OUTPUT_FORMAT]
114    #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
115    pub format: Option<Format>,
116}
117
118/// Runtime context passed to every command.
119pub struct RunContext {
120    /// Output format (text or json).
121    pub format: Format,
122    /// Whether the user requested a dry run.
123    pub dry_run: bool,
124    /// Whether the user requested quiet mode (suppress non-error output).
125    pub quiet: bool,
126}
127
128/// A CLI subcommand that can be executed.
129pub trait Runnable {
130    /// Execute this command.
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if the command fails.
135    fn run(&self, ctx: &RunContext) -> Result<()>;
136
137    /// Output formats this command supports.
138    ///
139    /// The default is text and modern. Commands that also support JSON
140    /// should override this to include `Format::Json`.
141    fn supported_formats(&self) -> &[Format] {
142        &[Format::Text, Format::Modern]
143    }
144
145    /// Whether this command supports the global --dry-run flag.
146    ///
147    /// Commands that do not support dry-run will cause an error if the user
148    /// passes --dry-run. Override this to return `true` in commands that
149    /// handle the flag.
150    fn supports_dry_run(&self) -> bool {
151        false
152    }
153}
154
155/// A command group that delegates to a leaf subcommand.
156///
157/// Implement this for parent commands (e.g. `BalanceCommand`,
158/// `DeviceCommand`) that simply dispatch to their subcommand.
159/// A blanket `Runnable` impl forwards all methods through `leaf()`.
160pub 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    /// Parse and run the CLI command.
244    ///
245    /// # Errors
246    ///
247    /// Returns an error if the command fails.
248    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            // Resolve from env manually rather than via clap's `env`
278            // attribute. Using `env` on a global flag makes clap treat
279            // the env var as a provided argument, which defeats
280            // `arg_required_else_help` on parent commands.
281            .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}