You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2025/06/19 08:11:24 UTC

(opendal) branch main updated: feat: implement --tree option for oli ls subcommand (#6311)

This is an automated email from the ASF dual-hosted git repository.

xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new d0bc8733e feat: implement --tree option for oli ls subcommand (#6311)
d0bc8733e is described below

commit d0bc8733e5508c31d521d6609b360ee1f7deb5d7
Author: Ruihang Xia <wa...@gmail.com>
AuthorDate: Thu Jun 19 16:11:19 2025 +0800

    feat: implement --tree option for oli ls subcommand (#6311)
    
    * feat: implement --tree option for oli ls subcommand
    
    Signed-off-by: Ruihang Xia <wa...@gmail.com>
    
    * test case
    
    Signed-off-by: Ruihang Xia <wa...@gmail.com>
    
    * snapshot test
    
    Signed-off-by: Ruihang Xia <wa...@gmail.com>
    
    ---------
    
    Signed-off-by: Ruihang Xia <wa...@gmail.com>
---
 bin/oli/src/commands/ls.rs      | 104 ++++++++++++++++++++++++++++++++++++++++
 bin/oli/tests/integration/ls.rs |  35 ++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/bin/oli/src/commands/ls.rs b/bin/oli/src/commands/ls.rs
index f9805577a..a2b65987f 100644
--- a/bin/oli/src/commands/ls.rs
+++ b/bin/oli/src/commands/ls.rs
@@ -17,11 +17,17 @@
 
 use anyhow::Result;
 use futures::TryStreamExt;
+use std::collections::BTreeMap;
 
 use crate::config::Config;
 use crate::make_tokio_runtime;
 use crate::params::config::ConfigParams;
 
+const TREE_LAST_ITEM: &str = "└── ";
+const TREE_MIDDLE_ITEM: &str = "├── ";
+const TREE_EMPTY_PREFIX: &str = "    ";
+const TREE_VERTICAL_LINE: &str = "│   ";
+
 #[derive(Debug, clap::Parser)]
 #[command(name = "ls", about = "List object", disable_version_flag = true)]
 pub struct LsCmd {
@@ -33,17 +39,42 @@ pub struct LsCmd {
     /// List objects recursively.
     #[arg(short, long)]
     pub recursive: bool,
+    /// List objects in a tree-like format.
+    #[arg(short = 'T', long)]
+    pub tree: bool,
+}
+
+/// A node in the tree representation of a directory.
+///
+/// It can be a directory or a file. A directory is a node with `is_dir` set to `true`.
+struct TreeNode {
+    is_dir: bool,
+    children: BTreeMap<String, TreeNode>,
+}
+
+impl TreeNode {
+    fn new() -> Self {
+        Self {
+            is_dir: false,
+            children: BTreeMap::new(),
+        }
+    }
 }
 
 impl LsCmd {
     pub fn run(self) -> Result<()> {
         make_tokio_runtime(1).block_on(self.do_run())
     }
+
     async fn do_run(self) -> Result<()> {
         let cfg = Config::load(&self.config_params.config)?;
 
         let (op, path) = cfg.parse_location(&self.target)?;
 
+        if self.tree {
+            return self.run_tree(&op, &path).await;
+        }
+
         if !self.recursive {
             let mut ds = op.lister(&path).await?;
             while let Some(de) = ds.try_next().await? {
@@ -58,4 +89,77 @@ impl LsCmd {
         }
         Ok(())
     }
+
+    /// List objects in a tree-like format.
+    ///
+    /// This function fetches all entries recursively, builds a `TreeNode` structure representing
+    /// the directory hierarchy, and then prints it using `print_tree`. The root of the tree
+    /// is displayed as `.`.
+    async fn run_tree(&self, op: &opendal::Operator, path: &str) -> Result<()> {
+        let mut root = TreeNode::new();
+
+        let mut ds = op.lister_with(path).recursive(true).await?;
+        while let Some(de) = ds.try_next().await? {
+            let p = de.path();
+            let is_dir = p.ends_with('/');
+            let rel_p = p.strip_prefix(path).unwrap_or(p);
+
+            let mut current_node = &mut root;
+            let components: Vec<&str> = rel_p.split('/').filter(|s| !s.is_empty()).collect();
+
+            if let Some((last, elements)) = components.split_last() {
+                for part in elements {
+                    current_node = current_node
+                        .children
+                        .entry(part.to_string())
+                        .or_insert_with(TreeNode::new);
+                    current_node.is_dir = true;
+                }
+                let node = current_node
+                    .children
+                    .entry(last.to_string())
+                    .or_insert_with(TreeNode::new);
+                node.is_dir |= is_dir;
+            }
+        }
+
+        println!(".");
+        print_tree(&root, "");
+
+        Ok(())
+    }
+}
+
+/// Print the tree structure recursively.
+///
+/// This function iterates through the children of a `TreeNode`, printing each one with appropriate
+/// connectors to form a tree-like structure. It determines whether a child is the last in the list
+/// to use the correct connector (`└── ` for the last, `├── ` for others). It then recursively
+/// calls itself for subdirectories, adjusting the prefix to maintain the tree alignment.
+fn print_tree(node: &TreeNode, prefix: &str) {
+    let mut it = node.children.iter().peekable();
+    while let Some((name, child_node)) = it.next() {
+        let is_last = it.peek().is_none();
+
+        let connector = if is_last {
+            TREE_LAST_ITEM
+        } else {
+            TREE_MIDDLE_ITEM
+        };
+        let display_name = if child_node.is_dir {
+            format!("{}/", name)
+        } else {
+            name.to_string()
+        };
+        println!("{}{}{}", prefix, connector, display_name);
+
+        if !child_node.children.is_empty() {
+            let new_prefix = if is_last {
+                TREE_EMPTY_PREFIX
+            } else {
+                TREE_VERTICAL_LINE
+            };
+            print_tree(child_node, &format!("{}{}", prefix, new_prefix));
+        }
+    }
 }
diff --git a/bin/oli/tests/integration/ls.rs b/bin/oli/tests/integration/ls.rs
index 1ba546f39..617ad4071 100644
--- a/bin/oli/tests/integration/ls.rs
+++ b/bin/oli/tests/integration/ls.rs
@@ -44,3 +44,38 @@ async fn test_basic_ls() -> Result<()> {
 
     Ok(())
 }
+
+#[tokio::test]
+async fn test_ls_tree() -> Result<()> {
+    let dir = tempfile::tempdir()?;
+    fs::create_dir_all(dir.path().join("a/b"))?;
+
+    let file_root = dir.path().join("file_root");
+    let file_a = dir.path().join("a/file_a");
+    let file_b = dir.path().join("a/b/file_b");
+
+    let content = "hello";
+    fs::write(&file_root, content)?;
+    fs::write(&file_a, content)?;
+    fs::write(&file_b, content)?;
+
+    let current_dir = dir.path().to_string_lossy().to_string() + "/";
+    let mut cmd = oli();
+    cmd.arg("ls").arg("--tree").arg(current_dir);
+
+    assert_cmd_snapshot!(cmd, @r#"
+success: true
+exit_code: 0
+----- stdout -----
+.
+├── a/
+│   ├── b/
+│   │   └── file_b
+│   └── file_a
+└── file_root
+
+----- stderr -----
+"#);
+
+    Ok(())
+}