Created
December 1, 2016 03:08
-
-
Save ncaq/a55b70cfbc19300633016042b493b961 to your computer and use it in GitHub Desktop.
rubyによるedの部分的実装
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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