[PATCH] Added miscutil/tree.c

rknecht at pm.me rknecht at pm.me
Sun Apr 10 11:24:35 UTC 2022


From: Roger Knecht <rknecht at pm.me>

Adds a new program which mimics the tree tool to list directories and files in a tree structure.

Signed-off-by: Roger Knecht <rknecht at pm.me>
---
Example:
```
$ ./busybox tree tree.tempdir/
tree.tempdir/
├── test1
├── test2
│   ├── a
│   │   ├── testfile1
│   │   ├── testfile2
│   │   └── testfile3
│   └── b
│       └── testfile4
└── test3
    ├── c
    │   └── testfile5
    └── d
        ├── testfile6
        └── testfile7

7 directories, 7 files
```

Note: To keep things simple this patch does not contain any command line options.
      I will add more options once this patch is accepted.

 AUTHORS              |   3 +
 miscutils/tree.c     | 189 +++++++++++++++++++++++++++++++++++++++++++
 testsuite/tree.tests |  68 ++++++++++++++++
 3 files changed, 260 insertions(+)
 create mode 100644 miscutils/tree.c
 create mode 100755 testsuite/tree.tests

diff --git a/AUTHORS b/AUTHORS
index 5c9a634c9..9ec0e2ee4 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -181,3 +181,6 @@ Jie Zhang <jie.zhang at analog.com>

 Maxime Coste <mawww at kakoune.org>
     paste implementation
