Skip to main content

btrfs_cli/subvolume/
list.rs

1use crate::{
2    Format, RunContext, Runnable,
3    util::{open_path, print_json},
4};
5use anyhow::{Context, Result};
6use btrfs_uapi::subvolume::{
7    SubvolumeFlags, SubvolumeListItem, subvolume_list,
8};
9use clap::Parser;
10use cols::Cols;
11use serde::Serialize;
12use std::{
13    cmp::Ordering, collections::BTreeMap, fmt::Write as _, os::unix::io::AsFd,
14    path::PathBuf, str::FromStr,
15};
16
17const HEADING_PATH_FILTERING: &str = "Path filtering";
18const HEADING_FIELD_SELECTION: &str = "Field selection";
19const HEADING_TYPE_FILTERING: &str = "Type filtering";
20const HEADING_SORTING: &str = "Sorting";
21
22/// List subvolumes and snapshots in the filesystem
23///
24/// The default output format matches btrfs-progs:
25///   ID NNN gen NNN top level NNN path NAME
26///
27/// Optional flags enable additional columns or filter the results.
28#[derive(Parser, Debug)]
29#[allow(clippy::struct_excessive_bools)]
30pub struct SubvolumeListCommand {
31    /// Print only subvolumes below the given path
32    #[clap(short = 'o', long, help_heading = HEADING_PATH_FILTERING)]
33    only_below: bool,
34
35    /// Print all subvolumes in the filesystem, including deleted ones, and
36    /// distinguish absolute and relative paths with respect to the given path
37    #[clap(short = 'a', long, help_heading = HEADING_PATH_FILTERING)]
38    all: bool,
39
40    /// Print parent ID column (same as top level for non-snapshots)
41    #[clap(short, long, help_heading = HEADING_FIELD_SELECTION)]
42    parent: bool,
43
44    /// Print ogeneration (generation at creation) column
45    #[clap(short = 'c', long, help_heading = HEADING_FIELD_SELECTION)]
46    ogeneration: bool,
47
48    /// Print generation column (already shown by default; kept for
49    /// compatibility with btrfs-progs CLI)
50    #[clap(short, long, help_heading = HEADING_FIELD_SELECTION)]
51    generation: bool,
52
53    /// Print UUID column
54    #[clap(short, long, help_heading = HEADING_FIELD_SELECTION)]
55    uuid: bool,
56
57    /// Print parent UUID column
58    #[clap(short = 'Q', long, help_heading = HEADING_FIELD_SELECTION)]
59    parent_uuid: bool,
60
61    /// Print received UUID column
62    #[clap(short = 'R', long, help_heading = HEADING_FIELD_SELECTION)]
63    received_uuid: bool,
64
65    /// List only snapshots (subvolumes with a non-nil parent UUID)
66    #[clap(short = 's', long, help_heading = HEADING_TYPE_FILTERING)]
67    snapshots_only: bool,
68
69    /// List only read-only subvolumes
70    #[clap(short = 'r', long, help_heading = HEADING_TYPE_FILTERING)]
71    readonly: bool,
72
73    /// List deleted subvolumes that are not yet cleaned
74    #[clap(short = 'd', long, help_heading = HEADING_TYPE_FILTERING)]
75    deleted: bool,
76
77    /// Print the result as a table
78    #[clap(short = 't', long, help_heading = "Other")]
79    table: bool,
80
81    /// Filter by generation: VALUE (exact), +VALUE (>= VALUE), -VALUE (<= VALUE)
82    #[clap(short = 'G', long, value_name = "[+|-]VALUE", allow_hyphen_values = true, help_heading = HEADING_SORTING)]
83    gen_filter: Option<GenFilter>,
84
85    /// Filter by ogeneration: VALUE (exact), +VALUE (>= VALUE), -VALUE (<= VALUE)
86    #[clap(short = 'C', long, value_name = "[+|-]VALUE", allow_hyphen_values = true, help_heading = HEADING_SORTING)]
87    ogen_filter: Option<GenFilter>,
88
89    /// Sort by comma-separated keys: gen, ogen, rootid, path
90    ///
91    /// Prefix with + (ascending, default) or - (descending).
92    /// Example: --sort=gen,-ogen,path
93    #[clap(
94        long,
95        value_name = "KEYS",
96        value_delimiter = ',',
97        allow_hyphen_values = true,
98        help_heading = HEADING_SORTING,
99    )]
100    sort: Vec<SortKey>,
101
102    /// Path to a mounted btrfs filesystem
103    path: PathBuf,
104}
105
106#[derive(Serialize)]
107struct SubvolListJson {
108    id: u64,
109    generation: u64,
110    ogeneration: u64,
111    parent: u64,
112    top_level: u64,
113    path: String,
114    uuid: String,
115    parent_uuid: String,
116    received_uuid: String,
117    readonly: bool,
118}
119
120impl SubvolListJson {
121    fn from_item(item: &SubvolumeListItem) -> Self {
122        Self {
123            id: item.root_id,
124            generation: item.generation,
125            ogeneration: item.otransid,
126            parent: item.parent_id,
127            top_level: item.parent_id,
128            path: if item.name.is_empty() {
129                "<unknown>".to_string()
130            } else {
131                item.name.clone()
132            },
133            uuid: fmt_uuid(&item.uuid),
134            parent_uuid: fmt_uuid(&item.parent_uuid),
135            received_uuid: fmt_uuid(&item.received_uuid),
136            readonly: item.flags.contains(SubvolumeFlags::RDONLY),
137        }
138    }
139}
140
141impl Runnable for SubvolumeListCommand {
142    fn supported_formats(&self) -> &[Format] {
143        &[Format::Text, Format::Json, Format::Modern]
144    }
145
146    fn run(&self, ctx: &RunContext) -> Result<()> {
147        let file = open_path(&self.path)?;
148
149        let mut items = subvolume_list(file.as_fd()).with_context(|| {
150            format!("failed to list subvolumes for '{}'", self.path.display())
151        })?;
152
153        let top_id = btrfs_uapi::inode::lookup_path_rootid(file.as_fd())
154            .with_context(|| "failed to get root id for path")?;
155
156        // Apply filters.
157        //
158        // Deleted subvolumes have parent_id == 0 (no ROOT_BACKREF found).
159        // By default they are hidden; -d shows only deleted; -a shows all.
160        if self.deleted {
161            items.retain(|item| item.parent_id == 0);
162        } else if !self.all {
163            items.retain(|item| item.parent_id != 0);
164        }
165        if self.readonly {
166            items.retain(|item| item.flags.contains(SubvolumeFlags::RDONLY));
167        }
168        if self.snapshots_only {
169            items.retain(|item| !item.parent_uuid.is_nil());
170        }
171        if let Some(ref f) = self.gen_filter {
172            items.retain(|item| f.matches(item.generation));
173        }
174        if let Some(ref f) = self.ogen_filter {
175            items.retain(|item| f.matches(item.otransid));
176        }
177        if self.only_below {
178            // -o: only list subvolumes that are direct children of the
179            // subvolume the fd is open on (i.e. whose parent_id matches the
180            // fd's root ID).
181            items.retain(|item| item.parent_id == top_id);
182        }
183
184        // -a: annotate paths of subvolumes outside the fd's subvolume with
185        // a <FS_TREE> prefix, matching btrfs-progs behaviour.
186        if self.all {
187            for item in &mut items {
188                if item.parent_id != 0
189                    && item.parent_id != top_id
190                    && !item.name.is_empty()
191                {
192                    item.name = format!("<FS_TREE>/{}", item.name);
193                }
194            }
195        }
196
197        // Sort.
198        if self.sort.is_empty() {
199            items.sort_by_key(|item| item.root_id);
200        } else {
201            items.sort_by(|a, b| {
202                for key in &self.sort {
203                    let ord = key.compare(a, b);
204                    if ord != Ordering::Equal {
205                        return ord;
206                    }
207                }
208                Ordering::Equal
209            });
210        }
211
212        match ctx.format {
213            Format::Modern => self.print_modern(&items),
214            Format::Text => {
215                if self.table {
216                    self.print_table(&items);
217                } else {
218                    self.print_default(&items);
219                }
220            }
221            Format::Json => {
222                let json: Vec<SubvolListJson> =
223                    items.iter().map(SubvolListJson::from_item).collect();
224                print_json("subvolume-list", &json)?;
225            }
226        }
227
228        Ok(())
229    }
230}
231
232impl SubvolumeListCommand {
233    fn print_default(&self, items: &[SubvolumeListItem]) {
234        for item in items {
235            let name = if item.name.is_empty() {
236                "<unknown>"
237            } else {
238                &item.name
239            };
240
241            // Build the output line incrementally in the same field order as
242            // btrfs-progs: ID, gen, [cgen,] top level, [parent,] path, [uuid,]
243            // [parent_uuid,] [received_uuid]
244            let mut line =
245                format!("ID {} gen {}", item.root_id, item.generation);
246
247            if self.ogeneration {
248                let _ = write!(line, " cgen {}", item.otransid);
249            }
250
251            let _ = write!(line, " top level {}", item.parent_id);
252
253            if self.parent {
254                let _ = write!(line, " parent {}", item.parent_id);
255            }
256
257            let _ = write!(line, " path {name}");
258
259            if self.uuid {
260                let _ = write!(line, " uuid {}", fmt_uuid(&item.uuid));
261            }
262
263            if self.parent_uuid {
264                let _ = write!(
265                    line,
266                    " parent_uuid {}",
267                    fmt_uuid(&item.parent_uuid)
268                );
269            }
270
271            if self.received_uuid {
272                let _ = write!(
273                    line,
274                    " received_uuid {}",
275                    fmt_uuid(&item.received_uuid)
276                );
277            }
278
279            println!("{line}");
280        }
281    }
282
283    fn print_table(&self, items: &[SubvolumeListItem]) {
284        // Collect column headers and data in order.
285        let mut headers: Vec<&str> = vec!["ID", "gen"];
286        if self.ogeneration {
287            headers.push("cgen");
288        }
289        headers.push("top level");
290        if self.parent {
291            headers.push("parent");
292        }
293        headers.push("path");
294        if self.uuid {
295            headers.push("uuid");
296        }
297        if self.parent_uuid {
298            headers.push("parent_uuid");
299        }
300        if self.received_uuid {
301            headers.push("received_uuid");
302        }
303
304        // Print header row.
305        println!("{}", headers.join("\t"));
306
307        // Print separator.
308        let sep: Vec<String> =
309            headers.iter().map(|h| "-".repeat(h.len())).collect();
310        println!("{}", sep.join("\t"));
311
312        // Print rows.
313        for item in items {
314            let name = if item.name.is_empty() {
315                "<unknown>"
316            } else {
317                &item.name
318            };
319
320            let mut cols: Vec<String> =
321                vec![item.root_id.to_string(), item.generation.to_string()];
322            if self.ogeneration {
323                cols.push(item.otransid.to_string());
324            }
325            cols.push(item.parent_id.to_string());
326            if self.parent {
327                cols.push(item.parent_id.to_string());
328            }
329            cols.push(name.to_string());
330            if self.uuid {
331                cols.push(fmt_uuid(&item.uuid));
332            }
333            if self.parent_uuid {
334                cols.push(fmt_uuid(&item.parent_uuid));
335            }
336            if self.received_uuid {
337                cols.push(fmt_uuid(&item.received_uuid));
338            }
339
340            println!("{}", cols.join("\t"));
341        }
342    }
343}
344
345#[derive(Cols)]
346struct SubvolRow {
347    #[column(right)]
348    id: u64,
349    #[column(header = "GEN", right)]
350    generation: u64,
351    #[column(header = "CGEN", right)]
352    cgen: u64,
353    #[column(right)]
354    parent: u64,
355    #[column(tree)]
356    path: String,
357    uuid: String,
358    parent_uuid: String,
359    received_uuid: String,
360    #[column(children)]
361    children: Vec<Self>,
362}
363
364impl SubvolRow {
365    fn from_item(item: &SubvolumeListItem) -> Self {
366        let name = if item.name.is_empty() {
367            "<unknown>".to_string()
368        } else {
369            item.name.clone()
370        };
371        Self {
372            id: item.root_id,
373            generation: item.generation,
374            cgen: item.otransid,
375            parent: item.parent_id,
376            path: name,
377            uuid: fmt_uuid(&item.uuid),
378            parent_uuid: fmt_uuid(&item.parent_uuid),
379            received_uuid: fmt_uuid(&item.received_uuid),
380            children: Vec::new(),
381        }
382    }
383}
384
385/// Recursively remove a row from the map and attach its children.
386fn attach_children(
387    id: u64,
388    rows: &mut BTreeMap<u64, SubvolRow>,
389    children_map: &BTreeMap<u64, Vec<u64>>,
390) -> Option<SubvolRow> {
391    let mut row = rows.remove(&id)?;
392    if let Some(child_ids) = children_map.get(&id) {
393        for &child_id in child_ids {
394            if let Some(child) = attach_children(child_id, rows, children_map) {
395                row.children.push(child);
396            }
397        }
398    }
399    Some(row)
400}
401
402/// Build a tree of `SubvolRow` from a flat list of items.
403///
404/// Items whose `parent_id` matches `top_id` become roots. All other items
405/// are nested under their parent. Items with no matching parent are added
406/// as roots to avoid losing them.
407fn build_tree(items: &[SubvolumeListItem]) -> Vec<SubvolRow> {
408    let mut rows: BTreeMap<u64, SubvolRow> = items
409        .iter()
410        .map(|i| (i.root_id, SubvolRow::from_item(i)))
411        .collect();
412
413    let mut children_map: BTreeMap<u64, Vec<u64>> = BTreeMap::new();
414    let mut roots = Vec::new();
415
416    // An item is a root if its parent is not in the item set (e.g.
417    // parent_id == 5 for FS_TREE which is not listed, or parent_id == 0
418    // for deleted subvolumes).
419    for item in items {
420        if rows.contains_key(&item.parent_id) {
421            children_map
422                .entry(item.parent_id)
423                .or_default()
424                .push(item.root_id);
425        } else {
426            roots.push(item.root_id);
427        }
428    }
429
430    let mut result: Vec<SubvolRow> = roots
431        .iter()
432        .filter_map(|&id| attach_children(id, &mut rows, &children_map))
433        .collect();
434
435    // Any remaining items (orphans with no matching parent) go at the root.
436    for (_, row) in rows {
437        result.push(row);
438    }
439
440    result
441}
442
443impl SubvolumeListCommand {
444    fn print_modern(&self, items: &[SubvolumeListItem]) {
445        let tree = build_tree(items);
446
447        let mut headers =
448            vec![SubvolRowHeader::Id, SubvolRowHeader::Generation];
449        if self.ogeneration {
450            headers.push(SubvolRowHeader::Cgen);
451        }
452        headers.push(SubvolRowHeader::Parent);
453        headers.push(SubvolRowHeader::Path);
454        if self.uuid {
455            headers.push(SubvolRowHeader::Uuid);
456        }
457        if self.parent_uuid {
458            headers.push(SubvolRowHeader::ParentUuid);
459        }
460        if self.received_uuid {
461            headers.push(SubvolRowHeader::ReceivedUuid);
462        }
463
464        let table = SubvolRow::to_table_with(&tree, &headers);
465        let mut out = std::io::stdout().lock();
466        let _ = cols::print_table(&table, &mut out);
467    }
468}
469
470fn fmt_uuid(u: &uuid::Uuid) -> String {
471    if u.is_nil() {
472        "-".to_string()
473    } else {
474        u.hyphenated().to_string()
475    }
476}
477
478/// A generation filter: exact match, >= (plus), or <= (minus).
479#[derive(Debug, Clone)]
480pub enum GenFilter {
481    Exact(u64),
482    AtLeast(u64),
483    AtMost(u64),
484}
485
486impl GenFilter {
487    fn matches(&self, value: u64) -> bool {
488        match self {
489            GenFilter::Exact(v) => value == *v,
490            GenFilter::AtLeast(v) => value >= *v,
491            GenFilter::AtMost(v) => value <= *v,
492        }
493    }
494}
495
496impl FromStr for GenFilter {
497    type Err = String;
498
499    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
500        if let Some(rest) = s.strip_prefix('+') {
501            let v: u64 = rest
502                .parse()
503                .map_err(|_| format!("invalid number: '{rest}'"))?;
504            Ok(GenFilter::AtLeast(v))
505        } else if let Some(rest) = s.strip_prefix('-') {
506            let v: u64 = rest
507                .parse()
508                .map_err(|_| format!("invalid number: '{rest}'"))?;
509            Ok(GenFilter::AtMost(v))
510        } else {
511            let v: u64 =
512                s.parse().map_err(|_| format!("invalid number: '{s}'"))?;
513            Ok(GenFilter::Exact(v))
514        }
515    }
516}
517
518/// A sort key with direction.
519#[derive(Debug, Clone)]
520pub struct SortKey {
521    field: SortField,
522    descending: bool,
523}
524
525#[derive(Debug, Clone)]
526enum SortField {
527    Gen,
528    Ogen,
529    Rootid,
530    Path,
531}
532
533impl SortKey {
534    fn compare(
535        &self,
536        a: &SubvolumeListItem,
537        b: &SubvolumeListItem,
538    ) -> Ordering {
539        let ord = match self.field {
540            SortField::Gen => a.generation.cmp(&b.generation),
541            SortField::Ogen => a.otransid.cmp(&b.otransid),
542            SortField::Rootid => a.root_id.cmp(&b.root_id),
543            SortField::Path => a.name.cmp(&b.name),
544        };
545        if self.descending { ord.reverse() } else { ord }
546    }
547}
548
549impl FromStr for SortKey {
550    type Err = String;
551
552    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
553        let (descending, field_str) = if let Some(rest) = s.strip_prefix('-') {
554            (true, rest)
555        } else if let Some(rest) = s.strip_prefix('+') {
556            (false, rest)
557        } else {
558            (false, s)
559        };
560
561        let field = match field_str {
562            "gen" => SortField::Gen,
563            "ogen" => SortField::Ogen,
564            "rootid" => SortField::Rootid,
565            "path" => SortField::Path,
566            _ => {
567                return Err(format!(
568                    "unknown sort key: '{field_str}' (expected gen, ogen, rootid, or path)"
569                ));
570            }
571        };
572
573        Ok(SortKey { field, descending })
574    }
575}