あまブログ

ドキドキ......ドキドキ2択クイ〜〜〜〜〜〜〜ズ!!

【Ruby3.1】wcコマンドを作る

この記事では、RubyでLinuxのwcコマンドを実装する方法を解説します。

gemを使わずにRubyの標準ライブラリのみで実装します。

後半にソースコードを載せているため、FJORD BOOT CAMP(フィヨルドブートキャンプ)でwcコマンドの課題に取り組まれている方はご注意ください。

1. 実行環境

  • macOS:12.5
  • Ruby:3.1.0

2. 作成するwcコマンドの要件

  • オプションなし
    • 標準入力とファイルの入力を受け取る
  • -cオプション
  • -lオプション
  • -wオプション
    • 単語の区切り文字は半角スペース、タブ、改行のみに対応

以下の機能は実装の対象外とします。

  • ノーブレークスペースを含むマルチバイト文字の対応

3. wcコマンドの仕様

macOS標準のwcコマンドの仕様は以下の通りです。(ソースコードはこちら)

(今回の要件に必要な箇所のみ)

  • 引数なし
    • 標準入力を受け取る
    • Ctrl + D(EOF)を受け取るまで入力を受け付ける
  • 引数にファイルを指定
    • ファイルを受け取る
    • 複数ファイルを指定した場合、出力の最終行に各ファイルの合計を表示
      • <合計の行数> <合計の単語数> <合計のバイト数> total
  • オプションなし
    • <行数> <単語数> <バイト数> <ファイル名>の順で表示
  • -cオプション
    • バイト数を表示する
  • -lオプション
    • 行数を表示する(改行コードの数)
  • -wオプション
    • 単語数を表示する
    • 半角スペース、タブ、改行で区切られた文字列の数(全角スペースはmacOS標準のwcコマンドも非対応)

4. ソースコード

  • ver1:自作→レビュー反映
  • ver2:ver1→他の人のコードを反映

4-1. ver1

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'optparse'

def exec
  options = parse_options
  options = { c: true, l: true, w: true } if options.empty?
  paths = ARGV
  paths.empty? ? display_word_count_from_stdin(options) : display_word_count_from_files(paths, options)
end

def parse_options
  opt = OptionParser.new
  options = {}
  opt.on('-c')
  opt.on('-l')
  opt.on('-w')
  opt.parse!(ARGV, into: options)
  options
end

def display_word_count_from_stdin(options)
  lines = $stdin.readlines
  word_count_map = build_word_count_map(lines)
  display_word_count(word_count_map, options)
end

def build_word_count_map(lines, path = '')
  {
    number_of_lines: count_line(lines),
    number_of_words: count_word(lines),
    bytesize: count_bytesize(lines),
    path: path
  }
end

def count_line(lines)
  lines.size
end

def count_word(lines)
  lines.sum { |line| line.split(/[ \t\n]+/).size }
end

def count_bytesize(lines)
  lines.sum(&:bytesize)
end

def display_word_count(word_count_map, options)
  word_counts = []
  word_counts << format_word_count(word_count_map[:number_of_lines]) if options[:l]
  word_counts << format_word_count(word_count_map[:number_of_words]) if options[:w]
  word_counts << format_word_count(word_count_map[:bytesize]) if options[:c]
  word_counts << " #{word_count_map[:path]}" unless word_count_map[:path].empty?
  puts word_counts.join
end

def format_word_count(word_count)
  word_count.to_s.rjust(8)
end

def display_word_count_from_files(paths, options)
  word_count_maps = build_word_count_maps(paths)
  word_count_maps.map { |word_count_map| display_word_count(word_count_map, options) }
  display_total_word_count(word_count_maps, options) if paths.size >= 2
end

def build_word_count_maps(paths)
  paths.map do |path|
    lines = File.readlines(path)
    build_word_count_map(lines, path)
  end
end

def display_total_word_count(word_count_maps, options)
  total_word_count_map = build_total_word_count_map(word_count_maps)
  display_word_count(total_word_count_map, options)
end

def build_total_word_count_map(word_count_maps)
  {
    number_of_lines: word_count_maps.sum { |word_count_map| word_count_map[:number_of_lines] },
    number_of_words: word_count_maps.sum { |word_count_map| word_count_map[:number_of_words] },
    bytesize: word_count_maps.sum { |word_count_map| word_count_map[:bytesize] },
    path: 'total'
  }
end

exec

4-2. ver2

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'optparse'

def exec
  options = parse_options
  options = { c: true, l: true, w: true } if options.empty?
  paths = ARGV
  counts = build_counts(paths: paths)
  counts.map { |count| display_count(count: count, options: options) }
  display_total_count(counts: counts, options: options) if paths.size >= 2
end

def parse_options
  opt = OptionParser.new
  options = {}
  opt.on('-c')
  opt.on('-l')
  opt.on('-w')
  opt.parse!(ARGV, into: options)
  options
end

def build_counts(paths:)
  if paths.empty?
    [build_count(text: $stdin.read)]
  else
    paths.map do |path|
      build_count(text: File.read(path), path: path)
    end
  end
end

def build_count(text:, path: '')
  {
    line_count: text.count("\n"),
    word_count: text.split(/\s+/).size,
    bytesize: text.bytesize,
    path: path
  }
end

def display_count(count:, options:)
  formatted_counts = []
  formatted_counts << format_count(count: count[:line_count]) if options[:l]
  formatted_counts << format_count(count: count[:word_count]) if options[:w]
  formatted_counts << format_count(count: count[:bytesize]) if options[:c]
  formatted_counts << " #{count[:path]}" unless count[:path].empty?
  puts formatted_counts.join
end

def format_count(count:)
  count.to_s.rjust(8)
end

def display_total_count(counts:, options:)
  total_count = build_total_count(counts: counts)
  display_count(count: total_count, options: options)
end

def build_total_count(counts:)
  {
    line_count: counts.sum { |count| count[:line_count] },
    word_count: counts.sum { |count| count[:word_count] },
    bytesize: counts.sum { |count| count[:bytesize] },
    path: 'total'
  }
end

exec

【参考】