tr 'a' 'b' file.txt는 동작하지 않는다. 반드시 cat file.txt | tr 'a' 'b' 또는 tr 'a' 'b' < file.txt로 써야 한다.
4. sed — 스트림 편집기
sed는 줄 단위로 텍스트를 치환, 삭제, 삽입하는 도구다. tr보다 강력하고, 정규식을 사용할 수 있다.
기본 치환
# 첫 번째 매칭만 치환echo "foo bar foo" | sed 's/foo/baz/' # baz bar foo# 모든 매칭 치환 (g 플래그)echo "foo bar foo" | sed 's/foo/baz/g' # baz bar baz# 파일 직접 수정 (-i)sed -i 's/old/new/g' file.txt # macOS: sed -i '' 's/old/new/g'
줄 단위 조작
# 특정 줄 삭제sed '3d' file.txt # 3번째 줄 삭제sed '/^#/d' file.txt # 주석 줄 삭제sed '/^$/d' file.txt # 빈 줄 삭제# 특정 줄만 출력 (-n + p)sed -n '5p' file.txt # 5번째 줄만 출력sed -n '3,7p' file.txt # 3~7번째 줄# 줄 앞/뒤에 추가sed 's/^/PREFIX: /' file.txt # 모든 줄 앞에 추가sed 's/$/ SUFFIX/' file.txt # 모든 줄 뒤에 추가
정규식 활용
# 영숫자와 언더스코어만 남기기echo "$RAW_COL" | sed 's/[^a-z0-9_]//g'# 그룹 캡처와 역참조echo "2026-06-01" | sed 's/\([0-9]*\)-\([0-9]*\)-\([0-9]*\)/\3\/\2\/\1/'# 01/06/2026# ERE (확장 정규식) 사용 — -E 옵션echo "2026-06-01" | sed -E 's/([0-9]+)-([0-9]+)-([0-9]+)/\3\/\2\/\1/'
5. awk — 패턴 매칭 + 필드 처리
awk는 텍스트를 필드(열) 단위로 처리하는 프로그래밍 언어다. tr(문자), sed(줄)보다 한 단계 강력하다.
기본 구조
awk 'pattern { action }' file
pattern: 조건 (생략하면 모든 줄에 적용)
action: 해당 줄에서 실행할 명령
# 기본: 모든 줄의 첫 번째 필드 출력awk '{ print $1 }' file.txt# 구분자 지정 (-F)awk -F',' '{ print $2 }' data.csv # CSV의 2번째 열awk -F':' '{ print $1, $3 }' /etc/passwd # 사용자명, UID# 조건부 출력awk '$3 > 100 { print $1, $3 }' data.txt # 3번째 필드가 100 초과인 줄# 내장 변수awk '{ print NR, NF, $0 }' file.txt# NR: 현재 줄 번호# NF: 현재 줄의 필드 개수# $0: 줄 전체
BEGIN {} pattern {} END {} — awk의 큰 흐름
awk 프로그램은 세 종류의 블록으로 짜인다. 입력을 한 줄씩 읽으면서 가운데 블록을 반복하고, 그 앞뒤를 BEGIN과 END가 한 번씩 감싼다.
awk 'BEGIN { ... } # 입력을 읽기 전에 딱 한 번 (변수 초기화, 구분자 설정, 헤더 출력)pattern { action } # 각 줄마다: pattern이 참인 줄에서만 action 실행END { ... } # 모든 줄을 다 읽은 뒤 딱 한 번 (집계 결과 출력)' file
세 블록 모두 선택이다. { action }만 쓰면 전 줄에 적용되고, pattern만 쓰면 매칭된 줄을 그대로 출력한다. 줄을 읽고 필드로 쪼개는 루프는 awk가 자동으로 돌리므로, for를 직접 짤 필요가 없다.
# BEGIN에서 초기화, 본문에서 누적, END에서 출력awk 'BEGIN { sum=0 } { sum += $1 } END { print "Total:", sum }' numbers.txt# BEGIN만 — 입력 없이 계산기처럼awk 'BEGIN { printf "%.2f\n", 10/3 }' # 3.33# END만 — 전체 줄 수 (NR은 마지막에 읽은 줄 번호)awk 'END { print NR }' file.txt
awk 고유 문법 — 연관 배열·조건문·삼항·exit
awk는 한 줄짜리 명령처럼 보여도 안은 작은 프로그래밍 언어다. 실무에서 자주 만나는 고유 문법을 한 예제에 모아 보면 이렇다. 아래는 파일의 첫 번째 필드에 중복이 있는지 검사하는 코드다.
if awk '{ seen[$1]++; if (seen[$1] > 1) { duplicated = 1 } } END { exit duplicated ? 0 : 1 }' "$lease_file"; then echo "중복된 첫 필드가 있다"fi
조각을 하나씩 뜯어보면:
문법
의미
seen[$1]++
연관 배열(해시). 키 $1(첫 필드)의 값을 1 증가. 없던 키는 0에서 시작
seen[$1] > 1
같은 첫 필드를 두 번 이상 봤다는 뜻
if (...) { ... }
action 안에서 쓰는 조건문. 괄호·중괄호 모두 C 문법과 같다
duplicated = 1
선언 없이 바로 쓰는 변수. awk 변수의 기본값은 0(또는 빈 문자열)
END { exit ... }
모든 줄을 읽은 뒤 종료 코드를 정한다
duplicated ? 0 : 1
삼항 연산자. 중복이 있으면 exit 0, 없으면 exit 1
awk의 exit와 쉘 종료 코드
awk의 exit N은 그대로 awk 프로세스의 종료 코드가 된다. 그래서 if awk '...'; then처럼 awk를 조건문으로 직접 쓸 수 있다. 위 예제는 “중복이 있으면 0(참)“으로 뒤집어, if가 중복을 발견했을 때 분기하도록 만든 것이다.
종료 코드 0 = 성공/참, 0이 아니면 실패/거짓 (쉘의 관례)
awk 변수는 초기화하지 않아도 숫자 자리에서는 0, 문자열 자리에서는 빈 문자열로 시작한다
실무 예제 분석
프로젝트 쉘 스크립트에서 이런 awk를 만날 수 있다.
awk -F"${DELIM}" -v start="${START_ROW}" -v col="${COL_CNT}" 'NR >= start && NR < start + 100 { if (NF != col) { printf "ERROR: column count mismatch at line %d (expected=%d, actual=%d)\n", NR, col, NF exit 1 }}' "${CSV_FILE}"
한 줄씩 분해하면:
부분
의미
-F"${DELIM}"
필드 구분자를 $DELIM 변수 값으로 설정
-v start="${START_ROW}"
쉘 변수를 awk 내부 변수 start로 전달
-v col="${COL_CNT}"
쉘 변수를 awk 내부 변수 col로 전달
NR >= start && NR < start + 100
패턴: start번째 줄부터 100줄만 처리
NF != col
현재 줄의 필드 개수가 기대값과 다르면
printf "ERROR: ..."
에러 메시지 출력
exit 1
awk를 비정상 종료
' "${CSV_FILE}"
홑따옴표로 awk 프로그램 끝, 입력 파일 지정
awk에 쉘 변수를 전달하는 방법
-v var="$SHELL_VAR" : 안전한 방법. awk 코드가 홑따옴표 안에 있어도 동작
awk 코드를 쌍따옴표로 감싸면 $ 충돌이 생긴다 (쉘이 먼저 해석하려 함)
항상 -v 옵션을 사용하는 것이 권장됨
6. 텍스트를 골라내는 도구 (필터)
여기 도구들은 입력에서 일부만 골라낼 뿐 내용을 바꾸지 않는다. tr/sed/awk처럼 프로그램을 짜는 게 아니라, 각자 정해진 한 가지 동작을 옵션으로 부른다. 그래서 따로따로 외우기보다 파이프로 조합해서 쓴다.
grep — 패턴으로 줄 고르기
grep "pattern" file.txt # 기본 검색grep -i "pattern" file.txt # 대소문자 무시grep -r "pattern" dir/ # 재귀 검색grep -n "pattern" file.txt # 줄 번호 표시grep -c "pattern" file.txt # 매칭 줄 수grep -v "pattern" file.txt # 매칭되지 않는 줄grep -l "pattern" dir/* # 매칭된 파일명만# -q : quiet 모드 (출력 없이 종료 코드만)if grep -q "ERROR" app.log; then echo "로그에 ERROR가 있다"fi
grep -q는 조건 검사에 최적
출력이 필요 없고 “있는지 없는지”만 알면 될 때 사용한다. 종료 코드 0(매칭됨) 또는 1(없음)만 반환한다.
head / tail — 앞·뒤에서 자르기
head -n 1 "$CSV_FILE" # 첫 줄만 (헤더 추출)head -n 20 file.txt # 앞 20줄tail -n 10 file.txt # 뒤 10줄tail -f /var/log/app.log # 실시간 로그 모니터링
cut — 열(필드) 뽑기
cut -d',' -f2 data.csv # CSV의 2번째 필드cut -d':' -f1,3 /etc/passwd # 1번째, 3번째 필드cut -c1-10 file.txt # 각 줄의 1~10번째 문자
sort / uniq — 정렬과 중복 제거
sort file.txt # 정렬sort -n numbers.txt # 숫자 기준 정렬sort -t',' -k2 data.csv # 2번째 필드 기준 정렬sort -u file.txt # 정렬 + 중복 제거# uniq는 연속 중복만 제거하므로 sort와 함께 사용sort file.txt | uniqsort file.txt | uniq -c # 중복 횟수 표시sort file.txt | uniq -d # 중복된 줄만 표시
shuf — 줄 무작위 섞기/추출
shuf는 입력 줄의 순서를 무작위로 섞는다. sort의 반대라고 보면 된다. 무작위 표본 추출이나 테스트 데이터 생성에 쓴다.
shuf file.txt # 모든 줄을 무작위 순서로shuf -n 5 file.txt # 무작위로 5줄만 추출shuf -e apple banana cherry # 인자로 준 항목들을 섞기shuf -i 1-100 # 1~100 범위의 숫자를 섞어서 출력shuf -i 1-100 -n 1 # 1~100 중 무작위 정수 하나 (난수 생성)# 무작위로 한 줄 뽑기 (예: 랜덤 명언)shuf -n 1 quotes.txt
shuf -n 1 vs $RANDOM
$RANDOM은 0~32767 범위라 큰 범위로 늘리면 편향이 생긴다. 균등한 난수가 필요하면 shuf -i 0-N -n 1이 더 안전하다.
macOS에는 shuf가 없다
shuf는 GNU coreutils 도구라 Linux엔 기본 탑재지만 macOS엔 없다. brew install coreutils로 설치하면 gshuf로 쓸 수 있다. 설치가 어렵다면 BSD·GNU 공통으로 동작하는 sort -R(줄 무작위 정렬)로 대체한다.
sort -R file.txt | head -n 5 # shuf -n 5 와 유사
7. read와 IFS
read 기본 — stdin에서 한 줄씩 읽기
read는 stdin에서 한 줄을 읽어 변수에 담는 쉘 빌트인이다. 파일을 줄 단위로 순회하거나 사용자 입력을 받을 때 쓴다.
read -r line # 한 줄을 읽어 line에 저장read -r first rest # 첫 단어는 first, 나머지는 rest (IFS 기준 분리)read -ra arr # 단어들을 배열 arr에 저장
여러 변수를 주면 IFS로 쪼개 앞에서부터 채우고, 마지막 변수에 남은 전체가 들어간다.
echo "a b c d" | { read -r x y z; echo "x=$x | y=$y | z=$z"; }# x=a | y=b | z=c d (z에 "c d"가 통째로)
자주 쓰는 옵션:
옵션
의미
-r
백슬래시를 이스케이프로 풀지 않음 (항상 붙이는 게 기본)
-p "프롬프트"
읽기 전에 프롬프트 출력 (read -rp "Name: " name)
-a 배열
입력을 배열로 저장
-d 구분자
줄바꿈 대신 지정한 문자까지 읽음 (-d ''는 NUL 구분, find -print0과 짝)
-n N
N글자를 읽으면 Enter 없이 즉시 반환
-s
입력을 화면에 표시하지 않음 (비밀번호)
-t N
N초 안에 입력이 없으면 실패 종료
-u FD
stdin(0) 대신 지정한 파일 디스크립터에서 읽음
# 비밀번호 입력 (에코 끔)read -rsp "Password: " pw; echo# 한 글자만 받고 Enter 불필요 (y/n 즉시 반응)read -rn1 -p "Continue? [y/n] " ans; echo# 5초 타임아웃if read -rt 5 -p "5초 안에 입력: " val; then echo "입력: $val"else echo "시간 초과"fi
read가 마지막 줄을 놓치는 경우
while read -r line 루프는 줄바꿈으로 끝나지 않는 마지막 줄을 빠뜨린다. read는 구분자(줄바꿈)를 만나야 종료 코드 0을 내는데, 파일 끝에 줄바꿈이 없으면 마지막 read가 실패 코드를 내며 루프를 끝내기 때문이다.
while read -r line || [[ -n "$line" ]]; do echo "$line"done < file.txt
|| [[ -n "$line" ]]는 “read가 실패했어도 line에 내용이 남아 있으면 한 번 더 처리”하라는 뜻이다.
IFS란
IFS(Internal Field Separator)는 Bash가 단어를 분리할 때 사용하는 구분자다. 기본값은 공백, 탭, 줄바꿈이다.
변수 할당과 명령을 한 줄에 쓰면, 해당 변수는 그 명령의 실행 환경에서만 유효하다. 명령이 끝나면 변수는 원래 값으로 돌아간다.
IFS=: read -ra parts <<< "a:b:c"echo "${parts[@]}" # a b cecho "$IFS" # 원래 IFS (변경 안 됨)
단, 이 문법은 명령어가 있어야 동작한다. IFS=":" arr=("a" "b")처럼 변수 할당 2개를 붙여쓰면 둘 다 현재 쉘에 영구 적용된다.
CSV 헤더 파싱 실전 예제
DELIM=$(printf '\x07') # BEL 문자를 구분자로 사용HEADER_LINE=$(head -n 1 "${CSV_FILE}")IFS="$DELIM" read -ra HEADER_ARR <<< "$HEADER_LINE"for col in "${HEADER_ARR[@]}"; do # 각 열 이름을 정규화: 따옴표 제거, 공백 제거, 소문자 변환, 특수문자 제거 clean=$(echo "$col" | tr -d '"' | tr -d ' ' | tr 'A-Z' 'a-z' | sed 's/[^a-z0-9_]//g') echo "$clean"done
실전 비교 — 각 줄의 첫 필드가 특정 값과 같은지 확인
lease 파일처럼 각 줄의 첫 필드가 포트 번호인 파일에서, 특정 포트가 이미 들어 있는지 확인한다고 하자. 같은 일을 grep, awk, read로 각각 풀 수 있고, 무엇을 더 하려는지에 따라 선택이 갈린다.
가정한 입력($lease_file):
8080 web active
9090 api active
5432 db idle
방법 1 — grep: 가장 짧지만 “필드” 개념이 없어 정규식으로 위치를 고정해야 한다.
# 줄 맨 앞(^)에서 포트 + 공백까지 매칭. -q는 출력 없이 종료 코드만if grep -qE "^${PORT}[[:space:]]" "$lease_file"; then echo "port $PORT 사용 중"fi
장점: 짧고 빠르다
단점: 줄 전체를 보는 도구라 위치 고정(^, 구분자)을 손으로 처리해야 한다. 구분자가 탭이나 가변 공백이면 정규식이 까다로워지고, ^${PORT} 뒤를 안 막으면 8080이 80800에도 걸린다
방법 2 — awk: 필드를 직접 다루므로 의도가 가장 또렷하다.
# $1(첫 필드)이 포트와 정확히 같은 줄을 봤으면 found=1, END에서 종료 코드 결정if awk -v p="$PORT" '$1 == p { found = 1 } END { exit found ? 0 : 1 }' "$lease_file"; then echo "port $PORT 사용 중"fi
장점: $1 == p로 “첫 필드가 정확히 일치”를 그대로 표현. 숫자 비교라 부분 매칭 사고가 없다
단점: 값 하나만 확인하기엔 살짝 무겁다
{ exit 0 } END { exit 1 }로 짜지 말 것
본문 블록에서 exit를 부르면 awk는 곧장 종료하지 않고 END 블록을 먼저 실행한다. 그래서 '$1 == p { exit 0 } END { exit 1 }'는 매칭이 돼도 END의 exit 1이 코드를 덮어써 항상 실패로 끝난다. 위처럼 플래그를 세우고 종료 코드는 END에서 한 번만 정하는 게 안전하다.
방법 3 — while read: 셸 안에서 각 필드를 변수로 받아 뒤에 로직을 더 붙이기 좋다.
found=0while read -r port name state _; do if [[ "$port" == "$PORT" ]]; then found=1 break fidone < "$lease_file"[[ $found -eq 1 ]] && echo "port $PORT 사용 중 (name=$name)"
장점: 매칭된 줄의 다른 필드(name, state)까지 꺼내 셸에서 가공하기 쉽다
단점: 가장 장황하고 느리다. 단순 존재 확인에는 과하다
어떤 걸 고를까
단순히 “있는지 없는지”만 → grep -q 또는 awk '... { exit 0 }'
필드 위치·정확 일치가 중요 → awk ($1 == 값)
매칭된 줄의 다른 필드까지 셸에서 가공 → while read -r
파이프(grep | awk | ...)를 길게 잇기 전에, awk 하나로 끝나는지부터 따져보는 게 좋다.
지금까지가 텍스트를 읽고·바꾸고·골라내는 도구들이다. 다음 편에서는 이 도구들로 견고한 스크립트를 짜는 법 — 임시 파일·Job Control·에러 핸들링·실전 템플릿 — 을 다룬다. 4편으로 이어진다.