Scanning and parsing strings

scan and split

Two string methods cover the simple cases. scan returns every match, and with groups it returns one array per match; split with a regex cuts the string wherever the pattern matches:

ruby -e 'p "a=1 b=2".scan(/(\w+)=(\d+)/)'

[["a", "1"], ["b", "2"]]

ruby -e 'p "a=1; b=2".split(/;\s*/)'

["a=1", "b=2"]

scan keeps the order of the matches and ignores everything between them, which is why it is the method for pulling ids or numbers out of a page. split keeps the parts, so it is the method for a line with a separator.

StringScanner

When the pieces have an order, a regex per field is clearer than one large regex. strscan is in the standard library, and each scan consumes from the front:

ruby -e 'require "strscan"; s = StringScanner.new("a=1"); p s.scan(/\w+/), s.scan(/=/), s.scan(/\d+/)'

"a"
"="
"1"

Each call moves the scanner forward, so the next pattern starts where the last match ended. A pattern that does not match returns nil and moves nothing, which is what lets a loop test eos? and stop at the end.

A key-value parser

Six lines of scanner is a parser for a=1; b=2. The loop reads a key, the equals sign with its optional spaces, the value up to the separator, and then the separator itself:

require "strscan" def parse(str) s = StringScanner.new(str) out = {} until s.eos? key = s.scan(/\w+/) s.scan(/\s*=\s*/) value = s.scan(/[^;]+/) out[key] = value.strip s.scan(/;\s*/) end out end p parse("a=1; b=2") p parse("name = Ada; role = admin")

ruby kv.rb

{"a" => "1", "b" => "2"}
{"name" => "Ada", "role" => "admin"}

The scanner makes the grammar explicit: each line of the loop is one token. A regex would have to repeat the same alternation and would report a failure as one anonymous nil instead of the step that failed.

scan_until and rest

scan_until consumes everything up to and including a pattern, and rest shows what is left. The pair is the way to skip a header:

ruby -e 'require "strscan"; s = StringScanner.new("id=42&x=1"); p s.scan_until(/=/); p s.rest'

"id="
"42&x=1"

check and peek look without consuming, which is what a parser needs when two rules start with the same character. pos reports the offset, so an error can point at the character that broke the grammar.

What to watch for

A scanner loop that does not consume anything runs forever: if every scan returns nil, the position stays put. Advance with getch or raise on an unexpected character.

split with a regex that can match an empty string produces surprising empty fields, and scan with an empty-capable pattern matches between every character. Anchor the pattern to what it must consume.

Next: A text pipeline

Article author: Arthur Isaev

Banner image