DEV Community

loading...

Shell Bonsai with tree

dmfay profile image Dian Fay Originally published at di.nmfay.com on ・3 min read

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"
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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\|/'
Enter fullscreen mode Exit fullscreen mode

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:

tr -d '\n'
Enter fullscreen mode Exit fullscreen mode

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 .gitignores transformed into this:

.git|src|pkg|**/*.tar.xz|
Enter fullscreen mode Exit fullscreen mode

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'"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (5)

Collapse
moopet profile image
Ben Sinclair • Edited

I think maybe you could use git check-ignore * instead of reaching for the local .gitignore file, because then it would work in subdirectories as well as the project root. Something like this works for me:

alias treo='tree -C -I $(git check-ignore * 2>/dev/null | tr "\n" "|").git'

I dug into this because your alias wouldn't work for me on my Mac (I have to use Macs at work...) and I couldn't quite figure out why.

Things I've learnt from your post include:

  • Using a single & as a separator
  • passing stuff into printf with xargs
  • tree can take colours

Neat!

Collapse
dmfay profile image
Dian Fay Author

I didn't know about git check-ignore! That makes this a lot simpler :)

Collapse
gonsie profile image
Elsa Gonsiorowski

OMG how have I never heard of tree?!?

This is a wonderful post... thank you!

Collapse
rhymes profile image
rhymes

Well written as well, thanks.

Thank you also for pointing me to rigrep, I switched to ack a long time ago but didn't know about this one.

Collapse
dmerand profile image
Donald Merand

This is amazing.

Forem Open with the Forem app