input:output: 블록

챕터 2에서 보았듯이, Snakemake는 입력을 출력에 연결하여 규칙을 자동으로 “체인(chain)”화합니다. 즉, Snakemake는 여러 단계가 걸리더라도 원하는 출력을 생성하기 위해 무엇을 실행할지 스스로 파악합니다.

챕터 3에서는 Snakemake가 input:output: 블록의 내용에 따라 셸 명령의 {input}{output} 부분을 채우는 것을 확인했습니다. 챕터 6에서 보았듯이 와일드카드를 사용하여 규칙을 일반화할 때, 와일드카드 값이 {input}{output} 값으로 적절하게 치환되므로 이 기능은 더욱 유용해집니다.

입력 및 출력 블록은 Snakemake 워크플로의 핵심 구성 요소입니다. 이번 장에서는 입력 및 출력 블록의 사용법을 좀 더 포괄적으로 살펴보겠습니다.

입력 및 출력 제공하기

앞서 보았듯이, Snakemake는 쉼표로 구분된 리스트를 통해 여러 입력 및 출력 값을 기꺼이 받아들여 셸 블록의 문자열로 치환합니다.

rule example:
   input:
       "file1.txt",
       "file2.txt",
   output:
       "output file1.txt",
       "output file2.txt",
   shell: """
       echo {input:q}
       echo {output:q}
       touch {output:q}
   """

이 값들이 {input}{output}을 통해 셸 명령어로 치환될 때, 공백으로 구분된 정렬된 리스트로 변환됩니다. 예를 들어, 위의 셸 명령은 먼저 file1.txt file2.txt를 출력하고 그 다음 output file1.txt output file2.txt를 출력한 후, touch를 사용하여 빈 출력 파일들을 생성합니다.

이 예제에서는 또한 :q를 사용하여 Snakemake에게 셸 명령어용 파일 이름을 따옴표로 감싸도록 요청하고 있습니다. 이는 파일 이름에 공백이나 작은따옴표, 큰따옴표 또는 기타 특수 문자가 포함된 경우 Python의 shlex.quote 함수를 사용하여 적절하게 이스케이프됨을 의미합니다. 예를 들어, 여기서 두 출력 파일 모두 공백을 포함하고 있으므로, 그냥 touch {output}을 사용하면 output file1.txtoutput file2.txt라는 정확한 두 파일이 아닌 output, file1.txt, file2.txt라는 세 개의 파일을 생성하게 됩니다.

셸 블록에서 실행되는 모든 것에는 항상 {...:q}를 사용한 파일 이름 따옴표 감싸기를 사용해야 합니다. 이는 해가 되지 않으며 심각한 버그를 예방할 수 있습니다!

Note쉼표를 어디에 (그리고 어디에 넣어야) 할까요?

위의 코드 예제에서 "file2.txt""output file2.txt" 뒤에 쉼표가 있는 것을 볼 수 있습니다.

rule example:
   input:
       "file1.txt",
       "file2.txt",
   output:
       "output file1.txt",
       "output file2.txt",
   shell: """
       echo {input:q}
       echo {output:q}
       touch {output:q}
   """

이 쉼표들이 꼭 필요한가요? 아니오. 위의 코드는 입력과 출력의 마지막 줄 뒤에 쉼표가 없는 아래 코드와 동일합니다.

rule example:
   input:
       "file1.txt",
       "file2.txt"
   output:
       "output file1.txt",
       "output file2.txt"
   shell: """
       echo {input:q}
       echo {output:q}
       touch {output:q}
   """

일반적인 규칙은 이렇습니다. 리스트의 항목들을 구분하기 위해 내부 쉼표가 필요합니다. 그렇지 않으면 문자열들이 서로 이어지기 때문입니다. 즉, "file1.txt" "file2.txt"는 사이에 줄바꿈이 있더라도 "file1.txtfile2.txt"가 됩니다! 하지만 마지막 파일 이름 뒤에 붙는 쉼표는 선택 사항(무시됨)입니다.

왜 그럴까요? 이것들은 Python 튜플(tuples)이며, 원한다면 마지막에 쉼표를 추가할 수 있습니다. a, b, c,a, b, c와 동일합니다. 이 문법에 대한 자세한 내용은 여기에서 읽어보실 수 있습니다.

그런데 왜 마지막 쉼표를 추가할까요? 저는 마지막 쉼표를 사용하는 것을 추천합니다. 새로운 입력이나 출력을 추가할 때 쉼표 넣는 것을 잊어버리기 쉽기 때문인데, 저도 자주 하는 실수입니다! 이것은 흔한 실수를 방지하기 위해 선택적 문법 규칙을 사용하는 방어적 프로그래밍(defensive programming)의 작고 간단하지만 유용한 예입니다.

입력 및 출력은 정렬된 리스트입니다

