Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby에서 외부 시스템 명령을 실행하는 다양한 방법 완벽 정리

Ruby 코드 안에서 외부 명령어를 실행하고 싶은 순간이 분명히 찾아옵니다.

예를 들어 wkhtmltopdf처럼 HTML 파일을 PDF로 변환해 주는 도구를 사용해야 할 때가 그렇죠.

다행히 Ruby에는 이런 작업에 활용할 수 있는 여러 가지 메서드가 준비되어 있습니다. 다만 어떤 메서드를 선택하느냐에 따라 결과와 동작 방식이 크게 달라진다는 점을 기억해야 합니다.

지금부터 각 메서드의 특징과 사용법을 하나씩 살펴보겠습니다.

Ruby의 system 메서드

system 메서드는 외부 명령어를 실행하는 가장 간단한 방법입니다.

사용법은 다음과 같습니다:

system("ls")

system은 명령어가 실행되는 즉시 그 출력 결과를 화면에 그대로 보여줍니다. 또한 명령어가 끝날 때까지 Ruby 프로그램이 대기 상태가 된다는 점도 중요합니다.

직접 확인해 볼까요?

system("sleep 2")

이 코드를 실행하면 프로그램이 2초간 멈추는 것을 볼 수 있습니다. 물론 뒤에서 소개할 방법들을 사용하면 백그라운드에서 명령어를 실행하는 것도 가능합니다.

system 메서드는 세 가지 값 중 하나를 반환합니다:

  • 명령어가 성공하면 true
  • 명령어가 오류 코드를 반환하면 false
  • 명령어 자체를 찾지 못해 실행에 실패하면 nil

또한 전역 변수 $?를 사용하면 마지막으로 실행한 외부 명령어의 종료 상태 코드(exit status)를 확인할 수 있습니다. 이 코드를 통해 명령어가 실패한 구체적인 원인을 파악하는 데 도움을 받을 수 있습니다.

셸 확장(Shell Expansion)이란?

system으로 외부 명령어를 실행하면 "셸 확장(shell expansion)" 효과가 적용됩니다.

셸 확장이란 셸(sh/bash/zsh)이 명령어에 포함된 특수 문자(*, ?, [)를 만나면 이를 실제 파일 목록으로 치환해 버리는 동작을 말합니다.

예시를 보겠습니다:

system("echo *")

이 코드는 현재 디렉터리에 있는 모든 파일과 폴더를 출력합니다. 명령어가 실행되기 전에 *가 파일 목록으로 대체되기 때문입니다.

만약 이런 특수 문자를 있는 그대로 사용하고 싶다면, 인자를 system의 두 번째 인자로 전달하면 됩니다.

다음과 같이 말이죠:

system("echo", "*")

이제 출력 결과는 문자 그대로 *가 됩니다.

환경 변수 전달하기

외부 명령어에 특정 환경 변수를 전달하고 싶다면 첫 번째 인자로 해시(hash)를 넘기면 됩니다.

예제를 살펴보겠습니다:

system({"rubyguides" => "best"}, "ruby", "-e p ENV['rubyguides']")

# "best"

이 방식의 장점은 현재 Ruby 애플리케이션의 환경 설정에는 아무런 영향을 주지 않고, 해당 명령어에만 일시적으로 환경 변수를 적용할 수 있다는 점입니다.

%x / Kernel#` — 출력 결과 받아오기

명령어의 출력을 화면에 보여주는 대신 직접 값을 받아서 활용하고 싶다면 %x 또는 Kernel#`(백틱) 메서드를 사용하세요. 두 방법은 동일하게 동작합니다.

백틱 사용 예시:

`ls`

%x를 사용한 예시:

%x|ls|

두 예제 모두 ls 명령어의 출력 결과가 담긴 문자열을 반환합니다.

단, 스레드(thread) 안에서 실행하지 않는 한 명령어가 끝날 때까지 기다려야 한다는 점은 동일합니다.

Fork + Exec로 별도 프로세스에서 실행하기

Fork는 현재 프로세스(Ruby 앱)의 복사본을 만들고, 이후 exec를 사용해 그 복사본을 다른 프로세스로 교체하는 방식입니다.

참고: fork 메서드는 Windows에서는 사용할 수 없습니다.

이 패턴은 Unix 세계에서 매우 널리 쓰이는 방식입니다.

동작 방식은 다음과 같습니다:

fork { exec("ls") }

이 코드는 ls를 별도의 프로세스에서 실행하고 그 출력을 화면에 표시합니다.

명령어가 다른 프로세스에서 실행되기 때문에, system 메서드나 %x와 달리 Ruby 앱의 실행을 막지(blocking) 않습니다.

주의할 점이 있습니다. fork 없이 exec만 단독으로 사용하면 현재 프로세스 자체가 교체됩니다. 즉, Ruby 프로그램이 그대로 종료되어 버립니다.

IO.popen으로 양방향 통신하기

다음과 같은 요구 사항이 있다면:

  • 프로세스에 대한 더 세밀한 제어
  • 외부 프로그램과의 양방향 통신

IO.popen 메서드가 바로 필요한 해답입니다.

아래 예제에서는 popen으로 irb 프로세스를 실행한 뒤, 입력값을 전송하고 그 출력을 읽어오는 과정을 보여줍니다.

예제 코드:

r = IO.popen("irb", "r+")

r.write "puts 123 + 1\n"

3.times { puts r.gets }

r.write "exit\n"

여기서 r 변수는 IO 객체입니다. 즉, 일반 파일처럼 읽고 쓰기가 가능합니다.

popen을 사용하면 외부 명령어가 자체 프로세스에서 실행되므로, 굳이 스레드를 만들 필요도 없습니다.

표준 라이브러리에는 popen3 메서드도 제공됩니다. 일반 popen과의 차이점은 정상 출력과 에러 메시지를 별개의 IO 객체로 분리해서 다룰 수 있다는 점입니다.

마무리

지금까지 Ruby에서 외부 명령어와 상호작용하는 다양한 방법을 알아보았습니다.

하지만 항상 주의해야 합니다! 보안 문제를 일으키고 싶지 않다면, 사용자 입력값을 절대로 이런 명령어에 그대로 전달하지 마세요. 셸 인젝션(shell injection) 공격의 주요 경로가 되기 때문입니다.

새로운 내용을 배웠다면 이 글을 공유해서 더 많은 사람들이 볼 수 있게 해주세요.

읽어주셔서 감사합니다 🙂