Shell 기초와 실행 환경

Shell 기초와 실행 환경

다음: 02_Parameter_Expansion.md


이 레슨에서는 쉘 실행 환경, 다양한 쉘 종류, 시작 메커니즘, 그리고 스크립트가 다른 컨텍스트에서 어떻게 동작하는지에 영향을 주는 기본 개념을 탐구합니다.

1. 쉘의 종류

다양한 쉘은 서로 다른 기능과 호환성 수준을 제공합니다. 이러한 차이점을 이해하는 것은 이식 가능한 스크립트를 작성하는 데 중요합니다.

쉘 비교 표

기능 bash sh (dash) zsh fish
POSIX 호환 대부분 대부분 아니오
배열(Arrays) 아니오 예 (더 좋음)
연관 배열(Associative arrays) 예 (4.0+) 아니오
[[ ]] 테스트 아니오 아니오
프로세스 치환(Process substitution) 아니오
Here 문자열(Here strings) 아니오
산술 연산 (( )) 아니오 아니오
명령 완성(Command completion) 좋음 기본 탁월 탁월
시작 성능 중간 빠름 느림 중간
스크립팅 초점 아니오
Debian/Ubuntu 기본값 bash dash (/bin/sh) bash fish
설정 문법 bash POSIX sh zsh-extended Fish-specific

각 쉘을 사용해야 하는 경우

bash: 범용 스크립팅, 대부분의 시스템에서 사용 가능, 기능과 이식성의 좋은 균형.

#!/bin/bash
# 배열, [[, 프로세스 치환이 필요한 스크립트에는 bash 사용
declare -A config
config[host]="localhost"
config[port]=8080

if [[ -n "${config[host]}" ]]; then
    echo "Host: ${config[host]}"
fi

sh (POSIX): 최대 이식성, 임베디드 시스템, 최소 환경.

#!/bin/sh
# POSIX 호환 스크립트 - bashisms 없음
# 배열 없음, [[ 없음, 프로세스 치환 없음

if [ -n "$HOST" ]; then
    echo "Host: $HOST"
fi

# 정규식을 사용하는 [[ 대신 case 사용
case "$filename" in
    *.txt) echo "Text file" ;;
    *.log) echo "Log file" ;;
esac

zsh: 대화형 사용, 고급 완성 기능, 더 나은 배열 처리.

#!/bin/zsh
# zsh는 더 강력한 배열 기능을 가짐
array=(one two three)
echo $array[1]  # zsh 배열은 1-indexed (bash는 0-indexed 사용)

# 고급 글로빙
setopt extended_glob
files=(^*.txt)  # .txt를 제외한 모든 파일

fish: 대화형 쉘, 사용자 친화적, 이식 가능한 스크립트에는 적합하지 않음.

#!/usr/bin/fish
# Fish는 다른 문법을 가짐 - POSIX 호환하지 않음
set host localhost
set port 8080

if test -n "$host"
    echo "Host: $host"
end

2. POSIX 호환성

POSIX (Portable Operating System Interface)는 쉘 동작에 대한 표준을 정의합니다. POSIX 호환 스크립트는 모든 POSIX 쉘(sh, bash, dash, ksh 등)에서 실행됩니다.

POSIX vs Bash-isms

기능 POSIX sh bash 확장
테스트 명령 [ ] [[ ]]
문자열 비교 [ "$a" = "$b" ] [[ $a == $b ]]
정규식 매칭 (grep 사용) [[ $str =~ regex ]]
배열 지원 안 됨 arr=(1 2 3)
함수 func() { } function func { }
산술 연산 expr, $(( )) let, (( ))
프로세스 치환 지원 안 됨 <(cmd), >(cmd)
Here 문자열 지원 안 됨 <<< "string"
지역 변수 POSIX에 없음 local var=value

이식 가능한 POSIX 스크립트 작성

#!/bin/sh
# POSIX 호환 스크립트 예제

# [[ ]] 대신 [ ] 사용
if [ "$1" = "start" ]; then
    echo "Starting service..."
fi

# 산술 연산에 $(( )) 사용 (이것은 POSIX입니다)
count=0
count=$((count + 1))

# 패턴 매칭에 case 사용
case "$filename" in
    *.tar.gz|*.tgz)
        echo "Compressed tarball"
        ;;
    *.zip)
        echo "ZIP archive"
        ;;
    *)
        echo "Unknown format"
        ;;
