Trying the Crystal Language

Added by to Programming on (Updated ) and tagged · crystal · taskwarrior ·

About 7 minutes to read (1331 words)

Taskwarrior Editing Refinements Chaotic Symmetry
Trying the Crystal Language
Trying the Crystal Language
(see original image in new window)

Crystal is a statically typed, compiled programming language that looks a lot like Ruby. Let’s try it out! Maybe even work on a Taskwarrior thing I’ve been wanting to do.

Note
Layout might be a little weird. I’m trying out AsciiDoctor, and my style adjustments still need to be — um — adjusted.

Motivation

I want to play with a compiled language, but I’m not in the mood for Go or Rust.

Crystal looks friendly. Similar syntax to Ruby. Statically typed, ruling out a category of silly mistakes I always make (passing the wrong kind of value). Uses type inference, which means it can figure out what type a variable is without us telling it. Compiled, so you can run things quicker than in Ruby.

The philosophy summarized on the front page of the Crystal home page is “Fast as C, slick as Ruby”. That works for me.

I don’t care if it compiles super fast or the executable is super fast. An executable that runs quicker than my utility scripts, written in a language just as friendly, will be nice.

Installation

The documentation provides instructions on how to install Crystal across several platforms. Today I’m on my Windows partition. Though they’re working on a full Windows port, the best path for now is to use Homebrew on WSL.

Caution
I’m using WSL 2, which is still in development. I haven’t tested any of this under WSL 1.
$ brew install crystal
...
$ crystal version
Crystal 0.30.1 (2019-08-15)
LLVM: 8.0.1
Default target: x86_64-unknown-linux-gnu

crystal provides a collection of subcommands.

$ crystal help

Usage: crystal [command] [switches] [program file] [--] [arguments]

Command:
    init                     generate a new project
    build                    build an executable
    docs                     generate documentation
    env                      print Crystal environment information
    eval                     eval code from args or standard input
    play                     starts Crystal playground server
    run (default)            build and run program
    spec                     build and run specs (in spec directory)
    tool                     run a tool
    help, --help, -h         show this help
    version, --version, -v   show version

Oh my there are some interesting commands in there! I’ll just focus on the ones that result in me running code.

One-liners with eval

Crystal does not ship with an interactive shell. Makes sense, considering that it’s supposed to be a compiled language. Still, it’s more flexible than I expected. If all I want is a one-liner, Crystal can do that!

$ crystal eval 'puts "Hi!"'
Hi!

Okay, that wasn’t super exciting. What about pretty-printing the current environment variables?

$ crystal eval 'pp ENV'
{"BF" => "$albumartist | $album | $track/$tracktotal | $title",
 "BREW_PREFIX" => "/home/linuxbrew/.linuxbrew",
 "CLICOLOR" => "1",
 ...
 "_" => "/home/linuxbrew/.linuxbrew/bin/crystal",
 "wg_date" => "#[]%Y-%m-%d %H:%M%z#[default]"}

ENV is a Hash-like accessor, which means you can access and iterate through environment variables as if they were keys in a Hash.

crystal eval 'ENV.each { |k, v| puts "#{k}: #{v}" }'

Except that the pp output was sorted. That’s fine. We’ll sort the keys.

$ crystal eval 'ENV.keys.sorted.each { |k| puts "#{k}: #{ENV[k]}" }'

Printing data as JSON is a pretty common task. Common enough that it’s a Hash method here. Except ENV isn’t a Hash. It’s a "Hash-like accessor."

No problem, we can make a Hash from ENV.

$ crystal eval 'require "json"; env = Hash.zip(ENV.keys, ENV.values); puts env.to_json'
{"NVM_DIR":"/home/random/.nvm","HOSTTYPE":"x86_64","PYENV_HOME":"/home/random/.pyenv", ... "WSLENV":"","_":"/home/linuxbrew/.linuxbrew/bin/crystal"}

Well. I lost my sorting and the JSON isn’t exactly pretty-printed. I know I can fix this from inside Crystal, but my one-liner would get clunky. Hey, this is a one-liner. Let’s pipe it to jq!

$ crystal eval 'require "json"; env = Hash.zip(ENV.keys, ENV.values); puts env.to_json' | jq --sort-keys '.'

There, now it’s all pretty-printed.

One-liners are fun.

What was I doing next? Oh right. Crystal source files.

Shell scripts with run

I have no idea how to do dependency management in Crystal yet. Something about shards. But even stock Crystal looks like it would work as a glue language for shell scripts, and the run command lets you ignore the build process and pretend your code is being run directly.

