커맨드 라인을 제대로 마스터하려면 수십 개, 어쩌면 수백 개에 달하는 작은 유틸리티 프로그램들을 익혀야 합니다. 각 도구는 조금씩 다르게 동작하기 때문에 상당히 부담스러울 수 있습니다.
다행히도 Ruby 같은 범용 프로그래밍 언어를 사용하면 이런 단일 목적의 도구들을 상당수 대체할 수 있습니다. 이미 알고 있는 Ruby 지식을 활용해 커맨드 라인 실력을 한 단계 끌어올릴 수 있는 것이죠.
이 글에서는 Ruby를 커맨드 라인 만능 도구로 활용하는 기본기를 살펴봅니다. 현란한 원라이너(한 줄 코드)를 나열하기보다는 실제 작동 방식을 이해하는 데 집중하겠습니다. 이를 통해 여러분 스스로 문제를 해결할 때 이 기법들을 적용할 수 있기를 바랍니다.
커맨드 라인에서 Ruby 사용하기
커맨드 라인에서 다음과 같이 Ruby 프로그램을 실행할 수 있다는 것은 잘 아실 겁니다.
$ ruby myprogram.rb
그런데 코드를 파이프로 넘겨서 Ruby가 실행하게 할 수 있다는 것도 알고 계셨나요?
$ echo "puts 2+2" | ruby
4
더욱 유용한 기능은 코드를 명령줄 인자로 직접 전달할 수 있다는 점입니다. 오늘 우리가 집중적으로 다룰 내용이 바로 이것입니다.
$ ruby -e 'puts 2+2'
4
초보자분들께 드리는 말씀
위 예제들이 다소 헷갈리시나요? 파이프(pipe)와 리다이렉션(redirection)에 아직 익숙하지 않아서일 수 있습니다. 관련 입문 글을 먼저 참고해 보시길 권합니다. 아래에서 파이프를 자주 사용하게 됩니다.
입력 다루기
대부분의 커맨드 라인 도구는 데이터를 입력받고, 처리한 다음, 결과를 출력하는 구조로 되어 있습니다.
입력을 받는 방법에는 두 가지 훌륭한 선택지가 있습니다. 바로 명령줄 인자(command-line arguments)와 STDIN입니다. 하나씩 살펴보겠습니다.
명령줄 인자(Command-line arguments)
스크립트에는 원하는 만큼 명령줄 인자를 전달할 수 있습니다. 명령어 맨 뒤에 붙여주기만 하면 됩니다.
$ ruby -e '<여기에 코드 작성>' arg1 arg2 arg3 등등
이 인자들은 ARGV 배열에 저장됩니다. 아래 예제에서는 배열 전체를 출력해서 어떤 내용이 담겨 있는지 보여드립니다.
$ ruby -e 'puts ARGV.inspect' apples bananas pears oranges
["apples", "bananas", "pears", "oranges"]
주목할 점은 이것이 Ruby가 원래 그렇게 동작하는 방식이라는 것입니다. 커맨드 라인을 사용한다고 해서 특별한 마법이 일어나는 게 아닙니다. ARGV에 대한 자세한 내용은 별도의 참고 자료를 확인해 보세요.
심플한 예제
잠깐 제가 엄청난 자취증 환자라고 상상해 봅시다. 저에게 가장 중요한 일은 제 이름이 웹에서 몇 번 언급되었는지 아는 것입니다. 지금까지 배운 기법을 활용하면 어떤 웹페이지든 이를 계산해 주는 원라이너를 손쉽게 작성할 수 있습니다.
$ ruby -e "require 'open-uri'; puts open(ARGV.first).read.scan(/starr/i).count" <URL 입력>
STDIN
명령줄 인자는 편리하지만 짧은 값에만 적합합니다. 예를 들어 소설 『모비 딕』의 전체 본문을 인자로 넘기고 싶지는 않겠죠. 그럴 때는 STDIN을 사용해야 합니다.
STDIN이 낯설게 느껴지더라도 겁먹지 마세요. 여기서의 목적상 STDIN은 읽기 모드로 열린 파일과 완전히 똑같이 동작합니다.
무슨 뜻인지 설명해 드리겠습니다. 아래 예제에서는 텍스트를 Ruby로 파이핑합니다. Ruby 스크립트는 STDIN에서 이를 읽어 화면에 출력합니다.
echo "bananas!" | ruby -e "puts STDIN.read"
bananas!
cat을 사용하면 더 많은 데이터도 쉽게 입력할 수 있습니다. 아래 예제는 파일 객체에서 사용할 수 있는 first 메서드를 활용해 텍스트의 첫 몇 줄을 가져옵니다.
cat moby.txt | ruby -e "puts STDIN.first(3)"
Call me Ishmael. Some years ago--never mind how long precisely--having
little or no money in my purse, and nothing particular to interest me on
shore, I thought I would sail about a little and see the watery part of
심플한 예제
STDIN을 다루는 방법을 알았으니 앞선 심플한 예제를 다시 작성해 봅시다. Ruby가 직접 웹페이지를 가져오는 대신 curl을 사용하고, Ruby는 패턴 매칭 용도로만 활용하는 것입니다.
curl <MY URL> | ruby -e "puts STDIN.read.scan(/starr/i).size"
편리한 문법 설탕(syntactic sugar)과 함께하는 STDIN!
STDIN을 다룰 때는 입력의 각 줄을 순회해야 하는 경우가 매우 흔합니다. 디렉토리에 있는 모든 파일의 확장자를 추출하고 싶다고 가정해 봅시다. 일반적인 STDIN 루프를 사용하면 다음과 같이 작성할 수 있습니다.
ls | ruby -e 'STDIN.each_line { |l| puts l.split(".").last }'
rb
rb
csv
STDIN 루프가 워낙 흔한 패턴이기 때문에 Ruby는 축약형 문법을 제공합니다. 스크립트를 -n 플래그와 함께 실행하면 Ruby가 자동으로 STDIN의 각 줄을 순회합니다. 현재 처리 중인 줄은 전역 변수 $_에 담기게 됩니다.
따라서 위 예제는 다음과 같이 다시 쓸 수 있습니다.
ls | ruby -n -e 'puts $_.split(".").last'
축약형을 사용할지 여부는 여러분의 선택에 달려 있습니다. 코드량이 확실히 줄어드는 장점이 있지만, -n이나 $_처럼 임의적이고 외워야 할 요소가 늘어난다는 단점도 있습니다.
출력 다루기
이런 상황에서는 보통 출력을 STDOUT에 작성하게 됩니다. STDOUT을 사용하면 출력을 다른 프로그램으로 파이핑하거나 필요에 따라 디스크로 리다이렉션할 수 있어 가장 큰 유연성을 얻을 수 있습니다.
다행히도 여러분이 이미 잘 알고 있을 print 계열 메서드를 그대로 사용할 수 있습니다. 다만 한 가지 주의할 점은 puts는 개행 문자(newline)를 추가한다는 것입니다. 원하는 동작일 수도, 아닐 수도 있습니다.
puts "hello world" # STDOUT에 "hello world\n" 전송
print "hello world" # 개행 문자 추가 안 함
전체 종합하기
Honeybadger는 미국 워싱턴주에 기반을 둔 회사입니다. 그래서 워싱턴주에 거주하는 결제 고객에게는 판매세(sales tax)를 부과해야 하는 의무가 있습니다.
상당히 단순화했지만, 기본적으로 해당 연도의 모든 거래 내역이 담긴 CSV 파일이 하나 있습니다. 대략 다음과 같은 형태입니다.
1/1/2015,100.00,WA
1/1/2015,50.00,NY
워싱턴주 거래의 빠른 합계를 구하려면 다음과 같은 원라이너를 사용할 수 있습니다.
$ cat cc.csv | ruby -e 'puts STDIN.inject(0) { |sum, x| date, amount, state = x.split(","); state.strip == "WA" ? sum + amount.to_f : sum }'
이 방식은 CSV 파일을 파싱하는 상당히 엉성한 방법이라는 점을 유념하세요. 실제 운영 환경에 이런 코드를 배포하지는 않을 것입니다. 하지만 일회성 문제를 해결하는 작은 프로그램을 작성하는 재미 중 하나는 온갖 엣지 케이스를 신경 쓰지 않아도 된다는 점입니다. 여기서는 그렇게 진행하겠습니다.