esac

# 배열 피하기 - 위치 매개변수나 임시 파일 사용
set -- "item1" "item2" "item3"
for item in "$@"; do
    echo "$item"
done

# 백틱이 아닌 명령 치환 $(cmd) 사용
current_dir=$(pwd)

# 이식 가능한 방식으로 명령 존재 확인
if command -v docker >/dev/null 2>&1; then
    echo "Docker is installed"
fi

3. 쉘 모드

쉘은 호출 방식에 따라 다른 모드로 작동합니다. 이는 어떤 시작 파일을 읽을지에 영향을 줍니다.

Login vs Non-Login 쉘

Login 쉘: 로그인할 때 시작됨(SSH, 콘솔 로그인, bash --login).

Non-login 쉘: 기존 세션에서 시작됨(GUI에서 터미널 열기, bash에서 bash 실행).

쉘이 login 쉘인지 테스트:

#!/bin/bash
# login 쉘로 실행 중인지 확인
if shopt -q login_shell; then
    echo "This is a login shell"
else
    echo "This is a non-login shell"
fi

# 대체 방법
case "$-" in
    *l*) echo "Login shell" ;;
    *) echo "Non-login shell" ;;
esac

Interactive vs Non-Interactive 쉘

Interactive: 터미널 연결됨, 사용자 입력 수락(일반 터미널 세션).

Non-interactive: 스크립트 실행, 터미널 상호작용 없음.

쉘이 대화형인지 테스트:

#!/bin/bash
# 대화형으로 실행 중인지 확인
if [[ $- == *i* ]]; then
    echo "Interactive shell"
else
    echo "Non-interactive shell (script)"
fi

# 대체 방법
case "$-" in
    *i*) echo "Interactive" ;;
    *) echo "Non-interactive" ;;
esac

# stdin이 터미널인지 확인
if [ -t 0 ]; then
    echo "stdin is a terminal"
else
    echo "stdin is not a terminal (piped/redirected)"
fi

4. 시작 파일 로딩 순서

bash가 설정 파일을 읽는 순서는 쉘 모드에 따라 다릅니다.

시작 순서 다이어그램

