From 0048e3883abdee22b62750960536a8b58275336c Mon Sep 17 00:00:00 2001 From: Claudio Bley Date: Wed, 23 Dec 2020 22:30:41 +0100 Subject: [PATCH] Group and process arguments in groups of files and directories Fixes #233 --- lib/colorls/core.rb | 161 ++++++++++++++++++++---------------- lib/colorls/fileinfo.rb | 21 +++-- lib/colorls/flags.rb | 38 ++++++--- lib/colorls/git.rb | 15 ++-- spec/color_ls/core_spec.rb | 6 +- spec/color_ls/flags_spec.rb | 44 ++++++++-- spec/color_ls/git_spec.rb | 2 +- 7 files changed, 179 insertions(+), 108 deletions(-) diff --git a/lib/colorls/core.rb b/lib/colorls/core.rb index f77c2e7..fae67de 100644 --- a/lib/colorls/core.rb +++ b/lib/colorls/core.rb @@ -31,34 +31,38 @@ module ColorLS init_long_format(mode,show_group,show_user) @tree = {mode: mode == :tree, depth: tree_depth} @horizontal = mode == :horizontal - process_git_status_details(git_status) + @git_status = init_git_status(git_status) init_colors colors init_icons end - def ls(input) - @input = (+input).force_encoding(ColorLS.file_encoding) - @contents = init_contents(@input) + def ls_dir(info) + if @tree[:mode] + print "\n" + return tree_traverse(info.path, 0, 1, 2) + end + + @contents = Dir.entries(info.path, encoding: ColorLS.file_encoding) + + filter_hidden_contents + + @contents.map! { |e| FileInfo.dir_entry(info.path, e, link_info: @long) } + + filter_contents if @show + sort_contents if @sort + group_contents if @group return print "\n Nothing to show here\n".colorize(@colors[:empty]) if @contents.empty? - layout = case - when @tree[:mode] - print "\n" - return tree_traverse(@input, 0, 1, 2) - when @horizontal - HorizontalLayout.new(@contents, item_widths, ColorLS.screen_width) - when @one_per_line || @long - SingleColumnLayout.new(@contents) - else - VerticalLayout.new(@contents, item_widths, ColorLS.screen_width) - end + ls + end - layout.each_line do |line, widths| - ls_line(line, widths) - end + def ls_files(files) + @contents = files + + ls end def display_report @@ -72,6 +76,23 @@ module ColorLS private + def ls + init_column_lengths + + layout = case + when @horizontal + HorizontalLayout.new(@contents, item_widths, ColorLS.screen_width) + when @one_per_line || @long + SingleColumnLayout.new(@contents) + else + VerticalLayout.new(@contents, item_widths, ColorLS.screen_width) + end + + layout.each_line do |line, widths| + ls_line(line, widths) + end + end + def init_colors(colors) @colors = colors @modes = Hash.new do |hash, key| @@ -85,12 +106,26 @@ module ColorLS end end - def init_long_format(mode,show_group,show_user) + def init_long_format(mode, show_group, show_user) @long = mode == :long @show_group = show_group @show_user = show_user end + def init_git_status(show_git) + return {}.freeze unless show_git + + # stores git status information per directory + Hash.new do |hash, key| + path = File.absolute_path key.parent + if hash.key? path + hash[path] + else + hash[path] = Git.status(path) + end + end + end + # how much characters an item occupies besides its name CHARS_PER_ITEM = 12 @@ -98,28 +133,6 @@ module ColorLS @contents.map { |item| Unicode::DisplayWidth.of(item.show) + CHARS_PER_ITEM } end - def init_contents(path) - info = FileInfo.new(path, link_info: @long) - - if info.directory? - @contents = Dir.entries(path, encoding: ColorLS.file_encoding) - - filter_hidden_contents - - @contents.map! { |e| FileInfo.new(File.join(path, e), link_info: @long) } - - filter_contents if @show - sort_contents if @sort - group_contents if @group - else - @contents = [info] - end - - init_column_lengths - - @contents - end - def filter_hidden_contents @contents -= %w[. ..] unless @all @contents.keep_if { |x| !x.start_with? '.' } unless @all || @almost_all @@ -230,42 +243,32 @@ module ColorLS mtime.colorize(@colors[:no_modifier]) end - def process_git_status_details(git_status) - @git_status = case - when !git_status then nil - when File.directory?(@input) then Git.status(@input) - else Git.status(File.dirname(@input)) - end - end - def git_info(content) - return '' unless @git_status + return '' unless (status = @git_status[content]) if content.directory? - git_dir_info(content.name) + git_dir_info(content, status) else - git_file_info(content.name) + git_file_info(status[content.name]) end end - def git_file_info(path) - unless @git_status[path] - return ' ✓ ' - .encode(Encoding.default_external, undef: :replace, replace: '=') - .colorize(@colors[:unchanged]) - end + def git_file_info(status) + return Git.colored_status_symbols(status, @colors) if status - Git.colored_status_symbols(@git_status[path], @colors) + ' ✓ ' + .encode(Encoding.default_external, undef: :replace, replace: '=') + .colorize(@colors[:unchanged]) end - def git_dir_info(path) - modes = if path == '.' - Set.new(@git_status.values).flatten + def git_dir_info(content, status) + modes = if content.path == '.' + Set.new(status.values).flatten else - @git_status[path] + status[content.name] end - if modes.empty? && Dir.empty?(File.join(@input, path)) + if modes.empty? && Dir.empty?(content.path) ' ' else Git.colored_status_symbols(modes, @colors) @@ -300,12 +303,12 @@ module ColorLS str.encode(Encoding.default_external, undef: :replace, replace: '') end - def fetch_string(path, content, key, color, increment) + def fetch_string(content, key, color, increment) @count[increment] += 1 value = increment == :folders ? @folders[key] : @files[key] logo = value.gsub(/\\u[\da-f]{4}/i) { |m| [m[-4..-1].to_i(16)].pack('U') } name = content.show - name = make_link(path, name) if @hyperlink + name = make_link(content) if @hyperlink name += content.directory? ? '/' : ' ' entry = "#{out_encode(logo)} #{out_encode(name)}" entry = entry.bright if !content.directory? && content.executable? @@ -317,7 +320,7 @@ module ColorLS padding = 0 line = +'' chunk.each_with_index do |content, i| - entry = fetch_string(@input, content, *options(content)) + entry = fetch_string(content, *options(content)) line << ' ' * padding line << ' ' << entry.encode(Encoding.default_external, undef: :replace) padding = widths[i] - Unicode::DisplayWidth.of(content.show) - CHARS_PER_ITEM @@ -354,12 +357,26 @@ module ColorLS [key, color, group] end + def tree_contents(path) + @contents = Dir.entries(path, encoding: ColorLS.file_encoding) + + filter_hidden_contents + + @contents.map! { |e| FileInfo.dir_entry(path, e, link_info: @long) } + + filter_contents if @show + sort_contents if @sort + group_contents if @group + + @contents + end + def tree_traverse(path, prespace, depth, indent) - contents = init_contents(path) + contents = tree_contents(path) contents.each do |content| icon = content == contents.last || content.directory? ? ' └──' : ' ├──' print tree_branch_preprint(prespace, indent, icon).colorize(@colors[:tree]) - print " #{fetch_string(path, content, *options(content))} \n" + print " #{fetch_string(content, *options(content))} \n" next unless content.directory? tree_traverse("#{path}/#{content}", prespace + indent, depth + 1, indent) if keep_going(depth) @@ -376,9 +393,9 @@ module ColorLS ' │ ' * (prespace/indent) + prespace_icon + '─' * indent end - def make_link(path, name) - uri = Addressable::URI.convert_path(File.absolute_path(File.join(path, name))) - "\033]8;;#{uri}\007#{name}\033]8;;\007" + def make_link(content) + uri = Addressable::URI.convert_path(File.absolute_path(content.path)) + "\033]8;;#{uri}\007#{content.name}\033]8;;\007" end end end diff --git a/lib/colorls/fileinfo.rb b/lib/colorls/fileinfo.rb index e8b9155..385d808 100644 --- a/lib/colorls/fileinfo.rb +++ b/lib/colorls/fileinfo.rb @@ -9,19 +9,24 @@ module ColorLS @@users = {} # rubocop:disable Style/ClassVars @@groups = {} # rubocop:disable Style/ClassVars - attr_reader :stats, :name - - def initialize(path, link_info: true) - @name = File.basename(path) - @stats = File.lstat(path) + attr_reader :stats, :name, :path, :parent + def initialize(name:, parent:, path: nil, link_info: true) + @name = name + @parent = parent + @path = path.nil? ? File.join(parent, name) : path + @stats = File.lstat(@path) @show_name = nil - handle_symlink(path) if link_info && @stats.symlink? + handle_symlink(@path) if link_info && @stats.symlink? end - def self.info(path) - FileInfo.new(path) + def self.info(path, link_info: true) + FileInfo.new(name: File.basename(path), parent: File.dirname(path), path: path, link_info: link_info) + end + + def self.dir_entry(dir, child, link_info: true) + FileInfo.new(name: child, parent: dir, link_info: link_info) end def show diff --git a/lib/colorls/flags.rb b/lib/colorls/flags.rb index 18f7b23..3ffed55 100644 --- a/lib/colorls/flags.rb +++ b/lib/colorls/flags.rb @@ -12,6 +12,7 @@ module ColorLS @opts = default_opts @show_report = false + @exit_status_code = 0 parse_options @@ -54,29 +55,42 @@ module ColorLS warn "WARN: #{e}, check your locale settings" end + def group_files_and_directories + infos = @args.flat_map do |arg| + FileInfo.info(arg) + rescue Errno::ENOENT + $stderr.puts "colorls: Specified path '#{arg}' doesn't exist.".colorize(:red) + @exit_status_code = 2 + [] + rescue SystemCallError => e + $stderr.puts "#{path}: #{e}".colorize(:red) + @exit_status_code = 2 + [] + end + + infos.group_by(&:directory?).values_at(true, false) + end + def process_args core = Core.new(**@opts) - exit_status_code = 0 + directories, files = group_files_and_directories - @args.sort!.each_with_index do |path, i| - unless File.exist?(path) - $stderr.puts "\n Specified path '#{path}' doesn't exist.".colorize(:red) - exit_status_code = 2 - next - end + core.ls_files(files) unless files.nil? - puts '' if i.positive? - puts "\n#{path}:" if Dir.exist?(path) && @args.size > 1 + directories&.sort_by! do |a| + CLocale.strxfrm(a.name) + end&.each do |dir| + puts "\n#{dir.show}:" if @args.size > 1 - core.ls(path) + core.ls_dir(dir) rescue SystemCallError => e - $stderr.puts "#{path}: #{e}".colorize(:red) + $stderr.puts "#{dir}: #{e}".colorize(:red) end core.display_report if @show_report - exit_status_code + @exit_status_code end def default_opts diff --git a/lib/colorls/git.rb b/lib/colorls/git.rb index bf87841..a2cb0b8 100644 --- a/lib/colorls/git.rb +++ b/lib/colorls/git.rb @@ -10,18 +10,23 @@ module ColorLS return unless success - prefix = Pathname.new(prefix) + prefix_path = Pathname.new(prefix) git_status = Hash.new { |hash, key| hash[key] = Set.new } git_subdir_status(repo_path) do |mode, file| - path = Pathname.new(file).relative_path_from(prefix) - - git_status[path.descend.first.cleanpath.to_s].add(mode) + if file == prefix + git_status.default = Set[mode].freeze + else + path = Pathname.new(file).relative_path_from(prefix_path) + git_status[path.descend.first.cleanpath.to_s].add(mode) + end end + warn "git status failed in #{repo_path}" unless $CHILD_STATUS.success? - git_status + git_status.default = Set.new.freeze if git_status.default.nil? + git_status.freeze end def self.colored_status_symbols(modes, colors) diff --git a/spec/color_ls/core_spec.rb b/spec/color_ls/core_spec.rb index 435a7cc..13d64f8 100644 --- a/spec/color_ls/core_spec.rb +++ b/spec/color_ls/core_spec.rb @@ -17,6 +17,7 @@ RSpec.describe ColorLS::Core do directory?: true, owner: 'user', name: imagenes, + path: '.', show: imagenes, nlink: 1, size: 128, @@ -58,10 +59,9 @@ RSpec.describe ColorLS::Core do allow(::Dir).to receive(:entries).and_return([camera]) - allow(ColorLS::FileInfo).to receive(:new).and_return(dir_info) - allow(ColorLS::FileInfo).to receive(:new).with(File.join(imagenes, camera), link_info: false) { file_info } + allow(ColorLS::FileInfo).to receive(:new).and_return(file_info) - expect { subject.ls('Imágenes') }.to output(/mara/).to_stdout + expect { subject.ls_dir(dir_info) }.to output(/mara/).to_stdout end end end diff --git a/spec/color_ls/flags_spec.rb b/spec/color_ls/flags_spec.rb index 92e4b7c..f73923d 100644 --- a/spec/color_ls/flags_spec.rb +++ b/spec/color_ls/flags_spec.rb @@ -94,7 +94,12 @@ RSpec.describe ColorLS::Flags do executable?: false ) - allow(ColorLS::FileInfo).to receive(:new).with("#{FIXTURES}/a.txt", link_info: true) { file_info } + allow(ColorLS::FileInfo).to receive(:new).with( + path: File.join(FIXTURES, 'a.txt'), + parent: FIXTURES, + name: 'a.txt', + link_info: true + ) { file_info } expect { subject }.to output(/r-Sr-Sr-T .* a.txt/mx).to_stdout end @@ -123,7 +128,12 @@ RSpec.describe ColorLS::Flags do executable?: false ) - allow(ColorLS::FileInfo).to receive(:new).with("#{FIXTURES}/a.txt", link_info: true) { file_info } + allow(ColorLS::FileInfo).to receive(:new).with( + path: File.join(FIXTURES, 'a.txt'), + parent: FIXTURES, + name: 'a.txt', + link_info: true + ) { file_info } expect { subject }.to output(/\S+\s+ 5 .* a.txt/mx).to_stdout end @@ -354,7 +364,7 @@ RSpec.describe ColorLS::Flags do let(:args) { ['not_exist_file'] } it 'exits with status code 2' do # rubocop:todo RSpec/MultipleExpectations - expect { subject }.to output(/ Specified path 'not_exist_file' doesn't exist./).to_stderr + expect { subject }.to output(/colorls: Specified path 'not_exist_file' doesn't exist./).to_stderr expect(subject).to eq 2 end end @@ -386,7 +396,12 @@ RSpec.describe ColorLS::Flags do executable?: false ) - allow(ColorLS::FileInfo).to receive(:new).with("#{FIXTURES}/a.txt", link_info: true) { file_info } + allow(ColorLS::FileInfo).to receive(:new).with( + path: File.join(FIXTURES, 'a.txt'), + parent: FIXTURES, + name: 'a.txt', + link_info: true + ) { file_info } end it 'lists without group info' do @@ -425,7 +440,12 @@ RSpec.describe ColorLS::Flags do executable?: false ) - allow(ColorLS::FileInfo).to receive(:new).with("#{FIXTURES}/a.txt", link_info: true) { file_info } + allow(ColorLS::FileInfo).to receive(:new).with( + path: File.join(FIXTURES, 'a.txt'), + parent: FIXTURES, + name: 'a.txt', + link_info: true + ) { file_info } end it 'lists with group info' do @@ -464,7 +484,12 @@ RSpec.describe ColorLS::Flags do executable?: false ) - allow(ColorLS::FileInfo).to receive(:new).with("#{FIXTURES}/a.txt", link_info: true) { file_info } + allow(ColorLS::FileInfo).to receive(:new).with( + path: File.join(FIXTURES, 'a.txt'), + parent: FIXTURES, + name: 'a.txt', + link_info: true + ) { file_info } end it 'lists without group info' do @@ -503,7 +528,12 @@ RSpec.describe ColorLS::Flags do executable?: false ) - allow(ColorLS::FileInfo).to receive(:new).with("#{FIXTURES}/a.txt", link_info: true) { file_info } + allow(ColorLS::FileInfo).to receive(:new).with( + path: File.join(FIXTURES, 'a.txt'), + parent: FIXTURES, + name: 'a.txt', + link_info: true + ) { file_info } end it 'lists without group info' do diff --git a/spec/color_ls/git_spec.rb b/spec/color_ls/git_spec.rb index 0f6daab..317971e 100644 --- a/spec/color_ls/git_spec.rb +++ b/spec/color_ls/git_spec.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: true +# frozen_string_literal: false require 'spec_helper'