리눅스 comm 명령어 사용법 - 두 목록의 교집합·차집합을 한 줄로 구하기
이 명령어를 언제 쓰는가
두 목록을 대조해서 "양쪽에 다 있는 것, A에만 있는 것, B에만 있는 것"을 구할 때 쓴다. 설치된 패키지 목록과 설치되어야 할 목록, 백업된 파일 목록과 원본 파일 목록, 어제 접속한 IP 와 오늘 접속한 IP 를 비교하는 식이다.
같은 일을 grep -vxF -f 나 sort | uniq -u 로도 할 수 있지만, comm 은 세 가지 결과를 한 번에 세 열로 보여 준다. 어느 쪽에만 있는지가 즉시 구분된다는 것이 차이다. 대신 두 입력이 정렬되어 있어야 한다는 조건이 붙는다.
기본 형식
comm [-123] 파일1 파일2
출력은 탭으로 구분된 세 개의 열이다.
- 1열 — 파일1에만 있는 줄 (들여쓰기 없음)
- 2열 — 파일2에만 있는 줄 (탭 1개)
- 3열 — 양쪽에 모두 있는 줄 (탭 2개)
-1, -2, -3 은 각각 그 열을 숨기는 옵션이다. 켜는 것이 아니라 끄는 것이라는 점이 늘 헷갈린다. "양쪽 공통만" 보려면 1열과 2열을 끄니까 comm -12 가 된다.
자주 쓰는 옵션
| 옵션 | 의미 | 예시 |
|---|---|---|
-12 | 교집합. 양쪽에 다 있는 줄만 | comm -12 a.txt b.txt |
-23 | 차집합 A-B. 파일1에만 있는 줄만 | comm -23 a.txt b.txt |
-13 | 차집합 B-A. 파일2에만 있는 줄만 | comm -13 a.txt b.txt |
-3 | 공통 줄을 빼고 서로 다른 것만 두 열로 | comm -3 a.txt b.txt |
-i | 대소문자를 무시하고 비교 (BSD 전용) | comm -i a.txt b.txt |
--nocheck-order | 정렬 검사 경고를 끈다 (GNU 전용) | comm --nocheck-order a b |
--total | 각 열의 건수 합계를 마지막 줄에 출력 (GNU 전용) | comm --total a b |
-i 는 BSD(macOS) 에만, --total·--check-order 계열은 GNU coreutils 에만 있다. 서로 없는 옵션이니 스크립트를 옮길 때 확인해야 한다.
실전 예제
아래 예제의 입력은 다음과 같다.
$ cat a.txt $ cat b.txt
a b
b c
c d
1) 세 열을 그대로 본다. 전체 그림을 한 번에 파악할 때 쓴다.
comm a.txt b.txt
a
b
c
d
2) 두 서버의 설치 패키지를 대조한다. 개발 서버에만 있고 운영 서버에 없는 패키지를 찾는 상황이다. 먼저 각 서버에서 목록을 뽑아 둔다.
ssh dev01 'dpkg-query -f "${Package}\n" -W' | sort > /tmp/pkg-dev
ssh prd01 'dpkg-query -f "${Package}\n" -W' | sort > /tmp/pkg-prd
comm -23 /tmp/pkg-dev /tmp/pkg-prd
RHEL 계열이라면 rpm -qa --qf '%{NAME}\n' 로 바꾼다. 공통 패키지만 보고 싶으면 -23 대신 -12 를 쓴다.
3) 한쪽에만 있는 것을 찾는다. 백업에서 빠진 파일을 잡아내는 전형적인 용례다.
cd /srv/data && find . -type f | sort > /tmp/src.list
cd /backup/data && find . -type f | sort > /tmp/dst.list
comm -23 /tmp/src.list /tmp/dst.list
출력에 나오는 파일이 백업되지 않은 파일이다. 아무것도 안 나오면 누락이 없다는 뜻이다.
4) 어제와 오늘의 접속 IP 를 비교해 새로 등장한 IP 를 찾는다. 이상 트래픽을 초동 확인할 때 쓴다.
awk '{print $1}' access.log.1 | sort -u > /tmp/ip-yesterday
awk '{print $1}' access.log | sort -u > /tmp/ip-today
comm -13 /tmp/ip-yesterday /tmp/ip-today
5) 건수만 빠르게 센다. 목록이 커서 눈으로 볼 수 없을 때는 wc 로 규모부터 본다.
comm -23 /tmp/src.list /tmp/dst.list | wc -l
6) 정렬을 파이프 안에서 끝낸다. 임시 파일 없이 처리하는 관용구다.
comm -12 <(sort -u a.txt) <(sort -u b.txt)
함정과 주의점
정렬되지 않은 입력은 조용히 엉뚱한 답을 낸다. comm 은 두 파일을 앞에서부터 한 번씩만 훑기 때문에 순서가 어긋나면 같은 값도 다른 것으로 취급한다.
$ printf 'c\na\n' > m3.txt
$ comm m3.txt b.txt
b
c
a
d
c 는 우연히 공통으로 잡혔지만 a 는 1열로 밀려났다. GNU comm 은 comm: file 1 is not in sorted order 경고를 내지만 BSD comm 은 아무 말도 하지 않는다. 항상 sort 를 먼저 태운다.
sort 의 로케일이 comm 의 비교 기준과 달라도 어긋난다. comm 은 바이트 단위로 비교하는데, ko_KR.UTF-8·en_US.UTF-8 로케일의 sort 는 구두점을 무시하는 등 다른 규칙으로 정렬한다. 파일 경로에 하이픈·언더스코어·공백이 섞이면 "분명히 정렬했는데 결과가 이상하다"가 된다. 양쪽 다 LC_ALL=C 로 고정하는 것이 정답이다.
LC_ALL=C sort a.txt > a.sorted
LC_ALL=C sort b.txt > b.sorted
LC_ALL=C comm -23 a.sorted b.sorted
출력 앞에 붙는 탭을 잊지 마라. comm -13 처럼 열 하나만 남기면 탭이 없지만, comm -3 은 2열 앞에 탭이 하나 붙은 채로 나온다. 그 결과를 그대로 다음 명령에 넘기면 값 앞에 탭이 섞여 매칭이 실패한다. 필요하면 | tr -d '\t' 또는 | sed 's/^\t//' 를 붙인다.
중복 줄이 있으면 결과가 헷갈린다. comm 은 중복을 알아서 접지 않는다. 목록 비교가 목적이라면 sort -u 로 중복부터 제거하고 넣는다.
함께 보면 좋은 명령어
sort— comm 의 전제 조건.sort -u로 중복까지 함께 정리한다.join— 줄 전체가 아니라 특정 키 필드로 대조해야 할 때 쓴다.diff— 목록이 아니라 파일 내용의 변경 위치를 보고 싶을 때 쓴다.