오늘은 지난 글에 이어서 항목 중 디렉터리가 포함되어 있을 경우 그 디렉터리 안의 항목들도 확인할 수 있도록 하위 디렉터리를 탐색하는 기능과 실제 사용자가 원하는 파일을 선택할 수 있는 기능을 함께 만들어 보자.
아래 코드는 지난 글에서 만든 최종 코드인데 여기서부터 다시 시작해 보자.
class FileSelector
def initialize(dir = Dir.pwd)
@dir = File.expand_path(dir)
end
def list
Dir.chdir(@dir)
files = Dir.glob("*")
width = files.size.to_s.length
files.each_with_index do |f, i|
no = (i + 1).to_s.rjust(width)
type = File.file?(f) ? "F" : "D"
puts "#{no}. [#{type}] #{f}"
end
end
end
지금까지 사용자 입력 처리를 여러 번 다뤘었는데, 특히 '스피드 연산 게임 만들기' 글에서는 사용자에게 메뉴를 보여주고 선택을 받아 처리하는 기능을 다른 프로그램에서도 재사용할 수 있도록 별도의 클래스로 분리하여 만들었었다.
그리고 그러한 메뉴 처리 기능을 만들 때 메뉴의 구조를 디렉터리 구조에 비유해서 설명을 했었는데, 메뉴와 디렉터리 모두 항목을 여러 개 가질 수 있고, 그 항목이 또다른 항목을 포함할 수 있는 구조이기 때문이다.
어떻게 생각하면 메뉴 처리 기능을 좀 더 범용적으로 사용할 수 있게 만들었다면, 어쩌면 FileSelector 클래스에서 해당 기능을 가져다 쓸 수도 있었을 것 같다.
하지만 그랬다면 메뉴 처리 기능의 코드가 지금보다는 더 복잡해졌을 것이고, 스피드 연산 게임이나 도서 관리 프로그램 등의 코드에서 메뉴 처리 기능을 사용할 때 좀 덜 직관적이게 되었을 수도 있다.
프로그램을 작성하다 보면 이렇게 모든 곳에서 사용할 수 있도록 만들고 싶은 욕심이 생기기도 하는데, 그러다 보면 코드는 더 복잡해지고 사용하기는 어려우며, 필요할 때 수정하기도 쉽지 않은 코드가 되어 버릴 수도 있다.
오히려 특정 기능만을 충실히 수행하도록 가볍게 만드는 것이, 그것을 가져다 그대로 쓰거나 기능을 확장하기가 더 쉬울 수 있다.
이제 아래 코드처럼 사용자에게 입력을 받아 처리하는 기능을 FileSelector 클래스 안에 추가해 보자.
select 메서드의 코드를 보면 기본적으로 loop 메서드를 사용하여 블록의 실행을 무한 반복하도록 했다.
블록 안에서는 기존에 list 메서드에 있던 코드를 가져와 @dir이 나타내는 경로로 현재 작업 디렉터리를 변경한 후 해당 디렉터리 안의 모든 항목을 담은 배열을 files 변수에 넣는다.
그리고 해당 항목들을 사용자에게 보여주기 위해 files 객체를 인수로 주고 list 메서드를 호출한다.
이어서 새로 만든 get_int 메서드를 통해 사용자에게 입력을 받는데 사용자가 별다른 입력 없이 그냥 엔터를 누르면 취소로 간주하고 select 메서드를 바로 종료하도록 했다.
그런데 사용자가 만약 파일을 선택했다면 해당 파일의 전체 경로를 반환하면서 select 메서드는 종료되고, 디렉터리를 선택했다면 해당 디렉터리 안의 항목들을 보여주기 위해 @dir의 값을 선택한 디렉터리의 경로로 수정하고 다시 블록을 실행한다.
class FileSelector
def initialize(dir = Dir.pwd)
@dir = File.expand_path(dir)
end
def select
loop do
Dir.chdir(@dir)
files = Dir.glob("*")
list(files)
no = get_int("> ", 1..files.size, true)
return if no.nil?
file = File.expand_path(files[no - 1])
return file if File.file?(file)
@dir = file
end
end
private
def list(files)
width = files.size.to_s.length
files.each_with_index do |f, i|
no = (i + 1).to_s.rjust(width)
type = File.file?(f) ? "F" : "D"
puts "#{no}. [#{type}] #{f}"
end
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
위의 코드를 다시 보면 중간에 private이 보이는데, 이렇게 하면 이후에 정의하는 메서드들은 모두 private으로 지정된다.
이제 D:/blog/ruby/file_selector 경로에서 irb를 실행하여 지금까지 만든 FileSelector 클래스를 테스트해 보자.
아래 그림을 보면 디렉터리를 선택했을 때 해당 디렉터리 안의 항목들을 잘 표시해 주고, 파일을 선택하면 해당 파일의 전체 경로를 반환하고 종료되는 것이 보인다.
그리고 select 메서드를 다시 실행하면 이전 select 메서드의 실행에서 사용자가 선택한 디렉터리 경로가 @dir에 할당되어 있기 때문에 그 디렉터리 경로부터 시작됨을 알 수 있다.
그런데 이미 뭔가 이상하다는 걸 눈치챘을텐데, 다시 상위 디렉터리로 돌아갈 방법이 없다.--;
그러면 사용자에게 보여주는 항목에 상위 경로(..)를 추가하여 상위 디렉터리로 돌아갈 수 있도록 코드를 수정해 보자.
아래 코드를 보면 기존에 select 메서드에서 하던 작업 중 일부를 get_list라는 별도의 메서드를 만들어 분리하였고, 그 메서드 안에서 상위 경로도 추가하고 있다.
File 클래스의 dirname 메서드는 상위 디렉터리의 경로를 돌려주는데, 결괏값이 인수의 값과 다르다면 아직 상위로 올라갈 경로가 있다는 뜻이므로 상위 경로(..)를 추가해 주면 된다.
그리고 기존의 list 메서드는 'print_list'라는 좀 더 명확한 이름으로 변경하였다.
class FileSelector
def initialize(dir = Dir.pwd)
@dir = File.expand_path(dir)
end
def select
loop do
files = get_list
print_list(files)
no = get_int("> ", 1..files.size, true)
return if no.nil?
file = File.expand_path(files[no - 1])
return file if File.file?(file)
@dir = file
end
end
private
def get_list
Dir.chdir(@dir)
files = Dir.glob("*")
files.unshift("..") if File.dirname(@dir) != @dir
files
end
def print_list(files)
width = files.size.to_s.length
files.each_with_index do |f, i|
no = (i + 1).to_s.rjust(width)
type = File.file?(f) ? "F" : "D"
puts "#{no}. [#{type}] #{f}"
end
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
아래 그림을 보면 상위 경로(..)가 잘 표시되고 하위 디렉터리로 이동했다가 다시 상위 디렉터리로 잘 돌아가는 것도 볼 수 있다.
몇 가지 더 추가해 볼 만한 기능들이 남아 있다.
그 중 하나는 화면에 디렉터리의 항목들을 표시해 줄 때 디렉터리의 경로를 젤 위에 함께 보여주는 것이다. 테스트할 때 디렉터리를 선택하며 몇 번을 왔다갔다 해보니 현재 디렉터리의 경로가 표시된다면 좀 더 편할 것 같다는 생각이 들었다.
그리고 또 하나는 화면에 표시될 때 정렬이 되어 디렉터리가 먼저 표시되고 파일들이 표시되면 좋을 것 같다.
마지막은 FileSelector 객체 생성 시 세 가지 옵션을 줄 수 있도록 하는 것이다.
이름 필터링에 대한 옵션, 파일 항목만 보여주는 옵션, 그리고 객체 생성 시 지정한 경로의 상위 디렉터리로는 이동하지 못하게 하는 옵션이다.
다음 글에서 이 세 가지 기능을 함께 만들어 보자.
See you again~~