'파일 선택기 만들기' 글을 원래 지난번에 마무리를 했었는데, FileSelector 클래스 안에서 현재 작업 디렉터리를 변경하는 부분을 아무래도 제거하는 것이 좋겠다는 생각이 들었다.
첫째, FileSelector 클래스를 사용하는 프로그램에서 현재 작업 디렉터리의 경로에 의존하는 코드가 있을 수도 있기 때문이다.
만약 그렇다면, FileSelector 객체의 select 메서드를 호출한 이후 프로그램의 동작에 문제가 생길 수도 있다.
두 번째는 현재 작업 디렉터리를 변경했던 목적이 Dir 클래스의 glob 메서드가 돌려주는 항목들의 이름에 경로명이 포함되지 않아야 한다는 단순한 이유 때문이다.
아래 그림을 보면 File 클래스의 basename 메서드를 사용하면 파일 경로를 제외하고 순수한 파일 또는 디렉터리의 이름만을 반환해 주고, File 클래스의 join 메서드를 사용하면 파일 경로와 이름을 쉽게 연결할 수 있다.
그리고 상위 경로(..)가 포함되어 있을 경우 File 클래스의 expand_path 메서드를 사용하면 상위 경로(..)를 평가하여 최종 경로를 잘 반환해 준다.
이제 수정한 FileSelector 클래스의 전체 코드를 보자.
우선 get_list 메서드에서 Dir.chdir(dir) 코드를 삭제하고, 그 대신 File 클래스의 join 메서드를 사용하여 현재 디렉터리 경로(dir) 아래에서 패턴(@filter)을 통해 항목을 조회하도록 변경하였다.
그리고 File 클래스의 basename 메서드를 사용하여, 조회한 항목들의 이름에서 경로를 제외하고 순수한 이름만을 돌려주도록 변경하였다.
print_list 메서드에는 현재 디렉터리 경로에 대한 정보를 받도록 dir 파라미터를 하나 추가하였고, 그 경로에 대한 출력도 print_list 메서드에서 하도록 했다.
그리고 해당 항목의 타입(D/F)을 알기 위해서는 전체 경로가 필요한데 File 클래스의 join 메서드를 사용하여 dir의 경로와 항목의 값을 연결하여 만든다.
이렇게 만든 전체 경로는 @file_info 객체의 get_info 메서드를 호출할 때도 사용한다.
또한 코드 정리를 위해 each_with_index 메서드의 블록 안에 있던 정렬과 파일의 추가 정보를 얻는 코드 등을 별도의 메서드로 분리하였다.
select 메서드는 dir 경로를 출력하는 코드를 제거하고, 그 대신 print_list 메서드로 dir 값을 인수로 전달한다.
그리고 사용자가 선택한 항목의 전체 경로를 얻기 위해 File 클래스의 join 메서드로 dir 값과 선택한 항목의 값을 연결하고, 상위 경로(..) 처리를 위해 File 클래스의 expand_path 메서드를 사용한다.
그 밖의 initialize 메서드와 get_int 메서드는 변경된 게 없다.
class FileSelector
def initialize(options = {})
@dir = File.expand_path(options[:dir] || Dir.pwd)
@filter = options[:filter] || "*"
@only_file = options[:only_file]
@no_parent = options[:no_parent]
@file_info = options[:file_info]
end
def select
dir = @dir
loop do
files = get_list(dir)
print_list(dir, files)
no = get_int("> ", 1..files.size, true)
return if no.nil?
file = File.expand_path(File.join(dir, files[no - 1]))
return file if File.file?(file)
dir = file
end
end
private
def get_list(dir)
files = Dir.glob(File.join(dir, @filter))
files = files.select { |f| File.file?(f) } if @only_file
files = files.sort_by { |f| "#{File.directory?(f) ? 0 : 1}#{f}" }
if !@no_parent || dir != @dir
files.unshift("..") if File.dirname(dir) != dir
end
files.map { |f| File.basename(f) }
end
def print_list(dir, files)
no_width = files.size.to_s.length
f_names = files.map { |f| f.encode("cp949") }
f_width = f_names.map(&:bytesize).max
puts dir
files.each_with_index do |f, i|
f = File.join(dir, f)
no = rjust((i + 1).to_s, no_width)
type = File.file?(f) ? "F" : "D"
name = ljust(f_names[i], f_width)
puts "#{no}. [#{type}] #{name} #{file_info(f)}"
end
end
def ljust(str, width)
str.ljust(width + (str.size - str.bytesize))
end
def rjust(str, width)
str.rjust(width + (str.size - str.bytesize))
end
def file_info(f)
@file_info.get_info(f) if @file_info && !f.end_with?("/..")
end
def get_int(prompt, int_range, return_if_empty = false)
loop do
print prompt
input = STDIN.gets.chomp
return if return_if_empty && input.empty?
if input =~ /^-?\d+$/ && int_range.include?(input.to_i)
return input.to_i
else
puts "#{int_range.min} ~ #{int_range.max} 사이의 정수를 입력해 주세요"
end
end
end
end
아래 그림처럼 테스트를 해보면 변경 전과 동일하게 잘 동작하는 것을 볼 수 있다.
See you again~~