Login Shell (bash --login 또는 SSH)
├── /etc/profile (시스템 전체)
│   └── /etc/profile.d/*.sh (/etc/profile이 소싱하는 경우)
└── 첫 번째로 발견된 파일:
    ├── ~/.bash_profile
    ├── ~/.bash_login  (~/.bash_profile이 없는 경우)
    └── ~/.profile     (위 두 파일이 모두 없는 경우)
        └── (많은 .bash_profile 파일이 ~/.bashrc를 소싱함)

Non-Login Interactive Shell (터미널 윈도우)
├── /etc/bash.bashrc (Debian/Ubuntu)
└── ~/.bashrc

Non-Interactive Shell (스크립트)
├── $BASH_ENV (설정된 경우, 소싱할 파일 경로)
└── (일반적으로 없음)

Login Shell 종료
└── ~/.bash_logout

시작 파일 목적

파일 목적 일반적인 내용
/etc/profile 시스템 전체 login 설정 PATH, LANG, umask
/etc/bash.bashrc 시스템 전체 대화형 설정 PS1, 별칭 (Debian/Ubuntu)
~/.bash_profile 사용자 login 설정 ~/.bashrc 소싱, PATH 설정
~/.bashrc 사용자 대화형 설정 별칭, 함수, PS1
~/.profile POSIX login 설정 이식 가능한 login 설정
~/.bash_logout 로그아웃 시 정리 화면 지우기, 임시 파일 정리

시작 파일 구조 예제

~/.bash_profile (login 쉘 진입점):

# ~/.bash_profile - login 쉘이 로딩

# 사용자 바이너리를 위한 PATH 설정
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"

# .bashrc가 존재하면 로딩 (대화형 login 쉘용)
if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

# Login 전용 설정
echo "Last login: $(date)" >> ~/.login_log

~/.bashrc (대화형 쉘 설정):

# ~/.bashrc - 대화형 non-login 쉘이 로딩

# 대화형으로 실행 중이 아니면, 아무것도 하지 않음
case $- in
    *i*) ;;
      *) return;;
esac

# 히스토리 설정
HISTCONTROL=ignoreboth
HISTSIZE=10000
HISTFILESIZE=20000
shopt -s histappend

# 프롬프트
PS1='\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '

# 별칭
alias ll='ls -lah'
alias grep='grep --color=auto'

# 별도 파일에서 함수 로딩
if [ -f ~/.bash_functions ]; then
    . ~/.bash_functions
fi

~/.profile (POSIX 호환 login 설정):

# ~/.profile - POSIX 호환 login 설정
# bash가 sh로 호출될 때, 또는 다른 POSIX 쉘에서 사용

# PATH 설정
PATH="$HOME/bin:$PATH"
export PATH

# 환경 변수
export EDITOR=vim
export PAGER=less

# bash인 경우, .bashrc 소싱
if [ -n "$BASH_VERSION" ]; then
    if [ -f "$HOME/.bashrc" ]; then
        . "$HOME/.bashrc"
    fi
fi

5. 종료 코드(Exit Codes)

종료 코드는 명령이 성공했는지 실패했는지를 나타냅니다. 관례적으로:

종료 코드 규칙

코드 의미
0 성공
1 일반 오류
2 쉘 내장 명령의 잘못된 사용
126 명령을 찾았지만 실행 불가
127 명령을 찾을 수 없음
128 잘못된 exit 인자
128+N 치명적 오류 시그널 N (130 = Ctrl+C (SIGINT=2))
255 종료 상태 범위 초과

종료 코드 사용하기

#!/bin/bash

# $?로 종료 코드 확인
grep "pattern" file.txt
if [ $? -eq 0 ]; then
    echo "Pattern found"
else
    echo "Pattern not found"
fi

# 더 좋은 방법: if에서 직접 명령 사용
if grep "pattern" file.txt > /dev/null; then
    echo "Pattern found"
fi

# 함수에서 사용자 정의 종료 코드 반환
validate_input() {
    local input="$1"

    if [ -z "$input" ]; then
        echo "Error: input is empty" >&2
        return 1
    fi

    if ! [[ "$input" =~ ^[0-9]+$ ]]; then
        echo "Error: input must be numeric" >&2
        return 2
    fi

    if [ "$input" -lt 0 ] || [ "$input" -gt 100 ]; then
        echo "Error: input must be between 0 and 100" >&2
        return 3
    fi

    return 0
}

# 함수 사용 및 종료 코드 확인
if validate_input "42"; then
    echo "Input is valid"
else
    case $? in
        1) echo "Empty input" ;;
        2) echo "Not numeric" ;;
        3) echo "Out of range" ;;
    esac
fi

# 특정 코드로 스크립트 종료
check_prerequisites() {
    if ! command -v docker >/dev/null 2>&1; then
        echo "Error: docker not found" >&2
        exit 127
    fi

    if [ ! -r /etc/config.conf ]; then
        echo "Error: config file not readable" >&2
        exit 1
    fi
}

종료 코드 모범 사례

#!/bin/bash
set -e  # 오류 시 종료 (이것은 주의해서 사용)

# 명시적으로 오류 처리
perform_backup() {
    local source="$1"
    local dest="$2"

    if ! tar czf "$dest" "$source" 2>/dev/null; then
        echo "Backup failed" >&2
        return 1
    fi

    echo "Backup successful"
    return 0
}

# 적절한 오류 처리로 명령 체이닝
if perform_backup "/data" "/backup/data.tar.gz"; then
    echo "Cleaning up old backups..."
    find /backup -name "*.tar.gz" -mtime +7 -delete
else
    echo "Backup failed, keeping old backups"
    exit 1
fi

6. 쉘 옵션 개요

쉘 옵션은 쉘 동작을 제어합니다. 두 명령으로 옵션을 관리합니다:

  • set: POSIX 표준 옵션
  • shopt: Bash 전용 확장 옵션

중요한 set 옵션

#!/bin/bash

# 현재 옵션 표시
echo "$-"  # 예: "himBH" (각 문자는 활성 옵션)

# 옵션 활성화
set -e  # 오류 시 종료 (errexit)
set -u  # 정의되지 않은 변수 사용 시 오류 (nounset)
set -o pipefail  # 파이프라인에서 하나라도 실패하면 실패
set -x  # 실행 전 명령 출력 (xtrace)

# 옵션 비활성화
set +e  # 오류 시 종료 안 함
set +x  # 명령 출력 중지

# 옵션 결합
set -euo pipefail  # 일반적인 "strict mode"

# 예제: noclobber는 파일 덮어쓰기 방지
set -o noclobber
echo "test" > file.txt  # 파일 생성
echo "test" > file.txt  # 오류: 파일 존재
echo "test" >| file.txt  # >|로 noclobber 무시

일반적인 set 옵션

옵션 짧은 형태 설명
-e (errexit) -e 명령 실패 시 종료
-u (nounset) -u 정의되지 않은 변수 사용 시 오류
-x (xtrace) -x 실행 전 명령 출력
-o pipefail (긴 형태만) 파이프라인에서 하나라도 실패하면 실패
-o noclobber -C >로 파일 덮어쓰기 방지
-o noglob -f 경로명 확장 비활성화
-o vi (긴 형태만) Vi 스타일 명령줄 편집
-o emacs (긴 형태만) Emacs 스타일 편집 (기본값)

중요한 shopt 옵션

#!/bin/bash

# bash 확장 옵션 활성화
shopt -s extglob  # 확장 패턴 매칭
shopt -s globstar  # 재귀 glob을 위한 **
shopt -s nullglob  # 매칭 안 되는 glob은 빈 것으로 확장
shopt -s dotglob  # glob에 숨김 파일 포함
shopt -s nocaseglob  # 대소문자 구분 안 하는 글로빙

# 옵션 비활성화
shopt -u dotglob  # 숨김 파일 제외

# 옵션이 설정되었는지 확인
if shopt -q nullglob; then
    echo "nullglob is enabled"
fi

# 예제: nullglob
shopt -s nullglob
files=(*.txt)
if [ ${#files[@]} -eq 0 ]; then
    echo "No .txt files found"
else
    echo "Found ${#files[@]} .txt files"
fi

# 예제: globstar
shopt -s globstar
# 재귀적으로 모든 Python 파일 찾기
for file in **/*.py; do
    echo "$file"
