In Progress
Unit 1, Lesson 1
In Progress

I got my flippy-floppies

Put on your your best beach shoes, because today we venture ever deeper into the world of Ruby one-liners! In a quest to count lines of fenced code in Markdown files, we’ll meet END blocks, flip-flops, and implicit print arguments!

Video transcript & code

Lately we've been using Ruby from the command-line to extract useful bits of information about a directory full of Markdown files.

ls *.md

Some of these files contain fenced code blocks.

Let's say we want to count the lines of code inside fenced code blocks.

As usual, we'll work up to our goal step-by-step. A good first step might be to consume the Markdown files and print out only the fenced code blocks.

Let's take a first crack at this.

We'll use Ruby -n to automatically loop over every line of input.

And -e to evaluate some code.

Our code will start by initializing a tracking variable named code to false.

The purpose of this variable is to keep track of whether we're currently looking at a fenced code block or not.

For each line of input, we'll toggle the state of this variable if the current line matches a code fence pattern.

Then we'll output the current line only if the code variable is set to true.

When we execute this, we get more or less the output we're looking for.

Although we do note that the output includes the starting code fences, but not the ending fences.

ruby -ne 'BEGIN{ code=false }; code = !code if $_ =~ /^```/; puts $_ if code' *.md

Before we move on, let's tighten up this code a little.

You may recall there's a shortcut for matching the current line against a pattern, where the match operation is implied by a bare regular expression.

ruby -ne 'BEGIN{ code=false }; code = !code if /^```/; puts $_ if code' *.md

Alternately, we can put the match operation into a sub-expression, and set the code variable based on a "complement-equals" with the match success.

ruby -ne 'BEGIN{ code=false }; code ^= ($_ =~ /^```/); puts $_ if code' *.md

Once we've done this, we can consolidate two statements into one by putting our state-toggling expression right into the statement-modifier.

ruby -ne 'BEGIN{ code=false }; puts $_ if code ^= ($_ =~ /^```/)' *.md

We can also shrink the puts $_ into a call to print. Without any arguments, print outputs the contents of the last-read-line variable.

ruby -ne 'BEGIN{ code=false }; print if code ^= ($_ =~ /^```/)' *.md

Oh right, we also wanted to fix the fact that we output the opening code fences but not the closing ones.

We can do that by printing the current line if either the code variable is true or the code fence regex produced match data.

ruby -ne 'BEGIN{ code=false }; print if (code ^= ($_ =~ /^```/)) || $&' *.md

It might seem like we've golfed this code down about as far as we can go. But in fact, Ruby has a special feature for working with delimited sections of input.

It's called a flip-flop, and it looks like this.

ruby -ne 'print if /^```/.../^```/' *.md    

You might be thinking: that's a Ruby exclusive Range object! And you'd be right.

But just as Ruby treats regular expressions after an if statement specially in these command-line one-liners, it also has some special treatment for exclusive ranges of regular expressions used as an if-statement condition.

When we run this, we can see that it produces the exact same output we achieved before.

What Ruby is doing behind the scenes is associating an anonymous variable with this regex range. The variable flips to the "on" state when the first regex matches, and back over to the "off" state after the second regex matches. The if modifier triggers so long as the flip-flop is in the "on" state.

So now we have a way of filtering our input for just fenced code blocks. But if we want to count lines of code, we need to get rid of the fences and only look at code from inside the fences.

One way to do this is to pipe the output of this command into a second Ruby command that filters out fences.

ruby -ne 'print if /^```/.../^```/' *.md | ruby -ne 'print unless /^```/'

That works. But we can also condense these two commands into one.

Remember that the -n flag causes all of our code to be executed inside an implicit loop over input lines.

And as with any Ruby loop, we can use flow control operators.

So, we can skip to the next iteration of the loop unless we are currently in a fenced code block.

Then, in a second statement---which we'll only execute if the previous statement allowed us to continue---we can print the current line unless there's regex match data. We use the $& shorthand for the last string matched by a regular expression.

Remember, there's only going to have been a successful regex match at the very beginning and very end of each fenced code block. In between the fences, the current matchdata will be nil.

Running this produces just the code, without fences.

ruby -ne 'next unless /^```/.../^```/; print unless $&' *.md

Now we want to count these lines.

If we're UNIX-proficient, we might pipe the output into the wc utility, with the -l flag to tell it to count lines.

ruby -ne 'next unless /^```/.../^```/; print unless $&' *.md | wc -l

On the other hand if we want to stick to pure Ruby, we could pipe into a second Ruby command.

Now, how do we count lines in this command?

Well, just as we've used BEGIN{} blocks to do one-time initialization inside the -n loop, we can use an END{} block to do an action just once after the loop finishes.

But what should we put in this block? Well, you might recall from another episode that Ruby keeps track of the count of input lines it has processed in a special $. variable.

ruby -ne 'next unless /^```/.../^```/; print unless $&' *.md | ruby -ne 'END { puts $. }'

Running this gets us our line count.

But we can also shrink this down to a single command if we want.

We can use a BEGIN{} block to initialize a line count variable.

Increment the line count instead of printing a line,

And then in an END block, print out the final line count.

ruby -ne 'BEGIN{ lc = 0 }; next unless /^```/.../^```/; lc += 1 unless $&; END { puts lc }' *.md

Today we've used pure-Ruby one-liners to accomplish tasks we might have otherwise used some combination of awk, grep, and wc for. In the process, we've learned about regex flip-flops, implicit printing of the last-read line, and the END block.

Happy hacking!

Responses