셸 스크립팅 · 반복과 함수 · 이론
반복은 글로브로, 반환은 종료 코드로
한 줄 요약
셸 함수는 값을 반환하지 않는다. 종료 코드로 참·거짓을 알리고, 값은 표준출력으로 내보낸다. 그리고 파일 목록을 얻으려고 ls 를 파싱하면 언젠가 반드시 깨진다.
왜 이게 필요했나
for f in $(ls *.log) 는 배우기 쉽고, 그래서 많이 쓰인다. 그런데 이 한 줄에는 함정이 세 개 겹쳐 있다.
1. ls 의 출력을 명령 치환으로 받으면 공백 기준으로 단어 분리된다. access 2026.log 는 두 개가 된다.
2. 매칭되는 파일이 하나도 없으면 ls 가 오류를 내고, 그 오류 메시지가 루프의 입력이 된다.
3. 파일 이름에 개행이나 글로브 문자가 들어 있으면 예측할 수 없게 동작한다.
정답은 셸에게 직접 시키는 것이다.
for f in /var/log/*.log; do [ -f "$f" ] || continue ...done여기서 [ -f "$f" ] || continue 가 핵심이다. 매칭되는 파일이 없으면 글로브 패턴이 그대로 문자열로 남기 때문에 이 방어선이 없으면 /var/log/*.log 라는 존재하지 않는 파일을 처리하려 든다. 실제 실습에서도 이 함정이 나온다.
어떻게 동작하나
반복 세 가지 형태를 상황에 맞게 쓴다.
for x in a b c- 정해진 목록for f in 경로/*.log- 파일 순회. 셸이 직접 확장한다while IFS=, read -r name role; do ... done < users.csv- 줄 단위 입력
세 번째 형태에서 IFS=, 를 read 앞에 붙이면 그 명령에만 구분자가 적용된다. -r 은 백슬래시를 이스케이프로 해석하지 말라는 뜻으로, 사실상 항상 붙인다.
함수의 인터페이스는 다음 세 가지로 이루어진다.
| 무엇 | 어디로 |
| --- | --- |
| 계산한 값 | 표준출력 (echo) |
| 성공/실패, 참/거짓 | 종료 코드 (return 0 / return 1) |
| 사람에게 보낼 메시지 | 표준에러 |
is_number 123 같은 판정 함수가 결과를 echo yes 로 출력하면 if is_number "$x" 처럼 쓸 수 없다. 판정은 반드시 종료 코드로 알린다.
인자 다루기에서 가장 중요한 것은 "$@" 와 $* 의 차이다. "$@" 는 인자 하나하나를 각각 따옴표로 감싼 것처럼 펼치고, $* 는 전부 하나의 문자열로 합친다. 명령을 그대로 다시 실행하는 재시도 함수 같은 곳에서는 반드시 "$@" 여야 한다.
함수 안의 변수는 기본적으로 전역이다. local 을 붙이지 않으면 호출한 쪽의 같은 이름 변수를 조용히 덮어쓴다. 함수 안에서 만든 변수는 예외 없이 local 로 선언하는 습관을 들이는 게 좋다.
현장에서 만나는 모습
라이브러리 분리. 여러 스크립트가 같은 함수를 복붙하고 있으면 고칠 곳이 여러 곳이 된다. lib.sh 에 함수를 모으고 . /root/bin/lib.sh 로 불러오면 한 곳만 고치면 된다. source 와 . 는 같은 명령이고, 새 프로세스를 만들지 않고 현재 셸에서 실행하기 때문에 함수 정의가 남는다.
루프 안의 외부 명령이 성능을 갉아먹는다. 1000줄을 도는 루프 안에서 $(echo "$v" | sed ...) 를 부르면 프로세스가 2000개 생긴다. 같은 일을 ${v//old/new} 로 하면 프로세스가 0개다. 셸 스크립트가 느리다는 인상의 절반은 여기서 온다.
병렬화가 필요할 때는 xargs -P 나 & + wait 를 쓴다. 다만 병렬로 돌린 작업의 실패를 놓치지 않으려면 각 PID 를 모아 두었다가 wait "$pid" 로 하나씩 상태를 확인해야 한다.
다음 실습에서 할 것
숫자 출력 루프에서 시작해 글로브 순회, CSV 읽기, 함수 분리와 source, 종료 코드 기반 판정, 공백이 든 인자 안전 처리, 가변 인자 합산을 거쳐, 마지막에는 디렉터리를 받아 요약을 내는 스크립트를 만든다.