done

# 예제: extglob (Lesson 04에서 더 자세히 다룸)
shopt -s extglob
rm !(*.txt|*.log)  # .txt와 .log 파일을 제외한 모든 파일 삭제

유용한 shopt 옵션

옵션 설명
extglob 확장 패턴 매칭 (!(pat), *(pat), 등)
globstar **는 재귀적으로 매칭
nullglob 매칭 안 되는 glob은 null로 확장, 리터럴 아님
dotglob 경로명 확장에 숨김 파일 포함
nocaseglob 대소문자 구분 안 하는 경로명 확장
failglob 매칭 안 되는 glob은 오류 발생
checkjobs 종료 전 실행 중인 작업 확인
autocd 디렉터리명만 입력해도 디렉터리 변경
cdspell cd 오류 자동 수정

Strict Mode 예제

#!/bin/bash
# 더 안전한 스크립트를 위한 strict mode

set -euo pipefail
IFS=$'\n\t'

# -e: 오류 시 종료
# -u: 정의되지 않은 변수 사용 시 오류
# -o pipefail: 파이프라인에서 하나라도 실패하면 실패
# IFS: 더 안전한 단어 분할

# 이제 오류가 스크립트를 중지시킴
command_that_fails  # 스크립트가 여기서 종료
echo "This won't execute"

# 명시적으로 오류 처리하려면:
if ! command_that_might_fail; then
    echo "Command failed, handling error"
    # 정리 작업 수행
    exit 1
fi

7. env 명령과 #!/usr/bin/env bash

#!/usr/bin/env bash를 사용하는 이유

셔뱅(shebang) 라인은 시스템에 어떤 인터프리터를 사용할지 알려줍니다. 두 가지 접근 방식:

직접 경로: #!/bin/bash - 빠름 (PATH 검색 없음) - 이식 가능하지 않음 (bash가 /usr/local/bin에 있을 수 있음)

env 접근: #!/usr/bin/env bash - 이식 가능 (PATH에서 bash를 찾음) - 현대 스크립트의 표준 - 다양한 시스템에서 작동

#!/usr/bin/env bash
# 이것은 PATH에서 bash가 어디 있든 찾아냄

# bash가 어디에 있는지 확인
which bash

