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//! # Stability
13//!
14//! This is a pre-1.0 release. Read-only commands are stable. The
15//! mutating commands that go through the new `btrfs-transaction`
16//! crate (offline `filesystem resize`, the `rescue` subcommands,
17//! and the `tune` conversions when built with the `tune` feature)
18//! are experimental and may have edge cases that testing doesn't
19//! cover. Take a backup before running them on filesystems you
20//! care about.
21
22#![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/// Output format for commands that support structured output.
54#[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/// Log verbosity level, ordered from most to least verbose.
71#[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/// User-space command-line tool for managing Btrfs filesystems.
83///
84/// btrfs is a modern copy-on-write filesystem for Linux that provides advanced features
85/// including subvolumes, snapshots, RAID support, compression, quotas, and checksumming.
86/// This tool allows you to create and manage filesystems, devices, subvolumes, snapshots,
87/// quotas, and perform various maintenance operations.
88///
89/// Most operations require CAP_SYS_ADMIN (root privileges) or special permissions for
90/// the specific filesystem.
91#[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/// Flags shared across all subcommands (verbosity, dry-run, output format).
105#[derive(Parser, Debug)]
106pub struct GlobalOptions {
107    /// Increase verbosity (repeat for more: -v, -vv, -vvv)
108    #[clap(global = true, short, long, action = ArgAction::Count, help_heading = GLOBAL_OPTIONS)]
109    pub verbose: u8,
110
111    /// Print only errors
112    #[clap(global = true, short, long, help_heading = GLOBAL_OPTIONS)]
113    pub quiet: bool,
114
115    /// If supported, do not do any active/changing actions
116    #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
117    pub dry_run: bool,
118
119    /// Set log level
120    #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
121    pub log: Option<Level>,
122
123    /// If supported, print subcommand output in that format. [env: BTRFS_OUTPUT_FORMAT]
124    #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
125    pub format: Option<Format>,
126}
127
128/// Runtime context passed to every command.
129pub struct RunContext {
130    /// Output format (text or json).
131    pub format: Format,
132    /// Whether the user requested a dry run.
133    pub dry_run: bool,
134    /// Whether the user requested quiet mode (suppress non-error output).
135    pub quiet: bool,
136}
137
138/// A CLI subcommand that can be executed.
139pub trait Runnable {
140    /// Execute this command.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if the command fails.
145    fn run(&self, ctx: &RunContext) -> Result<()>;
146
147    /// Output formats this command supports.
148    ///
149    /// The default is text and modern. Commands that also support JSON
150    /// should override this to include `Format::Json`.
151    fn supported_formats(&self) -> &[Format] {
152        &[Format::Text, Format::Modern]
153    }
154
155    /// Whether this command supports the global --dry-run flag.
156    ///
157    /// Commands that do not support dry-run will cause an error if the user
158    /// passes --dry-run. Override this to return `true` in commands that
159    /// handle the flag.
160    fn supports_dry_run(&self) -> bool {
161        false
162    }
163}
164
165/// A command group that delegates to a leaf subcommand.
166///
167/// Implement this for parent commands (e.g. `BalanceCommand`,
168/// `DeviceCommand`) that simply dispatch to their subcommand.
169/// A blanket `Runnable` impl forwards all methods through `leaf()`.
170pub 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    /// Parse and run the CLI command.
254    ///
255    /// # Errors
256    ///
257    /// Returns an error if the command fails.
258    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            // Resolve from env manually rather than via clap's `env`
288            // attribute. Using `env` on a global flag makes clap treat
289            // the env var as a provided argument, which defeats
290            // `arg_required_else_help` on parent commands.
291            .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}