+
+Roger Knecht <rknecht at pm.me>
+    tree
diff --git a/miscutils/tree.c b/miscutils/tree.c
new file mode 100644
index 000000000..37065a58d
--- /dev/null
+++ b/miscutils/tree.c
@@ -0,0 +1,189 @@
+/* vi: set sw=4 ts=4: */
+/*
+ * Copyright (C) 2022 Roger Knecht <rknecht at pm.me>
+ *
+ * Licensed under GPLv2, see file LICENSE in this source tree.
+ */
+//config:config TREE
+//config:	bool "tree"
+//config:	default n
+//config:	help
+//config:	List files and directories in a tree structure.
+//config:
+
+//applet:IF_TREE(APPLET(tree, BB_DIR_USR_BIN, BB_SUID_DROP))
+
+//kbuild:lib-$(CONFIG_TREE) += tree.o
+
+//usage:#define tree_trivial_usage NOUSAGE_STR
+//usage:#define tree_full_usage ""
+
+//usage:#define find_trivial_usage
+//usage:       "[PATH]"
+//usage:#define find_full_usage "\n\n"
+//usage:       "Print files and directories in a tree structure."
+//usage:       "Defaults: PATH is current directory\n"
+
+#include "libbb.h"
+
+#define INITIAL_ENTRY_ALLOCATION 10
+
+#define PREFIX_CHILD "├── "
+#define PREFIX_LAST_CHILD "└── "
+#define PREFIX_GRAND_CHILD "│   "
+#define PREFIX_LAST_GRAND_CHILD "    "
+
+static char DEFAULT_PATH[] = ".";
+static char DEFAULT_PREFIX[] = "";
+
+struct directory {
+	char* name;
+	char* path;
+	char* prefix_child;
+	char* prefix_last_child;
+	char* prefix_grand_child;
+	char* prefix_last_grand_child;
+};
+
+struct statistic {
+	int directories;
+	int files;
+};
+
+static int compare_dirent_alphabetically(const void* a, const void* b) {
+	return strcmp(((struct dirent*)a)->d_name, ((struct dirent*)b)->d_name);
+}
+
+static int read_sorted_directory_entries(char* path, struct dirent** entries) {
+	size_t index, size;
+	struct dirent *dirent;
+	DIR* dir;
+
+	size = INITIAL_ENTRY_ALLOCATION;
+	index = 0;
+	*entries = NULL;
+
+	// alloc directory entry array
+	if (!(*entries = malloc(sizeof(struct dirent) * size)))
+		return -ENOMEM;
+
+	// open directory
+	if (!(dir = opendir(path)))
+		return -EACCES;
+
+	// read each directory entry
+	while ((dirent = readdir(dir)) != NULL) {
+		// ignore hidden files
+		if (strncmp(dirent->d_name, ".", 1) == 0)
+			continue;
+
+		// check for free array slot
+		if (index >= size)
+			// enlarge directory entry array
+			if (!(*entries = realloc(*entries, sizeof(struct dirent) * (size *= 2)))) {
+				closedir(dir);
+				return -ENOMEM;
+			}
+
+		// copy directory entry
+		memcpy(&(*entries)[index], dirent, sizeof(struct dirent));
+		index++;
+	}
+
+	closedir(dir);
+
+	// sort directory alphabetically
+	qsort(*entries, index, sizeof(struct dirent), compare_dirent_alphabetically);
+
+	return index;
+}
+
+static void print_directory(struct directory* directory, struct statistic* statistic, bool is_last) {
+	struct dirent *entries, *dirent;
+	struct directory child_directory;
+	char *prefix, *child_prefix;
+	size_t child_prefix_strlen;
+	int index, entry_count;
+	bool is_last_child;
+
+	// read all directory entries
+	entry_count = read_sorted_directory_entries(directory->path, &entries);
+
+	// set directory prefix
+	prefix = is_last ? directory->prefix_last_child : directory->prefix_child;
+
+	// check if directory is readable
+	if (entry_count < 0) {
+		printf("%s%s [error opening dir]\n", prefix, directory->name);
+		return;
+	}
+
+	// print directory name
+	printf("%s%s\n", prefix, directory->name);
+
+	// print all directory entries
+	for (index = 0; index < entry_count; index++) {
+		dirent = &entries[index];
+
+		is_last_child = (index + 1) >= entry_count;
+		child_prefix = is_last ? directory->prefix_last_grand_child : directory->prefix_grand_child;
+		child_prefix_strlen = strlen(child_prefix);
+
+		// allocate memory for child directory
+		child_directory.name = dirent->d_name;
+		child_directory.path = alloca(strlen(directory->path) + strlen(dirent->d_name) + 2);
+		child_directory.prefix_child = alloca(child_prefix_strlen + sizeof(PREFIX_CHILD));
+		child_directory.prefix_last_child = alloca(child_prefix_strlen + sizeof(PREFIX_LAST_CHILD));
+		child_directory.prefix_grand_child = alloca(child_prefix_strlen + sizeof(PREFIX_GRAND_CHILD));
+		child_directory.prefix_last_grand_child = alloca(child_prefix_strlen + sizeof(PREFIX_LAST_GRAND_CHILD));
+
+		// concate child directory paths
+		sprintf(child_directory.path, "%s/%s", directory->path, dirent->d_name);
+		sprintf(child_directory.prefix_child, "%s%s", child_prefix, PREFIX_CHILD);
+		sprintf(child_directory.prefix_last_child, "%s%s", child_prefix, PREFIX_LAST_CHILD);
+		sprintf(child_directory.prefix_grand_child, "%s%s", child_prefix, PREFIX_GRAND_CHILD);
+		sprintf(child_directory.prefix_last_grand_child, "%s%s", child_prefix, PREFIX_LAST_GRAND_CHILD);
+
+		switch (dirent->d_type) {
+			// directories
+			case DT_DIR:
+				statistic->directories++;
+				print_directory(&child_directory, statistic, is_last_child);
+				break;
+
+			// regular files, symlinks, etc.
+			default:
+				statistic->files++;
+				printf("%s%s\n", is_last_child ? child_directory.prefix_last_child : child_directory.prefix_child, dirent->d_name);
+				break;
+		}
+	}
+
+	free(entries);
+}
+
+int tree_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
+int tree_main(int argc UNUSED_PARAM, char **argv UNUSED_PARAM)
+{
+	struct directory directory;
+	struct statistic statistic;
+
+	memset(&statistic, 0, sizeof(statistic));
+
+	// set directory path
+	directory.path = directory.name = (argc > 1) ? argv[1] : DEFAULT_PATH;
+
+	// initialize empty prefixes
+	directory.prefix_child = directory.prefix_last_child
+							= directory.prefix_grand_child
+							= directory.prefix_last_grand_child
+							= DEFAULT_PREFIX;
+
+	// print directories and files
+	print_directory(&directory, &statistic, true);
+
+	// print statistic
+	printf("\n%d directories, %d files\n", statistic.directories, statistic.files);
+
+	return EXIT_SUCCESS;
+}
diff --git a/testsuite/tree.tests b/testsuite/tree.tests
new file mode 100755
index 000000000..37586e530
--- /dev/null
+++ b/testsuite/tree.tests
@@ -0,0 +1,68 @@
+#!/bin/sh
+
+# Copyright 2022 by Roger Knecht <rknecht at pm.me>
+# Licensed under GPLv2, see file LICENSE in this source tree.
+
+. ./testing.sh -v
+
+# testing "description" "command" "result" "infile" "stdin"
+
+testing "tree error opening dir" \
+	"tree tree.tempdir" \
+	"\
+tree.tempdir [error opening dir]\n\
+\n\
+0 directories, 0 files\n" \
+	"" ""
+
+mkdir -p tree2.tempdir
+touch tree2.tempdir/testfile
+
+testing "tree single file" \
+	"cd tree2.tempdir && tree" \
+	"\
+.\n\
+└── testfile\n\
+\n\
+0 directories, 1 files\n" \
+	"" ""
+
+mkdir -p tree3.tempdir/test1 \
+	 tree3.tempdir/test2/a \
+	 tree3.tempdir/test2/b \
+	 tree3.tempdir/test3/c \
+	 tree3.tempdir/test3/d
+
+touch tree3.tempdir/test2/a/testfile1 \
+	tree3.tempdir/test2/a/testfile2 \
+	tree3.tempdir/test2/a/testfile3 \
+	tree3.tempdir/test2/b/testfile4 \
+	tree3.tempdir/test3/c/testfile5 \
+	tree3.tempdir/test3/d/testfile6 \
+	tree3.tempdir/test3/d/testfile7
+
+testing "tree nested directories and files" \
+	"cd tree3.tempdir && tree" \
+	"\
+.\n\
+├── test1\n\
+├── test2\n\
+│   ├── a\n\
+│   │   ├── testfile1\n\
+│   │   ├── testfile2\n\
+│   │   └── testfile3\n\
+│   └── b\n\
+│       └── testfile4\n\
+└── test3\n\
+    ├── c\n\
+    │   └── testfile5\n\
+    └── d\n\
+        ├── testfile6\n\
+        └── testfile7\n\
+\n\
+7 directories, 7 files\n" \
+	"" ""
+
+rm -rf tree.tempdir tree2.tempdir tree3.tempdir
+
+exit $FAILCOUNT
--
2.17.1




More information about the busybox mailing list