몇 시간에 달하는 지루한 반복 작업을 한 줄의 우아한 Bash 코드로 해결하는 것만큼 뿌듯한 일은 드뭅니다.
최근 Bash 스크립트로 노트북 환경을 자동으로 재구축하는 실험을 진행하면서, 새 머신에 GitHub 저장소들을 손쉽게 클론하는 방법이 필요했습니다. 여러 자료를 뒤진 끝에 이 작업을 수행하는 한 줄짜리 명령어를 완성했습니다.
그리고 ‘모든 계란을 한 바구니에 담지 않는다’는 정신으로, GitLab에 백업 저장소를 자동 생성하고 푸시하는 또 다른 원라이너도 만들었습니다. 바로 소개하겠습니다.
GitHub 저장소 전체를 클론하는 Bash 원라이너
먼저 주의할 점이 있습니다. 클론하려는 GitHub 저장소 목록이 필요합니다. 다행히 이 덕분에 모든 저장소를 무조건 받는 대신, 내 컴퓨터에 두고 싶은 저장소만 골라 담을 수 있다는 장점도 생깁니다.
매번 비밀번호를 입력하지 않고 GitHub 저장소를 클론하려면 15분간 캐시되는 HTTPS 자격 증명을 쓰거나, 필자가 선호하는 방식인 SSH 연결을 이용하면 됩니다. 간결함을 위해 후자를 사용한다고 가정하고, SSH 키가 이미 설정되어 있다고 하겠습니다.
gh-repos.txt 파일에 다음과 같이 GitHub URL 목록이 들어 있다고 합시다.
git@github.com:username/first-repository.git
git@github.com:username/second-repository.git
git@github.com:username/third-repository.git
다음 명령을 실행합니다.
xargs -n1 git clone < gh-repos.txt
목록에 있는 모든 저장소가 현재 폴더에 클론됩니다. URL만 적절히 바꾸면 GitLab에서도 똑같이 동작합니다.
어떻게 동작할까?
이 원라이너는 두 부분으로 나뉩니다. 직관에 어긋나지만 입력 부분은 오른쪽에, 실제 동작을 담당하는 부분은 왼쪽에 있습니다. 순서를 더 직관적으로 바꾸고 싶다면 같은 명령을 이렇게 쓸 수도 있습니다.
<gh-repos.txt xargs -n1 git clone
입력 파일 gh-repos.txt의 각 줄마다 명령을 실행하기 위해 xargs -n1을 사용합니다. xargs는 입력에서 항목을 읽어 명령을 실행하는 도구입니다(실행할 명령이 없으면 echo로 그대로 출력합니다). 기본적으로 항목이 공백으로 구분된다고 가정하지만, 줄바꿈으로 구분해도 동작하며 오히려 목록을 읽기가 더 편합니다. -n1 플래그는 명령 하나당 인수 1개, 즉 우리 경우 한 줄씩 사용하라고 알려주는 역할을 합니다. 이렇게 만든 git clone 명령을 xargs가 각 줄마다 실행해 줍니다. 끝!
GitLab에 저장소 여러 개를 만들고 푸시하는 Bash 원라이너
GitLab은 GitHub와 달리, 웹사이트에 접속해 새 저장소를 먼저 만들지 않아도 되는 편리한 기능을 제공합니다. 터미널에서 바로 새 GitLab 저장소를 생성할 수 있습니다. 단, 새로 만들어진 저장소는 기본적으로 Private으로 설정되므로 Public으로 공개하고 싶다면 나중에 수동으로 변경해야 합니다.
GitLab 공식 문서는 git push --set-upstream으로 새 프로젝트를 생성하라고 안내하지만, 백업 용도로는 다소 불편했습니다. 앞으로 저장소를 관리할 때 추가 작업 없이 GitHub와 GitLab 양쪽에 한 번에 푸시하는 명령 하나만 실행하고 싶었기 때문입니다.
이 원라이너를 사용하려면 GitLab용 저장소 URL 목록(아직 존재하지 않는 저장소)도 필요합니다. GitHub 저장소 목록을 복사한 뒤 Vim으로 열어 치환하면 손쉽게 만들 수 있습니다.
cp gh-repos.txt gl-repos.txt
vim gl-repos.txt
:%s/\<github\>/gitlab/g
:wq
이렇게 하면 다음과 같은 gl-repos.txt가 만들어집니다.
git@gitlab.com:username/first-repository.git
git@gitlab.com:username/second-repository.git
git@gitlab.com:username/third-repository.git
다음 명령을 실행하면 GitLab에 저장소를 생성하고, URL을 리모트로 추가한 뒤, 코드를 새 저장소에 푸시합니다.
awk -F'\/|(\.git)' '{system("cd ~/FULL/PATH/" $2 " && git remote set-url origin --add " $0 " && git push")}' gl-repos.txt
잠시 후 자세히 설명하겠습니다. 우선 ~/FULL/PATH/는 GitHub 저장소들이 들어 있는 디렉터리의 전체 경로여야 한다는 점만 기억하세요.
두 가지 전제 조건이 있습니다.
- 로컬 머신에서 저장소가 담긴 디렉터리 이름이 URL의 저장소 이름과 같아야 합니다(위의 원라이너로 클론했다면 자동으로 충족됩니다).
- 각 저장소가 현재 푸시하려는 브랜치(예:
master)에 체크아웃되어 있어야 합니다.
물론 원라이너를 확장해 이런 조건까지 처리할 수 있지만, 필자의 겸손한 의견으로는 그 단계에 이르면 차라리 Bash 스크립트를 작성하는 것이 맞습니다.
어떻게 동작할까?
이 원라이너는 gl-repos.txt 파일의 각 줄(URL)을 입력으로 사용합니다. awk로 로컬에 있는 저장소 디렉터리 이름을 분리해 내고, 이 정보로 더 큰 명령어를 조립합니다. awk의 출력을 print로 확인해 보면 다음과 같습니다.
cd ~/FULL/PATH/first-repository && git remote set-url origin --add git@gitlab.com:username/first-repository.git && git push
cd ~/FULL/PATH/second-repository && git remote set-url origin --add git@gitlab.com:username/second-repository.git && git push
cd ~/FULL/PATH/third-repository && git remote set-url origin --add git@gitlab.com:username/third-repository.git && git push
이 명령이 어떻게 조립되는지 살펴보겠습니다.
awk로 문자열 분리하기
awk는 필드 구분자를 기준으로 입력을 분리할 수 있습니다. 기본 구분자는 공백 문자지만, -F 플래그로 변경할 수 있으며 단일 문자 외에 정규표현식도 사용할 수 있습니다. 저장소 URL은 형식이 정해져 있으므로, 슬래시 문자 /와 URL 끝의 .git 사이에 있는 부분 문자열을 요청하면 저장소 이름을 얻을 수 있습니다.
이를 위해 정규식 \/|(\.git)을 사용합니다.
\/는 이스케이프 처리된/문자입니다.|는 ‘또는’을 의미하며, awk에게 두 표현식 중 하나를 매칭하라고 알려줍니다.(\.git)은 URL 끝에서 ‘.git’과 매칭되는 캡처 그룹으로,.문자가 이스케이프되어 있습니다. 사실 ‘.git’은 엄밀히 말해 아무것도 분리하지 않습니다(뒤에 아무것도 없으니까요). 하지만 이 부분을 잘라내기에는 가장 간편한 방법입니다.
awk에게 분리 위치를 알려주면, 필드 연산자로 원하는 부분 문자열을 가져올 수 있습니다. 필드는 $ 문자와 열 번호로 참조합니다. 이 예제에서는 두 번째 필드인 $2가 필요합니다. 모든 부분 문자열은 다음과 같습니다.
1: git@gitlab.com:username
2: first-repository
문자열 전체, 즉 URL 전체를 사용하려면 필드 연산자 $0를 씁니다. 명령어를 작성할 때는 저장소 이름과 URL 자리에 필드 연산자를 넣으면 됩니다. 조립 과정에서 print로 출력해 보면 공백이 올바른지 확인하는 데 큰 도움이 됩니다.
awk -F'\/|(\.git)' '{print "cd ~/FULL/PATH/" $2 " && git remote set-url origin --add " $0 " && git push"}' gl-repos.txt
명령 실행하기
실제 명령은 system() 괄호 안에서 조립됩니다. 이를 awk의 출력으로 사용하면 각 명령이 만들어지는 즉시 실행됩니다. system() 함수는 명령을 실행하는 자식 프로세스를 만들고, 명령이 완료되면 반환합니다. 쉽게 말해, awk가 입력 파일을 처리하는 메인 프로세스를 벗어나지 않고도 각 저장소에 대해 Git 명령을 하나씩 순차적으로 수행할 수 있습니다. 최종 명령어는 다음과 같습니다.
awk -F'\/|(\.git)' '{system("cd ~/FULL/PATH/" $2 " && git remote set-url origin --add " $0 " && git push")}' gl-repos.txt
백업 저장소 활용하기
GitLab URL을 리모트로 추가했기 때문에 외부 호스팅 저장소 두 곳에 푸시하는 절차가 크게 간단해졌습니다. 저장소 디렉터리에서 git remote -v를 실행하면 다음과 같이 표시됩니다.
origin git@github.com:username/first-repository.git (fetch)
origin git@github.com:username/first-repository.git (push)
origin git@gitlab.com:username/first-repository.git (push)
이제 인자 없이 git push만 실행하면 현재 브랜치가 두 개의 리모트 저장소에 모두 푸시됩니다.
참고로 git pull은 일반적으로 처음 클론한 원격 저장소, 즉 위 예제에서 (fetch)로 표시된 URL에서만 가져오려 합니다. 여러 Git 저장소에서 동시에 풀하는 것도 가능하지만 상당히 복잡하며 이 글의 범위를 벗어납니다. 궁금하다면 멀티 리모트 푸시/풀 관련 자료나 Git 공식 문서의 remotes 섹션을 참고하시길 권합니다.
Bash 원라이너의 간결함에 대해 덧붙이며
Bash 원라이너는 제대로 이해하고 나면 재미있고 유용한 지름길이 됩니다. 최소한 xargs, awk 같은 도구를 알고 있으면 업무의 많은 지루함을 자동화하고 덜어낼 수 있습니다. 물론 단점도 있습니다.
‘이해하기 쉽고, 유지보수 가능하며, 접근성 좋은 도구’라는 관점에서 보면 Bash 원라이너는 형편없습니다. if나 while 루프를 쓰는 Bash 스크립트보다 작성하기 어렵고, 읽기는 당연히 더 어렵습니다. 작성하다 보면 어딘가에서 따옴표 하나나 닫는 괄호 하나를 빠뜨리기 십상이고, 이 글에서 보셨듯이 설명하는 데도 상당한 분량이 필요합니다. 그런데도 왜 사용할까요?
케이크 굽는 레시피를 단계별로 읽는다고 상상해 보세요. 방법과 재료를 이해하고 준비물을 챙깁니다. 그런데 곰곰이 생각해 보니 재료를 정확한 순서로 오븐에 던져 넣기만 하면 케이크가 즉석에서 완성된다는 사실을 깨닫게 됩니다. 실제로 해보니 정말 됩니다!
꽤 짜릿하겠죠?