Skip to main content

btrfs_cli/subvolume/
list.rs

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