Shell Bonsai with tree

The shell has just about all the tooling I need for day-to-day operation of a computer: navigating and managing directories and files, text editing, and building, testing, and running projects I'm working on. What it isn't so great at is layouts, or really, displaying anything that isn't a text file (as fun as it is, I'm unwilling to switch out a proper image viewer for tiv).

Directory trees are one of the more commonly-encountered layouts that don't do too well with monospaced ASCII. There's the venerable tree -- and that just about covers the possibilities, because there aren't many more ways to display that kind of structure under those constraints. Fortunately, tree comes with amenities, from pattern-matching to JSON output.

I also do a lot of work on projects which contain certain files I don't care about. With git, I use a .gitignore file in the project root to ensure I don't accidentally add and commit them. This file gets used by more than git, too: my search utility of choice, ripgrep, respects .gitignore rules, as do many other tools all the way up to graphical IDEs.

tree, which predates git by something like a decade at absolute minimum, does not care about your .gitignore. When inspecting the layout of a repository with a moderately-sized ignore ruleset and/or something like node_modules, this makes it all but unusable.

One of tree's features is the -I flag, which ignores files matching a wildcard pattern similar to that used in .gitignore. That means it should be possible to hack something together which respects .gitignore rules without mucking around in coreutils: other system tools output and manipulate files, xargs can manage other commands' arguments, and pipes hook the whole thing together.

Here's the full alias from my .zshrc, if you're just interested in that part (note it all needs to be on one line):

alias trii="(cat .gitignore & echo '.git') |
  sed 's/^\(.\+\)$/\1\|/' |
  tr -d '\n' |
  xargs printf \"-I '%s'\" |
  xargs tree -C"

With the exception of -I, you can still pass tree's arguments to trii, so the rest of its toolkit is still available. It's also safe if there's no ignore file in the current directory.

Now, in more depth:

(cat .gitignore & echo '.git')

cat dumps the ignore file to standard output (the console) and echo simply repeats the string ".git" to ensure that the full ruleset excludes the repository directory itself (only a problem with the -a switch which displays hidden files and directories). The single & is just a separator to ensure that both commands run in sequence, as opposed to the more common double && which aborts at the first non-zero exit code. The parentheses run the whole thing in a subshell, returning the full output to be piped into the next segment.

sed 's/^\(.\+\)$/\1\|/'

You can't specify multiple -I values: the last one always wins. Instead, -I can read multiple patterns which are joined together with pipe | characters. That's possible, but it's going to take a couple of steps.

sed is a stream editor which modifies each line coming from the previous segment. Here, it's simply appending the pipe character. Because sed operates on each line as a discrete entity, it can't join them together; that's up to the next segment:

tr -d '\n'

Unlike sed, tr (translate) operates on standard input as it comes in, instead of line by line. The -d switch deletes characters, here the newline. This completes the ignore pattern, with a sample project's .gitignores transformed into this:

.git|src|pkg|**/*.tar.xz|

There's a terminating pipe, but it doesn't make a difference to tree. This line gets passed to yet another command:

xargs printf "-I '%s'"

xargs passes lines from standard input to another command. Here there's only one line, since tr removed all the newline characters, and it's being passed to printf. This is not to be confused with the C standard library function printf: it's a standalone program in the GNU coreutils, although it does much the same thing as its near relative. The net effect of this command is to print the -I switch and the concatenated ignore list together.

xargs tree -C

Finally, it's time to invoke tree! The -C flag adds color to the output. xargs passes the combined -I and ignorelist into the command string, and the result is a tree that excludes everything from the .gitignore.