Let’s start with a list of the directory’s contents. We can worry about summarizing them later.
Dir knows all about directories and their contents. Open a directory with a string containing a path, and ask for its children.
Dir#children gets you all the files in a directory except the special . and .. items. If you need those, use Dir#entries.
I need to look at each child if I want a readable summary of the directory. I could mess with the Array returned by Dir#children. There’s a better way, though. Crystal provides a handy iterator with
Dir#each_child.
That’s much easier to read. Yes. I can work with Dir#each_child to create a summary.
Summarize the directory contents
I want file names, sizes, and modification times. I already have the names. File.info provides size and time details. Formatting can be handled with a mix of sprintf and Number#format.
I worked these column widths out manually. There are more robust approaches. In fact, I’ll get to one of them in a few paragraphs.
This is nice and tidy! Of course, now I have more thoughts. The items need to be sorted — by name is good enough. I also want a more obvious indicator which ones are directories.
If a trailing / for directories is good enough for ls -F, it’s good enough for me.
This is better! I can use this information. Time to look at arbitrary directories.
Specifying a directory via ARGV
ARGV is a top level array holding arguments intended for your program. If we called a compiled Crystal program like this:
~/Sync/Books/computer would be the first and only item in ARGV.
NOTE
Some languages include the program name in their list of arguments. Crystal keeps the program name in PROGRAM_NAME, and the arguments in ARGV.
If I needed anything more than “grab the first item in ARGV,” I’d probably use OptionParser. But all I need is “grab the first item in ARGV.”
list.cr
NOTE
When using crystal run to execute a script, use -- to split arguments for crystal and those for your script. list.cr is for Crystal. ~/Sync/pictures/ is for the script.
This works, if you use it exactly right. Right now is where I’m tempted to say “Error handling is left as an exercise for the reader.” But no. Not this time.
Let’s build this up so it handles common errors and concerns.
Writing list.cr
There are a few things I want this program to do.
Tell me if I forgot the argument.
Tell me if the argument isn’t a real path.
If the argument is a directory, summarize the contents of that directory.
If the argument is a file, not a directory? Um — make a listing with one entry for the file.
I really want to be a little more precise with the column sizes.
That covers the likeliest possibilities running this program on my own computer. Besides, Crystal will let me know I forgot something.
I assembled this top-down, describing what I want to do and then describing how to do it. And even though Crystal doesn’t require a main method, that seems like a good place to start. If nothing else, it keeps the core logic in one place.
What does main do? It displays a summary_table of whatever I hand to it. If anything goes wrong, it quits with a fatal_error.
I don’t need to consider every possible error. But I should make sure we’re polite about the errors we do encounter. Rescue any exceptions that occur and hand them to fatal_error.
fatal_error prints its error message and usage info to STDERR.
That non-zero exit tells the shell something went wrong. Handy for piped commands and customized shell prompts that incorporate execution status.
The summary table glues together a collection of summary rows — even if it’s just a collection of one — composed from file summaries and formatted according to some basic guidelines about column size.
Short-circuit assignment uses the or operator || to succinctly set our summaries. We got a directory summary? Use it. No? Okay, try treating it as a single file. Whichever one returns a useful value first gets assigned to summaries.
Since we’re going top-down, we can say that a directory summary is a sorted collection of files summaries and move on.
Returning early for non-directories simplifies short-circuit assignment. This method knows it may be handed a regular file. Stopping right away prevents that from being treated the same as an error.
Oh here’s the work of summarizing. Build a name. Describe the size. Turn the file’s modification time into something we can read.
Okay that’s not much work after all. Especially considering that I already figured out how to describe size.
That’s a lot of methodchaining. Method chains are useful, but brittle. Temped to at least hide it in a new describe_time method. Oh well. Next time.
Yep. Turned that Proc from the other day into a method.
Number#humanize is a delightful convenience method for readable numbers. It adds commas where expected. It trims floating point numbers to more digestible precision. No word yet on whether it slices or dices.
column_sizes is dangerously close to clever — the bad kind of smart where I’m likely to miss a mistake. The intent is reasonable enough. Find how long each field is in each summary. Figure out which is the longest value for each column. But there’s probably a more legible way to do it.
Oh thank goodness. Back to fairly legible code with summary_row. Although. Honestly? I’m being so specific with how each item in the summary is treated. That calls out for a class, or at least a struct.
Not enough time to rewrite the whole program, though. Sometimes it’s more important to get to the next task than to get this one perfect.
Like most languages, Crystal’s String class has many methods to make life easier. String#ljust pads the end of a string. String#rjust pads at the start, which is nice for number columns. Though my humanized numbers do reduce the effectiveness of a numeric column.
That’s it? I’m done? Excellent!
Let’s build it and look at a random folder in my Sync archive.
Oh hey. Stuff from a couple old music management posts. Getting back to those is on the task list. I’ll get there.
Anyways. My list program works!
I learned a fair bit about managing collections in Crystal. Also, the “small methods” approach that served me well in Ruby seems just as handy here.
Yeah, I know
If file information was all I needed, I could get the same details and more with
ls.
But I wouldn’t have learned anything about Crystal. I wouldn’t have had nearly as much fun, either. And — not counting other concerns like “paying rent” or “eating” — fun is the most important part!