hello-user.cr
#!/usr/bin/env crystal # (1)

print "Who are you? "
name = gets

if name == ENV["USER"]
  puts "Hey there #{name}!"
  tasks = `task +DUE count`.to_i # (2)
  puts "You have #{tasks} tasks due."
  puts "Better get on it!" if tasks > 0
else
  puts "I was not expecting you to say \"#{name}\"!"
end
  1. Since run is the default command, you could make a plain old Crystal script! What can I say, I like the old ways.

  2. Crystal ignores trailing whitespace in to_i but you’ll usually want to chomp when examining the output of a command.

$ chmod 700 hello-user.cr
$ ./hello-user.cr # (1)
Who are you? random
Hey there random!
You have 3 tasks due.
Better get on it!
  1. Remember, this is just me using #! to say crystal run hello-user.cr

It looks a lot like Ruby. You could take out that #! and it would almost work as-is.

$ ruby hello-user.cr
Who are you? random
I was not expecting you to say "random
"!

Almost.

build

Okay, being able to directly run Crystal files is great for testing or some types of quick tasks, but Crystal is a compiled language. Let’s compile something.

Paul Fenwick and Dave Jacoby have — or had, no idea if they still do — a neat trick to show quick task status in their shell prompt. I’ll do the same thing, but in Crystal.

task-indicator.cr
URGENT       = '\u{2757}'  # exclamation
DUE_TOMORROW = '\u{1f4c5}' # calendar
DUE_TODAY    = '\u{1f631}' # screaming
OVERDUE      = '\u{2620}'  # ded
OK           = "$"         # normal

def has_ready_tasks(extra_filter)
  `task +READY #{extra_filter} count`.to_i > 0
end

def task_indicator
  case
  when has_ready_tasks("+OVERDUE")
    OVERDUE
  when has_ready_tasks("+TODAY")
    DUE_TODAY
  when has_ready_tasks("+TOMORROW")
    DUE_TOMORROW
  when has_ready_tasks("urgency \\> 10")
    URGENT
  else
    "$"
  end
end

print task_indicator

Does it work?

$ crystal run task-indicator.cr
Output of task-indicator.cr showing something due today
Output of task-indicator.cr showing something due today
(see original image in new window)

Yeah, I know. I’m working on it. Anyways let’s build this thing.

$ crystal build task-indicator.cr
$ ls -lhF
total 1.3M
-rwxrwxr-x 1 random random 1.3M Aug 25 14:17 task-indicator*
-rw-rw-r-- 1 random random  546 Aug 25 14:05 task-indicator.cr

There’s a new executable called task-indicator. It’s not exactly small, but it produces the same output. Yes, the screaming face.

build --release

The default build includes a lot of debugging information that I won’t need in my daily usage. I’ll make a release version.

$ crystal build --release task-indicator.cr -o task-indicator-release
$ ls -lhF
total 2.0M
-rwxrwxr-x 1 random random 1.3M Aug 25 14:17 task-indicator*
-rwxrwxr-x 1 random random 665K Aug 25 14:23 task-indicator-release*
-rw-rw-r-- 1 random random  546 Aug 25 14:05 task-indicator.cr

I didn’t have to give it a different name, but it helps see the size difference between a debug build and a release build.

$ cp task-indicator-release ~/bin/task-indicator

Then over in my .bashrc

.bashrc
# Terminal colours (after installing GNU coreutils)
NM="\[\033[0;38m\]"  # means no background and white lines
HI="\[\033[0;34m\]"  # change this for username letter colors
HII="\[\033[0;31m\]" # change this for hostname letter colors
SI="\[\033[0;33m\]"  # this is for the current directory
IN="\[\033[0;0m\]"

PS1="$NM[ $HI\u $SI\w$NM ] \$(__git_ps1) $IN\n\$(task-indicator) "

Nice.

Note
2019-08-28
At first I put the output of task-indicator in a variable, and put the variable in $PS1. Except that variable was only evaluated on shell start. Instead put the invocation directly in $PS1 with a leading backslash. Now the indicator is live, as I expected it to be.

Done!

I wanted to learn some basic Crystal usage, seeing if there were ways I can work the language into my daily shell routine. With eval, run, and build all at my disposal, it sure looks that way! I even used Crystal to make a Taskwarrior indicator, which has been on my task list since last year.

I don’t know yet if Crystal is better than Ruby, but even at this early point it’s just as useful and just as much fun. Since "be useful and have fun" is a major thing for me, I’ll be exploring Crystal more!