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