第33回 Ruby/Rails 勉強会 - 演習 2

演習 2

  • cat コマンドを実装してください。
  • see also cat(1)

MacBSD cat なので、オプションはこれだけ。
でも usage の表示だけ GNU cat からパクってきた。

#! /usr/local/bin/ruby19

require 'optparse'

Encoding.default_internal = Encoding.find('ASCII-8BIT')
Encoding.default_external = Encoding.find('ASCII-8BIT')

$NUMBER_WIDTH = 6

$C_OFFSET   = "\C-@".ord - '@'.ord
$M_OFFSET   = "\M-@".ord - '@'.ord
$M_C_OFFSET = "\M-\C-@".ord - '@'.ord


opt = {
  show: {
    ends:        false,
    tabs:        false,
    nonprinting: false,
  },
  number:        false,
  squeeze:       false,
  help:          false,
}

parser = OptionParser.new
parser.on('-b', '--number-nonblank',
          'number nonempty output lines') do
  opt[:number] = :nonblank
end
parser.on('-e', '--show-ends',
          'display $ at end of each line') do
  opt[:show][:ends] = true
  opt[:show][:nonprinting] = true
end
parser.on('-n', '--number',
          'number all output lines') do
  opt[:number] = :all
end
parser.on('-s', '--squeeze-blank',
          'suppress repeated empty output lines') do
  opt[:squeeze] = true
end
parser.on('-t', '--show-tabs',
          'display TAB characters as ^I') do
  opt[:show][:tabs] = true
  opt[:show][:nonprinting] = true
end
parser.on('-v', '--show-nonprinting',
          'use ^ and M- notation, except for LFD and TAB') do
  opt[:show][:nonprinting] = true
end
parser.on('-h', '--help',
          'display this help and exit') do
  opt[:help] = true
  STDERR.puts parser.help
  exit 0
end

parser.banner = <<USAGE
usage: #{File.basename $0} [options] [file ...]
options:
USAGE


def squeeze_blank!(buf)
  buf.gsub!(/(\r?\n)\1+/, '\1\1')
end

def count_lines_and_print(opt, n, line)
  return n  unless opt

  if opt == :all || opt == :nonblank && /^\r?$/ !~ line
    n += 1
    printf("%*d  ", $NUMBER_WIDTH, n)
  end

  n
end


def show_ends_hash
  {"\n" => '$'}
end

def show_tabs_hash
  {"\t" => '^I'}
end

def to_printable_array(codes, offset)
  codes.map{|c| [(c.ord+offset).chr, yield(c)]}
end

def show_nonprinting_hash
  c  = to_printable_array('@'..'_', $C_OFFSET){|c| "^#{c}"}
  m  = to_printable_array('@'..'_', $M_OFFSET){|c| "M-#{c}"}
  mc = to_printable_array('@'..'_', $M_C_OFFSET){|c| "M-^#{c}"}

  nltab = to_printable_array(%W[I J], $C_OFFSET){|c| "^#{c}"}

  Hash[*(c + m + mc - nltab).flatten]
end

def to_printable!(convertor, line)
  convertor.each do |src, dst|
    line.gsub!(src, dst)
  end
  line
end


def cat(opt, buf)
  lineno = 0

  squeeze_blank!(buf)  if opt[:squeeze]

  conv = []
  conv = conv | show_ends_hash.to_a         if opt[:show][:ends]
  conv = conv | show_tabs_hash.to_a         if opt[:show][:tabs]
  conv = conv | show_nonprinting_hash.to_a  if opt[:show][:nonprinting]

  buf.each_line do |line|
    lineno = count_lines_and_print(opt[:number], lineno, line)  if opt[:number]
    puts to_printable!(Hash[*conv.flatten], line)
  end
end


begin
  parser.parse! ARGV
rescue OptionParser::ParseError => e
  STDERR.puts "#{File.basename $0}: #{e.to_s}"
  STDERR.puts parser.help
  exit 1
end

cat(opt, ARGF.binmode.read)
  • parser.on を追加すれば GNU cat にもなれる (はず)
  • opt[:show][:tabs] みたいに 2 段にする意味はないけど、なんとなく。
  • 改行コードに \r\n\n が混っていると squeeze が腐る。
  • Mac の改行コードは相手にしない。
  • 最初に全部読み込むんで、デカいファイルだと死ねる。