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// Test code uses literal byte buffers and small cast conversions that
27// pedantic clippy flags but that are intentional in unit tests.
28#![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 replace;
56mod rescue;
57mod restore;
58mod scrub;
59mod send;
60mod subvolume;
61mod util;
62
63pub use crate::{
64    balance::*, check::*, device::*, filesystem::*, inspect::*, property::*,
65    qgroup::*, quota::*, receive::*, replace::*, rescue::*, restore::*,
66    scrub::*, send::*, subvolume::*,
67};
68
69/// Output format for commands that support structured output.
70#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
71pub enum Format {
72    #[default]
73    Text,
74    Json,
75    Modern,
76}
77
78impl std::str::FromStr for Format {
79    type Err = String;
80
81    fn from_str(s: &str) -> Result<Self, Self::Err> {
82        <Self as clap::ValueEnum>::from_str(s, true).map_err(|e| e.clone())
83    }
84}
85
86/// Log verbosity level, ordered from most to least verbose.
87#[derive(
88    Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum,
89)]
90pub enum Level {
91    Debug,
92    #[default]
93    Info,
94    Warn,
95    Error,
96}
97
98/// User-space command-line tool for managing Btrfs filesystems.
99///
100/// btrfs is a modern copy-on-write filesystem for Linux that provides advanced features
101/// including subvolumes, snapshots, RAID support, compression, quotas, and checksumming.
102/// This tool allows you to create and manage filesystems, devices, subvolumes, snapshots,
103/// quotas, and perform various maintenance operations.
104///
105/// Most operations require CAP_SYS_ADMIN (root privileges) or special permissions for
106/// the specific filesystem.
107#[derive(Parser, Debug)]
108#[allow(clippy::doc_markdown)]
109#[clap(version, infer_subcommands = true, arg_required_else_help = true)]
110pub struct Arguments {
111    #[clap(flatten)]
112    pub global: GlobalOptions,
113
114    #[clap(subcommand)]
115    pub command: Command,
116}
117
118const GLOBAL_OPTIONS: &str = "Global options";
119
120/// Flags shared across all subcommands (verbosity, dry-run, output format).
121#[derive(Parser, Debug)]
122pub struct GlobalOptions {
123    /// Increase verbosity (repeat for more: -v, -vv, -vvv)
124    #[clap(global = true, short, long, action = ArgAction::Count, help_heading = GLOBAL_OPTIONS)]
125    pub verbose: u8,
126
127    /// Print only errors
128    #[clap(global = true, short, long, help_heading = GLOBAL_OPTIONS)]
129    pub quiet: bool,
130
131    /// If supported, do not do any active/changing actions
132    #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
133    pub dry_run: bool,
134
135    /// Set log level
136    #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
137    pub log: Option<Level>,
138
139    /// If supported, print subcommand output in that format. [env: BTRFS_OUTPUT_FORMAT]
140    #[clap(global = true, long, help_heading = GLOBAL_OPTIONS)]
141    pub format: Option<Format>,
142}
143
144/// Runtime context passed to every command.
145pub struct RunContext {
146    /// Output format (text or json).
147    pub format: Format,
148    /// Whether the user requested a dry run.
149    pub dry_run: bool,
150    /// Whether the user requested quiet mode (suppress non-error output).
151    pub quiet: bool,
152}
153
154/// A CLI subcommand that can be executed.
155pub trait Runnable {
156    /// Execute this command.
157    ///
158    /// # Errors
159    ///
160    /// Returns an error if the command fails.
161    fn run(&self, ctx: &RunContext) -> Result<()>;
162
163    /// Output formats this command supports.
164    ///
165    /// The default is text and modern. Commands that also support JSON
166    /// should override this to include `Format::Json`.
167    fn supported_formats(&self) -> &[Format] {
168        &[Format::Text, Format::Modern]
169    }
170
171    /// Whether this command supports the global --dry-run flag.
172    ///
173    /// Commands that do not support dry-run will cause an error if the user
174    /// passes --dry-run. Override this to return `true` in commands that
175    /// handle the flag.
176    fn supports_dry_run(&self) -> bool {
177        false
178    }
179}
180
181/// A command group that delegates to a leaf subcommand.
182///
183/// Implement this for parent commands (e.g. `BalanceCommand`,
184/// `DeviceCommand`) that simply dispatch to their subcommand.
185/// A blanket `Runnable` impl forwards all methods through `leaf()`.
186pub trait CommandGroup {
187    fn leaf(&self) -> &dyn Runnable;
188}
189
190impl<T: CommandGroup> Runnable for T {
191    fn run(&self, ctx: &RunContext) -> Result<()> {
192        self.leaf().run(ctx)
193    }
194
195    fn supported_formats(&self) -> &[Format] {
196        self.leaf().supported_formats()
197    }
198
199    fn supports_dry_run(&self) -> bool {
200        self.leaf().supports_dry_run()
201    }
202}
203
204#[derive(Parser, Debug)]
205pub enum Command {
206    Balance(BalanceCommand),
207    Check(CheckCommand),
208    Device(DeviceCommand),
209    Filesystem(FilesystemCommand),
210    #[command(alias = "inspect-internal")]
211    Inspect(InspectCommand),
212    #[cfg(feature = "mkfs")]
213    Mkfs(btrfs_mkfs::args::Arguments),
214    Property(PropertyCommand),
215    Qgroup(QgroupCommand),
216    Quota(QuotaCommand),
217    Receive(ReceiveCommand),
218    Replace(ReplaceCommand),
219    Rescue(RescueCommand),
220    Restore(RestoreCommand),
221    Scrub(ScrubCommand),
222    Send(SendCommand),
223    Subvolume(SubvolumeCommand),
224    #[cfg(feature = "tune")]
225    Tune(btrfs_tune::args::Arguments),
226}
227
228#[cfg(feature = "mkfs")]
229impl Runnable for btrfs_mkfs::args::Arguments {
230    fn run(&self, _ctx: &RunContext) -> Result<()> {
231        btrfs_mkfs::run::run(self)
232    }
233}
234
235#[cfg(feature = "tune")]
236impl Runnable for btrfs_tune::args::Arguments {
237    fn run(&self, _ctx: &RunContext) -> Result<()> {
238        btrfs_tune::run::run(self)
239    }
240}
241
242impl CommandGroup for Command {
243    fn leaf(&self) -> &dyn Runnable {
244        match self {
245            Command::Balance(cmd) => cmd,
246            Command::Check(cmd) => cmd,
247            Command::Device(cmd) => cmd,
248            Command::Filesystem(cmd) => cmd,
249            Command::Inspect(cmd) => cmd,
250            #[cfg(feature = "mkfs")]
251            Command::Mkfs(cmd) => cmd,
252            Command::Property(cmd) => cmd,
253            Command::Qgroup(cmd) => cmd,
254            Command::Quota(cmd) => cmd,
255            Command::Receive(cmd) => cmd,
256            Command::Replace(cmd) => cmd,
257            Command::Rescue(cmd) => cmd,
258            Command::Restore(cmd) => cmd,
259            Command::Scrub(cmd) => cmd,
260            Command::Send(cmd) => cmd,
261            Command::Subvolume(cmd) => cmd,
262            #[cfg(feature = "tune")]
263            Command::Tune(cmd) => cmd,
264        }
265    }
266}
267
268impl Arguments {
269    /// Parse and run the CLI command.
270    ///
271    /// # Errors
272    ///
273    /// Returns an error if the command fails.
274    pub fn run(&self) -> Result<()> {
275        let level = if let Some(explicit) = self.global.log {
276            match explicit {
277                Level::Debug => log::LevelFilter::Debug,
278                Level::Info => log::LevelFilter::Info,
279                Level::Warn => log::LevelFilter::Warn,
280                Level::Error => log::LevelFilter::Error,
281            }
282        } else if self.global.quiet {
283            log::LevelFilter::Error
284        } else {
285            match self.global.verbose {
286                0 => log::LevelFilter::Warn,
287                1 => log::LevelFilter::Info,
288                2 => log::LevelFilter::Debug,
289                _ => log::LevelFilter::Trace,
290            }
291        };
292        env_logger::Builder::new().filter_level(level).init();
293
294        if self.global.dry_run && !self.command.supports_dry_run() {
295            anyhow::bail!(
296                "the --dry-run option is not supported by this command"
297            );
298        }
299
300        let format = self
301            .global
302            .format
303            // Resolve from env manually rather than via clap's `env`
304            // attribute. Using `env` on a global flag makes clap treat
305            // the env var as a provided argument, which defeats
306            // `arg_required_else_help` on parent commands.
307            .or_else(|| {
308                std::env::var("BTRFS_OUTPUT_FORMAT")
309                    .ok()
310                    .and_then(|s| s.parse().ok())
311            })
312            .unwrap_or_default();
313        if !self.command.supported_formats().contains(&format) {
314            anyhow::bail!(
315                "the --format {format:?} option is not supported by this command",
316            );
317        }
318
319        let ctx = RunContext {
320            format,
321            dry_run: self.global.dry_run,
322            quiet: self.global.quiet,
323        };
324        self.command.run(&ctx)
325    }
326}