-
Notifications
You must be signed in to change notification settings - Fork 0
Implement 'wc' in Ruby #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f329d43
f61c5ec
3f3ff39
eda1b68
83141a5
b010690
de45d45
d09bfa9
313f081
129503d
fe98d84
5a1e75f
14cccf1
7b9d052
7a74cac
b813989
a0c5c48
3f63735
d416cda
5e48a33
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'optparse' | ||
| require_relative './word_count' | ||
|
|
||
| OPTION_STRING = 'lwc' | ||
|
|
||
| OPTION_NAME_TO_WORD_COUNT_TYPE = OPTION_STRING.chars.zip(WordCount::TYPES).to_h.freeze | ||
|
|
||
| def main(args) | ||
| displayed_items = parse_args(args) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| wc_paths = args.empty? ? [WordCount::Pathname.new] : args.map { WordCount::Pathname.new(_1) } | ||
|
|
||
| word_count_types = WordCount.extract_types(displayed_items) | ||
|
|
||
| output_format = displayed_output_format(displayed_items, wc_paths) | ||
|
|
||
| results = | ||
| wc_paths.map { _1.word_count_result(word_count_types) } | ||
| .each { print_word_count_result(output_format, _1) } | ||
|
|
||
| if wc_paths.size >= 2 | ||
| count_total = | ||
| word_count_types.to_h { [_1, 0] } | ||
| .merge!(*results.filter_map { _1[:count] }) { |_, total, count| total + count } | ||
|
|
||
| print_word_count_result(output_format, { path: 'total', count: count_total, message: nil }) | ||
| end | ||
|
|
||
| results.any? { _1[:message] } ? 1 : 0 | ||
| end | ||
|
|
||
| def parse_args(args) | ||
| parsed_options = OptionParser.new.getopts(args, OPTION_STRING).transform_keys(OPTION_NAME_TO_WORD_COUNT_TYPE) | ||
|
|
||
| # `wc [file ...]` == `wc -lwc [file ...]` | ||
| parsed_options.transform_values! { |_| true } unless parsed_options.value?(true) | ||
|
|
||
| parsed_options[:path] = !args.empty? | ||
|
|
||
| parsed_options.select { |_, val| val }.keys | ||
| end | ||
|
|
||
| def displayed_output_format(displayed_items, wc_paths) | ||
| one_type_one_operand = WordCount.extract_types(displayed_items).size == 1 && wc_paths.size == 1 | ||
|
|
||
| digit = one_type_one_operand ? 1 : adjust_digit(wc_paths) | ||
|
|
||
| displayed_items.map { output_format_string(_1, digit) }.join(' ') | ||
| end | ||
|
|
||
| def adjust_digit(wc_paths) | ||
| default_digit = wc_paths.any?(&:exist_non_regular_file?) ? 7 : 1 | ||
|
|
||
| total_bytes_digit = wc_paths.sum(&:regular_file_size).to_s.size | ||
|
|
||
| [default_digit, total_bytes_digit].max | ||
| end | ||
|
|
||
| def output_format_string(displayed_item, digit) | ||
| case displayed_item | ||
| when *WordCount::TYPES | ||
| "%<#{displayed_item}>#{digit}d" | ||
| when :path | ||
| '%<path>s' | ||
| else | ||
| raise ArgumentError, "displayed_item: allow only #{[*WordCount::TYPES, :path].map(&:inspect).join(', ')}" | ||
| end | ||
| end | ||
|
|
||
| def print_word_count_result(output_format, result) | ||
| warn "wc: #{result[:path]}: #{result[:message]}" if result[:message] | ||
|
|
||
| puts format(output_format, **result[:count], path: result[:path]) unless result[:count].nil? | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module WordCount | ||
| TYPES = %i[newline word bytesize].freeze | ||
|
|
||
| def extract_types(types) | ||
| TYPES & types | ||
| end | ||
|
|
||
| module_function :extract_types | ||
| end | ||
|
|
||
| class WordCount::Pathname | ||
| USE_FILETEST_MODULE_FUNCTIONS = %i[directory? file? readable? size].freeze | ||
|
|
||
| private_constant :USE_FILETEST_MODULE_FUNCTIONS | ||
|
|
||
| def initialize(path = nil) | ||
| @path = path.to_s | ||
| end | ||
|
|
||
| def to_path | ||
| return '-' if @path.empty? | ||
|
|
||
| @path | ||
| end | ||
|
|
||
| def stdin? | ||
| to_path == '-' | ||
| end | ||
|
|
||
| def inspect | ||
| "#<#{self.class}:#{to_path}>" | ||
| end | ||
|
|
||
| def open(mode = 'r', perm = 0o0666, &block) | ||
| return block&.call($stdin) || $stdin if stdin? | ||
|
|
||
| File.open(to_path, mode, perm, &block) | ||
| end | ||
|
|
||
| def exist? | ||
| stdin? || FileTest.exist?(to_path) | ||
| end | ||
|
|
||
| USE_FILETEST_MODULE_FUNCTIONS.each do |method| | ||
| define_method(method) { stdin? ? $stdin.stat.public_send(method) : FileTest.public_send(method, to_path) } | ||
| end | ||
|
|
||
| def regular_file_size | ||
| file? ? size : 0 | ||
| end | ||
|
|
||
| def exist_non_regular_file? | ||
| exist? && !file? | ||
| end | ||
|
|
||
| def word_count_result(word_count_types = WordCount::TYPES) | ||
| path = @path.empty? ? 'standard input' : @path | ||
|
|
||
| return { path:, count: nil, message: exist? ? 'Permission denied' : 'No such file or directory' } unless readable? | ||
|
|
||
| return { path:, count: word_count_types.to_h { [_1, 0] }, message: 'Is a directory' } if directory? | ||
|
|
||
| { path:, count: word_count(word_count_types), message: nil } | ||
| rescue Errno::EPERM => e | ||
| { path:, count: nil, message: e.message.partition(' @ ').first } | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def word_count(word_count_types) | ||
| return open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types) } unless file? && word_count_types.include?(:bytesize) | ||
|
|
||
| return { bytesize: size } if word_count_types == %i[bytesize] | ||
|
|
||
| counts = open { _1.set_encoding('ASCII-8BIT').word_count(word_count_types - %i[bytesize]) } | ||
|
|
||
| { **counts, bytesize: size } | ||
| end | ||
| end | ||
|
|
||
| module WordCount::IO | ||
| BUFFER_SIZE = 16 * 1024 | ||
|
|
||
| private_constant :BUFFER_SIZE | ||
|
|
||
| def word_count(word_count_types = WordCount::TYPES, bufsize: BUFFER_SIZE) | ||
| each_buffer(bufsize).inject(:<<).to_s.word_count(word_count_types) | ||
| end | ||
|
|
||
| def each_buffer(limit = BUFFER_SIZE) | ||
| return to_enum(__callee__, limit) unless block_given? | ||
|
|
||
| loop do | ||
| yield readpartial(limit) | ||
| rescue EOFError | ||
| break | ||
| end | ||
|
|
||
| self | ||
| end | ||
|
Comment on lines
+88
to
+102
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. このあたりもかなり複雑な書き方をされていますが、単に まずはこれらのメソッドで対応できないか、見てみてください。 |
||
| end | ||
|
|
||
| module WordCount::String | ||
| def word_count(word_count_types = WordCount::TYPES) | ||
| word_count_types.to_h do |type| | ||
| case type | ||
| when *WordCount::TYPES | ||
| [type, __send__(type)] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| else | ||
| raise ArgumentError, "word_count_type: allow only #{WordCount::TYPES.map(&:inspect).join(', ')}" | ||
| end | ||
| end | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def newline | ||
| count("\n") | ||
| end | ||
|
|
||
| def word | ||
| num = 0 | ||
|
|
||
| split { num += 1 if _1.match?(/[[:graph:]]/) } | ||
|
|
||
| num | ||
| end | ||
| end | ||
|
|
||
| class IO | ||
| include WordCount::IO | ||
| end | ||
|
|
||
| class String | ||
| include WordCount::String | ||
| end | ||
|
Comment on lines
+132
to
+138
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. オープンクラスを活用してのクラスの書き換えは実務では基本的にやらないほうがよいです。 ここではシンプルに別メソッドとして定義したほうがよいのではと思います。 |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| #!/usr/bin/env ruby | ||
| # frozen_string_literal: true | ||
|
|
||
| require_relative './lib/wc_methods' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. あまり今回はファイルを分ける意味がないかなと思ったんですが、どうでしょうか。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 分けた理由は、定義した各々のメソッドの動作を There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 遅くなりましたが、なるほどですね。 if __FILE__ == $0
errno = main(ARGV)
exit(errno)
end参考: https://stackoverflow.com/questions/4687680/what-does-if-file-0-mean-in-ruby 一旦ここではこのままでよしとします。 |
||
|
|
||
| errno = main(ARGV) | ||
|
|
||
| exit(errno) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GitHub上には可視化されていないですが、Page breakがところどころはいっているようですね、これは意図的ですか?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
エディタでカーソルを移動する際に、目印のように使用していました。
削除いたします。