Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
76: StringScanner for Text Parsing
I've spent a lot of time writing regexes over the years, and while they're powerful, they can quickly turn into "write-only" code—meaning you can write it, but six months later, you have no idea how to read it. I recently ran into a situation where I had to parse a custom event log format that looked something like this: EVENT:user_signup | ID:402 | AT:2023-10-01 | TAGS:[beta, referral].
My first instinct was to just use split('|'). Let's see how that looks.
log_line = "EVENT:user_signup | ID:402 | AT:2023-10-01 | TAGS:[beta, referral]" parts = log_line.split('|') # ["EVENT:user_signup ", " ID:402 ", " AT:2023-10-01 ", " TAGS:[beta, referral]"]The fragility of splitting
Right away, I'm seeing those annoying leading and trailing spaces. I'd have to
stripevery single element. But the bigger problem is the data inside. If one of the tags happened to contain a pipe character—which is possible in some log formats—my whole parsing logic collapses. I need a way to move through the string linearly, consuming pieces as I recognize them, rather than hacking the string into chunks and hoping for the best.This is where
StringScannercomes in. It's a standard library tool that acts like a cursor moving across a string. It doesn't change the original string; it just remembers where it is.require 'strscan' log_line = "EVENT:user_signup | ID:402 | AT:2023-10-01 | TAGS:[beta, referral]" scanner = StringScanner.new(log_line) # Let's try to grab the first key-value pair if scanner.scan(/(\w+):([^|]+)/) puts "Key: #{$1}, Value: #{$2}" end # Output: Key: EVENT, Value: user_signupManaging the cursor
That worked for the first part, but notice what happened:
scanmoved the cursor to the end ofuser_signup. The cursor is now sitting right before that first pipe character. If I try to run that samescanagain immediately, it'll fail because the next character is|, not a word.I need to account for the "noise" between the data I actually care about. In a real-world parser, you spend as much time skipping garbage as you do capturing data.
require 'strscan' log_line = "EVENT:user_signup | ID:402 | AT:2023-10-01 | TAGS:[beta, referral]" scanner = StringScanner.new(log_line) data = {} until scanner.eos? # eos? means "End Of String" # Skip any whitespace or pipe delimiters scanner.scan(/\s*\|\s*/) if scanner.scan(/(\w+):([^|]+)/) key = $1 val = $2.strip data[key] = val else # If we hit something we don't recognize, we have to move the cursor # otherwise we'll be stuck in an infinite loop. scanner.getch end end p data # {"EVENT"=>"user_signup", "ID"=>"402", "AT"=>"2023-10-01", "TAGS"=>"[beta, referral]"}Peek and Precision
There's one more thing. Sometimes you need to know what's coming next without actually consuming it. This is the difference between
scan(which moves the cursor) andpeek(which just looks).Suppose I want to handle those
TAGS:[...]blocks differently—maybe I want to parse the contents of the brackets into an array. I can usepeekto check if the value starts with a bracket before I decide how to process it.# Inside our loop, instead of a generic value scan: if scanner.scan(/(\w+):/) key = $1 if scanner.peek(1) == '[' # We know it's a list! scanner.scan(/\[([^\]]+)\]/) data[key] = $1.split(',').map(&:strip) else scanner.scan(/([^|]+)/) data[key] = $1.strip end endBy combining
scanfor consumption andpeekfor decision-making, I've turned a messy string-splitting exercise into a proper lexical scanner. It's much more robust, and because I'm handling the string linearly, it's generally more performant for very long lines of text.
📋 Practical Task
Build a Custom Command Line Argument Parser
You are building a small tool that accepts custom command strings in the following format: COMMAND:upload FILE:image.jpg VERBOSE:true RETRIES:3. Some values are booleans, some are integers, and some are strings.
Write a Ruby script using StringScanner that parses this string into a Hash. Your parser must:
- Correctly identify the key and the value.
- Convert the value
"true"or"false"into actual Rubytrueorfalse(Boolean). - Convert values that are purely numeric (like
"3") intoIntegerobjects. - Keep all other values as
String.
Test input: "COMMAND:sync FILE:data.csv VERBOSE:false RETRIES:5"
Expected output: {"COMMAND"=>"sync", "FILE"=>"data.csv", "VERBOSE"=>false, "RETRIES"=>5}
There are no comments for now.