class Mustermann::StringScanner

Class inspired by Ruby's StringScanner to scan an input string using multiple patterns.

@example

require 'mustermann/string_scanner'
scanner = Mustermann::StringScanner.new("here is our example string")

scanner.scan("here") # => "here"
scanner.getch        # => " "

if scanner.scan(":verb our")
  scanner.scan(:noun, capture: :word)
  scanner[:verb]  # => "is"
  scanner[:nound] # => "example"
end

scanner.rest # => "string"

@note

This structure is not thread-safe, you should not scan on the same StringScanner instance concurrently.
Even if it was thread-safe, scanning concurrently would probably lead to unwanted behaviour.

Constants

PATTERN_CACHE
ScanError

Exception raised if scan/unscan operation cannot be performed.

Attributes

params[R]

Params from all previous matches from {#scan} and {#scan_until}, but not from {#check} and {#check_until}. Changes can be reverted with {#unscan} and it can be completely cleared via {#reset}.

@return [Hash] current params

pattern_options[R]

@return [Hash] default pattern options used for {#scan} and similar methods @see initialize

pos[RW]

@return [Integer] current scan position on the input string

pos=[RW]

@return [Integer] current scan position on the input string

position[RW]

@return [Integer] current scan position on the input string

Public Class Methods

cache_size() click to toggle source

@return [Integer] number of cached patterns @see clear_cache @api private

# File lib/mustermann/string_scanner.rb, line 45
def self.cache_size
  PATTERN_CACHE.size
end
clear_cache() click to toggle source

Patterns created by {#scan} will be globally cached, since we assume that there is a finite number of different patterns used and that they are more likely to be reused than not. This method allows clearing the cache.

@see Mustermann::PatternCache

# File lib/mustermann/string_scanner.rb, line 38
def self.clear_cache
  PATTERN_CACHE.clear
end
new(string = "", **pattern_options) click to toggle source

@example with different default type

require 'mustermann/string_scanner'
scanner = Mustermann::StringScanner.new("foo/bar/baz", type: :shell)
scanner.scan('*')     # => "foo"
scanner.scan('**/*')  # => "/bar/baz"

@param [String] string the string to scan @param [Hash] pattern_options default options used for {#scan}

# File lib/mustermann/string_scanner.rb, line 133
def initialize(string = "", **pattern_options)
  @pattern_options = pattern_options
  @string          = String(string).dup
  reset
end

Public Instance Methods

<<(string) click to toggle source

Appends the given string to the string being scanned

@example

require 'mustermann/string_scanner'
scanner = Mustermann::StringScanner.new
scanner << "foo"
scanner.scan(/.+/) # => "foo"

@param [String] string will be appended @return [Mustermann::StringScanner] the scanner itself

# File lib/mustermann/string_scanner.rb, line 236
def <<(string)
  @string << string
  self
end
[](key) click to toggle source

Shorthand for accessing {#params}. Accepts symbols as keys.

# File lib/mustermann/string_scanner.rb, line 270
def [](key)
  params[key.to_s]
end
beginning_of_line?() click to toggle source

@return [true, false] whether or not the current position is at the start of a line

# File lib/mustermann/string_scanner.rb, line 247
def beginning_of_line?
  @position == 0 or @string[@position - 1] == "\n"
end
check(pattern, **options) click to toggle source

Checks if the given pattern matches any substring starting at the current position.

Does not affect {#position} or {#params}.

@param (see Mustermann.new) @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match

# File lib/mustermann/string_scanner.rb, line 196
def check(pattern, **options)
  params, length = create_pattern(pattern, **options).peek_params(rest)
  ScanResult.new(self, @position, length, params) if params
end
check_until(pattern, **options) click to toggle source

Checks if the given pattern matches any substring starting at any position after the current position.

Does not affect {#position} or {#params}.

@param (see Mustermann.new) @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match

# File lib/mustermann/string_scanner.rb, line 207
def check_until(pattern, **options)
  check_until_with_prefix(pattern, **options).first
end
eos?() click to toggle source

@return [true, false] whether or not the end of the string has been reached

# File lib/mustermann/string_scanner.rb, line 242
def eos?
  @position >= @string.size
end
getch() click to toggle source

Reads a single character and advances the {#position} by one. @return [Mustermann::StringScanner::ScanResult, nil] the character, nil if at end of string

# File lib/mustermann/string_scanner.rb, line 222
def getch
  track_result ScanResult.new(self, @position, 1) unless eos?
end
inspect() click to toggle source

@!visibility private

# File lib/mustermann/string_scanner.rb, line 292
def inspect
  "#<%p %d/%d @ %p>" % [ self.class, @position, @string.size, @string ]
end
peek(length = 1) click to toggle source

Allows to peek at a number of still unscanned characters without advacing the {#position}.

@param [Integer] length how many characters to look at @return [String] the substring

# File lib/mustermann/string_scanner.rb, line 265
def peek(length = 1)
  @string[@position, length]
end
reset() click to toggle source

Resets the {#position} to the start and clears all {#params}. @return [Mustermann::StringScanner] the scanner itself

# File lib/mustermann/string_scanner.rb, line 141
def reset
  @position = 0
  @params   = {}
  @history  = []
  self
end
rest() click to toggle source

@return [String] outstanding string not yet matched, empty string at end of input string

# File lib/mustermann/string_scanner.rb, line 252
def rest
  @string[@position..-1] || ""
end
rest_size() click to toggle source

@return [Integer] number of character remaining to be scanned

# File lib/mustermann/string_scanner.rb, line 257
def rest_size
  @position > size ? 0 : size - @position
end
scan(pattern, **options) click to toggle source

Checks if the given pattern matches any substring starting at the current position.

If it does, it will advance the current {#position} to the end of the substring and merges any params parsed from the substring into {#params}.

@param (see Mustermann.new) @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match

# File lib/mustermann/string_scanner.rb, line 162
def scan(pattern, **options)
  track_result check(pattern, **options)
end
scan_until(pattern, **options) click to toggle source

Checks if the given pattern matches any substring starting at any position after the current position.

If it does, it will advance the current {#position} to the end of the substring and merges any params parsed from the substring into {#params}.

@param (see Mustermann.new) @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match

# File lib/mustermann/string_scanner.rb, line 173
def scan_until(pattern, **options)
  result, prefix = check_until_with_prefix(pattern, **options)
  track_result(prefix, result)
end
size() click to toggle source

@return [Integer] size of the input string

# File lib/mustermann/string_scanner.rb, line 287
def size
  @string.size
end
terminate() click to toggle source

Moves the position to the end of the input string. @return [Mustermann::StringScanner] the scanner itself

# File lib/mustermann/string_scanner.rb, line 150
def terminate
  track_result ScanResult.new(self, @position, size - @position)
  self
end
to_h() click to toggle source

(see params)

# File lib/mustermann/string_scanner.rb, line 275
def to_h
  params.dup
end
to_s() click to toggle source

@return [String] the input string @see initialize @see <<

# File lib/mustermann/string_scanner.rb, line 282
def to_s
  @string.dup
end
unscan() click to toggle source

Reverts the last operation that advanced the position.

Operations advancing the position: {#terminate}, {#scan}, {#scan_until}, {#getch}. @return [Mustermann::StringScanner] the scanner itself

# File lib/mustermann/string_scanner.rb, line 182
def unscan
  raise ScanError, 'unscan failed: previous match record not exist' if @history.empty?
  previous = @history[0..-2]
  reset
  previous.each { |r| track_result(*r) }
  self
end

Private Instance Methods

check_until_with_prefix(pattern, **options) click to toggle source
# File lib/mustermann/string_scanner.rb, line 211
def check_until_with_prefix(pattern, **options)
  start      = @position
  @position += 1 until eos? or result = check(pattern, **options)
  prefix     = ScanResult.new(self, start, @position - start) if result
  [result, prefix]
ensure
  @position  = start
end
create_pattern(pattern, **options) click to toggle source

@!visibility private

# File lib/mustermann/string_scanner.rb, line 297
def create_pattern(pattern, **options)
  PATTERN_CACHE.create_pattern(pattern, **options, **pattern_options)
end
track_result(*results) click to toggle source

@!visibility private

# File lib/mustermann/string_scanner.rb, line 302
def track_result(*results)
  results.compact!
  @history << results if results.any?
  results.each do |result|
    @params.merge! result.params
    @position += result.length
  end
  results.last
end