Shell Bonsai with tree
Tags: bash, git, linux
Published: 2018-07-01 16:05:38

---
title: Shell Bonsai with tree
published: true
tags: bash,git,linux
canonical_url: https://di.nmfay.com/bonsai
---

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](https://github.com/radare/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](https://www.systutorials.com/docs/linux/man/1-tree/).

I also do a lot of work on projects which contain certain files I don't care about. With git, I use a [`.gitignore`](https://git-scm.com/docs/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](https://github.com/BurntSushi/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):

```bash
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:

```bash
(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.

```bash
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 **s** tream **ed** itor 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:

```bash
tr -d '\n'
```

Unlike `sed`, `tr` ( **tr** anslate) 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 `.gitignore`s transformed into this:

```bash
.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:

```bash
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.

```bash
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`.