대괄호를 사용하여 리스트처럼 인덱싱함으로써(0번 위치부터 시작) 개별 입력 및 출력 항목을 참조할 수도 있습니다.

rule example:
   ...
   shell: """
       echo first input is {input[0]:q}
       echo second input is {input[1]:q}
       echo first output is {output[0]:q}
       echo second output is {output[1]:q}
       touch {output}
   """

하지만 이 방법은 권장하지 않습니다. 부서지기 쉽기 때문입니다. 입력 및 출력의 순서를 변경하거나 새로운 입력을 추가하면, 매치되도록 인덱스를 일일이 조정해야 합니다. 리스트의 인덱스 번호와 위치에 의존하는 것은 실수가 발생하기 쉽고 나중에 Snakefile을 변경하기 어렵게 만듭니다!

입력 및 출력 파일에 키워드 사용하기

키워드(keyword) 문법을 사용하여 특정 입력 및 출력에 이름을 붙일 수 있으며, input.output. 접두사를 사용하여 이를 참조할 수 있습니다. 다음 Snakefile 규칙이 그 예입니다.

rule example:
   input:
       a="file1.txt",
       b="file2.txt",
   output:
       a="output file1.txt",
       c="output file2.txt"
   shell: """
       echo first input is {input.a:q}
       echo second input is {input.b:q}
       echo first output is {output.a:q}
       echo second output is {output.c:q}
       touch {output:q}
   """

여기서 입력 블록의 ab, 그리고 출력 블록의 ac는 입력 및 출력 파일의 키워드 이름입니다. 셸 명령어에서는 각각 {input.a}, {input.b}, {output.a}, {output.c}로 참조할 수 있습니다. 유효한 변수 이름이라면 무엇이든 사용할 수 있으며, 위의 input.aoutput.a처럼 입력 및 출력 블록에서 동일한 이름을 충돌 없이 별개 값으로 사용할 수 있습니다.

이것이 특정 입력 및 출력 파일을 참조하는 저희의 권장 방식입니다. 읽기에 더 명확하고, 재배열이나 추가에도 견고하며, (아마도 가장 중요하게는) 독자(“미래의 당신” 포함)에게 각 입력 및 출력의 목적을 안내하는 데 도움이 됩니다.

셸 코드에서 잘못된 키워드 이름을 사용하면 오류 메시지가 나타납니다. 예를 들어, 아래 코드는:

# 실패 예상

# ANCHOR: 내용
rule example:
   input:
       a="file1.txt",
   output:
       a="output file1.txt",
   shell: """
       echo first input is {input.z:q}
   """
# ANCHOR_END: 내용

다음과 같은 오류 메시지를 출력합니다.

AttributeError: 'InputFiles' object has no attribute 'z', when formatting the following:

       echo first input is {input.z:q}
    

예시: 유연한 명령줄 작성하기

특정 입력을 참조할 수 있는 기능이 특히 유용한 예 중 하나는, 입력 파일 이름을 선택적 인자(optional arguments)로 지정해야 하는 프로그램을 실행할 때입니다. 그러한 프로그램 중 하나가 페어드엔드(paired-end) 입력 리드를 실행할 때의 megahit 어셈블러입니다. 다음 Snakefile을 살펴보세요.

# 대상: -n

# ANCHOR: 내용

rule all:
    input:
        "assembly_out"

rule assemble:
    input:
        R1="sample_R1.fastq.gz",
        R2="sample_R2.fastq.gz",
    output:
        directory("assembly_out")
    shell: """
        megahit -1 {input.R1} -2 {input.R2} -o {output}
    """
# ANCHOR_END: 내용

여기 셸 명령어에서 입력 리드를 두 개의 별개 파일로 제공해야 하며, 하나 앞에는 -1, 두 번째 앞에는 -2를 붙여야 합니다. 결과적으로 셸 명령어가 매우 읽기 쉬워진다는 장점도 있습니다!

입력 함수 및 고급 기능

Python 프로그래밍에 의존하는 좀 더 고급적인 입력 및 출력 사용법들이 있습니다. 예를 들어, 아래와 같이 동적으로 값을 생성하기 위해 호출되는 Python 함수를 정의할 수 있습니다.

# 대상: output5.txt

# ANCHOR: 내용
def multiply_by_5(w):
    return f"file{int(w.val) * 5}.txt"
    
    
rule make_file:
    input:
        # output{val}.txt를 만들도록 요청받으면 입력 파일 file{val*5}.txt를 찾습니다.
        filename=multiply_by_5,
    output:
        "output{val}.txt"
    shell: """
        cp {input} {output:q}
    """
# ANCHOR_END: 내용

output5.txt를 생성하도록 요청받으면, 이 규칙은 file25.txt를 입력으로 찾게 됩니다.

이 기능은 Python 지식뿐만 아니라 와일드카드에 대한 지식도 필요하므로, 자세한 논의는 나중으로 미루겠습니다!

참고 문헌 및 링크