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(())
+}