스크립트는 명령어로 만든 작은 작업자입니다
터미널에서 같은 명령을 매일 반복한다면, 그 명령들은 이미 하나의 작업 절차가 된 것입니다. Bash 스크립트는 그 절차를 파일에 적어 두고 필요할 때 다시 실행하는 방법입니다. 예를 들어 로그를 백업하고, 오래된 임시 파일을 찾고, 서버 상태를 확인하는 일을 매번 손으로 입력하지 않아도 됩니다.
스크립트는 특별한 마법이 아닙니다. 지금까지 배운 cp, mv, grep, find, tar, df, systemctl 같은 명령어를 순서대로 적은 텍스트 파일입니다. 차이는 셸이 그 파일을 한 줄씩 읽고 실행한다는 점입니다. 명령어 지식이 실제 시간 절약으로 바뀌는 가장 직접적인 단계가 바로 스크립트입니다.
이번 장의 실습은 이전에 만든 작업 공간을 기준으로 합니다.
mkdir -p ~/linux-practice/scripts
cd ~/linux-practice/scripts
pwdpwd 결과가 .../linux-practice/scripts인지 먼저 확인하세요. 자동화는 편하지만, 잘못된 위치에서 실행하면 잘못된 파일을 자동으로 처리할 수도 있습니다.
AI 생성 이미지
첫 Bash 스크립트 만들기
스크립트 파일을 하나 만듭니다.
nano hello.sh다음 내용을 입력합니다.
#!/usr/bin/env bash
echo "Hello, Linux"
echo "현재 사용자: $USER"
echo "현재 위치: $(pwd)"첫 줄의 #!를 shebang이라고 합니다. 이 파일을 어떤 프로그램으로 실행할지 알려 주는 표시입니다. #!/usr/bin/env bash는 현재 환경에서 찾을 수 있는 bash로 이 스크립트를 실행하라는 뜻입니다.
저장한 뒤 먼저 bash 명령으로 실행해 봅니다.
bash hello.sh이 방식은 파일에 실행 권한이 없어도 됩니다. 셸 프로그램인 bash에게 파일을 읽어 실행하라고 직접 부탁했기 때문입니다.
이번에는 파일 자체를 실행할 수 있게 권한을 줍니다.
chmod +x hello.sh
./hello.sh여기서 ./hello.sh의 ./는 "현재 디렉터리에 있는 hello.sh"라는 뜻입니다. Linux는 보통 현재 디렉터리를 자동으로 명령 검색 경로에 넣지 않습니다. 그래서 현재 위치의 스크립트는 hello.sh가 아니라 ./hello.sh처럼 실행합니다.
자주 쓰는 스크립트라면 셸 환경을 내 작업 방식에 맞게 바꾸기에서 다룬 것처럼 ~/.local/bin에 넣고 PATH에 포함시킬 수 있습니다. 다만 처음에는 현재 디렉터리에서 ./스크립트명으로 실행하며 동작을 확인하는 편이 안전합니다.
스크립트가 실행되는 위치 이해하기
초보자가 자주 헷갈리는 부분이 있습니다. 스크립트 파일이 있는 위치와 스크립트를 실행한 현재 위치는 항상 같지 않습니다.
예를 들어 홈 디렉터리에서 다음처럼 실행할 수 있습니다.
cd ~
~/linux-practice/scripts/hello.sh이때 $(pwd)는 스크립트가 저장된 디렉터리가 아니라, 명령을 실행한 현재 디렉터리를 출력합니다. 스크립트 안에서 상대 경로를 사용할 때 이 차이가 중요합니다.
스크립트 파일이 있는 디렉터리를 기준으로 작업해야 한다면 다음과 같은 코드를 사용할 수 있습니다.
script_dir="$(cd "$(dirname "$0")" && pwd)"
echo "스크립트 위치: $script_dir"처음부터 모든 스크립트에 넣을 필요는 없습니다. 하지만 상대 경로로 설정 파일이나 보조 파일을 읽는 스크립트라면 "현재 위치 기준인지, 스크립트 위치 기준인지"를 반드시 정해야 합니다.
변수와 인자로 스크립트에 값 전달하기
변수는 반복해서 사용할 값에 이름을 붙이는 방법입니다.
#!/usr/bin/env bash
backup_dir="$HOME/linux-practice/backups"
today="$(date +%Y%m%d)"
echo "백업 디렉터리: $backup_dir"
echo "오늘 날짜: $today"변수를 사용할 때는 가능한 한 큰따옴표로 감쌉니다.
"$backup_dir"경로에 공백이 들어 있거나 값이 비어 있을 때도 의도하지 않은 단어 분리가 일어나지 않게 하기 위해서입니다. 스크립트에서 따옴표는 장식이 아니라 안전장치입니다.
스크립트 실행 시 뒤에 붙이는 값은 인자로 전달됩니다.
nano greet.sh#!/usr/bin/env bash
name="${1:-guest}"
echo "안녕하세요, $name 님"실행해 봅니다.
chmod +x greet.sh
./greet.sh
./greet.sh minsu$1은 첫 번째 인자입니다. ${1:-guest}는 첫 번째 인자가 있으면 그 값을 쓰고, 없으면 guest를 쓰라는 뜻입니다. 자주 쓰는 특수 변수는 다음과 같습니다.
$0: 실행된 스크립트 이름$1,$2: 첫 번째, 두 번째 인자$#: 전달된 인자 개수$@: 전달된 모든 인자$?: 직전 명령의 종료 코드
인자를 제대로 받았는지 확인하는 습관도 중요합니다.
#!/usr/bin/env bash
if [ "$#" -ne 1 ]; then
echo "사용법: $0 파일경로" >&2
exit 1
fi
file="$1"
echo "대상 파일: $file">&2는 메시지를 표준 오류로 보내는 리다이렉션입니다. 정상 결과와 오류 메시지를 구분할 수 있어 스크립트가 다른 명령과 연결될 때 유용합니다.
조건문으로 상황에 따라 다르게 실행하기
스크립트는 단순히 위에서 아래로 실행할 수도 있지만, 실제 자동화에서는 조건 판단이 필요합니다. "파일이 있으면 백업하고, 없으면 오류를 낸다" 같은 흐름입니다.
#!/usr/bin/env bash
file="$1"
if [ -f "$file" ]; then
echo "일반 파일입니다: $file"
elif [ -d "$file" ]; then
echo "디렉터리입니다: $file"
else
echo "파일이나 디렉터리를 찾을 수 없습니다: $file" >&2
exit 1
fi[는 조건을 검사하는 명령입니다. 자주 쓰는 검사는 다음과 같습니다.
-f 경로: 일반 파일이면 참-d 경로: 디렉터리이면 참-e 경로: 존재하면 참-r 경로: 읽을 수 있으면 참-w 경로: 쓸 수 있으면 참-x 경로: 실행할 수 있으면 참문자열1 = 문자열2: 문자열이 같으면 참숫자1 -gt 숫자2: 왼쪽 숫자가 더 크면 참
조건문에서는 명령 자체의 성공 여부를 바로 판단할 수도 있습니다.
if systemctl is-active --quiet ssh; then
echo "ssh 서비스가 실행 중입니다."
else
echo "ssh 서비스가 실행 중이 아닙니다."
fiUbuntu 계열에서는 SSH 서비스 이름이 보통 ssh이고, Fedora/RHEL/Arch 계열에서는 sshd인 경우가 많습니다. 서비스 이름을 모를 때는 다음처럼 확인합니다.
systemctl list-units --type=service | grep -E 'ssh|sshd'반복문으로 여러 파일 처리하기
반복문은 같은 작업을 여러 대상에 적용할 때 씁니다. 예를 들어 디렉터리 안의 .log 파일을 차례대로 확인할 수 있습니다.
#!/usr/bin/env bash
log_dir="$HOME/linux-practice/logs"
for file in "$log_dir"/*.log; do
[ -e "$file" ] || continue
echo "파일: $file"
wc -l "$file"
done[ -e "$file" ] || continue는 매칭되는 .log 파일이 하나도 없을 때를 대비한 안전장치입니다. 파일이 없는데도 *.log라는 글자 자체가 처리되는 일을 피합니다.
목록을 직접 지정할 수도 있습니다.
for name in web db cache; do
echo "점검 대상: $name"
done조건이 참인 동안 반복하려면 while을 씁니다. 파일을 한 줄씩 읽을 때 자주 사용합니다.
while IFS= read -r line; do
echo "읽은 줄: $line"
done < servers.txtIFS= read -r 형태는 줄 앞뒤 공백과 역슬래시를 최대한 그대로 읽기 위한 습관적인 형태입니다. 설정 파일이나 목록 파일을 안전하게 읽을 때 유용합니다.
함수와 종료 코드로 실패를 다루기
스크립트가 길어지면 같은 코드가 반복됩니다. 함수는 여러 명령을 이름 붙인 작업 단위로 묶는 방법입니다.
#!/usr/bin/env bash
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
die() {
echo "오류: $*" >&2
exit 1
}
file="$1"
[ -n "$file" ] || die "파일 경로를 입력하세요."
[ -f "$file" ] || die "일반 파일이 아닙니다: $file"
log "파일 확인 완료: $file"exit 0은 성공, exit 1처럼 0이 아닌 값은 실패를 뜻합니다. Linux 명령어는 이 종료 코드를 통해 성공과 실패를 알려 줍니다. 스크립트도 마찬가지입니다.
직전 명령의 종료 코드는 $?로 볼 수 있습니다.
ls /no-such-file
echo "$?"하지만 실제 스크립트에서는 $?를 따로 저장하기보다 if 명령; then 형태로 바로 판단하는 편이 읽기 쉽습니다.
if cp "$src" "$dest"; then
echo "복사 성공"
else
echo "복사 실패" >&2
exit 1
fi자동화에서 실패를 무시하면 문제가 커집니다. 백업이 실패했는데도 성공처럼 다음 단계로 넘어가면, 나중에 복구할 파일이 없을 수 있습니다. 스크립트는 "실패했을 때 멈추고 알려 주는 것"까지 포함해야 합니다.
안전한 스크립트를 위한 기본 설정
실전 스크립트의 앞부분에는 다음 설정을 자주 둡니다.
set -euo pipefail각 옵션의 의미는 다음과 같습니다.
set -e: 명령이 실패하면 스크립트를 중단합니다.set -u: 정의되지 않은 변수를 사용하면 오류로 처리합니다.set -o pipefail: 파이프라인 중간 명령이 실패해도 실패로 감지합니다.
이 설정은 안전망이지만 모든 실수를 막아 주지는 않습니다. 어떤 명령은 실패가 예상되는 상황에서 일부러 조건문 안에 넣어야 합니다.
if grep -q "ERROR" app.log; then
echo "오류가 발견되었습니다."
else
echo "오류가 없습니다."
figrep은 찾는 내용이 없으면 종료 코드 1을 반환합니다. 이것은 "프로그램이 고장 났다"가 아니라 "검색 결과가 없다"는 뜻입니다. 그래서 이런 경우에는 if 안에서 의미를 해석해야 합니다.
임시 파일을 만들 때는 mktemp와 trap을 사용합니다.
tmp_file="$(mktemp)"
trap 'rm -f "$tmp_file"' EXIT
echo "작업 중" > "$tmp_file"
cat "$tmp_file"mktemp는 충돌 가능성이 낮은 임시 파일 이름을 만들어 줍니다. trap ... EXIT는 스크립트가 끝날 때 임시 파일을 지우도록 예약합니다. 중간에 오류가 나도 정리 작업이 실행된다는 점이 중요합니다.
삭제 명령을 자동화할 때는 특히 조심합니다. 먼저 대상을 출력하고, 그다음 삭제하도록 나누어야 합니다.
find "$target_dir" -type f -name '*.tmp' -mtime +7 -print출력된 목록이 맞을 때만 삭제 명령으로 바꿉니다.
find "$target_dir" -type f -name '*.tmp' -mtime +7 -deleterm -rf "$some_dir" 같은 명령은 변수 값이 비어 있거나 예상과 다르면 큰 피해를 줄 수 있습니다. 자동 삭제 스크립트에서는 pwd, 대상 경로 출력, find -print 확인, 조건 검사 순서를 생략하지 않습니다.
스크립트 검사용 도구로 shellcheck를 사용할 수도 있습니다.
Ubuntu/Debian 계열:
sudo apt update
sudo apt install shellcheckFedora/RHEL 계열:
sudo dnf install ShellCheckArch 계열:
sudo pacman -S shellcheck사용법은 간단합니다.
shellcheck backup-logs.sh경고가 모두 치명적인 오류는 아니지만, 따옴표 누락이나 위험한 변수 사용을 찾는 데 도움이 됩니다.
실전 예제 1: 로그 백업 스크립트
먼저 연습용 로그를 만듭니다.
mkdir -p ~/linux-practice/logs
printf 'INFO startnERROR failednINFO endn' > ~/linux-practice/logs/app.log이제 로그 디렉터리를 압축 백업하는 스크립트를 만듭니다.
nano backup-logs.sh#!/usr/bin/env bash
set -euo pipefail
src_dir="$HOME/linux-practice/logs"
backup_dir="$HOME/linux-practice/backups"
timestamp="$(date +%Y%m%d-%H%M%S)"
backup_file="$backup_dir/logs-$timestamp.tar.gz"
[ -d "$src_dir" ] || { echo "로그 디렉터리가 없습니다: $src_dir" >&2; exit 1; }
mkdir -p "$backup_dir"
tar -czf "$backup_file" -C "$src_dir" .
echo "백업 완료: $backup_file"
ls -lh "$backup_file"실행합니다.
chmod +x backup-logs.sh
./backup-logs.shtar -czf는 gzip 압축된 tar 파일을 만듭니다. -C "$src_dir" .는 먼저 로그 디렉터리로 이동한 것처럼 처리한 뒤 그 안의 내용을 백업하라는 뜻입니다. 이렇게 하면 압축 파일 안에 /home/사용자명/... 같은 긴 절대 경로가 들어가지 않습니다.
실제 서버의 /var/log를 백업하려면 읽기 권한이 필요할 수 있습니다. 처음부터 sudo를 붙여 전체 로그를 압축하기보다, 어떤 로그가 필요한지 ls -l /var/log와 du -sh /var/log/*로 확인한 뒤 범위를 좁히는 편이 안전합니다.
실전 예제 2: 오래된 임시 파일 정리
삭제 자동화는 반드시 "미리 보기"가 먼저입니다.
nano cleanup-old-files.sh#!/usr/bin/env bash
set -euo pipefail
target_dir="${1:-$HOME/linux-practice/tmp}"
days="${2:-7}"
[ -d "$target_dir" ] || { echo "디렉터리가 없습니다: $target_dir" >&2; exit 1; }
if ! [[ "$days" =~ ^[0-9]+$ ]]; then
echo "보관 일수는 숫자여야 합니다: $days" >&2
exit 1
fi
echo "대상 디렉터리: $target_dir"
echo "기준: ${days}일보다 오래된 *.tmp 파일"
echo "삭제 예정 목록:"
find "$target_dir" -type f -name '*.tmp' -mtime +"$days" -print
if [ "${CONFIRM_DELETE:-no}" = "yes" ]; then
find "$target_dir" -type f -name '*.tmp' -mtime +"$days" -delete
echo "삭제 완료"
else
echo "미리 보기만 실행했습니다. 실제 삭제는 다음처럼 실행하세요:"
echo "CONFIRM_DELETE=yes $0 '$target_dir' '$days'"
fi실행해 봅니다. find -mtime +7 조건을 확인할 수 있도록 일부러 오래된 파일을 만듭니다.
mkdir -p ~/linux-practice/tmp
touch ~/linux-practice/tmp/recent.tmp
touch -d '8 days ago' ~/linux-practice/tmp/old.tmp
chmod +x cleanup-old-files.sh
./cleanup-old-files.sh ~/linux-practice/tmp 7출력 목록에 old.tmp만 보이는지 확인한 뒤에만 실제 삭제를 실행합니다.
CONFIRM_DELETE=yes ./cleanup-old-files.sh ~/linux-practice/tmp 7환경 변수 CONFIRM_DELETE=yes를 요구하게 만든 이유는 실수로 스크립트를 실행했을 때 바로 삭제되지 않게 하기 위해서입니다. 자동화할수록 확인 절차는 더 명확해야 합니다.
실전 예제 3: 서버 상태 점검 스크립트
서버를 운영하다 보면 디스크, 메모리, 서비스 상태를 자주 확인합니다. 다음 스크립트는 기본 상태를 한 번에 출력하고 로그 파일로도 남깁니다.
nano check-server.sh#!/usr/bin/env bash
set -euo pipefail
service_name="${1:-ssh}"
log_dir="$HOME/linux-practice/status-logs"
log_file="$log_dir/status-$(date +%Y%m%d).log"
mkdir -p "$log_dir"
{
echo "===== $(date '+%Y-%m-%d %H:%M:%S') ====="
echo "[디스크]"
df -h /
echo
echo "[메모리]"
free -h
echo
echo "[서비스: $service_name]"
if command -v systemctl >/dev/null 2>&1; then
if systemctl is-active --quiet "$service_name"; then
echo "실행 중"
else
echo "실행 중이 아님"
fi
else
echo "systemctl을 사용할 수 없습니다."
fi
echo
} | tee -a "$log_file"Ubuntu에서 SSH 서비스를 확인한다면 다음처럼 실행합니다.
chmod +x check-server.sh
./check-server.sh sshFedora, RHEL, Arch 계열에서 서비스 이름이 sshd라면 다음처럼 실행합니다.
./check-server.sh sshdtee -a는 화면에도 보여 주고 파일에도 추가합니다. 상태 점검 스크립트는 나중에 cron이나 systemd timer와 연결할 수 있습니다. 이때는 상대 경로보다 절대 경로를 쓰고, 로그 파일을 남기는 것이 좋습니다.
실습 과제
backup-logs.sh를 수정해 백업이 끝난 뒤tar -tzf 백업파일 | head로 압축 파일 안의 목록을 확인하게 만들어 보세요.greet.sh를 수정해 인자가 2개 이상이면 사용법을 출력하고 종료 코드 1로 끝나게 만들어 보세요.check-server.sh에uptime출력을 추가해 현재 부하와 실행 시간을 함께 기록해 보세요.cleanup-old-files.sh에서 삭제 대상을*.tmp가 아니라 첫 번째 인자로 받은 패턴으로 바꾸려면 어떤 검사가 추가로 필요할지 생각해 보세요.
문제 해결 시나리오: 직접 실행하면 되는데 예약 작업에서는 실패한다
스크립트는 터미널에서 잘 실행되는데 cron이나 systemd timer에서는 실패할 수 있습니다. 흔한 원인은 다음과 같습니다.
첫째, 현재 디렉터리가 다릅니다. 예약 작업은 내가 생각한 디렉터리에서 실행되지 않을 수 있습니다. 스크립트 안에서는 중요한 경로를 절대 경로로 적거나, 시작 부분에서 작업 디렉터리를 명확히 이동합니다.
cd "$HOME/linux-practice" || exit 1둘째, PATH가 다릅니다. 터미널에서는 찾는 명령을 예약 작업에서는 못 찾을 수 있습니다. 이때는 command -v tar, command -v rsync로 위치를 확인하고, 필요하면 /usr/bin/tar처럼 절대 경로를 사용합니다.
셋째, 오류가 화면에만 출력되고 기록되지 않습니다. 예약 작업에서는 결과를 로그로 남겨야 원인을 볼 수 있습니다.
/home/user/linux-practice/scripts/check-server.sh ssh >> /home/user/linux-practice/status.log 2>&1넷째, 권한이 다릅니다. 내 계정으로는 읽을 수 없는 파일을 백업하거나, 관리자 권한이 필요한 서비스를 조작하려 하면 실패합니다. 이때는 무조건 sudo를 붙이기보다 어떤 파일과 명령에 권한이 필요한지 ls -l, systemctl status, 로그 출력으로 먼저 확인합니다.
스크립트 자동화의 핵심은 많이 쓰는 명령을 한 파일에 넣는 것에서 끝나지 않습니다. 입력값을 확인하고, 실패를 감지하고, 삭제 전에 보여 주고, 실행 결과를 로그로 남기는 습관까지 포함해야 합니다. 그렇게 만든 스크립트는 단순한 명령 모음이 아니라 믿고 다시 실행할 수 있는 작업 도구가 됩니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.