State machine for handling Telnet sequences for an FTP server I've asked Code Climate to generate metrics for the ftpd Ruby gem.  It correctly identified the God class; I know what to do about that.  But one of the smaller classes has me stumped.  This is telnet.rb:

# -*- ruby encoding: us-ascii -*-

module Ftpd

  # Handle the limited processing of Telnet sequences required by the
  # FTP RFCs.
  #
  # Telnet option processing is quite complex, but we need do only a
  # simple subset of it, since we can disagree with any request by the
  # client to turn on an option (RFC-1123 4.1.2.12).  Adhering to
  # RFC-1143 ("The Q Method of Implementing TELNET Option Negiation"),
  # and supporting only what's needed to keep all options turned off:
  #
  # * Reply to WILL sequence with DONT sequence
  # * Reply to DO sequence with WONT sequence
  # * Ignore WONT sequence
  # * Ignore DONT sequence
  #
  # We also handle the "interrupt process" and "data mark" sequences,
  # which the client sends before the ABORT command, by ignoring them.
  #
  # All Telnet sequence start with an IAC, followed by at least one
  # character.  Here are the sequences we care about:
  #
  #     SEQUENCE             CODES
  #     -----------------    --------------------
  #     WILL                 IAC WILL option-code
  #     WONT                 IAC WONT option-code
  #     DO                   IAC DO option-code
  #     DONT                 IAC DONT option-code
  #     escaped 255          IAC IAC
  #     interrupt process    IAC IP
  #     data mark            IAC DM
  #
  # Any pathalogical sequence (e.g. IAC + \x01), or any sequence we
  # don't recognize, we pass through.

  class Telnet

    # The command with recognized Telnet sequences removed

    attr_reader :plain

    # Any Telnet sequences to send

    attr_reader :reply

    # Create a new instance with a command that may contain Telnet
    # sequences.
    # @param command [String]

    def initialize(command)
      telnet_state_machine command
    end

    private

    module Codes
      IAC  = 255.chr    # 0xff
      DONT = 254.chr    # 0xfe
      DO   = 253.chr    # 0xfd
      WONT = 252.chr    # 0xfc
      WILL = 251.chr    # 0xfb
      IP   = 244.chr    # 0xf4
      DM   = 242.chr    # 0xf2
    end
    include Codes

    def telnet_state_machine (command)
      @plain = ''
      @reply = ''
      state = :idle
      command.each_char do |c|
        case state
        when :idle
          if c == IAC
            state = :iac
          else
            @plain &lt;&lt; c
          end
        when :iac
          case c
          when IAC
            @plain &lt;&lt; c
            state = :idle
          when WILL
            state = :will
          when WONT
            state = :wont
          when DO
            state = :do
          when DONT
            state = :dont
          when IP
            state = :idle
          when DM
            state = :idle
          else
            @plain &lt;&lt; IAC + c
            state = :idle
          end
        when :will
          @reply &lt;&lt; IAC + DONT + c
          state = :idle
        when :wont
          state = :idle
        when :do
          @reply &lt;&lt; IAC + WONT + c
          state = :idle
        when :dont
          state = :idle
        else
          raise "Unknown state #{state.inspect}"
        end
      end
    end

  end
end


Code Climate does not like the complexity of #telnet_state_machine.  I agree, but I don't know how to reduce the complexity without also making the state machine harder to follow.  State machines never seem to be all that readable as it is.  What would you suggest?

Note: If you want to try some refactoring, this class has rspec test coverage.  Just do "git clone" of the ftpd project.
As suggested by @m_x, This solution uses a parser of sorts, driven by the StringScanner built-in class.  This is very compact, pretty readable, and gets rid of the state machine altogether:

Some methods to handle actions:

def accept(scanner)   
  @plain &lt;&lt; scanner[1]
end

def reply_dont(scanner)
  @reply &lt;&lt; IAC + DONT + scanner[1]
end

def reply_wont(scanner)
  @reply &lt;&lt; IAC + WONT + scanner[1]
end

def ignore(scanner)
end


A list of telnet sequences:

# Telnet sequences to handle, and how to handle them

SEQUENCES = [
  [/#{IAC}(#{IAC})/, :accept],
  [/#{IAC}#{WILL}(.)/m, :reply_dont],
  [/#{IAC}#{WONT}(.)/m, :ignore],
  [/#{IAC}#{DO}(.)/m, :reply_wont],
  [/#{IAC}#{DONT}(.)/m, :ignore],
  [/#{IAC}#{IP}/, :ignore],
  [/#{IAC}#{DM}/, :ignore],
  [/(.)/m, :accept],
]


And the parser that uses them:

# Parse the the command.  Sets @plain and @reply

def parse_command(command)
  @plain = ''
  @reply = ''
  scanner = StringScanner.new(command)
  while !scanner.eos?
    SEQUENCES.each do |regexp, method|
      if scanner.scan(regexp)
        send method, scanner
        break
      end
    end
  end
end