들어가며

기계인간님이 마켓컬리 기술 블로그에 작성하신 새로 입사한 개발자가 프로젝트에 기여하는 방법 한 가지를 읽었습니다. 부끄럽게도 중간중간에 나오는 셸 명령어 중에 모르는 것이 많더군요. 지금 일하는 곳에선 셸을 쓸 일보다는 주로 코드를 뚫어지게 들어다보며 자신의 멍청함을 한탄하는 시간이 많았거든요. 하지만 저도 셸을 아예 사용하지 않는 것은 아니고, 알아두면 언젠가 더 멋진 것을 할 수 있지 않을까요? 이런 이유로 부족하게나마 기계인간님의 글에서 다룬 명령어에 대하여 조사해 보았습니다.

이하 소제목과 명령어는 기계인간님의 글에서 그대로 가져왔습니다.

코딩 스타일 가이드를 읽다가 작성한 Pull Request

find . -name '*.java' \
  | xargs egrep '^\s*if[^\{]*\s*$' --no-filename
  • find: 현재 디렉토리와 그 하위에서 *.java를 검색합니다.
  • xargs: 파이프로 연결하여 find의 출력이 egrep의 마지막 인자로 들어가도록 합니다.
    • egrep: grep -extended-regexp 또는 grep -E와 동일하며, 확장 정규식을 지원합니다. 인자로 주어진 정규식과 매칭되는 부분을 파일에서 찾아 출력할 것입니다.
      • '^\s*if[^\{]*\s*$'
        • ^: 문자열의 시작
        • \s*: 공백 0자 이상
        • if: if
        • [^\{]*: {가 아닌 문자 0자 이상
        • \s*: 공백 0자 이상. 바로 위에서 매칭되므로 없어도 될 것 같습니다.
        • $: 문자열의 끝
      • --no-filename: grep은 기본적으로 한 개 이상의 파일이 주어지면 --with-filename가 적용된 것과 같이 파일 이름을 출력하는데요, 이를 보이지 않게 한 것입니다.

if, for 문에 붙은 괄호에 공백을 주자

KEYWORDS="(if|for|while|try)"
ag "$KEYWORDS\(" -l \
  | xargs sed -i ''  -E "s/$KEYWORDS\(/\1 (/"
  • "(if|for|while|try)": if, for, while, try 중 한 단어와 매칭되도록 합니다. 괄호로 묶었으니 그룹으로 지정됩니다.
  • ag: the_silver_searcher, 코드 검색을 위한 툴로 빠를 뿐 아니라 여러 편리한 옵션이 있습니다.
    • "$KEYWORDS\(": KEYWORDS(가 붙어 있는 경우를 매칭합니다.
    • -l: 검색된 내용은 제외하고 파일명만 출력하도록 합니다.
  • sed: Stream editor. 파일을 한 줄 한 줄 읽어서 대치 등의 명령을 수행할 수 있습니다.
    • -i '': 수정 결과를 표준 출력으로 내보내지 않고 파일에 직접 씁니다. Suffix가 빈 칸으로 주어졌으므로 백업은 생성하지 않습니다.
    • -E: 확장 정규식을 사용할 수 있도록 합니다.
    • "s/$KEYWORDS\(/\1 (/": s/패턴1/패턴2/와 같이 사용하면 패턴1을 2로 대치합니다. /를 기준으로 아래와 같이 나눠볼 수 있겠네요.
      • s: Substitute 명령
      • $KEYWORDS\(: 패턴1입니다.
      • \1 (: 패턴2입니다. \1은 그룹1, 즉 $KEYWORDS를 말하며 이와 ( 사이에 공백을 한 칸 넣어주었습니다.

대문자로 시작하는 lambda 변수 이름을 소문자로 시작하게 바꾸자

ag '\(([A-Z]\w*)\s?\-\>\s*\1' -l \
  | xargs gsed -i.orig -e '/\.filter/ s,(\([A-Z]\),(\L\1,; s,-> \([A-Z]\),-> \L\1,'
  • ag
    • '\(([A-Z]\w*)\s?\-\>\s*\1': (Meow -> Meow 형식의 문자열을 매칭합니다
      • \(: (
      • ([A-Z]\w*): 대문자로 시작하는 문자열을 매칭합니다. 그룹1입니다.
      • \s?: 공백 0~1자
      • \-\>: ->
      • \s*: 공백 0자 이상
      • \1: 그룹1
  • gsed: Mac OS의 sed에는 제약사항이 있어 GNU의 sed를 사용할 수 있게 만들어주는 것 같습니다.
    • -i.orig: 파일을 직접 수정하되 suffix가 주어졌으므로 원본 파일에 .orig 을 붙여 백업합니다.
    • -e '/\.filter/ s,(\([A-Z]\),(\L\1,; s,-> \([A-Z]\),-> \L\1,'
      • /\.filter/: .filter가 나오는 줄만 선택합니다.
      • s,(\([A-Z]\),(\L\1,: s 명령 다음에 나오는 single-byte 문자는 구분자로 취급된다고 합니다. 여기서는 ,를 구분자로 사용했습니다. 즉 s/(\([A-Z]\)/(\L\1/와 동일한 거죠. 참고
        • (\([A-Z]\): ( 다음의 대문자로 시작하는 변수 한 글자를 그룹1로 지정하였습니다.
        • (\L\1: \L\E가 나올 때까지 대문자를 소문자로 변환합니다. 그룹1에서 대문자를 소문자로 변환할 것입니다.
      • s,-> \([A-Z]\),-> \L\1,
        • -> \([A-Z]\): -> 다음의 대문자로 시작하는 변수 한 글자를 그룹1로 지정하였습니다.
        • -> \L\1: 그룹1에서 대문자를 소문자로 변환합니다.

화살표 연산자 좌우에 스페이스를 1개 추가하자

find . -name '*.java' \
  | xargs ag '\-\>(?=\S)|(?<=\S)\-\>' -l \
  | xargs sed -i.orig -E "s,([^ ])->,\1 ->,; s,->([^ ]),-> \1,"
  • ag
    • '\-\>(?=\S)|(?<=\S)\-\>': |(or)로 구분되어 있습니다.
      • \-\>(?=\S): ?=는 전방탐색을 말합니다. \S(공백 제외 문자)까지 매칭을 시키지만 \S는 소비되지 않습니다. 예를 들어 ->ABC라면 ->A까지 매칭 조건에 포함되지만 실제 매칭 문자열은 ->만입니다.
      • (?<=\S)\-\>: ?<=는 후방탐색을 말합니다. 예를 들어 ABC->라면 C->까지 매칭 조건에 포함되지만 실제 매칭 문자열은 ->만입니다.
  • sed
    • "s,([^ ])->,\1 ->,; s,->([^ ]),-> \1,": 두 개의 명령이 ;로 구분되어 있습니다.
      • s,([^ ])->,\1 ->,: 공백을 제외한 문자 바로 다음에 ->가 나오면 사이에 공백을 추가합니다.
      • s,->([^ ]),-> \1,: -> 바로 다음에 공백을 제외한 문자가 나오면 사이에 공백을 추가합니다.

프로젝트 전체에서 탭 문자를 sed로 2 spaces로 교체하자

find . -name '*.java' \
  | xargs ag '\t' -l \
  | xargs sed -E -i '' "s/[[:cntrl:]]/  /g"
  • find . -name '*.java': java 파일만 찾습니다.
  • ag '\t' -l: Tab을 포함하고 있는 파일의 이름만 출력합니다.
  • sed
    • -i '': 수정 결과를 표준 출력으로 내보내지 않고 파일에 직접 씁니다. Suffix가 빈 칸으로 주어졌으므로 백업은 생성하지 않습니다.
    • "s/[[:cntrl:]]/ /g"
      • s: 대치
      • [[:cntrl:]]: 제어 문자를 말합니다. Tab이 포함되겠습니다. 참고
      • ` `: 2 spaces
      • g: 이 플래그를 주지 않으면 각 라인에서 최초로 매칭되는 부분만 대치됩니다.

여는 중괄호 앞에 스페이스를 1개 추가하자

ag '\)\{' -l | xargs sed -i.orig 's/){/) {/'

위에서 모두 나왔던 용례로 파일 내 ){) {” 바꾸는 명령입니다.

마치며

앞으로 언젠간 분명 활용할 일이 있을 것 같은 예제를 가지고 공부하니 오랜만에 신이 났습니다. 막연하게 셸로 많은 것을 할 수 있지만 사용법은 복잡하겠지 하고 생각해 왔었는데 알고 나니 조금 고민하면 여기저기 써 먹을 수 있겠다는 자신감이 붙네요. (물론 쓸 일이 생길 때 쯤엔 이 글을 다시 찾아봐야 하겠지만요…) 항상 좋은 글을 써 주셔서 기계인간님께는 감사할 따름입니다.