Skip to main content

btrfs_cli/subvolume/
list.rs

1use crate::{Format, Runnable, util::open_path};
2use anyhow::{Context, Result};
3use btrfs_uapi::subvolume::{
4    SubvolumeFlags, SubvolumeListItem, subvolume_list,
5};
6use clap::Parser;
7use std::{
8    cmp::Ordering, fmt::Write as _, os::unix::io::AsFd, path::PathBuf,
9    str::FromStr,
10};
11
12const HEADING_PATH_FILTERING: &str = "Path filtering";
13const HEADING_FIELD_SELECTION: &str = "Field selection";
14const HEADING_TYPE_FILTERING: &str = "Type filtering";
15const HEADING_SORTING: &str = "Sorting";
16
17/// List subvolumes and snapshots in the filesystem
18///
19/// The default output format matches btrfs-progs:
20///   ID NNN gen NNN top level NNN path NAME
21///
22/// Optional flags enable additional columns or filter the results.
23#[derive(Parser, Debug)]
24#[allow(clippy::struct_excessive_bools)]
25pub struct SubvolumeListCommand {
26    /// Print only subvolumes below the given path
27    #[clap(short = 'o', long, help_heading = HEADING_PATH_FILTERING)]
28    only_below: bool,
29
30    /// Print all subvolumes in the filesystem, including deleted ones, and
31    /// distinguish absolute and relative paths with respect to the given path
32    #[clap(short = 'a', long, help_heading = HEADING_PATH_FILTERING)]
33    all: bool,
34
35    /// Print parent ID column (same as top level for non-snapshots)
36    #[clap(short, long, help_heading = HEADING_FIELD_SELECTION)]
37    parent: bool,
38
39    /// Print ogeneration (generation at creation) column
40    #[clap(short = 'c', long, help_heading = HEADING_FIELD_SELECTION)]
41    ogeneration: bool,
42
43    /// Print generation column (already shown by default; kept for
44    /// compatibility with btrfs-progs CLI)
45    #[clap(short, long, help_heading = HEADING_FIELD_SELECTION)]
46    generation: bool,
47
48    /// Print UUID column
49    #[clap(short, long, help_heading = HEADING_FIELD_SELECTION)]
50    uuid: bool,
51
52    /// Print parent UUID column
53    #[clap(short = 'Q', long, help_heading = HEADING_FIELD_SELECTION)]
54    parent_uuid: bool,
55
56    /// Print received UUID column
57    #[clap(short = 'R', long, help_heading = HEADING_FIELD_SELECTION)]
58    received_uuid: bool,
59
60    /// List only snapshots (subvolumes with a non-nil parent UUID)
61    #[clap(short = 's', long, help_heading = HEADING_TYPE_FILTERING)]
62    snapshots_only: bool,
63
64    /// List only read-only subvolumes
65    #[clap(short = 'r', long, help_heading = HEADING_TYPE_FILTERING)]
66    readonly: bool,
67
68    /// List deleted subvolumes that are not yet cleaned
69    #[clap(short = 'd', long, help_heading = HEADING_TYPE_FILTERING)]
70    deleted: bool,
71
72    /// Print the result as a table
73    #[clap(short = 't', long, help_heading = "Other")]
74    table: bool,
75
76    /// Filter by generation: VALUE (exact), +VALUE (>= VALUE), -VALUE (<= VALUE)
77    #[clap(short = 'G', long, value_name = "[+|-]VALUE", allow_hyphen_values = true, help_heading = HEADING_SORTING)]
78    gen_filter: Option<GenFilter>,
79
80    /// Filter by ogeneration: VALUE (exact), +VALUE (>= VALUE), -VALUE (<= VALUE)
81    #[clap(short = 'C', long, value_name = "[+|-]VALUE", allow_hyphen_values = true, help_heading = HEADING_SORTING)]
82    ogen_filter: Option<GenFilter>,
83
84    /// Sort by comma-separated keys: gen, ogen, rootid, path
85    ///
86    /// Prefix with + (ascending, default) or - (descending).
87    /// Example: --sort=gen,-ogen,path
88    #[clap(
89        long,
90        value_name = "KEYS",
91        value_delimiter = ',',
92        allow_hyphen_values = true,
93        help_heading = HEADING_SORTING,
94    )]
95    sort: Vec<SortKey>,
96
97    /// Path to a mounted btrfs filesystem
98    path: PathBuf,
99}
100
101impl Runnable for SubvolumeListCommand {
102    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
103        let file = open_path(&self.path)?;
104
105        let mut items = subvolume_list(file.as_fd()).with_context(|| {
106            format!("failed to list subvolumes for '{}'", self.path.display())
107        })?;
108
109        let top_id = btrfs_uapi::inode::lookup_path_rootid(file.as_fd())
110            .with_context(|| "failed to get root id for path")?;
111
112        // Apply filters.
113        //
114        // Deleted subvolumes have parent_id == 0 (no ROOT_BACKREF found).
115        // By default they are hidden; -d shows only deleted; -a shows all.
116        if self.deleted {
117            items.retain(|item| item.parent_id == 0);
118        } else if !self.all {
119            items.retain(|item| item.parent_id != 0);
120        }
121        if self.readonly {
122            items.retain(|item| item.flags.contains(SubvolumeFlags::RDONLY));
123        }
124        if self.snapshots_only {
125            items.retain(|item| !item.parent_uuid.is_nil());
126        }
127        if let Some(ref f) = self.gen_filter {
128            items.retain(|item| f.matches(item.generation));
129        }
130        if let Some(ref f) = self.ogen_filter {
131            items.retain(|item| f.matches(item.otransid));
132        }
133        if self.only_below {
134            // -o: only list subvolumes that are direct children of the
135            // subvolume the fd is open on (i.e. whose parent_id matches the
136            // fd's root ID).
137            items.retain(|item| item.parent_id == top_id);
138        }
139
140        // -a: annotate paths of subvolumes outside the fd's subvolume with
141        // a <FS_TREE> prefix, matching btrfs-progs behaviour.
142        if self.all {
143            for item in &mut items {
144                if item.parent_id != 0
145                    && item.parent_id != top_id
146                    && !item.name.is_empty()
147                {
148                    item.name = format!("<FS_TREE>/{}", item.name);
149                }
150            }
151        }
152
153        // Sort.
154        if self.sort.is_empty() {
155            items.sort_by_key(|item| item.root_id);
156        } else {
157            items.sort_by(|a, b| {
158                for key in &self.sort {
159                    let ord = key.compare(a, b);
160                    if ord != Ordering::Equal {
161                        return ord;
162                    }
163                }
164                Ordering::Equal
165            });
166        }
167
168        if self.table {
169            self.print_table(&items);
170        } else {
171            self.print_default(&items);
172        }
173
174        Ok(())
175    }
176}
177
178impl SubvolumeListCommand {
179    fn print_default(&self, items: &[SubvolumeListItem]) {
180        for item in items {
181            let name = if item.name.is_empty() {
182                "<unknown>"
183            } else {
184                &item.name
185            };
186
187            // Build the output line incrementally in the same field order as
188            // btrfs-progs: ID, gen, [cgen,] top level, [parent,] path, [uuid,]
189            // [parent_uuid,] [received_uuid]
190            let mut line =
191                format!("ID {} gen {}", item.root_id, item.generation);
192
193            if self.ogeneration {
194                let _ = write!(line, " cgen {}", item.otransid);
195            }
196
197            let _ = write!(line, " top level {}", item.parent_id);
198
199            if self.parent {
200                let _ = write!(line, " parent {}", item.parent_id);
201            }
202
203            let _ = write!(line, " path {name}");
204
205            if self.uuid {
206                let _ = write!(line, " uuid {}", fmt_uuid(&item.uuid));
207            }
208
209            if self.parent_uuid {
210                let _ = write!(
211                    line,
212                    " parent_uuid {}",
213                    fmt_uuid(&item.parent_uuid)
214                );
215            }
216
217            if self.received_uuid {
218                let _ = write!(
219                    line,
220                    " received_uuid {}",
221                    fmt_uuid(&item.received_uuid)
222                );
223            }
224
225            println!("{line}");
226        }
227    }
228
229    fn print_table(&self, items: &[SubvolumeListItem]) {
230        // Collect column headers and data in order.
231        let mut headers: Vec<&str> = vec!["ID", "gen"];
232        if self.ogeneration {
233            headers.push("cgen");
234        }
235        headers.push("top level");
236        if self.parent {
237            headers.push("parent");
238        }
239        headers.push("path");
240        if self.uuid {
241            headers.push("uuid");
242        }
243        if self.parent_uuid {
244            headers.push("parent_uuid");
245        }
246        if self.received_uuid {
247            headers.push("received_uuid");
248        }
249
250        // Print header row.
251        println!("{}", headers.join("\t"));
252
253        // Print separator.
254        let sep: Vec<String> =
255            headers.iter().map(|h| "-".repeat(h.len())).collect();
256        println!("{}", sep.join("\t"));
257
258        // Print rows.
259        for item in items {
260            let name = if item.name.is_empty() {
261                "<unknown>"
262            } else {
263                &item.name
264            };
265
266            let mut cols: Vec<String> =
267                vec![item.root_id.to_string(), item.generation.to_string()];
268            if self.ogeneration {
269                cols.push(item.otransid.to_string());
270            }
271            cols.push(item.parent_id.to_string());
272            if self.parent {
273                cols.push(item.parent_id.to_string());
274            }
275            cols.push(name.to_string());
276            if self.uuid {
277                cols.push(fmt_uuid(&item.uuid));
278            }
279            if self.parent_uuid {
280                cols.push(fmt_uuid(&item.parent_uuid));
281            }
282            if self.received_uuid {
283                cols.push(fmt_uuid(&item.received_uuid));
284            }
285
286            println!("{}", cols.join("\t"));
287        }
288    }
289}
290
291fn fmt_uuid(u: &uuid::Uuid) -> String {
292    if u.is_nil() {
293        "-".to_string()
294    } else {
295        u.hyphenated().to_string()
296    }
297}
298
299/// A generation filter: exact match, >= (plus), or <= (minus).
300#[derive(Debug, Clone)]
301pub enum GenFilter {
302    Exact(u64),
303    AtLeast(u64),
304    AtMost(u64),
305}
306
307impl GenFilter {
308    fn matches(&self, value: u64) -> bool {
309        match self {
310            GenFilter::Exact(v) => value == *v,
311            GenFilter::AtLeast(v) => value >= *v,
312            GenFilter::AtMost(v) => value <= *v,
313        }
314    }
315}
316
317impl FromStr for GenFilter {
318    type Err = String;
319
320    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
321        if let Some(rest) = s.strip_prefix('+') {
322            let v: u64 = rest
323                .parse()
324                .map_err(|_| format!("invalid number: '{rest}'"))?;
325            Ok(GenFilter::AtLeast(v))
326        } else if let Some(rest) = s.strip_prefix('-') {
327            let v: u64 = rest
328                .parse()
329                .map_err(|_| format!("invalid number: '{rest}'"))?;
330            Ok(GenFilter::AtMost(v))
331        } else {
332            let v: u64 =
333                s.parse().map_err(|_| format!("invalid number: '{s}'"))?;
334            Ok(GenFilter::Exact(v))
335        }
336    }
337}
338
339/// A sort key with direction.
340#[derive(Debug, Clone)]
341pub struct SortKey {
342    field: SortField,
343    descending: bool,
344}
345
346#[derive(Debug, Clone)]
347enum SortField {
348    Gen,
349    Ogen,
350    Rootid,
351    Path,
352}
353
354impl SortKey {
355    fn compare(
356        &self,
357        a: &SubvolumeListItem,
358        b: &SubvolumeListItem,
359    ) -> Ordering {
360        let ord = match self.field {
361            SortField::Gen => a.generation.cmp(&b.generation),
362            SortField::Ogen => a.otransid.cmp(&b.otransid),
363            SortField::Rootid => a.root_id.cmp(&b.root_id),
364            SortField::Path => a.name.cmp(&b.name),
365        };
366        if self.descending { ord.reverse() } else { ord }
367    }
368}
369
370impl FromStr for SortKey {
371    type Err = String;
372
373    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
374        let (descending, field_str) = if let Some(rest) = s.strip_prefix('-') {
375            (true, rest)
376        } else if let Some(rest) = s.strip_prefix('+') {
377            (false, rest)
378        } else {
379            (false, s)
380        };
381
382        let field = match field_str {
383            "gen" => SortField::Gen,
384            "ogen" => SortField::Ogen,
385            "rootid" => SortField::Rootid,
386            "path" => SortField::Path,
387            _ => {
388                return Err(format!(
389                    "unknown sort key: '{field_str}' (expected gen, ogen, rootid, or path)"
390                ));
391            }
392        };
393
394        Ok(SortKey { field, descending })
395    }
396}