Skip to content

Instantly share code, notes, and snippets.

@ncaq
Created December 1, 2016 03:08
Show Gist options
  • Save ncaq/a55b70cfbc19300633016042b493b961 to your computer and use it in GitHub Desktop.
Save ncaq/a55b70cfbc19300633016042b493b961 to your computer and use it in GitHub Desktop.
rubyによるedの部分的実装
#!/usr/bin/env ruby
# coding: utf-8
require 'pp'
require 'readline'
class Ed
class Command
def initialize(secondary, primary, type, param)
@secondary = secondary
@primary = primary
@type = type
@param = param
end
attr_accessor :secondary, :primary, :type, :param
end
def initialize
@buffer = []
@command = nil
@current = 0
@error = nil
@file = nil
@prompt = ''
@shell = nil
@verbose = false
end
@@forward_regexp = /\/((\\\/|[^\/])*)\//
@@backward_regexp = /\?((\\\?|[^\?])*)\?/
@@address_regexp = /(\.|\$|\d+|-|\^\d+|\+|\+\d+|,|;|#{@@forward_regexp}|#{@@backward_regexp}|'.*)/
@@type_regexp = /(wq|.|\z)/
@@command_regexp = /^\s*((?<one>#{@@address_regexp})?(\s*(,|;)\s*(?<two>#{@@address_regexp}))?)?\s*(?<type>#{@@type_regexp})\s*(?<param>.+)?\s*$/
def read
# 行番号は無限に指定できるため,原理上は正規表現だけでパースするのは不可能である
# 3つ以上の行番号を利用するコマンドは存在しないため,実用上は問題はない
line = Readline.readline(@prompt, true)
m = @@command_regexp.match(line)
if m[:two]
secondary = m[:one]
primary = m[:two]
else
primary = m[:one] || m[:two]
end
case
when primary == ','
secondary = 1
primary = '$'
when primary == ';'
secondary = '.'
primary = '$'
when re = @@forward_regexp.match(primary)
r = Regexp.new(re[1])
if primary = @buffer.drop(@current - 1).index { |b| r.match(b) }
primary += @current
secondary = primary
else
raise 'No match'
end
when re = @@backward_regexp.match(primary)
r = Regexp.new(re[1])
if primary = @buffer.take(@current).rindex { |b| r.match(b) }
secondary = primary
else
raise 'No match'
end
end
@command = Command.new(line_to_i(secondary), line_to_i(primary), m[:type], m[:param])
end
def eval
case @command.type
when 'a'
@command.primary ||= line_to_i('.')
ins = self.insert
@buffer.insert(@command.primary, *ins)
@current = @command.primary + ins.size
when 'c'
@command.secondary ||= line_to_i('.')
@command.primary ||= line_to_i('.')
ins = self.insert
@buffer.fill(@command.secondary - 1 .. @command.primary - 1, ins).flatten!
@current = @command.primary + ins.size
when 'd'
@command.secondary ||= line_to_i('.')
@command.primary ||= line_to_i('.')
@buffer.slice!(@command.secondary - 1 .. @command.primary - 1)
@current = @command.primary - 1
when 'f'
@file = @command.param
when 'g'
@command.secondary ||= line_to_i('1')
@command.primary ||= line_to_i('$')
regexp = Regexp.union(@@forward_regexp, @@backward_regexp)
m = /(?<re>#{regexp})(?<command_list>.*)/.match(@command.param)
re = regexp.match(m[:re])
reg = Regexp.new(re[1] || re[2])
@buffer[@command.secondary - 1 .. @command.primary - 1].each.with_index { |l, i|
if reg === l
@command.secondary = i + 1
@command.primary = i + 1
cm = @@command_regexp.match(m[:command_list])
@command.type = cm[:type]
@command.param = cm[:param]
eval
end
}
when 'H'
@verbose = ! @verbose
when 'h'
pp @error
when 'i'
@command.primary ||= line_to_i('.')
ins = self.insert
@buffer.insert(@command.primary - 1, *ins)
@current = @command.primary + ins.size - 1
when 'j'
@command.secondary ||= line_to_i('.')
@command.primary ||= line_to_i('.') + 1
@buffer[@command.secondary - 1 .. @command.primary - 1] =
@buffer[@command.secondary - 1 .. @command.primary - 1].join
@current = @command.secondary
when 'n'
@command.secondary ||= line_to_i('.')
@command.primary ||= line_to_i('.')
puts (@command.secondary .. @command.primary).to_a.
zip(@buffer[@command.secondary - 1 .. @command.primary - 1]).
map{ |(n, l)| n.to_s + "\t" + l }
@current = @command.primary
when 'p'
@command.secondary ||= line_to_i('.')
@command.primary ||= line_to_i('.')
# -1が末尾になってしまうためオリジナルのedとは少し動作が違うが,
# 制限がゆるい分にはまあ問題ないだろう…
puts @buffer[@command.secondary - 1 .. @command.primary - 1]
@current = @command.primary
when 'P'
@prompt = '*'
when 'q'
exit
when 'r'
@command.primary ||= line_to_i('$')
if @command.param.start_with?('!')
@command.param[0] = ''
o = shell(@command.param)
else
@command.param ||= @file
o = IO.read(@command.param)
end
@buffer.insert(@command.primary, o)
when 'w'
@command.param ||= @file
b = @buffer.join("\n")
unless b.end_with?("\n")
b << "\n"
end
IO.write(@command.param, b)
@file = @command.param
when 'wq'
@command.type = 'w'
eval
@command.type = 'q'
eval
when '!'
puts shell(@command.param)
puts '!'
when '='
@command.primary ||= line_to_i('$')
puts @command.primary
when ''
@command.primary ||= line_to_i('.') + 1
puts @buffer[@command.primary - 1]
@current = @command.primary
else
raise 'Unknown command'
end
end
def loop
Kernel.loop {
begin
read
eval
@command = nil
rescue
if @verbose
pp $!
pp $!.backtrace
pp self
else
puts '?'
end
@error = $!
end
}
end
def line_to_i(l)
case
when l == nil
nil
when l.is_a?(Integer)
l
when l == '.'
@current
when l == '$'
@buffer.size
when /^\d$/.match(l)
Integer(l)
when l == '-'
@current - 1
when /^\^(\d)$/.match(l)
@current - Integer(/^\^(\d)$/.match(l)[1])
when l == '+'
@current + 1
when /^'(\p{Lower})$/
raise 'k command un impl'
else
raise 'Invalid address'
end
end
def shell(c)
if c.start_with?('!')
if @shell
c.sub!('!', @shell)
else
raise 'No previous command'
end
end
file_regexp = /(?<!\\)%/
if file_regexp === c
if @file
c.gsub!(file_regexp, @file)
else
raise 'No current filename'
end
end
@shell = c
`#{c}`
end
def insert
buf = []
while l = Readline.readline('', false)
if l == '.'
return buf
else
buf << l
end
end
end
end
Ed.new.loop
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment