任意のコマンドをreadline化するRubyスクリプト

telnetなどのreadlineが組み込まれていない環境で入力を間違えた場合、ctrl+BSで文字を削除する(bash環境)。これでも十分ではあるが、慣れ親しんだemacsキーバインドが使えた方が便利だと思った。

そこで、任意のコマンドをreadline環境下で実行するスクリプトrl.rbを作ってみた。使い方は下記のようになる。

rl.rb telnet d.hatena.ne.jp 80

対話環境でctrl+h(1文字削除), ctrl+a(行の先頭へ移動)などが使えるようになる。また、ヒストリも有効にしてある。ただし、^]などのエスケープ・シークエンスが送れなくなるので注意すること。

コードは下記。ライセンスはrubyと同一とする。

#! /usr/bin/env ruby
# rl.rb - readline wrapper
# by llamerada (http://d.hatena.ne.jp/llamerada)
#
# You may redistribute it and/or modify it under the same license
# terms as Ruby.
#
require "readline"
require "optparse"

def parse_opt
  parser = OptionParser.new
  parser.banner = "Usage: #{$0} [OPTION] command"
  parser.on('-v', '--vi', 'vi editing mode.') {
    Readline.vi_editing_mode
  }
  parser.on('-e', '--emacs', 'emacs editing mode.') {
    Readline.emacs_editing_mode
  }
  parser.on('--help', 'Prints this message and quit.') {
    puts parser.help
    exit 0
  }
  def parser.error(msg = nil)
    $stderr.puts msg if msg
    $stderr.puts help()
    exit 1
  end
  begin
    parser.parse!
  rescue OptionParser::ParseError => err
    parser.error err.message
  end
  parser.error 'no input command' if ARGV.empty?
end

def setup_int_signal
  # for ctrc-c
  stty_save = `stty -g`.chomp
  trap("INT") { 
    system "stty", stty_save; 
    exit 0
  }
end

parse_opt
setup_int_signal

io = IO.popen(ARGV.join(" "), "r+")
at_exit do 
  io.close
end

# writing thread
Thread.new do 
  while line = io.gets
    puts line
  end
  exit 0
end

# reading thread
while buf = Readline.readline("", true)
  Readline::HISTORY.pop if /^\s*$/ =~ buf
  begin
    Readline::HISTORY.pop if Readline::HISTORY[-2] == buf
  rescue IndexError
  end

  io.puts buf
end