ShellTree 3: Modifying and Optimizing the One Line Command Implementation

This post is part of an educational series on building a shell script to graphically display the structure of a directory.

Previously

  1. We broke down Dem Pilafian’s one line command to display a tree of a directory
  2. We broke down Dem Pilafian’s script that uses the one line command

Setup

Directory and Script Setup

Please check the first and second post on the tree command to see our directory and script setup.

Issues

  1. I would like to see the files in my directory, or, at least, have an option that allows me to see the files.
  2. I can’t see hidden directories (directories starting with a .)
  3. I would like a clearer presentation of the structure.
  4. If a non-existant directory is given as an argument, it should tell us that it does not exist and exit.

Optimizations

Seeing the Files

This should be easy. Let’s first see how the ls -R command looks like:

[ahmed@amayem .git]$ ls -R
.:
branches  config  description  HEAD  hooks  info  objects  refs  test.sh

./branches:

./hooks:
applypatch-msg.sample  post-update.sample     pre-commit.sample          pre-rebase.sample
commit-msg.sample      pre-applypatch.sample  prepare-commit-msg.sample  update.sample

./info:
exclude

./objects:
info  pack

./objects/info:

./objects/pack:

./refs:
heads  tags

./refs/heads:

./refs/tags:

We notice a small problem, the directories are listed more than once. Once inside the parent directory, and once when its contents are listed:

branches  config  description  HEAD  hooks  info  objects  refs  test.sh

./branches:

We have two branches listed. Let’s differentiate the contents of each directory using the -p flag:

-p, --indicator-style=slash
          append / indicator to directories

This will add a / to the directories:

[ahmed@amayem .git]$ ls -Rp
.:
branches/  config  description  HEAD  hooks/  info/  objects/  refs/  test.sh

./branches:

./hooks:
applypatch-msg.sample  post-update.sample     pre-commit.sample          pre-rebase.sample
commit-msg.sample      pre-applypatch.sample  prepare-commit-msg.sample  update.sample

./info:
exclude

./objects:
info/  pack/

./objects/info:

./objects/pack:

./refs:
heads/  tags/

./refs/heads:

./refs/tags:

Great, now let’s get rid of the directories as listed in the parent directory. We will use grep "[^/]$" to capture all the lines that don’t end with a /.

[ahmed@amayem .git]$ ls -RF | grep "[^/]$"
.:
config
description
HEAD
test.sh*
./branches:
./hooks:
applypatch-msg.sample*
commit-msg.sample*
post-update.sample*
pre-applypatch.sample*
pre-commit.sample*
prepare-commit-msg.sample*
pre-rebase.sample*
update.sample*
./info:
exclude
./objects:
./objects/info:
./objects/pack:
./refs:
./refs/heads:
./refs/tags:

I would like to use sed now to add the beginning dashes, but I have a problem. How do I figure out how many dashes to put before a file name? We figured out how many dashes to put before a directory based on the number of slashes, / in its pathname, e.g. I can replace objects/ in ./objects/info: with two dashes. After some digging around I found this command:

[ahmed@amayem .git]$ ls -1 */*
hooks/applypatch-msg.sample
hooks/commit-msg.sample
hooks/post-update.sample
hooks/pre-applypatch.sample
hooks/pre-commit.sample
hooks/prepare-commit-msg.sample
hooks/pre-rebase.sample
hooks/update.sample
info/exclude

objects/info:

objects/pack:

refs/heads:

refs/tags:

I got some of the files with their relative paths, however this command is not recursive. That means that to get the files three levels down I need to run ls -1 */*/*. It also has the obvious problem of not displaying the files and directories above the requested depth.

Trying find

During my digging around I found out I could use the nifty command find.

[ahmed@amayem .git]$ find
.
./branches
./hooks
./hooks/prepare-commit-msg.sample
./hooks/pre-applypatch.sample
./hooks/pre-rebase.sample
./hooks/applypatch-msg.sample
./hooks/commit-msg.sample
./hooks/post-update.sample
./hooks/pre-commit.sample
./hooks/update.sample
./info
./info/exclude
./config
./test.sh
./description
./refs
./refs/tags
./refs/heads
./objects
./objects/info
./objects/pack
./HEAD

That’s great. Now we can get rid of the the grep ":$" | sed -e 's/:$//' portion of the original command.

[ahmed@amayem .git]$ find | sed -e 's/[^-][^/]*//--/g' -e 's/^/   /' -e 's/-/|/' 
   .
   |-branches
   |-hooks
   |---prepare-commit-msg.sample
   |---pre-applypatch.sample
   |---pre-rebase.sample
   |---applypatch-msg.sample
   |---commit-msg.sample
   |---post-update.sample
   |---pre-commit.sample
   |---update.sample
   |-info
   |---exclude
   |-config
   |-test.sh
   |-description
   |-refs
   |---tags
   |---heads
   |-objects
   |---info
   |---pack
   |-HEAD

That’s much better. However, I think we can do better. With this output I can’t tell whether config for example is a directory or a file. It does not seem that find has the ability to put a slash, /, after directories like ls. I also like the options that ls has, because it would be more compatible with the tree command that we are designing, in that it is a listing of contents, while as the find command searches for files.

Let’s move on to making a larger script that does what I want in the next post

Next steps

  1. Building a new recursive script that looks better and has more functionality

Note

We don’t have to add the -1 flag to print everything out in a list, because when ls output is piped it is automatically printed as a list:

 -1      (The numeric digit ``one''.)  Force output to be one entry per line.  This is the default when output is not to a terminal.

References

  1. Dem Pilafian on centerkey
  2. Matthew Scharley‘s and user431529‘s answers to this stackoverflow question.

Ahmed Amayem has written 90 articles

A Web Application Developer Entrepreneur.