diff --git a/07.ls_object/bin/ls.rb b/07.ls_object/bin/ls.rb new file mode 100755 index 0000000000..87e7cc6fa2 --- /dev/null +++ b/07.ls_object/bin/ls.rb @@ -0,0 +1,21 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'io/console' +require 'optparse' +require_relative '../lib/ls_command' + +if __FILE__ == $PROGRAM_NAME + opt = OptionParser.new + + params = { dot_match: false } + opt.on('-a') { |v| params[:dot_match] = v } + opt.on('-r') { |v| params[:reverse] = v } + opt.on('-l') { |v| params[:long_format] = v } + opt.parse!(ARGV) + path = ARGV[0] || '.' + width = IO.console.winsize[1] + + ls = LsCommand.new(path, width:, **params) + puts ls.formatted_output +end diff --git a/07.ls_object/lib/file/file_data.rb b/07.ls_object/lib/file/file_data.rb new file mode 100755 index 0000000000..851757fdd9 --- /dev/null +++ b/07.ls_object/lib/file/file_data.rb @@ -0,0 +1,82 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'etc' +require 'pathname' + +class FileData + attr_reader :name, :file_status + + PERMISSION_START_POSITION = 7 + PERMISSION_END_POSITION = 9 + + FILE_TYPE = { + 'fifo' => 'p', + 'characterSpecial' => 'c', + 'directory' => 'd', + 'blockSpecial' => 'b', + 'file' => '-', + 'link' => 'l', + 'socket' => 's' + }.freeze + + SPECIAL_PERMISSION = { + 0 => 's', + 1 => 's', + 2 => 't' + }.freeze + + PERMISSION_TYPE = { + '000' => '---', + '001' => '--x', + '010' => '-w-', + '011' => '-wx', + '100' => 'r--', + '101' => 'r-x', + '110' => 'rw-', + '111' => 'rwx' + }.freeze + + def initialize(name, path) + @name = name + @full_path = Pathname(File.absolute_path(@name, path)) + @file_type = File.ftype(@full_path) + @file_status = build_file_status(File.lstat(@full_path)) + end + + private + + def build_file_status(status) + { + type: FILE_TYPE[status.ftype], + mode: mode(status), + hardlink_nums: status.nlink.to_s, + owner_name: Etc.getpwuid(status.uid).name, + group_name: Etc.getgrgid(status.gid).name, + bytesize: rdev_or_bytesize(status), + latest_modify_datetime: status.mtime.strftime('%_m %e %H:%M'), + filename: file_name(status), + blocks: status.blocks + } + end + + def file_name(status) + status.symlink? ? "#{@name} -> #{File.readlink(@full_path)}" : @name + end + + def mode(status) + mode_binary_numbers = status.mode.to_s(2).rjust(16, '0') + # SUID、SGID、STICKEYBITの順番で特殊権限をチェック + mode_binary_numbers[4..6].each_char.with_index.map do |special_permission, i| + range_start = PERMISSION_START_POSITION + (3 * i) + range_end = PERMISSION_END_POSITION + (3 * i) + permission = PERMISSION_TYPE[mode_binary_numbers[range_start..range_end]].dup + permission[2] = permission[2] == 'x' ? SPECIAL_PERMISSION[i] : SPECIAL_PERMISSION[i].upcase if special_permission == '1' + permission + end.join + end + + def rdev_or_bytesize(status) + %w[characterSpecial blockSpecial].include?(@file_type) ? format('%#01x', status.rdev.to_s(10)) : status.size.to_s + end +end diff --git a/07.ls_object/lib/format/formatter.rb b/07.ls_object/lib/format/formatter.rb new file mode 100644 index 0000000000..9a2579f38c --- /dev/null +++ b/07.ls_object/lib/format/formatter.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Formatter + def initialize(files) + @files = files + end + + def format; end +end diff --git a/07.ls_object/lib/format/long_formatter.rb b/07.ls_object/lib/format/long_formatter.rb new file mode 100644 index 0000000000..3a0d1b9ffe --- /dev/null +++ b/07.ls_object/lib/format/long_formatter.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require_relative 'formatter' + +class LongFormatter < Formatter + def initialize(files, path) + super(files) + @path = path + @max_sizes = build_max_sizes + @total_block = sum_blocks + @long_format_data = build_long_format_data + end + + def format + total = "total #{@total_block}" if File.directory?(@path) || @files.size > 1 + [total, *@long_format_data].compact.join("\n") + end + + private + + def build_long_format_data + @files.map { |file| format_row(file.file_status, *@max_sizes) } + end + + def build_max_sizes + %i[hardlink_nums owner_name group_name bytesize].map do |key| + find_max_size(key) + end + end + + def find_max_size(key) + @files.map { |file| file.file_status[key].size }.max + end + + def format_row(status, max_nlink, max_user, max_group, max_size) + [ + "#{status[:type]}#{status[:mode]}", + " #{status[:hardlink_nums].rjust(max_nlink)}", + " #{status[:owner_name].ljust(max_user)}", + " #{status[:group_name].ljust(max_group)}", + " #{status[:bytesize].rjust(max_size)}", + " #{status[:latest_modify_datetime]}", + " #{status[:filename]}" + ].join + end + + def sum_blocks + @files.sum { |file| file.file_status[:blocks] } + end +end diff --git a/07.ls_object/lib/format/short_formatter.rb b/07.ls_object/lib/format/short_formatter.rb new file mode 100644 index 0000000000..2119ac87aa --- /dev/null +++ b/07.ls_object/lib/format/short_formatter.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require_relative 'formatter' + +class ShortFormatter < Formatter + def initialize(files, width) + super(files) + @width = width + @max_file_name = @files.map { |file| File.basename(file.name).size }.max + @col_count = calculate_col_count + @row_count = calculate_row_count + @short_format_data = build_short_format_data + end + + def format + @short_format_data.join("\n") + end + + private + + def build_short_format_data + safe_transpose.map { |row_files| build_short_format_row(row_files) } + end + + def build_short_format_row(row_files) + row_files.map do |file| + basename = file ? File.basename(file.name) : '' + basename.ljust(@max_file_name + 1) + end.join.rstrip + end + + def calculate_col_count + @width / (@max_file_name + 1) + end + + def calculate_row_count + @col_count.zero? ? @files.count : (@files.count.to_f / @col_count).ceil + end + + def safe_transpose + nested_file_names = @files.each_slice(@row_count).to_a + nested_file_names[0].zip(*nested_file_names[1..]) + end +end diff --git a/07.ls_object/lib/ls_command.rb b/07.ls_object/lib/ls_command.rb new file mode 100644 index 0000000000..6ad6e0940e --- /dev/null +++ b/07.ls_object/lib/ls_command.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative 'file/file_data' +require_relative 'format/long_formatter' +require_relative 'format/short_formatter' + +class LsCommand + def initialize(path, width: 80, dot_match: false, reverse: false, long_format: false) + @path = path + @width = width + @dot_match = dot_match + @reverse = reverse + @long_format = long_format + @files = build_dot_match_and_sorted_files + end + + def formatted_output + @long_format ? LongFormatter.new(@files, @path).format : ShortFormatter.new(@files, @width).format + end + + private + + def build_dot_match_and_sorted_files + files = generate_files + matched_files = @dot_match ? files : files.filter { |file| !/^\./.match?(file.name) } + @reverse ? matched_files.sort_by(&:name).reverse : matched_files.sort_by(&:name) + end + + def generate_files + if File.directory?(@path) + Dir.open(@path).entries.map { |name| FileData.new(name, @path) } + else + [FileData.new(@path, File.dirname(@path))] + end + end +end diff --git a/07.ls_object/test/ls_command_test.rb b/07.ls_object/test/ls_command_test.rb new file mode 100644 index 0000000000..1d4033f054 --- /dev/null +++ b/07.ls_object/test/ls_command_test.rb @@ -0,0 +1,191 @@ +# frozen_string_literal: true + +require_relative 'test_helper' +require_relative '../lib/ls_command' + +class LsCommandTest < Minitest::Test + def test_display_a_file + ls_command = LsCommand.new('test/test_dir/test_one_file') + assert_equal 'test.txt', ls_command.formatted_output + end + + def test_display_files + ls_command = LsCommand.new('test/test_dir/test_include_dir_and_dot_files') + assert_equal 'dir test.txt test_2.txt', ls_command.formatted_output + end + + def test_display_match_dot_files + params = { dot_match: true } + ls_command = LsCommand.new('test/test_dir/test_include_dir_and_dot_files', **params) + assert_equal '. .. .test dir test.txt test_2.txt', ls_command.formatted_output + end + + def test_display_reverse_sort_files + params = { reverse: true, dot_match: false } + ls_command = LsCommand.new('test/test_dir/test_include_dir_and_dot_files', **params) + assert_equal 'test_2.txt test.txt dir', ls_command.formatted_output + end + + def test_display_dot_match_and_reverse_sort_file + params = { reverse: true, dot_match: true } + ls_command = LsCommand.new('test/test_dir/test_include_dir_and_dot_files', **params) + assert_equal 'test_2.txt test.txt dir .test .. .', ls_command.formatted_output + end + + def test_display_long_format_file + # Output example + # total 8 + # -rw-r--r-- 1 username staff 10 9 15 14:34 test.txt + path = 'test/test_dir/test_one_file' + params = { long_format: true } + expected = `ls -l #{path}`.chomp + ls_command = LsCommand.new(path, **params) + assert_equal expected, ls_command.formatted_output + end + + def test_display_long_format_and_dot_match_file + # Output example + # total 16 + # drwxr-xr-x 6 username staff 192 9 16 15:18 . + # drwxr-xr-x 5 username staff 160 9 19 18:20 .. + # -rw-r--r-- 1 username staff 0 9 16 15:14 .test + # drwxr-xr-x 2 username staff 64 9 16 15:18 dir + # -rw-r--r-- 1 username staff 10 9 16 15:14 test.txt + # -rw-r--r-- 1 username staff 10 9 16 15:14 test_2.txt + path = 'test/test_dir/test_include_dir_and_dot_files' + params = { long_format: true, dot_match: true } + expected = `ls -al #{path}`.chomp + ls_command = LsCommand.new(path, **params) + assert_equal expected, ls_command.formatted_output + end + + def test_display_long_format_and_dot_match_and_reverse_file + # Output example + # total 16 + # -rw-r--r-- 1 username staff 10 9 16 15:14 test_2.txt + # -rw-r--r-- 1 username staff 10 9 16 15:14 test.txt + # drwxr-xr-x 2 username staff 64 9 16 15:18 dir + # -rw-r--r-- 1 username staff 0 9 16 15:14 .test + # drwxr-xr-x 5 username staff 160 9 19 18:20 .. + # drwxr-xr-x 6 username staff 192 9 16 15:18 . + params = { reverse: true, long_format: true, dot_match: true } + path = 'test/test_dir/test_include_dir_and_dot_files' + expected = `ls -arl #{path}`.chomp + ls_command = LsCommand.new(path, **params) + assert_equal expected, ls_command.formatted_output + end + + def test_display_width_eighty + params = { dot_match: true } + ls_command = LsCommand.new('test/test_dir/test_include_dir_and_dot_files', **params) + assert_equal '. .. .test dir test.txt test_2.txt', ls_command.formatted_output + end + + def test_display_width_fourty + params = { dot_match: true } + ls_command = LsCommand.new('test/test_dir/test_include_dir_and_dot_files', width: 40, **params) + expected = <<~LS_RESULT.chomp + . .test test.txt + .. dir test_2.txt + LS_RESULT + assert_equal expected, ls_command.formatted_output + end + + def test_display_width_thirty + params = { dot_match: true } + ls_command = LsCommand.new('test/test_dir/test_include_dir_and_dot_files', width: 30, **params) + expected = <<~LS_RESULT.chomp + . dir + .. test.txt + .test test_2.txt + LS_RESULT + assert_equal expected, ls_command.formatted_output + end + + def test_display_width_twenty + params = { dot_match: true } + ls_command = LsCommand.new('test/test_dir/test_include_dir_and_dot_files', width: 20, **params) + expected = <<~LS_RESULT.chomp + . + .. + .test + dir + test.txt + test_2.txt + LS_RESULT + assert_equal expected, ls_command.formatted_output + end + + def test_display_special_permission + # Output example + # total 0 + # -rwxr-sr-x 1 username staff 0 10 7 21:46 set_group_id_file.txt + # -rwxr-Sr-x 1 username staff 0 10 7 22:05 set_group_id_file_not_x_permission.txt + # -r-Sr-Sr-T 1 username staff 0 10 17 00:18 set_user_id_and_group_id_and_sticky_bit_permission.txt + # -rwsr-sr-x 1 username staff 0 10 17 00:17 set_user_id_and_group_id_permission.txt + # -rwsr-xr-x 1 username staff 0 10 7 21:46 set_user_id_file.txt + # -r-Sr-xr-x 1 username staff 0 10 7 22:05 set_user_id_file_not_x_permission.txt + # -rwxr-xr-t 1 username staff 0 10 7 21:46 sticky_bit_file.txt + # -rwxr-xr-T 1 username staff 0 10 7 21:51 sticky_bit_file_not_x_permission.txt + params = { long_format: true } + path = 'test/test_dir/special_permission_dir' + `chmod 2755 #{path}/set_group_id_file.txt` # group permissionに実行権限xと特殊権限を付与 + `chmod 2745 #{path}/set_group_id_file_not_x_permission.txt` # group permissionの実行権限xを外し特殊権限を付与 + `chmod 4755 #{path}/set_user_id_file.txt` # owner permissionに実行権限xと特殊権限を付与 + `chmod 4455 #{path}/set_user_id_file_not_x_permission.txt` # owner permissionの実行権限xを外し特殊権限を付与 + `chmod 1755 #{path}/sticky_bit_file.txt` # other permissionに実行権限xと特殊権限を付与 + `chmod 1754 #{path}/sticky_bit_file_not_x_permission.txt` # other permissionの実行権限xを外し特殊権限を付与 + `chmod 6755 test/test_dir/special_permission_dir/set_user_id_and_group_id_permission.txt` + `chmod 7444 test/test_dir/special_permission_dir/set_user_id_and_group_id_and_sticky_bit_permission.txt` + expected = `ls -l #{path}`.chomp + ls_command = LsCommand.new(path, **params) + assert_equal expected, ls_command.formatted_output + end + + def test_display_some_file_types + # Output example + # total 0 + # drwxr-xr-x 2 username staff 64 10 7 23:12 test_dir + # -rw-r--r-- 1 username staff 0 10 7 23:07 test_file.txt + # lrwxr-xr-x 1 username staff 102 10 7 22:59 test_link -> ../07.ls_object/test/test_dir/link_dir/link_test.txt # フルパスのため省略 + # prw-r--r-- 1 username staff 0 10 8 01:17 testfile + params = { long_format: true } + path = 'test/test_dir/file_type_dir' + # パイプファイルの作成 + `mkfifo test/test_dir/file_type_dir/test_fifo_file` unless File.exist?('test/test_dir/file_type_dir/test_fifo_file') + expected = `ls -l #{path}`.chomp + ls_command = LsCommand.new(path, **params) + assert_equal expected, ls_command.formatted_output + end + + def test_display_block_device_file_type + # Output example + # brw-r----- 1 root operator 0x1000004 9 8 09:43 /dev/disk1 + params = { long_format: true } + path = '/dev/disk1' + expected = `ls -l #{path}`.chomp + ls_command = LsCommand.new(path, **params) + assert_equal expected, ls_command.formatted_output + end + + def test_display_character_device_file_file_type + # Output example + # total 0 + # crw-rw-rw- 1 root wheel 0x3000002 10 8 01:26 /dev/null + params = { long_format: true } + path = '/dev/null' + expected = `ls -l #{path}`.chomp + ls_command = LsCommand.new(path, **params) + assert_equal expected, ls_command.formatted_output + end + + def test_display_local_domain_sockets_file_type + # Output example + # srw------- 1 root daemon 0 9 8 09:43 /var/run/vpncontrol.sock + params = { long_format: true } + path = '/var/run/vpncontrol.sock' + expected = `ls -l #{path}`.chomp + ls_command = LsCommand.new(path, **params) + assert_equal expected, ls_command.formatted_output + end +end diff --git a/07.ls_object/test/test_dir/file_type_dir/link_test b/07.ls_object/test/test_dir/file_type_dir/link_test new file mode 120000 index 0000000000..4fb2b3f711 --- /dev/null +++ b/07.ls_object/test/test_dir/file_type_dir/link_test @@ -0,0 +1 @@ +/test/test_dir/link_dir/link_test.txt \ No newline at end of file diff --git a/07.ls_object/test/test_dir/file_type_dir/test_file.txt b/07.ls_object/test/test_dir/file_type_dir/test_file.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/test_dir/link_dir/link_test.txt b/07.ls_object/test/test_dir/link_dir/link_test.txt new file mode 100644 index 0000000000..4b0a21d6fc --- /dev/null +++ b/07.ls_object/test/test_dir/link_dir/link_test.txt @@ -0,0 +1 @@ +testtest \ No newline at end of file diff --git a/07.ls_object/test/test_dir/special_permission_dir/set_group_id_file.txt b/07.ls_object/test/test_dir/special_permission_dir/set_group_id_file.txt new file mode 100755 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/test_dir/special_permission_dir/set_group_id_file_not_x_permission.txt b/07.ls_object/test/test_dir/special_permission_dir/set_group_id_file_not_x_permission.txt new file mode 100755 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/test_dir/special_permission_dir/set_user_id_and_group_id_and_sticky_bit_permission.txt b/07.ls_object/test/test_dir/special_permission_dir/set_user_id_and_group_id_and_sticky_bit_permission.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/test_dir/special_permission_dir/set_user_id_and_group_id_permission.txt b/07.ls_object/test/test_dir/special_permission_dir/set_user_id_and_group_id_permission.txt new file mode 100755 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/test_dir/special_permission_dir/set_user_id_file.txt b/07.ls_object/test/test_dir/special_permission_dir/set_user_id_file.txt new file mode 100755 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/test_dir/special_permission_dir/set_user_id_file_not_x_permission.txt b/07.ls_object/test/test_dir/special_permission_dir/set_user_id_file_not_x_permission.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/test_dir/special_permission_dir/sticky_bit_file.txt b/07.ls_object/test/test_dir/special_permission_dir/sticky_bit_file.txt new file mode 100755 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/test_dir/special_permission_dir/sticky_bit_file_not_x_permission.txt b/07.ls_object/test/test_dir/special_permission_dir/sticky_bit_file_not_x_permission.txt new file mode 100755 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/test_dir/test_include_dir_and_dot_files/.test b/07.ls_object/test/test_dir/test_include_dir_and_dot_files/.test new file mode 100644 index 0000000000..e69de29bb2 diff --git a/07.ls_object/test/test_dir/test_include_dir_and_dot_files/test.txt b/07.ls_object/test/test_dir/test_include_dir_and_dot_files/test.txt new file mode 100644 index 0000000000..910898e128 --- /dev/null +++ b/07.ls_object/test/test_dir/test_include_dir_and_dot_files/test.txt @@ -0,0 +1 @@ +testです \ No newline at end of file diff --git a/07.ls_object/test/test_dir/test_include_dir_and_dot_files/test_2.txt b/07.ls_object/test/test_dir/test_include_dir_and_dot_files/test_2.txt new file mode 100644 index 0000000000..910898e128 --- /dev/null +++ b/07.ls_object/test/test_dir/test_include_dir_and_dot_files/test_2.txt @@ -0,0 +1 @@ +testです \ No newline at end of file diff --git a/07.ls_object/test/test_dir/test_one_file/test.txt b/07.ls_object/test/test_dir/test_one_file/test.txt new file mode 100644 index 0000000000..910898e128 --- /dev/null +++ b/07.ls_object/test/test_dir/test_one_file/test.txt @@ -0,0 +1 @@ +testです \ No newline at end of file diff --git a/07.ls_object/test/test_helper.rb b/07.ls_object/test/test_helper.rb new file mode 100644 index 0000000000..90d46e9bc6 --- /dev/null +++ b/07.ls_object/test/test_helper.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'minitest/pride'