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