diff --git a/07.ls_object/lib/file_metadata.rb b/07.ls_object/lib/file_metadata.rb new file mode 100644 index 0000000000..46e2a3602f --- /dev/null +++ b/07.ls_object/lib/file_metadata.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'etc' + +class FileMetadata + def initialize(path, stat) + @path = path + @stat = stat + end + + def name + File.basename(@path) + end + + def type + format_type(@stat) + end + + def mode + format_mode(@stat) + end + + def setuid? + @stat.setuid? + end + + def setgid? + @stat.setgid? + end + + def sticky? + @stat.sticky? + end + + def nlink + @stat.nlink + end + + def username + Etc.getpwuid(@stat.uid).name + end + + def groupname + Etc.getgrgid(@stat.gid).name + end + + def bytesize + @stat.size + end + + def mtime + @stat.mtime + end + + def blocks + @stat.blocks + end + + private + + def format_type(stat) + stat.mode.to_s(8)[..-5] + end + + def format_mode(stat) + stat.mode.to_s(8)[-3..] + end +end diff --git a/07.ls_object/lib/formatter/long_formatter.rb b/07.ls_object/lib/formatter/long_formatter.rb new file mode 100644 index 0000000000..aca475d6f5 --- /dev/null +++ b/07.ls_object/lib/formatter/long_formatter.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +class LongFormatter + def initialize(file_metadata_list) + @file_metadata_list = file_metadata_list + end + + FILETYPE = { + '1' => 'p', + '2' => 'c', + '4' => 'd', + '6' => 'b', + '10' => '-', + '12' => 'l', + '14' => 's' + }.freeze + + REGULAR_MODE = { + '0' => '---', + '1' => '--x', + '2' => '-w-', + '3' => '-wx', + '4' => 'r--', + '5' => 'r-x', + '6' => 'rw-', + '7' => 'rwx' + }.freeze + + SUID_SGID = { + '0' => '---', + '1' => '--s', + '2' => '-wS', + '3' => '-ws', + '4' => 'r-S', + '5' => 'r-s', + '6' => 'rwS', + '7' => 'rws' + }.freeze + + STICKY_BIT = { + '0' => '---', + '1' => '--t', + '2' => '-wT', + '3' => '-wt', + '4' => 'r-T', + '5' => 'r-t', + '6' => 'rwT', + '7' => 'rwt' + }.freeze + + private_constant :FILETYPE, :REGULAR_MODE, :SUID_SGID, :STICKY_BIT + + def format_output + max_size = build_max_size + "#{build_total_row}\n#{build_body(max_size)}\n" + end + + private + + def build_max_size + { + nlink: @file_metadata_list.map { |file_metadata| file_metadata.nlink.to_s.size }.max, + username: @file_metadata_list.map { |file_metadata| file_metadata.username.size }.max, + groupname: @file_metadata_list.map { |file_metadata| file_metadata.groupname.size }.max, + bytesize: @file_metadata_list.map { |file_metadata| file_metadata.bytesize.to_s.size }.max + } + end + + def build_total_row + total = @file_metadata_list.sum { |file_metadata| file_metadata.blocks.to_i } + "total: #{total}" + end + + def build_body(max_size) + @file_metadata_list.map do |file_metadata| + [ + "#{format_type(file_metadata)}#{format_mode(file_metadata)}", + file_metadata.nlink.to_s.rjust(max_size[:nlink] + 1), + file_metadata.username.rjust(max_size[:username] + 1), + file_metadata.groupname.rjust(max_size[:groupname] + 1), + file_metadata.bytesize.to_s.rjust(max_size[:bytesize] + 1), + " #{format_mtime(file_metadata.mtime)}", + " #{file_metadata.name}" + ].join + end.join("\n") + end + + def format_type(file_metadata) + FILETYPE[file_metadata.type] + end + + def format_mode(file_metadata) + user, group, others = file_metadata.mode.split('') + user_permission = file_metadata.setuid? ? SUID_SGID : REGULAR_MODE + group_permission = file_metadata.setgid? ? SUID_SGID : REGULAR_MODE + others_permission = file_metadata.sticky? ? STICKY_BIT : REGULAR_MODE + [user_permission[user], group_permission[group], others_permission[others]].join + end + + def format_mtime(mtime) + format('%2d %2d %2d:%2d', mon: mtime.mon, mday: mtime.mday, hour: mtime.hour, min: mtime.min) + end +end diff --git a/07.ls_object/lib/formatter/short_formatter.rb b/07.ls_object/lib/formatter/short_formatter.rb new file mode 100644 index 0000000000..59fb6836db --- /dev/null +++ b/07.ls_object/lib/formatter/short_formatter.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class ShortFormatter + def initialize(file_metadata_list) + @file_metadata_list = file_metadata_list + end + + COLUMN = 3 + + private_constant :COLUMN + + def format_output + entries = justfy_entries + row = count_row(entries) + sliced_entries = slice_entries(entries, row) + "#{transpose(sliced_entries).map { |entry| entry.join.rstrip }.join("\n")}\n" + end + + private + + def justfy_entries + max_length = @file_metadata_list.map { |file_metadata| file_metadata.name.size }.max + @file_metadata_list.map { |file_metadata| file_metadata.name.ljust(max_length + 1) } + end + + def count_row(entries) + (entries.size.to_f / COLUMN).ceil + end + + def slice_entries(entries, row) + entries.each_slice(row).to_a + end + + def transpose(sliced_entries) + sliced_entries[0].zip(*sliced_entries[1..]) + end +end diff --git a/07.ls_object/lib/options.rb b/07.ls_object/lib/options.rb new file mode 100644 index 0000000000..eba60abcaa --- /dev/null +++ b/07.ls_object/lib/options.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'optparse' + +class Options + def initialize(arguments) + opts = OptionParser.new + opts.on('-l') { |v| @long_format = v } + opts.on('-r') { |v| @reverse = v } + opts.on('-a') { |v| @dot_match = v } + opts.parse!(arguments) + end + + def long_format? + @long_format + end + + def reverse? + @reverse + end + + def dot_match? + @dot_match + end +end diff --git a/07.ls_object/lib/paths.rb b/07.ls_object/lib/paths.rb new file mode 100644 index 0000000000..7fa6ed5edc --- /dev/null +++ b/07.ls_object/lib/paths.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'pathname' + +class Paths + attr_reader :paths + + def initialize(pathname, dot_match, reverse) + @pathname = pathname + @dot_match = dot_match + @reverse = reverse + @paths = collect_paths + end + + private + + def collect_paths + paths = @dot_match ? Dir.glob(@pathname, File::FNM_DOTMATCH).sort : Dir.glob(@pathname) + reverse(paths) + end + + def reverse(paths) + @reverse ? paths.reverse : paths + end +end diff --git a/07.ls_object/ls.rb b/07.ls_object/ls.rb new file mode 100755 index 0000000000..f2949d052c --- /dev/null +++ b/07.ls_object/ls.rb @@ -0,0 +1,43 @@ +#!/usr/bin/env ruby + +# frozen_string_literal: true + +require_relative './lib/options' +require_relative './lib/file_metadata' +require_relative './lib/paths' +require_relative './lib/formatter/short_formatter' +require_relative './lib/formatter/long_formatter' +require 'pathname' + +class Ls + def initialize(pathname, options) + @pathname = pathname + @options = options + end + + def run + paths = collect_paths(@pathname, @options.dot_match?, @options.reverse?) + file_metadata_list = build_file_metadata_list(paths) + formatter = select_formatter(file_metadata_list, @options.long_format?) + formatter.format_output + end + + private + + def collect_paths(pathname, dot_match, reverse) + Paths.new(pathname, dot_match, reverse).paths + end + + def build_file_metadata_list(paths) + paths.map { |path| FileMetadata.new(path, File::Stat.new(path)) } + end + + def select_formatter(file_metadata_list, long_format) + long_format ? LongFormatter.new(file_metadata_list) : ShortFormatter.new(file_metadata_list) + end +end + +pathname = Pathname('./*') +options = Options.new(ARGV) +ls = Ls.new(pathname, options) +puts ls.run diff --git a/07.ls_object/test/fixtures/test_ls/.secret_file_1 b/07.ls_object/test/fixtures/test_ls/.secret_file_1 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/fixtures/test_ls/.secret_file_2 b/07.ls_object/test/fixtures/test_ls/.secret_file_2 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/fixtures/test_ls/file_1.txt b/07.ls_object/test/fixtures/test_ls/file_1.txt new file mode 100644 index 0000000000..6f9be27fe8 --- /dev/null +++ b/07.ls_object/test/fixtures/test_ls/file_1.txt @@ -0,0 +1,5 @@ +るに馬の頭部の近辺に或ある異常な光の現象が起こるというふうに解釈される。 + 次に注意すべきは、この怪異の起こる時の時間的分布である。すなわち「濃州のうしゅうでは四月から七月までで、別して五六月が多いという。七月になりかかると、秋風が立ち初める、とギバの難は影を隠してしまう。武州ぶしゅう常州じょうしゅうあたりでもやはり四月から七月と言っている」。また晴天には現われず「晴れては曇り曇っては晴れる、村雲などが出たりはいったりする日に限って」現われるとある。また一日じゅうの時刻については「朝五つ時前(午前八時)、夕七つ時過ぎ(午後四時)にはかけられない、多くは日盛りであるという」とある。 + またこの出現するのにおのずから場所が定まっている傾向があり、たとえば一里塚いちりづかのような所の例があげられている。 + もう一つ参考になるのは、馬をギバの難から救う方法として、これが襲いかかった時に、半纏はんてんでも風呂敷ふろしきでも莚むしろでも、そういうものを馬の首からかぶせるといいということがある。もちろん、その上に、尾の上の背骨に針を打ち込んだりするそうであるが、このようにものをかぶせる事が「針よりも大切なまじない」だと考えられている。またこれと共通な点のあるのは、平生のギバよけのまじないとして、馬に腹当てをさせるとよい、ただしそれは「大津東町上下仕合」と白く染めぬいたものを用いる。「このアブヨケをした馬がギバにかけられてたおれたのを見た事がないと、言われている」。 + 別の説として美濃みのでは「ギバは白虻しろあぶのような、目にも見えない虫だという説がある、また常陸ひたちではその虫を大津虫と呼んでいる。虫は玉虫色をしていて diff --git a/07.ls_object/test/fixtures/test_ls/file_2.txt b/07.ls_object/test/fixtures/test_ls/file_2.txt new file mode 100644 index 0000000000..08836b5e55 --- /dev/null +++ b/07.ls_object/test/fixtures/test_ls/file_2.txt @@ -0,0 +1,3 @@ + これに反して、ギバがなんらかの空中放電によるものと考えると、たてがみが立ち上がったり、光の線条が見えたり、玉虫色の光が馬の首を包んだりする事が、全部生きた科学的記述としての意味をもって来る。また衣服その他で頭をおおい、また腹部を保護するという事は、つまり電気の半導体で馬の身体の一部を被覆して、放電による電流が直接にその局部の肉体に流れるのを防ぐという意味に解釈されて来るのである。 + またこういう放電現象が夏期に多い事、および日中に多い事は周知の事実であるので、前述の時間分布は、これときわめてよく符合する事になる。 + 場所のおのずから定まる傾向については、自分は何事も具体的のことをいうだけの材料を持ち合わせないが、これも調べてみたら、おそらく放電現象の多い場所と符合するようなことがありはし diff --git a/07.ls_object/test/fixtures/test_ls/file_3.txt b/07.ls_object/test/fixtures/test_ls/file_3.txt new file mode 100644 index 0000000000..1fb7f7233d --- /dev/null +++ b/07.ls_object/test/fixtures/test_ls/file_3.txt @@ -0,0 +1 @@ + 物理学の学徒としての自分は、日常普通に身辺に起こる自然現象に不思議を感ずる事は多いが、古来のいわゆる「怪異」なるものの存在を信ずることはできない。しかし昔からわれわれの祖先が多くの「怪異」に遭遇しそれを「目撃」して来たという人事的現象としての「事実」を否定するものではない。われわれの役目はただそれらの怪異現象の記録を現代科学上の語彙ごいを借りて翻訳するだけの事でなければならない。この仕事はしかしはなはだ困難なものである。錯覚や誇張さらに転訛てんかのレンズによってはなはだしくゆがめられた影像からその本体を言い当てなければならない。それを的確に成効しうるためにはそのレンズに関する方則を正確に知らなければならない、のみならず、またその個々の場合における決定条件として多様の因子を逐一に明らかにしなければならない。この前者の方則については心理学のほうから若干の根拠は供給されるとしても、後者に関する資料はほとんどすべての場合において永久に失われている。従ってほんとうに科学的な推定を下すということはほとんど望み難いことである。ただできうる唯一の方法としては、有るだけの材料から、科学的に合理的な一つの「可能性」を指摘するに過ぎない。もっともこの可能性が非常に多様であれば、その中の二三を指摘してみても、それは結局なんらの価値もない漫談となってしまうであろうが、多くの場合に必ずしもそうとは限らない。ことにある一種の怪異に関する記録が豊富にあればあるほど、この可能性の範囲はかなりまで押しせばめられる。従ってやや「もっともらしい仮説」というまでには漕こぎつけられる見込みがあるのである。そこまで行けば、それはともかくも一つの仮説として存在する価値を認めなければならず、また実際科学者たちにある暗示を提供するだけの効果をもつ事も有りうるであろうと思われる。 diff --git a/07.ls_object/test/fixtures/test_ls/file_4.txt b/07.ls_object/test/fixtures/test_ls/file_4.txt new file mode 100644 index 0000000000..fbb7cd705b --- /dev/null +++ b/07.ls_object/test/fixtures/test_ls/file_4.txt @@ -0,0 +1,5 @@ + その怪異の第一は、自分の郷里高知こうち付近で知られている「孕はらみのジャン」と称するものである。孕は地名で、高知の海岸に並行する山脈が浦戸湾うらどわんに中断されたその両側の突端の地とその海峡とを込めた名前である。この現象については、最近に、土佐とさ郷土史きょうどしの権威として知られた杜山居士とざんこじ寺石正路てらいしまさみち氏が雑誌「土佐史壇」第十七号に「郷土史断片」その三〇として記載されたものがある。「(前略)昔はだいぶ評判の事であったが、このごろは全くその沙汰さたがない、根拠の無き話かと思えば、「土佐今昔物語」という書に、沼澄ぬまずみ(鹿持雅澄かもちまさずみ翁おう)の名をもって左のとおりしるされている。 +孕の海にジャンと唱うる稀有けうのものありけり、たれしの人もいまだその形を見たるものなく、その物は夜半にジャーンと鳴り響きて海上を過ぎ行くなりけり、漁業をして世を渡るどちに、夜半に小舟浮かべて、あるは釣つりをたれ、あるいは網を打ちて幸さち多かるも、このも[#「も」に「原」の注記]海上を行き過ぐればたちまちに魚騒ぎ走りて、時を移すともその夜はまた幸さちなかりけり、高知ほとりの方言に、ものの破談になりたる事をジャンになりたりというも、この海上行き過ぐるものよりいでたることなん語り伝えたりとや。 + この文は鹿持翁の筆なればおおよそ小百年前のことにして孕はらみのジャンはこのほどの昔よりもすでにその伝があったことが知れる(後略)。」寺石氏はこのジャンの意味の転用に関する上記の説の誤謬ごびゅうを指摘している。また終わりに諏訪湖すわこの神渡りの音響の事を引き、孕のジャンは「何か微妙な地の震動に関したことではあるまいか」と述べておられる。 + 私は幼時近所の老人からたびたびこれと同様な話を聞かされた。そしてもし記憶の誤りでなければ、このジャンの音響とともに「水面にさざ波が立つ」という事が上記の記載に付加されていた。 + この話を導き出しそうな音の原因に関する自分のはじめの考えは、もしや昆虫こんちゅうかあるいは鳥類の群れが飛び立つ音ではないかと思ってみたが、しかしそれは夜半の事だというし、また魚が釣つれなくなるという事が確実とすれば単に空中の音波のためとは考えにくいと思われた。ところが先年筑波山つくばさんの北側の柿岡かきおかの盆地へ行った時にかの地には珍しくない「地鳴り」の現象を数回体験した。その時に自分は全く神来的に「孕はらみのジャンはこれだ」と感じた。この地鳴りの音は考え方によってはやはりジャーンとも形容されうる種類の雑音であるし、またその地盤の性質、地表の形状や被覆物の種類によってはいっそうジャーンと聞こえやすくなるであろうと思われうるたちのものである。そして明らかに一方から一方へ「過ぎ行く」音で、それが空中ともなく地中ともなく過ぎ去って行くのは実際他に比較するもののない奇異の感じを起こさせるものである。ちょうど自分が観測室内にいた時に起こった地鳴りの際には、磁力計の頂上に付いている管が共鳴してその頭が少なくも数ミリほど振動するのを明らかに認める事ができたし、また山中で聞いた時は立っている靴くつの底に明らかにきわめて短週期の震動を感じた。これだけの振動があれば、適当な境界条件の下に水面のさざ波を起こしうるはずであるし、また水中の魚類の耳石等にもこれを感じなければならないわけである。もっとも、魚類がこの種の短週期弾性波に対してどう反応するかについて自分はあまりよく知らないが、これだけの振動に全然無感覚であろうとは想像し難い。 diff --git a/07.ls_object/test/fixtures/test_ls/sgid_A b/07.ls_object/test/fixtures/test_ls/sgid_A new file mode 100755 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/fixtures/test_ls/sgid_B b/07.ls_object/test/fixtures/test_ls/sgid_B new file mode 100755 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/fixtures/test_ls/suid_A b/07.ls_object/test/fixtures/test_ls/suid_A new file mode 100755 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/fixtures/test_ls/suid_B b/07.ls_object/test/fixtures/test_ls/suid_B new file mode 100644 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/fixtures/test_ls/suid_sgid_and_sticky_bit_A b/07.ls_object/test/fixtures/test_ls/suid_sgid_and_sticky_bit_A new file mode 100755 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/fixtures/test_ls/suid_sgid_and_sticky_bit_B b/07.ls_object/test/fixtures/test_ls/suid_sgid_and_sticky_bit_B new file mode 100644 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/test_ls.rb b/07.ls_object/test/test_ls.rb new file mode 100644 index 0000000000..1e673b5c94 --- /dev/null +++ b/07.ls_object/test/test_ls.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'pathname' +require_relative '../ls' +require_relative '../lib/options' + +class TestLs < Minitest::Test + def setup + @fixture_path = Pathname('test/fixtures/test_ls') + end + + def test_ls_run + expected = <<~TEXT + dir_1 file_3.txt sticky_bit_B + dir_2 file_4.txt suid_A + dir_3 sgid_A suid_B + file_1.txt sgid_B + file_2.txt sticky_bit_A + TEXT + + @options = Options.new([]) + @ls = Ls.new(@fixture_path.join('*'), @options) + assert_equal expected, @ls.run + end + + def test_run_with_reverse + expected = <<~TEXT + suid_B sgid_A dir_3 + suid_A file_4.txt dir_2 + sticky_bit_B file_3.txt dir_1 + sticky_bit_A file_2.txt + sgid_B file_1.txt + TEXT + + @options = Options.new(['-r']) + @ls = Ls.new(@fixture_path.join('*'), @options) + assert_equal expected, @ls.run + end + + def test_run_with_dot_match + expected = <<~TEXT + . file_1.txt sticky_bit_A + .secret_file_1 file_2.txt sticky_bit_B + .secret_file_2 file_3.txt suid_A + dir_1 file_4.txt suid_B + dir_2 sgid_A + dir_3 sgid_B + TEXT + + @options = Options.new(['-a']) + @ls = Ls.new(@fixture_path.join('*'), @options) + assert_equal expected, @ls.run + end + + def test_run_with_long_option + expected = <<~TEXT + total: 40 + drwxr-xr-x 2 miya staff 64 12 19 14: 9 dir_1 + drwxr-xr-x 2 miya staff 64 12 19 14: 9 dir_2 + drwxr-xr-x 2 miya staff 64 12 19 14: 9 dir_3 + -rw-r--r-- 1 miya staff 2135 12 19 20:50 file_1.txt + -rw-r--r-- 1 miya staff 1044 12 19 20:51 file_2.txt + -rw-r--r-- 1 miya staff 2332 12 19 20:49 file_3.txt + -rw-r--r-- 1 miya staff 4439 12 19 20:50 file_4.txt + -rwxrwsrwx 1 miya staff 0 12 19 14: 4 sgid_A + -rwxrwSrwx 1 miya staff 0 12 19 14: 4 sgid_B + drwxrwxrwt 2 miya staff 64 12 19 14: 5 sticky_bit_A + drwxrwxrwT 2 miya staff 64 12 19 14: 5 sticky_bit_B + -rwsrwxrwx 1 miya staff 0 12 19 14: 5 suid_A + -rwSrwxrwx 1 miya staff 0 12 19 14: 5 suid_B + TEXT + + @options = Options.new(['-l']) + @ls = Ls.new(@fixture_path.join('*'), @options) + assert_equal expected, @ls.run + end + + def test_run_with_all_options + expected = <<~TEXT + total: 40 + -rwSrwxrwx 1 miya staff 0 12 19 14: 5 suid_B + -rwsrwxrwx 1 miya staff 0 12 19 14: 5 suid_A + drwxrwxrwT 2 miya staff 64 12 19 14: 5 sticky_bit_B + drwxrwxrwt 2 miya staff 64 12 19 14: 5 sticky_bit_A + -rwxrwSrwx 1 miya staff 0 12 19 14: 4 sgid_B + -rwxrwsrwx 1 miya staff 0 12 19 14: 4 sgid_A + -rw-r--r-- 1 miya staff 4439 12 19 20:50 file_4.txt + -rw-r--r-- 1 miya staff 2332 12 19 20:49 file_3.txt + -rw-r--r-- 1 miya staff 1044 12 19 20:51 file_2.txt + -rw-r--r-- 1 miya staff 2135 12 19 20:50 file_1.txt + drwxr-xr-x 2 miya staff 64 12 19 14: 9 dir_3 + drwxr-xr-x 2 miya staff 64 12 19 14: 9 dir_2 + drwxr-xr-x 2 miya staff 64 12 19 14: 9 dir_1 + -rw-r--r-- 1 miya staff 0 12 19 18:30 .secret_file_2 + -rw-r--r-- 1 miya staff 0 12 19 18:29 .secret_file_1 + drwxr-xr-x 17 miya staff 544 12 19 20:50 . + TEXT + + @options = Options.new(['-arl']) + @ls = Ls.new(@fixture_path.join('*'), @options) + assert_equal expected, @ls.run + end +end