# macOS: /bin/bash
# FreeBSD: /usr/local/bin/bash
# Nix: /nix/store/.../bin/bash

env를 사용하여 환경 설정

#!/usr/bin/env -S bash -euo pipefail
# -S 플래그는 여러 인자 전달 허용 (GNU env 8.30+)

# 이전 env를 위한 대안:
#!/usr/bin/env bash
set -euo pipefail

깨끗한 환경을 위한 env

# 깨끗한 환경으로 명령 실행
env -i bash --norc --noprofile

# 특정 변수만으로 실행
env -i HOME=/tmp USER=testuser bash

# 특정 변수 제거
env -u DISPLAY firefox

# 변수 추가
env FOO=bar ./script.sh

# 환경 검사
env | sort

모범 사례를 적용한 스크립트 템플릿

#!/usr/bin/env bash
# Script: example.sh
# Description: 모범 사례를 적용한 예제 스크립트
# Author: Your Name
# Created: 2026-02-13

# Strict mode
set -euo pipefail
IFS=$'\n\t'

# 오류 트랩
trap 'echo "Error on line $LINENO" >&2' ERR

# 유용한 쉘 옵션
shopt -s nullglob globstar

# 상수
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"

# 함수
usage() {
    cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] <argument>

Options:
    -h, --help      이 도움말 메시지 표시
    -v, --verbose   상세 출력 활성화
    -d, --debug     디버그 모드 활성화

Examples:
    $SCRIPT_NAME input.txt
    $SCRIPT_NAME -v input.txt
EOF
}

main() {
    # 메인 스크립트 로직
    echo "Running from: $SCRIPT_DIR"
    echo "Script name: $SCRIPT_NAME"
}

# 인자 파싱
while [[ $# -gt 0 ]]; do
    case "$1" in
        -h|--help)
            usage
            exit 0
            ;;
        -v|--verbose)
            set -x
            shift
            ;;
        -d|--debug)
            set -x
            shift
            ;;
        -*)
            echo "Unknown option: $1" >&2
            usage >&2
            exit 1
            ;;
        *)
            break
            ;;
    esac
done

# 메인 함수 실행
main "$@"

연습 문제

문제 1: 쉘 감지 스크립트

다음을 감지하는 스크립트를 작성하세요: - 실행 중인 쉘 (bash, zsh, sh 등) - login 쉘인지 non-login 쉘인지 - 대화형인지 비대화형인지 - 쉘 버전

예상 출력:

Shell: bash
Version: 5.1.16
Type: non-login, interactive

문제 2: 시작 파일 분석기

다음을 수행하는 스크립트를 작성하세요: - 시스템에 존재하는 모든 bash 시작 파일 나열 - login vs non-login 쉘에서 로딩되는 순서 표시 - 각 파일의 첫 5줄 표시 - 일반적인 실수 확인 (.bash_profile 대신 .bashrc에 별칭 설정 등)

문제 3: 종료 코드 로거

모든 명령을 래핑하는 함수를 작성하세요: - 실행되는 명령 로깅 - 종료 코드 캡처 및 로깅 - 실행 시간 로깅 - 로그 파일에 추가: timestamp | command | exit_code | duration

사용 예제:

log_command ls -la /nonexistent
# 로깅되어야 함: 2026-02-13 10:30:45 | ls -la /nonexistent | 2 | 0.003s

문제 4: 이식 가능한 스크립트 검사기

다른 bash 스크립트를 분석하고 다음을 보고하는 스크립트를 작성하세요: - 사용된 비POSIX 구조 ([[, 배열 등) - sh에서 실패할 Bashisms - 더 이식 가능하게 만들기 위한 제안 - "이식성 점수" (0-100%)

힌트: [[, declare, function 키워드 등의 패턴을 검색하세요.

문제 5: 환경 스냅샷

다음을 수행하는 스크립트를 작성하세요: - 현재 환경 변수를 파일에 저장 - 현재 쉘 옵션(set -o, shopt -p)을 파일에 저장 - 저장된 상태에서 환경을 복원할 수 있음 - 저장된 상태와 현재 상태의 차이 표시

사용법:

./envsnap.sh save snapshot.env
# ... 변경 수행 ...
./envsnap.sh diff snapshot.env
./envsnap.sh restore snapshot.env

다음: 02_Parameter_Expansion.md

to navigate between lessons