2019년 3월 13일 수요일

[Bash, shell] 구분자가 포함된 문자열의 조작 기법

예전에는 늘 Perl을 써서 작업하던 것을 shell 수준에서 구현하기 위해 노력하고 있다. 제목에서 밝힌 문자열은 사실은 파일의 제목을 의미한다. 오늘 글을 쓰는 목적은 NCBI에서 다운로드한 파일을 하나 가득 들어 놓고는 제목을 한꺼번에 변경하기 위한 방법을 정리하기 위함이다. 특정 균주(Paenibacillus polymyxa E681)에 대한 RefSeq assembly 다운로드 사이트(링크)를 방문해 보자.


파일의 이름은 '필드1_필드2_필드3_..' 형태이다. 여기서 구분자는 밑줄 문자(_, under)이다. 위치를 기준으로 하여 원하는 필드만 조합하여 파일명을 단순하게 바꾸는 사례를 알아보자. NCBI에서 유전체 정보를 일괄적으로 다운로드하여 이름을 바꾸는 방법은 나의 위키 페이지(링크)에 상세히 설명하였다. 여기에 나온대로 따라서 하면 다음과 같은 형태의 파일을 얻는다.

Rhodococcus_sp._H-CA8f_GCA_002501585.1.fna

때로는 빨갛게 표시한 부분만을 떼어서 파일명으로 삼고 싶을 때가 있다. iTOL 서버를 쓰는 경우에는 단순하게 표시된 자료로 만든 newick tree file을 업로드한 다음 실제로 표시할 label을 별도로 제공하는 것이 더 합리적이다.

그렇다면 위에서 보인 파일명을 구분자(_)로 나눈 뒤 5, 6번째 필드를 붙인 것으로 바꾸면 된다. 이때 유용한 명령어는 cut이다.

$ ls *fna | while read F
> do
> mv $F $(cut -d'_' -f5,6 <<<$F)
> done

꺾쇠 세 개가 연달아 표시된 줄이 약간 난해한데, 어쨌든 잘 작동은 한다. 그러나 이 방법은 필드의 수가 엄격하게 고정된 경우에 한하여 쓸 수 있다. 파일명의 세번째 필드, 그러니까 strain 명에 해당하는 곳에 또 밑줄이 들어간 경우가 종종 있다. 'ATCC 842'와 같이 원래는 공백이지만 파일명에 공백을 넣는 것이 별로 바람직하지 않기 때문에 밑줄을 추가적으로 넣게 된 것이다. 이러한 상황에서는 '구분자를 떼어낸 다음 몇 번째 필드를 취하라' 하는 식으로는 원하는 결과를 얻기 어렵다. 차라리 '앞에서 처음, 혹은 뒤에서 처음'하는 방법이 더 낫다. 이때 BASH의 문자열 조작 기법이 매우 유용하게 떠오른다.

상세하게 공부를 하려면 Advanced Bash-Scripting Guide의 제10장 Manipulating Variables를 숙독하라. 나는 여기에서 꼭 필요한 부분만을 골라서 설명하겠다. 다음의 예제만 잘 보면 뭐가 어떻게 돌아가는지를 잘 알 수 있다. #와 %는 앞 또는 뒤로부터 시작하는 매치를 삭제하는 것이다. *를 쓰지 않고 ${NAME#_}라고만 쓰면 앞부분부터 시작해서 첫번째 '_'가 나오는 곳까지를 삭제할 것만 강렬한 욕구가 느껴지지만 이건 오해다(사실 이것 때문에 혼동을 많이 했다). 반드시 *_라고 써야 하고, #를 사용하는 경우에는 _*라고 해야 한다. #와 ##, %와 %%의 차이는 shortest(하나) or longest match(둘)를 의미하는 것이다.

$  NAME=abc_def_123_456.txt
$ echo ${NAME#*_}
def_123_456.txt
$ echo ${NAME##*_}
456.txt
$ echo ${NAME%_*}
abc_def_123
$ echo ${NAME%%_*}
abc
$ echo ${NAME/_/XYZ}
abcXYZdef_123_456.txt
$ echo ${NAME/_*/}
abc
$ echo ${NAME/*_/}
456.txt

그러면 파일명을 구성하는 필드의 수가 일정하지 않다 하더라도 맨 뒷부분을 이용하여 assembly accession만을 떼어내는 것이 가능하다.

$ ls *fna | while read F
> do
> mv $F GCA_${F##*_}
> done

변수 F의 앞에서부터 시작하여 *_를 longest match로 삭제하면 ...GCA_의 나머지 부분이 남는다. 이렇게 얻은 값의 앞에 다시 GCA_를 붙이면 된다. 혹은 while-do 블록 내의 명령어를 mv $F GCA_${F#*_GCA_}으로 써도 결과는 같다. _GCA_는 변수 F의 값에서 단 한번만 존재하므로(철저한 사람이라면 균주명에 앞뒤에 공백을 포함하는 GCA라는 이름이 들어있지는 않은지 확인을 해 보겠지만) #를 한 번만 써도 된다.

대충 써 왔던 기법이 이제 좀 정히가 되었다.

댓글 없음: