오늘은 프로그램에서 특정 파일을 선택해야 할 때 사용할 수 있는 파일 선택기를 만들어 보려고 한다.
우리가 사용하는 많은 응용 프로그램들에서 프로그램 안으로 로드할 파일을 선택하게 하거나 아니면 현재 작성한 내용을 저장할 파일 위치를 선택하게 할 때 파일 선택기를 많이 사용한다.
이 파일 선택기 프로그램을 만들어 보면서 루비에서 파일 탐색은 어떻게 하는지 살펴보도록 하자.
프로그램을 작성하기에 앞서 현재 작업 디렉터리의 경로를 확인하는 방법, 특정 경로 상의 파일과 디렉터리 목록을 가져오는 방법, 현재의 작업 디렉터리를 다른 디렉터리로 변경하는 방법, 그리고 상대 경로를 절대 경로로 변경하는 방법 등을 알아보자.
작업 디렉터리는 프로그램이 현재 실행되는 경로를 의미한다. 이 글에서 '디렉터리'와 '폴더'를 혼용해서 사용하며, 특정 디렉터리에 포함된 '파일과 디렉터리'를 '항목'으로 표현하기도 함을 미리 알려둔다.
테스트를 위해 D:/blog/ruby/file_selector 폴더 아래 test 폴더를 하나 만들었고 그 아래 다음 그림처럼 폴더와 파일들을 생성하였다.
이제 D:/blog/ruby/file_selector/test 경로에서 irb를 실행한 후 아래 그림처럼 하나씩 입력해 보면서 결과를 확인해 보자.
제일 먼저, Dir 클래스의 클래스 메서드인 pwd('print working directory'의 약자)는 현재 작업 디렉터리의 경로를 문자열로 돌려준다.
D:/blog/ruby/file_selector/test 경로에서 irb를 실행했으니 당연히 Dir.pwd는 결과로 "D:/blog/ruby/file_selector/test" 을 반환한다.
그리고 Dir 클래스의 glob 메서드는 인수로 건낸 특정 경로 상에 위치한 모든 파일과 디렉터리의 이름을 배열로 돌려주는데, Dir.glob("*")은 현재 작업 디렉터리인 test 폴더 바로 아래에 있는 모든 파일과 디렉터리의 이름을 반환한다.
Dir.glob("a/*")은 현재 작업 디렉터리인 test 폴더 바로 아래 폴더인 a폴더 바로 아래의 모든 파일과 디렉터리들의 이름을 돌려준다.
만약, 바로 아래 위치한 파일이나 디렉터리들뿐만 아니라 그 아래 디렉터리들의 아래에 위치한 파일과 디렉터리들까지도 계속 따라 들어가면서 모든 항목들을 가져오려면 glob 메서드에 전달하는 경로에 "**" 을 포함 시키면 된다.
따라서, Dir.glob("a/**/*")을 실행하면 a 폴더 아래에 있는 aa 폴더 안의 파일과 디렉터리들까지도 결과 배열에 포함되는 걸 볼 수 있다.
그리고 Dir.glob("**/*")은 현재 작업 디렉터리인 test 폴더 안의 모든 파일과 디렉터리들의 목록을 반환한다.
Dir.glob("**/*")와 Dir.glob("*/*")을 헛갈리지 않길 바란다. Dir.glob("*/*")은 현재 작업 디렉터리인 test 폴더 바로 아래에 있는 폴더인 a와 b의 바로 아래에 있는 항목들만 반환해 주는 것이다.
마지막 두 예제는 이름으로 대상 항목을 필터링하고 있는데, Dir.glob("**/*.docx")은 현재 작업 디렉터리인 test 폴더 안에서 이름이 '.docx'으로 끝나는 모든 항목을 돌려주고,
Dir.glob("**/a*.docx")은 현재 작업 디렉터리인 test 폴더 안에서 이름이 'a'로 시작하고 '.docx'으로 끝나는 모든 항목을 돌려준다.
결과를 보면 알겠지만 MS 워드 파일의 확장자인 '.docx'으로 필터링을 해도 glob 메서드가 반환하는 목록에는 해당 이름을 갖는 디렉터리도 포함될 수 있다.
해당 항목이 파일인지 디렉터리인지는 File 클래스의 file? 메서드 또는 directory? 메서드를 사용하면 알 수 있다.
Dir 클래스의 pwd 메서드가 현재 작업 디렉터리의 경로를 알려준다는 것을 알았다. 그렇다면 현재 작업 디렉터리를 다른 디렉터리로 변경하고 싶다면 어떻게 해야 할까?
다시 D:/blog/ruby/file_selector/test 경로에서 irb를 실행하자.
아래 그림처럼 pwd 메서드를 호출하면 irb를 실행한 현재 경로를 돌려주는데, Dir.chdir("a")를 실행한 후 다시 pwd 메서드의 호출 결과를 확인해 보면 현재 작업 디렉터리의 경로가 D:/blog/ruby/file_selector/test/a 으로 변경된 것을 볼 수 있다.
실제 그런지 Dir.glob("*")을 실행해 보면 정말 a폴더 아래에 있는 항목들의 이름을 돌려준다.
여기서 상위 디렉터리(부모 디렉터리)를 의미하는 ".." 을 인수로 주고 chdir 메서드를 호출하면 다시 원래의 작업 디렉터리(test 폴더)로 이동하게 될 것인데, 그 아래 pwd 메서드의 호출 결과를 보면 작업 디렉터리가 다시 변경된 걸 알 수 있다.
그리고 특정 파일이나 디렉터리의 상위 디렉터리 경로를 알고 싶으면 File 클래스의 dirname 메서드를 사용하면 된다.
지금까지 본 예제에서 glob 메서드나 chdir 메서드에 인수로 건넨 경로는 모두 현재 작업 디렉터리를 기준으로 한 상대 경로이다.
상대 경로를 절대 경로로 변경하려면 File 클래스의 expand_path 메서드를 사용하면 된다.
File.expand_path(".")는 현재 디렉터리(".")의 절대 경로를 반환하고, File.expand_path("a")는 현재 작업 디렉터리 아래에 있는 폴더 a의 절대 결로를 반환하며, 마지막 File.expand_path("a.txt")는 현재 작업 디렉터리 아래에 있는 a.txt 파일의 절대 경로를 반환한다.
이제 현재 작업 디렉터리의 의미와 파일 시스템의 디렉터리를 탐색하는 기본적인 방법들을 알아봤으니, 실제 파일 선택기 프로그램을 작성해 보자.
아래 코드처럼 디렉터리 경로 하나를 주고 객체를 생성한 후 list 메서드를 호출하면 해당 디렉터리 안의 항목들을 출력해 주는 간단한 코드부터 시작하자.
class FileSelector
def initialize(dir = Dir.pwd)
@dir = File.expand_path(dir)
end
def list
puts Dir.glob("#{@dir}/*")
end
end
작성한 FileSelector 클래스의 코드가 잘 동작하는지 확인하기 위해 D:/blog/ruby/file_selector 폴더 아래 file_selector.rb 파일에 코드를 저장하고 해당 폴더에서 irb를 실행한 후 아래 그램처럼 테스트를 진행해 보자.
test폴더 안에 있는 항목들이 잘 표시되는 것을 볼 수 있다.
다음 단계로 항목 번호와 해당 항목이 파일인지 디렉터리인지를 함께 표시해 주자.
class FileSelector
def initialize(dir = Dir.pwd)
@dir = File.expand_path(dir)
end
def list
files = Dir.glob("#{@dir}/*")
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
다시 테스트를 해보면, 항목 번호와 해당 항목이 파일인지 디렉터리인지가 잘 표시되는 걸 볼 수 있다.
irb에서 메서드를 호출하면 결괏값을 바로 화면에 표시해주기 때문에 list 메서드 안에서 직접 출력하는 내용들 다음에 항목들을 담은 배열 정보가 이어서 표시된다.
list 안에서 마지막으로 호출한 each_with_index 메서드가 대상 객체(files)를 결괏값으로 반환하기 때문이다.
이게 출력되는 게 보기 싫다면 list 메서드 마지막 라인에 nil을 적어 주거나, 아니면 irb 에서 코드를 작성할 때 fs.list; nil 이렇게 작성해 줘도 된다.
세미콜론(;)은 fs.list 와 nil 이라는 코드를 서로 다른 행으로 구분지어 주고, irb는 마지막 행(nil)의 실행 결괏값을 화면에 표시해 준다.
그런데, 항목이 표시될 때 현재 디렉터리의 이름은 빼고 순수하게 항목명만 표시하고 싶으면 어떻게 해야 할까? 쉬운 방법은 chdir 메서드를 사용하여 현재 작업 디렉터리의 경로를 변경하면 된다.
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
아래 그림처럼 동일한 코드로 다시 테스트를 해보면 원하는 대로 파일 또는 디렉터리 이름만 잘 출력되는 것을 볼 수 있다.
그런데, list 메서드 안에서 현재 작업 디렉터리를 변경하게 되면 아래 그림처럼 또 다른 FileSelector 객체를 생성해서 list 메서드를 호출할 때 해당 경로를 찾지 못해 예외가 발생 할 수 있다.
irb를 D:/blog/ruby/file_selector 경로에서 실행했기 때문에 현재 작업 디렉터리를 변경하지 않았다면 상대 경로인 "test"와 "test/a"는 올바른 경로가 맞지만, 작업 디렉터리를 D:/blog/ruby/file_selector/test 로 변경했다면
"test"와 "test/a"는 test 폴더 안의 test 폴더와 test/a 폴더를 나타내게 되므로 문제가 될 수 있다.
따라서, FileSelector 객체를 여러 개 생성해서 사용해야 한다면 절대 경로를 사용하는 것이 보다 안전한 방법이다.
아래 테스트 그림을 보면 FileSelector 객체 두 개를 생성하면서 둘 다 절대 경로를 인수로 주었고, 두 객체 모두에서 list 메서드가 예외 발생 없이 잘 실행되는 걸 볼 수 있다.
그리고, 현재 작업 디렉터리의 경로가 젤 처음에는 D:/blog/ruby/file_selector 이었는데 fs1.list 를 실행하면서
D:/blog/ruby/file_selector/test 로 변경되었고, 다시 fs2.list 를 실행하면서 D:/blog/ruby/file_selector/test/a 로 변경된 것을 볼 수 있다.
하지만, FileSelector 클래스를 사용하게 될 어떤 프로그램의 소스안에 현재 작업 디렉터리의 경로에 의존하는 코드가 존재할 수도 있기 때문에 되도록이면 FileSelector 클래스 안에서 작업 디렉터리를 변경하지 않도록 코드를 수정하는 것이 더 좋을 것 같다.
항목 중 디렉터리가 포함되어 있다면 그 디렉터리 안의 항목들도 확인할 수 있어야 하고 최종적으로는 원하는 파일을 선택할 수 있어야 하는데, 이 부분은 다음 글에서 만들어 보도록 하자.
See you again~~