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 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'
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:
.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
.
Top comments (5)
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: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:
&
as a separatorprintf
withxargs
tree
can take coloursNeat!
I didn't know about
git check-ignore
! That makes this a lot simpler :)OMG how have I never heard of
tree
?!?This is a wonderful post... thank you!
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.
This is amazing.