Browse Source

tree: new applet

Adds the tree program to list directories and files in a tree structure.

function                                             old     new   delta
tree_print                                             -     343    +343
scandir64                                              -     330    +330
scandir                                                -     330    +330
tree_main                                              -      86     +86
.rodata                                           105150  105228     +78
packed_usage                                       34511   34557     +46
alphasort64                                            -      31     +31
alphasort                                              -      31     +31
strcoll                                                -       5      +5
applet_names                                        2801    2806      +5
applet_main                                         1616    1620      +4
applet_suid                                          101     102      +1
applet_install_loc                                   202     203      +1
------------------------------------------------------------------------------
(add/remove: 11/0 grow/shrink: 6/0 up/down: 1291/0)          Total: 1291 bytes

Signed-off-by: Roger Knecht <rknecht@pm.me>
Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
Roger Knecht 1 year ago
parent
commit
20a4f70eca
3 changed files with 221 additions and 0 deletions
  1. 3 0
      AUTHORS
  2. 118 0
      miscutils/tree.c
  3. 100 0
      testsuite/tree.tests

+ 3 - 0
AUTHORS

@@ -181,3 +181,6 @@ Jie Zhang <jie.zhang@analog.com>
 
 Maxime Coste <mawww@kakoune.org>
     paste implementation
+
+Roger Knecht <rknecht@pm.me>
+    tree

+ 118 - 0
miscutils/tree.c

@@ -0,0 +1,118 @@
+/* vi: set sw=4 ts=4: */
+/*
+ * Copyright (C) 2022 Roger Knecht <rknecht@pm.me>
+ *
+ * Licensed under GPLv2, see file LICENSE in this source tree.
+ */
+//config:config TREE
+//config:	bool "tree (0.6 kb)"
+//config:	default y
+//config:	help
+//config:	List files and directories in a tree structure.
+
+//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 ""
+
+#include "libbb.h"
+#include "common_bufsiz.h"
+
+#define prefix_buf bb_common_bufsiz1
+
+static void tree_print(unsigned count[2], const char* directory_name, char* prefix_pos)
+{
+	struct dirent **entries;
+	int index, size;
+
+	// read directory entries
+	size = scandir(directory_name, &entries, NULL, alphasort);
+
+	if (size < 0) {
+		fputs_stdout(directory_name);
+		puts(" [error opening dir]");
+		return;
+	}
+
+	// print directory name
+	puts(directory_name);
+
+	// switch to sub directory
+	xchdir(directory_name);
+
+	// print all directory entries
+	for (index = 0; index < size;) {
+		struct dirent *dirent = entries[index++];
+
+		// filter hidden files and directories
+		if (dirent->d_name[0] != '.') {
+			int status;
+			struct stat statBuf;
+
+//TODO: when -l is implemented, use stat, not lstat, if -l
+			status = lstat(dirent->d_name, &statBuf);
+
+			if (index == size) {
+				strcpy(prefix_pos, "└── ");
+			} else {
+				strcpy(prefix_pos, "├── ");
+			}
+			fputs_stdout(prefix_buf);
+
+			if (status == 0 && S_ISLNK(statBuf.st_mode)) {
+				// handle symlink
+				char* symlink_path = xmalloc_readlink(dirent->d_name);
+				printf("%s -> %s\n", dirent->d_name, symlink_path);
+				free(symlink_path);
+				count[1]++;
+			} else if (status == 0 && S_ISDIR(statBuf.st_mode)
+			 && (prefix_pos - prefix_buf) < (COMMON_BUFSIZE - 16)
+			) {
+				// handle directory
+				char* pos;
+				if (index == size) {
+					pos = stpcpy(prefix_pos, "    ");
+				} else {
+					pos = stpcpy(prefix_pos, "│   ");
+				}
+				tree_print(count, dirent->d_name, pos);
+				count[0]++;
+			} else {
+				// handle file
+				puts(dirent->d_name);
+				count[1]++;
+			}
+		}
+
+		// release directory entry
+		free(dirent);
+	}
+
+	// release directory array
+	free(entries);
+
+	// switch to parent directory
+	xchdir("..");
+}
+
+int tree_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
+int tree_main(int argc UNUSED_PARAM, char **argv)
+{
+	unsigned count[2] = { 0, 0 };
+
+	setup_common_bufsiz();
+
+	if (!argv[1])
+		*argv-- = (char*)".";
+
+	// list directories given as command line arguments
+	while (*(++argv))
+		tree_print(count, *argv, prefix_buf);
+
+	// print statistic
+	printf("\n%u directories, %u files\n", count[0], count[1]);
+
+	return EXIT_SUCCESS;
+}

+ 100 - 0
testsuite/tree.tests

@@ -0,0 +1,100 @@
+#!/bin/sh
+
+# Copyright 2022 by Roger Knecht <rknecht@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
+
+(cd tree3.tempdir/test2/a && ln -s ../b/testfile4 .)
+(cd tree3.tempdir/test2/b && ln -s ../../test3 .)
+
+testing "tree nested directories and files" \
+	"cd tree3.tempdir && tree" \
+	"\
+.\n\
+├── test1\n\
+├── test2\n\
+│   ├── a\n\
+│   │   ├── testfile1\n\
+│   │   ├── testfile2\n\
+│   │   ├── testfile3\n\
+│   │   └── testfile4 -> ../b/testfile4\n\
+│   └── b\n\
+│       ├── test3 -> ../../test3\n\
+│       └── testfile4\n\
+└── test3\n\
+    ├── c\n\
+    │   └── testfile5\n\
+    └── d\n\
+        └── testfile6\n\
+\n\
+7 directories, 8 files\n" \
+	"" ""
+#note: tree v2.0.1 says "8 directories, 7 files":
+#it counts "test3 -> ../../test3" as a directory, even though it does not follow this symlink
+
+testing "tree multiple directories" \
+	"tree tree2.tempdir tree3.tempdir" \
+	"\
+tree2.tempdir\n\
+└── testfile\n\
+tree3.tempdir\n\
+├── test1\n\
+├── test2\n\
+│   ├── a\n\
+│   │   ├── testfile1\n\
+│   │   ├── testfile2\n\
+│   │   ├── testfile3\n\
+│   │   └── testfile4 -> ../b/testfile4\n\
+│   └── b\n\
+│       ├── test3 -> ../../test3\n\
+│       └── testfile4\n\
+└── test3\n\
+    ├── c\n\
+    │   └── testfile5\n\
+    └── d\n\
+        └── testfile6\n\
+\n\
+7 directories, 9 files\n" \
+	"" ""
+#note: tree v2.0.1 says "8 directories, 7 files" (not "8 files", probably a/testfile4 -> ../b/testfile4 and b/testfile4 are counted as one file, not 2?)
+
+rm -rf tree.tempdir tree2.tempdir tree3.tempdir
+
+exit $FAILCOUNT