Shell 스크립팅 실전 가이드 | Bash, Zsh, PowerShell 비교
이 글의 핵심
같은 반복 작업이라도 리눅스 서버에서는 Bash, macOS 터미널에서는 Zsh, 윈도우에서는 PowerShell을 쓰게 되는 경우가 많습니다. 텍스트를 넘기는 Unix 파이프와 객체를 넘기는 PowerShell 파이프라인의 차이에서 출발해, 각 셸로 실무 자동화 스크립트를 작성하고 어떤 상황에서 어떤 셸을 고를지 정리합니다.
같은 명령이 Bash에서는 되는데 PowerShell에서는 안 되거나, 로컬 Zsh에서 잘 돌던 스크립트가 CI의 Bash에서 깨지는 일은 셸마다 문법과 동작이 다르기 때문에 생깁니다. 이 글은 Bash, Zsh, PowerShell의 차이를 비교한 뒤 파이프와 리다이렉션, 스크립트 작성과 에러 처리, 백업·배포·로그 분석 자동화 예제, 디버깅과 트러블슈팅까지 차례로 다룹니다.
사전 지식 (초보자를 위한 기초)
Shell이란?
Shell은 사용자와 운영체제를 연결하는 명령어 해석기입니다. 사용자가 입력한 명령어를 운영체제가 이해할 수 있는 형태로 변환하고, 결과를 다시 사용자에게 보여 줍니다.
예를 들어 ls 명령어를 입력하면, Shell이 이를 해석해서 운영체제에 파일 목록 조회를 요청하고, 결과를 터미널에 출력합니다.
GUI vs CLI:
GUI (Graphical User Interface):
- 마우스 클릭
- 아이콘, 창
- 직관적, 느림
CLI (Command Line Interface):
- 키보드 입력
- 텍스트 명령어
- 빠름, 자동화 가능
터미널 vs Shell
터미널 (Terminal)
- Shell을 실행하는 프로그램
- 예: iTerm2, Windows Terminal, GNOME Terminal
Shell
- 명령어를 해석하는 프로그램
- 예: Bash, Zsh, PowerShell
┌─────────────────────────────────┐
│ Terminal (창) │
│ ┌───────────────────────────┐ │
│ │ Shell (명령어 해석기) │ │
│ │ $ ls │ │
│ │ file1.txt file2.txt │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
기본 명령어
# 현재 디렉토리 확인
pwd
# /home/user
# 파일 목록
ls
# file1.txt file2.txt folder/
# 디렉토리 이동
cd folder
# 파일 내용 보기
cat file1.txt
# 파일 복사
cp file1.txt file2.txt
# 파일 이동/이름 변경
mv file1.txt renamed.txt
# 파일 삭제
rm file1.txt
# 디렉토리 생성
mkdir new_folder
# 명령어 도움말
man ls
Shell이란?
Shell의 역할
1. 명령어 해석
$ echo "Hello"
→ echo 프로그램 실행
2. 파이프라인
$ cat file.txt | grep "error" | wc -l
→ 3개 명령어 연결
3. 리다이렉션
$ ls > output.txt
→ 출력을 파일로 저장
4. 변수 및 제어 구조
$ for i in 1 2 3; do echo $i; done
→ 반복문
5. 스크립트 실행
$ ./script.sh
→ 여러 명령어를 한 번에
Shell의 종류
Unix Shell 계열:
- sh (Bourne Shell, 1979)
- bash (Bourne Again Shell, 1989) ← 가장 보편적
- zsh (Z Shell, 1990) ← 최신 기능
- fish (Friendly Interactive Shell, 2005)
Windows:
- cmd.exe (Command Prompt, 1981)
- PowerShell (2006) ← 현대적, 강력함
기타:
- dash (Debian Almquist Shell) ← 빠름
- ksh (Korn Shell)
- tcsh (TENEX C Shell)
Bash 기초
Bash란?
Bash (Bourne Again Shell)는 Linux·macOS에서 흔히 쓰이는 기본 셸입니다.
# Bash 버전 확인
bash --version
# GNU bash, version 5.2.15
# 현재 Shell 확인
echo $SHELL
# /bin/bash
기본 문법
변수 - Shell의 핵심 메커니즘:
# 변수 선언 (공백 없이!)
name="Alice"
age=25
# ⚠️ 흔한 실수: 공백 넣기
name = "Alice" # ❌ 에러: 'name' 명령어를 '=' 인자와 함께 실행
# Shell은 공백을 구분자로 사용:
# name = "Alice"
# ↑ ↑ ↑
# 명령어 인자1 인자2
# ✅ 올바른 방법
name="Alice" # 공백 없이
변수 참조의 다양한 방식:
# 기본 참조
echo "Name: $name, Age: $age"
# Name: Alice, Age: 25
# 중괄호 사용 (권장)
echo "Name: ${name}, Age: ${age}"
# 왜 중괄호?
file="report"
echo "$file_v2.txt" # ❌ ".txt" (변수 file_v2를 찾음 → 비어 있음)
echo "${file}_v2.txt" # ✅ report_v2.txt (변수: file)
# 참고: "$file.txt"는 괜찮음 — '.'은 변수 이름에 쓸 수 없는 문자라 여기서 끊김
# 기본값 설정
echo "${undefined:-default}" # undefined가 없으면 "default"
echo "${undefined:=default}" # undefined에 "default" 할당
echo "${undefined:?error}" # undefined가 없으면 에러 발생
# 변수 존재 확인
echo "${name:+exists}" # name이 있으면 "exists", 없으면 빈 문자열
Bash에서 가장 많은 버그를 만드는 규칙은 따옴표 없는 변수 확장입니다. $var를 따옴표 없이 쓰면 셸은 값을 공백·탭·줄바꿈 기준으로 쪼개고(word splitting), 쪼갠 조각에 *가 있으면 파일 이름으로 펼칩니다(globbing). 그래서 rm $file은 파일 이름이 my report.txt일 때 my와 report.txt 두 파일을 지우려 하고, 변수가 비어 있으면 [ $x = "yes" ]가 [ = "yes" ]가 되어 unary operator expected 에러를 냅니다. 규칙은 단순합니다. 쪼개지기를 의도한 경우가 아니라면 변수와 명령 치환은 항상 "$var", "$(cmd)"처럼 큰따옴표로 감쌉니다. ShellCheck(19장)가 경고하는 항목의 상당수가 이 규칙 위반(SC2086)입니다.
명령어 치환 (Command Substitution):
# 명령어 결과 저장 - 최신 문법
current_date=$(date)
echo "Today: $current_date"
# Today: Fri Apr 17 10:30:00 KST 2026
# 레거시 문법 (백틱)
current_date=`date` # ❌ 가독성 떨어짐, 중첩 어려움
# 명령어 치환의 내부 동작:
# 1. Shell이 $(...) 내부 명령어 실행
# 2. 새로운 서브셸(subshell) 생성
# 3. 명령어 출력을 문자열로 캡처
# 4. 변수에 할당
# 서브셸 예제
count=1
(count=2; echo "Inside subshell: $count") # 2
echo "Outside: $count" # 1 (원본 유지)
# () 안의 변수 변경은 부모 셸에 영향 없음
서브셸 규칙은 파이프에서 자주 사람을 괴롭힙니다. Bash에서 파이프의 각 단계는 별도 서브셸에서 실행되므로, count=0; cat list.txt | while read line; do count=$((count+1)); done; echo $count는 항상 0을 출력합니다. 루프 안에서 늘린 count는 서브셸이 끝나면서 사라지기 때문입니다. while read line; do ...; done < list.txt처럼 리다이렉션으로 입력을 주거나, 명령 출력이라면 done < <(cmd)(프로세스 치환)를 쓰면 루프가 현재 셸에서 돌아 값이 유지됩니다. 참고로 Zsh는 파이프의 마지막 단계를 현재 셸에서 실행하므로 같은 코드가 Zsh에서는 기대대로 동작하는데, 이 차이 때문에 로컬 Zsh에서 확인한 스크립트가 CI의 Bash에서 다른 결과를 내기도 합니다.
환경 변수 vs 셸 변수:
# 셸 변수 (현재 셸에만)
local_var="value"
bash -c 'echo $local_var' # 빈 출력 (자식 프로세스는 모름)
# 환경 변수 (자식 프로세스에 전달)
export PATH="/usr/local/bin:$PATH"
bash -c 'echo $PATH' # ✅ 출력됨
# 환경 변수의 내부 동작:
# 1. export로 표시된 변수는 환경 배열에 추가
# 2. fork() 시 자식 프로세스에 복사
# 3. 자식은 부모의 환경 변수를 읽기만 가능 (수정 불가)
# PATH 환경 변수 이해
echo $PATH
# /usr/local/bin:/usr/bin:/bin
# Shell이 명령어를 찾는 순서:
# 1. /usr/local/bin/
# 2. /usr/bin/
# 3. /bin/
# PATH 추가 (앞에)
export PATH="/my/custom/bin:$PATH"
# /my/custom/bin이 우선 검색됨
# PATH 추가 (뒤에)
export PATH="$PATH:/my/custom/bin"
# 기존 경로 우선, /my/custom/bin은 마지막
특수 변수:
# $0: 스크립트 이름
echo $0 # /bin/bash 또는 스크립트 파일명
# $1, $2, ...: 위치 매개변수
# script.sh arg1 arg2
echo $1 # arg1
echo $2 # arg2
# $#: 인자 개수
echo $# # 2
# $@: 모든 인자 (각각 개별 단어)
for arg in "$@"; do
echo $arg
done
# $*: 모든 인자 (하나의 문자열)
echo "$*" # arg1 arg2
# $?: 마지막 명령어의 종료 상태
ls /tmp
echo $? # 0 (성공)
ls /nonexistent
echo $? # 2 (실패)
# $$: 현재 셸의 PID
echo $$ # 12345
# $!: 백그라운드 프로세스의 PID
sleep 100 &
echo $! # 12346
변수 타입과 배열:
# Bash는 기본적으로 문자열
number="123"
echo $number # 123 (문자열)
# 산술 연산 (정수만 가능)
result=$((10 + 20))
echo $result # 30
result=$((10 / 3))
echo $result # 3 (정수 나눗셈)
# 배열
fruits=("apple" "banana" "cherry")
echo ${fruits[0]} # apple
echo ${fruits[@]} # 모든 요소
echo ${#fruits[@]} # 배열 크기: 3
# 배열 순회
for fruit in "${fruits[@]}"; do
echo $fruit
done
# 연관 배열 (Bash 4.0+)
declare -A colors
colors[red]="#FF0000"
colors[green]="#00FF00"
echo ${colors[red]} # #FF0000
조건문:
# if 문
if [ "$age" -gt 18 ]; then
echo "Adult"
else
echo "Minor"
fi
# 파일 존재 확인
if [ -f "file.txt" ]; then
echo "File exists"
fi
# 디렉토리 존재 확인
if [ -d "folder" ]; then
echo "Directory exists"
fi
# 문자열 비교
if [ "$name" = "Alice" ]; then
echo "Hello Alice"
fi
# 숫자 비교
# -eq (equal), -ne (not equal)
# -gt (greater than), -lt (less than)
# -ge (greater or equal), -le (less or equal)
반복문:
# for 문
for i in 1 2 3 4 5; do
echo "Number: $i"
done
# 범위
for i in {1..10}; do
echo $i
done
# 파일 순회
for file in *.txt; do
echo "Processing $file"
cat "$file"
done
# while 문
count=1
while [ $count -le 5 ]; do
echo "Count: $count"
count=$((count + 1))
done
함수:
# 함수 정의
greet() {
local name=$1 # 첫 번째 인자
echo "Hello, $name!"
}
# 함수 호출
greet "Alice"
# Hello, Alice!
# 반환값
add() {
local result=$(($1 + $2))
echo $result
}
sum=$(add 10 20)
echo "Sum: $sum"
# Sum: 30
파이프와 리다이렉션 - Unix 철학의 핵심
파이프 (|) - 프로세스 간 통신의 마법
파이프의 내부 동작:
# 간단한 예제
cat log.txt | grep "error"
# 내부에서 일어나는 일:
# 1. Shell이 파이프(pipe) 생성: pipe()
# - 읽기 파일 디스크립터 (fd[0])
# - 쓰기 파일 디스크립터 (fd[1])
#
# 2. 첫 번째 프로세스 (cat) 생성: fork()
# - stdout(fd 1)을 파이프의 쓰기 끝(fd[1])으로 리다이렉트
# - 파일 읽기 끝(fd[0]) 닫기
#
# 3. 두 번째 프로세스 (grep) 생성: fork()
# - stdin(fd 0)을 파이프의 읽기 끝(fd[0])으로 리다이렉트
# - 파일 쓰기 끝(fd[1]) 닫기
#
# 4. 부모 Shell은 파이프 양쪽 끝 모두 닫기
#
# 5. 데이터 흐름:
# cat → [pipe buffer] → grep
#
# 파이프 버퍼: 커널 메모리 (보통 64KB)
# 버퍼가 가득 차면 cat은 블록됨
# grep이 읽으면 cat 재개
파이프 체인의 효율성:
# 비효율적 (임시 파일 사용)
cat log.txt > tmp1.txt
grep "error" tmp1.txt > tmp2.txt
sort tmp2.txt > tmp3.txt
uniq tmp3.txt > result.txt
rm tmp1.txt tmp2.txt tmp3.txt
# 문제:
# - 디스크 I/O 느림
# - 임시 파일 관리 번거로움
# 효율적 (파이프 사용)
cat log.txt | grep "error" | sort | uniq > result.txt
# 장점:
# - 메모리 버퍼 사용 (빠름)
# - 병렬 실행 (각 프로세스 동시 실행)
# - 첫 번째 출력이 나오면 즉시 다음 명령으로 전달
실전 파이프 패턴:
# 1. 파일에서 "error" 찾기
cat log.txt | grep "error"
# 더 나은 방법:
grep "error" log.txt # cat 불필요 (grep이 직접 파일 읽기)
# 2. 중복 제거 후 정렬
cat names.txt | sort | uniq
# sort는 먼저 정렬해야 uniq가 작동:
# uniq는 연속된 중복만 제거
# 예: "a b a" → "a b a" (중복 제거 안 됨)
# 예: "a a b" → "a b" (중복 제거됨)
# 3. 상위 10개 프로세스 (CPU 사용률)
ps aux | sort -k3 -r | head -10
# ps aux: 모든 프로세스
# sort -k3 -r: 3번째 컬럼(CPU) 기준 역순 정렬
# head -10: 상위 10개만
# 4. 파일 개수 세기
ls | wc -l
# wc -l: line count
# 주의: 파일명에 공백이나 줄바꿈이 있으면 부정확
# 더 나은 방법:
find . -maxdepth 1 -type f | wc -l
# 5. JSON 파싱 (jq 사용)
curl https://api.example.com/users | jq '.[] | .name'
# jq: JSON 처리 전용 도구
# .[]: 배열 각 요소
# .name: name 필드 추출
# 6. 로그 분석 실전
cat access.log |
grep "404" | # 404 에러만
awk '{print $1}' | # IP 주소 추출
sort | # 정렬
uniq -c | # 개수 세기
sort -rn | # 개수 역순 정렬
head -10 # 상위 10개
# 결과: 404 에러를 가장 많이 발생시킨 IP 10개
리다이렉션 - 파일 디스크립터 조작
파일 디스크립터의 이해:
# 표준 파일 디스크립터:
# 0: stdin (표준 입력)
# 1: stdout (표준 출력)
# 2: stderr (표준 에러)
# 프로세스가 시작될 때:
# fd 0 → 키보드 (또는 파이프)
# fd 1 → 터미널 (또는 파이프)
# fd 2 → 터미널
# ls 명령어 예시:
ls /tmp # stdout → 터미널 (파일 목록 출력)
ls /xyz # stderr → 터미널 ("No such file" 에러)
출력 리다이렉션 상세:
# 1. stdout 리다이렉션 (덮어쓰기)
echo "Hello" > output.txt
# 내부 동작:
# - output.txt 파일을 쓰기 모드로 열기
# - fd 1(stdout)을 파일로 리다이렉트
# - echo 실행 → 출력이 파일로 감
# 2. stdout 리다이렉션 (추가)
echo "World" >> output.txt
# >> 는 O_APPEND 플래그로 열기
# 파일 끝에 추가
# 3. stderr 리다이렉션
command 2> error.log
# 2>: fd 2(stderr)를 error.log로 리다이렉트
# stdout은 여전히 터미널로 출력
# 4. stdout과 stderr 모두 리다이렉트
command > output.log 2>&1
# 순서 중요!
# 1) 1> output.log (stdout → output.log)
# 2) 2>&1 (stderr → stdout이 가리키는 곳, 즉 output.log)
# ❌ 잘못된 순서:
command 2>&1 > output.log
# 1) 2>&1 (stderr → 현재 stdout, 즉 터미널)
# 2) > output.log (stdout → output.log)
# 결과: stderr는 터미널, stdout만 파일
# 최신 Bash 문법 (간결):
command &> output.log # stdout + stderr
command &>> output.log # stdout + stderr (추가)
# 5. /dev/null로 버리기 (Black Hole)
command > /dev/null 2>&1
# /dev/null: 특수 파일, 쓰기는 무시, 읽기는 EOF
# 사용 예: 출력을 완전히 무시하고 싶을 때
# 6. 출력 분리
command > stdout.log 2> stderr.log
# stdout과 stderr를 다른 파일로
입력 리다이렉션:
# 1. 파일에서 입력 받기
mysql -u root -p < schema.sql
# < : stdin을 파일로 리다이렉트
# mysql은 키보드 대신 schema.sql에서 SQL 읽기
# 2. Here Document (여러 줄 입력)
cat << EOF > config.txt
server {
listen 80;
server_name example.com;
}
EOF
# 내부 동작:
# 1. Shell이 EOF까지의 모든 줄을 임시 버퍼에 저장
# 2. cat의 stdin을 버퍼로 리다이렉트
# 3. cat이 읽고 stdout으로 출력
# 4. stdout은 config.txt로 리다이렉트
# Here Document 활용:
ssh user@server << 'ENDSSH'
cd /var/www
git pull
sudo systemctl restart nginx
ENDSSH
# 'ENDSSH': 따옴표로 감싸면 변수 치환 안 함
# 3. Here String (한 줄 입력)
grep "pattern" <<< "This is a test string"
# <<<: 문자열을 stdin으로 전달
# 동등한 명령:
echo "This is a test string" | grep "pattern"
고급 리다이렉션 기법:
# 1. 파일 디스크립터 3+ 사용
exec 3> output.txt # fd 3을 파일로
echo "Hello" >&3 # fd 3에 쓰기
exec 3>&- # fd 3 닫기
# 2. 파일 디스크립터 복사
exec 3>&1 # fd 3 = 현재 stdout 복사
exec 1> output.txt # stdout을 파일로 변경
echo "To file" # 파일로 출력
exec 1>&3 # stdout 복원
exec 3>&- # fd 3 닫기
echo "To terminal" # 터미널로 출력
# 3. 양방향 리다이렉션 (프로세스 치환)
diff <(ls dir1) <(ls dir2)
# <(...): 명령어 출력을 임시 파일처럼 사용
# 내부 동작:
# 1. /dev/fd/63 같은 임시 파일 디스크립터 생성
# 2. 백그라운드에서 명령어 실행
# 3. 출력을 파이프로 연결
# 4. 출력 tee (화면 + 파일 동시)
command | tee output.txt
# tee: stdout을 복제
# - 하나는 터미널로 (화면에 출력)
# - 하나는 파일로
command | tee -a output.txt # 추가 모드
리다이렉션 성능 고려사항:
# ❌ 비효율적: 파일을 반복해서 열기
for i in {1..1000}; do
echo "Line $i" >> output.txt # 1000번 파일 열기/닫기
done
# ✅ 효율적: 파일을 한 번만 열기
{
for i in {1..1000}; do
echo "Line $i"
done
} > output.txt # 1번만 파일 열기
# 또는
for i in {1..1000}; do
echo "Line $i"
done > output.txt
Zsh와 Oh My Zsh
Zsh란?
Zsh (Z Shell)는 Bash를 호환하면서 확장이 많은 대안 셸입니다. (macOS Catalina부터 기본) Bash vs Zsh:
Bash:
- 표준, 호환성 높음
- 기본 기능
Zsh:
- 강력한 자동 완성
- 테마 및 플러그인
- 더 나은 히스토리
- 스펠링 교정
“Bash 호환”이라는 말은 대화형으로 쓰는 명령 대부분이 똑같이 동작한다는 뜻이지, Bash 스크립트가 그대로 돈다는 뜻은 아닙니다. 스크립트를 옮길 때 부딪히는 대표적인 차이는 다음과 같습니다.
- word splitting: Zsh는 따옴표 없는
$var를 공백으로 쪼개지 않습니다. Bash에서for f in $files로 여러 항목을 돌던 코드가 Zsh에서는 한 번만 돕니다. (setopt SH_WORD_SPLIT이나${=files}로 Bash처럼 동작시킬 수 있습니다.) - 배열 인덱스: Zsh 배열은 기본이 1부터 시작해
${arr[0]}이 비어 있습니다. - glob 실패: 매칭되는 파일이 없으면 Bash는 패턴 문자열을 그대로 넘기지만, Zsh는
zsh: no matches found: *.log에러를 내고 명령을 실행하지 않습니다.rm *.tmp같은 정리 명령이 Zsh에서만 실패하는 이유입니다.
그래서 팀 스크립트는 첫 줄에 #!/usr/bin/env bash를 명시하고 bash script.sh로 실행해, 사용자의 로그인 셸이 무엇이든 Bash가 해석하도록 하는 것이 안전합니다.
Zsh 설치
# macOS (기본 설치됨)
chsh -s /bin/zsh
# Ubuntu/Debian
sudo apt install zsh
chsh -s $(which zsh)
# 로그아웃 후 재로그인
Oh My Zsh 설치
Oh My Zsh는 Zsh 설정을 묶어 둔 프레임워크입니다.
# 설치
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
# 설치 후 ~/.zshrc 파일 생성됨
Oh My Zsh 테마
# ~/.zshrc 편집
nano ~/.zshrc
# 테마 변경
ZSH_THEME="robbyrussell" # 기본
ZSH_THEME="agnoster" # 인기
ZSH_THEME="powerlevel10k/powerlevel10k" # 최고 인기
# 적용
source ~/.zshrc
Powerlevel10k 설치 (추천):
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k
# ~/.zshrc
ZSH_THEME="powerlevel10k/powerlevel10k"
# 설정 마법사
p10k configure
Oh My Zsh 플러그인
# ~/.zshrc
plugins=(
git # Git 단축키
zsh-autosuggestions # 명령어 자동 제안
zsh-syntax-highlighting # 문법 하이라이팅
docker # Docker 자동 완성
kubectl # Kubernetes 자동 완성
npm # npm 자동 완성
)
# zsh-autosuggestions 설치
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
# zsh-syntax-highlighting 설치
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
# 적용
source ~/.zshrc
Zsh 강력한 기능
1) 자동 완성:
# 디렉토리 이동 (Tab으로 자동 완성)
cd /u/l/b # Tab → /usr/local/bin
# 명령어 옵션 자동 완성
git co # Tab → git checkout, git commit, ...
# 파일 이름 자동 완성 (대소문자 무시)
ls doc # Tab → Documents/
2) 히스토리 검색:
# Ctrl+R: 히스토리 검색
# 입력: git
# → 이전에 실행한 git 명령어 표시
# 히스토리 공유 (여러 터미널)
setopt share_history
3) 스펠링 교정:
$ gti status
# zsh: correct 'gti' to 'git' [nyae]? y
# → git status 실행
PowerShell 기초
PowerShell이란?
PowerShell은 Windows에서 널리 쓰이는 객체 중심 셸입니다. cmd.exe vs PowerShell:
cmd.exe (옛날):
- 텍스트 기반
- 제한적인 기능
- 레거시
PowerShell (현대):
- 객체 기반
- .NET 통합
- 강력한 스크립팅
- 크로스 플랫폼 (Linux/macOS 지원)
PowerShell 설치
# Windows (기본 설치됨)
# PowerShell 7 (최신 버전) 설치
winget install Microsoft.PowerShell
# macOS
brew install powershell/tap/powershell
# Ubuntu
sudo snap install powershell --classic
# 실행
pwsh
PowerShell 기본 명령어
Cmdlet (Command-let):
# 동사-명사 형식
Get-Process # 프로세스 목록
Get-Service # 서비스 목록
Get-ChildItem # 파일 목록 (ls 별칭)
Set-Location # 디렉토리 이동 (cd 별칭)
Copy-Item # 파일 복사 (cp 별칭)
Remove-Item # 파일 삭제 (rm 별칭)
# 별칭 확인
Get-Alias ls
# CommandType Name
# ----------- ----
# Alias ls -> Get-ChildItem
객체 기반 파이프라인:
# Bash (텍스트 기반)
ps aux | grep chrome | awk '{print $2}'
# PowerShell (객체 기반)
Get-Process | Where-Object {$_.Name -like "*chrome*"} | Select-Object Id
# 더 간단하게
Get-Process chrome | Select-Object Id, CPU, WorkingSet
객체 파이프라인의 장점은 “몇 번째 칸이 PID인가”를 신경 쓸 필요가 없다는 것입니다. Bash의 awk '{print $2}'는 ps 출력 형식이 바뀌면 조용히 틀린 값을 뽑지만, PowerShell의 Select-Object Id는 속성 이름으로 접근하므로 출력 모양과 무관합니다. 대신 외부 프로그램(git, docker 등)의 출력은 여전히 문자열 배열로 들어오므로, 이 경우에는 PowerShell에서도 텍스트를 파싱해야 합니다.
Bash 경험자가 PowerShell에서 자주 걸리는 함정도 있습니다. Windows PowerShell 5.1에서 curl과 wget은 실제 curl이 아니라 Invoke-WebRequest의 별칭이라 curl -X POST ... 같은 옵션이 A parameter cannot be found that matches parameter name 'X' 에러로 실패합니다(curl.exe로 부르면 Windows에 내장된 진짜 curl이 실행됩니다). 또 5.1의 > 리다이렉션은 파일을 UTF-16LE로 저장해서, 그 파일을 다른 도구가 읽으면 글자 사이에 널 바이트가 끼어 보입니다. PowerShell 7부터는 기본 인코딩이 BOM 없는 UTF-8로 바뀌었습니다. 외부 명령의 성공 여부는 $?보다 $LASTEXITCODE로 확인하는 편이 정확합니다.
PowerShell 변수
# 변수 선언
$name = "Alice"
$age = 25
# 변수 사용
Write-Host "Name: $name, Age: $age"
# 배열
$fruits = @("Apple", "Banana", "Cherry")
$fruits[0] # Apple
# 해시테이블
$user = @{
Name = "Alice"
Age = 25
Email = "[email protected]"
}
$user.Name # Alice
$user["Age"] # 25
Shell 스크립트 작성
Bash 스크립트
기본 구조:
#!/bin/bash
# Shebang: 어떤 Shell로 실행할지 지정
# 스크립트 설명
# 작성자: Alice
# 날짜: 2026-03-31
# 에러 발생 시 중단
set -e
# 변수
NAME="World"
# 함수
greet() {
echo "Hello, $1!"
}
# 실행
greet "$NAME"
실행 방법:
# 실행 권한 부여
chmod +x script.sh
# 실행
./script.sh
# 또는
bash script.sh
실전 예제 1: 백업 스크립트
#!/bin/bash
# 설정
SOURCE_DIR="/home/user/documents"
BACKUP_DIR="/backup"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="backup_$DATE.tar.gz"
# 백업 디렉토리 생성
mkdir -p "$BACKUP_DIR"
# 압축 백업
echo "Starting backup..."
tar -czf "$BACKUP_DIR/$BACKUP_FILE" "$SOURCE_DIR"
# 결과 확인
if [ $? -eq 0 ]; then
echo "Backup successful: $BACKUP_FILE"
# 7일 이상 된 백업 삭제
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +7 -delete
echo "Old backups cleaned"
else
echo "Backup failed!"
exit 1
fi
실전 예제 2: 로그 분석
#!/bin/bash
LOG_FILE="/var/log/nginx/access.log"
echo "=== Nginx 로그 분석 ==="
echo
# 총 요청 수
total_requests=$(wc -l < "$LOG_FILE")
echo "총 요청 수: $total_requests"
# 상위 10개 IP
echo
echo "상위 10개 IP:"
awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
# 상위 10개 URL
echo
echo "상위 10개 URL:"
awk '{print $7}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
# HTTP 상태 코드 분포
echo
echo "HTTP 상태 코드:"
awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn
# 시간대별 요청 수
echo
echo "시간대별 요청 수:"
awk '{print $4}' "$LOG_FILE" | cut -d: -f2 | sort | uniq -c
실전 예제 3: 배포 자동화
#!/bin/bash
set -e # 에러 시 중단
PROJECT_DIR="/var/www/myapp"
BRANCH="main"
echo "🚀 배포 시작..."
# 1. Git Pull
echo "📥 최신 코드 가져오기..."
cd "$PROJECT_DIR"
git fetch origin
git reset --hard origin/$BRANCH
# 2. 의존성 설치
echo "📦 의존성 설치..."
npm ci --production
# 3. 빌드
echo "🔨 빌드..."
npm run build
# 4. 서비스 재시작
echo "🔄 서비스 재시작..."
pm2 restart myapp
# 5. 헬스 체크
echo "🏥 헬스 체크..."
sleep 5
response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health)
if [ "$response" = "200" ]; then
echo "✅ 배포 성공!"
else
echo "❌ 배포 실패! (HTTP $response)"
# 롤백
git reset --hard HEAD~1
npm ci --production
npm run build
pm2 restart myapp
exit 1
fi
PowerShell 스크립트
기본 문법
# 변수
$name = "Alice"
$age = 25
# 조건문
if ($age -gt 18) {
Write-Host "Adult"
} else {
Write-Host "Minor"
}
# 반복문
foreach ($i in 1..5) {
Write-Host "Number: $i"
}
# 파일 순회
Get-ChildItem *.txt | ForEach-Object {
Write-Host "Processing $($_.Name)"
Get-Content $_.FullName
}
# 함수
function Greet {
param($Name)
Write-Host "Hello, $Name!"
}
Greet -Name "Alice"
실전 예제 1: 파일 정리
#!/usr/bin/env pwsh
# 30일 이상 된 로그 파일 삭제
$logDir = "C:\Logs"
$daysOld = 30
Get-ChildItem -Path $logDir -Filter "*.log" |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$daysOld) } |
ForEach-Object {
Write-Host "Deleting: $($_.Name)"
Remove-Item $_.FullName -Force
}
Write-Host "Cleanup complete!"
실전 예제 2: 시스템 모니터링
#!/usr/bin/env pwsh
# CPU 사용률
$cpu = Get-Counter '\Processor(_Total)\% Processor Time' |
Select-Object -ExpandProperty CounterSamples |
Select-Object -ExpandProperty CookedValue
Write-Host "CPU Usage: $([math]::Round($cpu, 2))%"
# 메모리 사용률
$os = Get-CimInstance Win32_OperatingSystem
$totalMemory = $os.TotalVisibleMemorySize / 1MB
$freeMemory = $os.FreePhysicalMemory / 1MB
$usedMemory = $totalMemory - $freeMemory
$memoryPercent = ($usedMemory / $totalMemory) * 100
Write-Host "Memory Usage: $([math]::Round($memoryPercent, 2))% ($([math]::Round($usedMemory, 2))GB / $([math]::Round($totalMemory, 2))GB)"
# 디스크 사용률
Get-PSDrive -PSProvider FileSystem |
Where-Object { $_.Used -ne $null } |
ForEach-Object {
$usedGB = $_.Used / 1GB
$freeGB = $_.Free / 1GB
$totalGB = $usedGB + $freeGB
$percent = ($usedGB / $totalGB) * 100
Write-Host "$($_.Name): $([math]::Round($percent, 2))% ($([math]::Round($usedGB, 2))GB / $([math]::Round($totalGB, 2))GB)"
}
# 상위 10개 프로세스 (메모리)
Get-Process |
Sort-Object WorkingSet -Descending |
Select-Object -First 10 Name, @{Name="Memory(MB)";Expression={[math]::Round($_.WorkingSet / 1MB, 2)}} |
Format-Table -AutoSize
실전 자동화 예제
예제 1: Git 자동화
Bash:
#!/bin/bash
# Git 저장소 일괄 업데이트
REPOS_DIR="$HOME/projects"
for repo in "$REPOS_DIR"/*; do
if [ -d "$repo/.git" ]; then
echo "Updating $(basename $repo)..."
cd "$repo"
# 현재 브랜치 확인
branch=$(git branch --show-current)
# Pull
git pull origin "$branch"
# 변경사항 확인
if [ -n "$(git status --porcelain)" ]; then
echo " ⚠️ Uncommitted changes"
else
echo " ✅ Up to date"
fi
echo
fi
done
예제 2: 서버 헬스 체크
#!/bin/bash
# 서버 목록
SERVERS=(
"https://api.example.com/health"
"https://web.example.com/health"
"https://admin.example.com/health"
)
echo "=== Server Health Check ==="
echo "Time: $(date)"
echo
for server in "${SERVERS[@]}"; do
response=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$server")
if [ "$response" = "200" ]; then
echo "✅ $server - OK"
else
echo "❌ $server - FAILED (HTTP $response)"
# Slack 알림 (선택)
# curl -X POST https://hooks.slack.com/... \
# -d "{\"text\":\"Server down: $server\"}"
fi
done
예제 3: 데이터베이스 백업
#!/bin/bash
# 설정
DB_HOST="localhost"
DB_USER="admin"
DB_NAME="mydb"
BACKUP_DIR="/backup/db"
DATE=$(date +%Y%m%d_%H%M%S)
# 백업 디렉토리 생성
mkdir -p "$BACKUP_DIR"
# PostgreSQL 백업
echo "Starting database backup..."
pg_dump -h "$DB_HOST" -U "$DB_USER" "$DB_NAME" | gzip > "$BACKUP_DIR/backup_$DATE.sql.gz"
# 결과 확인
if [ $? -eq 0 ]; then
echo "✅ Backup successful: backup_$DATE.sql.gz"
# 파일 크기 확인
size=$(du -h "$BACKUP_DIR/backup_$DATE.sql.gz" | cut -f1)
echo "Size: $size"
# 30일 이상 된 백업 삭제
find "$BACKUP_DIR" -name "backup_*.sql.gz" -mtime +30 -delete
# S3 업로드 (선택)
# aws s3 cp "$BACKUP_DIR/backup_$DATE.sql.gz" s3://my-bucket/backups/
else
echo "❌ Backup failed!"
exit 1
fi
유용한 명령어 모음
파일 및 디렉토리
# 파일 찾기
find . -name "*.log"
find . -type f -mtime +7 # 7일 이상 된 파일
# 파일 내용 검색
grep -r "error" ./logs
grep -i "warning" file.txt # 대소문자 무시
# 디스크 사용량
df -h
du -sh *
# 파일 권한 변경
chmod 755 script.sh
chmod +x script.sh
# 소유자 변경
chown user:group file.txt
프로세스 관리
프로세스 실행의 내부 메커니즘:
Shell이 명령어를 실행하는 과정:
1. 명령어 입력:
$ ls -la /home
2. Shell 파싱:
- 명령어: ls
- 인자: ["-la", "/home"]
- 리다이렉션: 없음
- 파이프: 없음
3. fork() 시스템 콜:
Shell 프로세스 복제
Parent (Shell): Child (복제본):
PID: 1234 PID: 5678
계속 대기 다음 단계 진행
메모리 구조 (Copy-on-Write):
┌─────────────────┐ ┌─────────────────┐
│ Shell Memory │ │ Child Memory │
│ (공유, 읽기만) │ ←→ │ (필요시 복사) │
└─────────────────┘ └─────────────────┘
4. execve() 시스템 콜 (Child에서):
Child 프로세스를 ls 프로그램으로 교체
Before: After:
Child (bash 복제) → Child (ls 프로그램)
PID: 5678 PID: 5678 (유지)
메모리: bash 코드 메모리: ls 코드
execve("/bin/ls", ["-la", "/home"], environ)
5. ls 프로그램 실행:
- 시스템 콜: openat("/home"), getdents64(), ...
- 파일 목록 읽기
- 포맷팅 (컬러, 권한 등)
- stdout에 출력
6. exit() 시스템 콜:
ls 프로세스 종료
Exit Code: 0 (성공)
7. wait() 시스템 콜 (Parent에서):
Shell이 Child 종료 대기
Exit Code 수신
$? = 0 # 마지막 명령어 종료 코드
프로세스 계층:
systemd (PID 1)
└─ bash (PID 1234) ← Shell
└─ ls (PID 5678) ← 명령어
(완료 후 사라짐)
백그라운드 실행의 내부 동작:
Foreground (기본):
$ sleep 10
Shell: wait(child_pid) ← 10초 동안 대기
User: (입력 불가)
Background (&):
$ sleep 10 &
[1] 5678
Shell:
- Child PID 기록 (Job Table)
- wait() 호출 안 함
- 즉시 프롬프트 반환
User: (다른 명령어 입력 가능)
Job Table:
Job ID PID Status Command
[1] 5678 Running sleep 10
SIGCHLD 시그널:
Child 종료 시 Parent에게 알림
Shell이 SIGCHLD 받으면:
- Job Table 업데이트
- "[1]+ Done sleep 10" 출력
Signal (시그널) 메커니즘:
주요 시그널:
SIGTERM (15):
정상 종료 요청
kill <PID> (기본)
프로그램 동작:
1. SIGTERM 수신
2. 정리 작업:
- 열린 파일 닫기
- 네트워크 연결 종료
- 임시 파일 삭제
3. exit(0)
SIGKILL (9):
강제 종료 (막을 수 없음)
kill -9 <PID>
커널이 즉시 프로세스 종료
정리 작업 없음
→ 데이터 손실 위험
SIGINT (2):
Ctrl+C
인터럽트 요청
프로그램이 핸들러 등록 가능:
trap 'cleanup; exit' INT
SIGHUP (1):
터미널 연결 끊김
SSH 세션 종료 시
nohup으로 무시:
nohup ./task.sh &
→ SIGHUP 무시, 계속 실행
SIGSTOP (19), SIGCONT (18):
일시정지 / 재개
Ctrl+Z: SIGSTOP
bg / fg: SIGCONT
Signal 전달 과정:
User: kill -TERM 5678
↓
Kernel: 시그널 큐에 SIGTERM 추가
↓
Scheduler: 프로세스 5678 실행 시
↓
Process 5678: signal_handler() 실행
↓
Handler: cleanup() → exit(0)
프로세스 명령어:
# 프로세스 목록 (상세)
ps aux
# a: 모든 사용자
# u: 사용자 이름 표시
# x: 터미널 없는 프로세스도 표시
출력:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 169564 13140 ? Ss Apr01 0:05 /sbin/init
john 5678 2.5 1.2 450120 98304 pts/0 S+ 10:30 0:03 node server.js
컬럼 의미:
- PID: Process ID
- %CPU: CPU 사용률
- %MEM: 메모리 사용률
- VSZ: 가상 메모리 크기 (KB)
- RSS: 실제 메모리 크기 (KB)
- TTY: 연결된 터미널 (?: 없음)
- STAT: 상태
* S: Sleeping (대기 중)
* R: Running (실행 중)
* Z: Zombie (종료되며, Parent가 wait() 안 함)
* T: sTopped (일시정지)
* +: Foreground
* <: 높은 우선순위
# 프로세스 트리
pstree -p
systemd(1)─┬─sshd(1234)───sshd(5678)───bash(5679)───node(5680)
├─nginx(2345)─┬─nginx(2346)
│ └─nginx(2347)
└─docker(3456)
# 특정 프로세스 찾기
pgrep -a nginx
2345 nginx: master
2346 nginx: worker
2347 nginx: worker
# 프로세스 종료
kill <PID> # SIGTERM (15) - 정상 종료
kill -9 <PID> # SIGKILL (9) - 강제 종료
kill -STOP <PID> # SIGSTOP (19) - 일시정지
kill -CONT <PID> # SIGCONT (18) - 재개
killall nginx # 이름으로 종료 (모든 nginx)
pkill -f "node.*server" # 패턴 매칭
# 백그라운드 실행
./long_running_task.sh &
[1] 5678 # Job ID와 PID
# nohup (로그아웃 후에도 실행)
nohup ./task.sh > output.log 2>&1 &
# SIGHUP 무시
# stdout/stderr → output.log
# 작업 목록
jobs
[1]+ Running ./task.sh &
[2]- Stopped vim file.txt
# 작업 제어
fg %1 # Job 1을 Foreground로
bg %2 # Job 2를 Background로
kill %1 # Job 1 종료
파이프 (Pipe)의 내부 동작:
명령어: ls -la | grep txt | wc -l
Shell이 실행하는 과정:
1. 파이프 생성 (pipe() 시스템 콜):
Pipe 1: [read_fd1, write_fd1]
Pipe 2: [read_fd2, write_fd2]
커널 메모리에 버퍼 생성 (보통 64KB)
2. 프로세스 생성 (3개):
ls -la:
fork() → Child 1 (PID 5678)
dup2(write_fd1, STDOUT) ← stdout을 파이프로
close(read_fd1, write_fd1)
execve("/bin/ls", ["-la"])
grep txt:
fork() → Child 2 (PID 5679)
dup2(read_fd1, STDIN) ← stdin을 파이프에서
dup2(write_fd2, STDOUT) ← stdout을 파이프로
close(...)
execve("/bin/grep", ["txt"])
wc -l:
fork() → Child 3 (PID 5680)
dup2(read_fd2, STDIN) ← stdin을 파이프에서
close(...)
execve("/usr/bin/wc", ["-l"])
3. 데이터 흐름:
ls (5678):
파일 목록 생성
→ write(STDOUT)
→ Pipe 1 Buffer
grep (5679):
read(STDIN) ← Pipe 1
"txt" 포함 줄만 필터링
→ write(STDOUT)
→ Pipe 2 Buffer
wc (5680):
read(STDIN) ← Pipe 2
줄 수 카운트
→ write(STDOUT) → Terminal
출력: 3
4. 프로세스 종료:
ls → EOF → Pipe 1 close
grep → EOF 감지 → Pipe 2 close
wc → EOF 감지 → 출력 후 종료
5. Shell 복귀:
wait(5678)
wait(5679)
wait(5680)
모두 완료 → 프롬프트 반환
파이프 버퍼:
- 크기: 64KB (Linux), 8KB (macOS)
- 버퍼 가득 차면: write() 블록
- 버퍼 비면: read() 블록
- 비동기 I/O (Producer-Consumer 패턴)
예시:
ls 출력: 100KB
→ 64KB 쓰기 → 블록 (grep 읽을 때까지)
→ grep 64KB 읽기 → ls 계속 쓰기 (36KB)
확인:
ulimit -p # 파이프 버퍼 크기
cat /proc/sys/fs/pipe-max-size # 최대 크기
리다이렉션의 내부 동작:
명령어: echo "Hello" > file.txt
1. Shell이 처리:
open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644)
→ fd = 3
2. 파일 디스크립터 복제:
dup2(3, STDOUT_FILENO) # fd 3 → stdout (fd 1)
프로세스 파일 디스크립터 테이블:
0: STDIN → /dev/pts/0 (terminal)
1: STDOUT → file.txt ← 변경됨!
2: STDERR → /dev/pts/0 (terminal)
3. echo 실행:
write(STDOUT, "Hello\n", 6)
→ file.txt에 쓰임
4. 종료 후 Shell 복구:
dup2(saved_stdout, STDOUT)
리다이렉션 종류:
> : 덮어쓰기 (O_TRUNC)
>> : 추가 (O_APPEND)
< : 입력
2> : stderr 리다이렉션
&> : stdout + stderr
2>&1: stderr → stdout
예시:
command > out.txt 2> err.txt
stdout: out.txt (fd 3)
stderr: err.txt (fd 4)
command &> all.txt
stdout: all.txt (fd 3)
stderr: dup2(3, 2) ← stdout과 같은 곳
command 2>&1 | tee log.txt
stderr → stdout
stdout → 파이프 → tee
tee → 화면 + log.txt
# 프로세스 목록 (상세)
ps aux
ps aux | grep nginx
# 프로세스 종료
kill <PID> # SIGTERM (15)
kill -9 <PID> # SIGKILL (9) - 강제
killall nginx # 이름으로 종료
pkill -f "pattern" # 패턴 매칭
# 백그라운드 실행
./long_running_task.sh &
# nohup (로그아웃 후에도 실행)
nohup ./task.sh > output.log 2>&1 &
# 작업 목록
jobs
# 백그라운드 → 포그라운드
fg %1
네트워크
# 포트 확인
netstat -tulpn
ss -tulpn # 더 빠름
# 특정 포트 사용 프로세스
lsof -i :8080
# HTTP 요청
curl https://api.example.com
curl -X POST -H "Content-Type: application/json" -d '{"key":"value"}' https://api.example.com
# 다운로드
wget https://example.com/file.zip
# DNS 조회
dig example.com
nslookup example.com
# 핑
ping -c 4 google.com
# 경로 추적
traceroute google.com
시스템 정보
# CPU 정보
lscpu
cat /proc/cpuinfo
# 메모리 정보
free -h
cat /proc/meminfo
# 디스크 정보
lsblk
fdisk -l
# OS 정보
uname -a
cat /etc/os-release
# 시스템 부하
uptime
top
htop # 더 보기 좋음
고급 Shell 기법
명령어 치환
# $() 사용 (권장)
current_date=$(date +%Y-%m-%d)
echo "Today: $current_date"
# 백틱 (옛날 방식)
current_date=`date +%Y-%m-%d`
# 중첩 가능
files_count=$(ls $(pwd) | wc -l)
배열
# 배열 선언
fruits=("Apple" "Banana" "Cherry")
# 접근
echo ${fruits[0]} # Apple
echo ${fruits[@]} # 모든 요소
echo ${#fruits[@]} # 배열 크기
# 순회
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# 추가
fruits+=("Date")
문자열 처리
text="Hello World"
# 길이
echo ${#text} # 11
# 부분 문자열
echo ${text:0:5} # Hello
echo ${text:6} # World
# 치환
echo ${text/World/Bash} # Hello Bash
echo ${text//o/0} # Hell0 W0rld (모든 o)
# 대소문자 변환
echo ${text^^} # HELLO WORLD (대문자)
echo ${text,,} # hello world (소문자) — Bash 4.0 이상 필요
# 기본값
echo ${name:-"Guest"} # name이 없으면 "Guest"
${text^^} 같은 대소문자 변환과 연관 배열(declare -A), mapfile은 Bash 4에서 추가된 기능입니다. macOS에 기본으로 들어 있는 /bin/bash는 라이선스 문제로 2007년의 3.2 버전에 머물러 있어서, 이 문법을 쓴 스크립트는 맥에서 bad substitution 에러를 냅니다. 맥에서 최신 Bash가 필요하면 brew install bash로 설치하고, 스크립트 첫 줄을 #!/usr/bin/env bash로 두어 PATH에서 먼저 찾은 Bash를 쓰게 합니다. #!/bin/bash로 고정하면 새로 설치한 Bash가 아니라 3.2가 실행됩니다.
에러 처리
#!/bin/bash
# 에러 발생 시 중단
set -e
# 에러 발생 시 함수 호출
trap 'echo "Error on line $LINENO"' ERR
# 종료 시 정리
cleanup() {
echo "Cleaning up..."
rm -f /tmp/temp_file
}
trap cleanup EXIT
# 명령어 실행
if ! command -v git &> /dev/null; then
echo "Git is not installed"
exit 1
fi
# 명령어 성공 여부 확인
if git pull; then
echo "Pull successful"
else
echo "Pull failed"
exit 1
fi
set -e는 “명령이 실패하면 스크립트를 멈춘다”고 설명되지만, 실제로는 예외가 많아서 믿고 기대기 어렵습니다. if, while의 조건, &&·|| 목록의 앞부분, !로 뒤집은 명령에서의 실패는 무시되고, 이 규칙이 함수 안까지 적용되어 if my_func; then으로 부른 함수 내부의 실패도 멈추지 않습니다. 또 local out=$(cmd)는 cmd가 실패해도 local 명령 자체가 성공하므로 스크립트가 계속 진행됩니다. local out; out=$(cmd)처럼 선언과 대입을 나누어야 실패가 전파됩니다. 파이프는 기본적으로 마지막 명령의 종료 코드만 보므로 curl ... | tar xz에서 다운로드가 실패해도 tar가 성공하면 통과하는데, 이것을 막는 것이 set -o pipefail입니다. trap ... ERR도 기본으로는 함수 안의 에러에 반응하지 않아 set -E(errtrace)를 함께 켜야 합니다. 이런 이유로 set -euo pipefail은 안전장치로 켜 두되, 중요한 명령은 위 예제의 if git pull처럼 명시적으로 결과를 확인하는 방식을 함께 쓰는 것이 좋습니다.
Shell 비교
문법 비교
변수:
# Bash/Zsh
name="Alice"
echo $name
# PowerShell
$name = "Alice"
Write-Host $name
조건문:
# Bash/Zsh
if [ "$age" -gt 18 ]; then
echo "Adult"
fi
# PowerShell
if ($age -gt 18) {
Write-Host "Adult"
}
반복문:
# Bash/Zsh
for i in {1..5}; do
echo $i
done
# PowerShell
foreach ($i in 1..5) {
Write-Host $i
}
파이프:
# Bash/Zsh (텍스트)
ps aux | grep nginx
# PowerShell (객체)
Get-Process | Where-Object {$_.Name -eq "nginx"}
성능 비교
스크립트 실행 속도는 셸 자체보다 외부 명령을 몇 번 띄우는지에 더 크게 좌우됩니다. 일반적인 경향만 정리하면 다음과 같습니다.
- dash(Debian·Ubuntu의
/bin/sh)는 기능이 적은 대신 시작과 실행이 가벼워, 시스템 부팅 스크립트처럼 짧은 POSIX 스크립트를 많이 실행하는 곳에 쓰입니다. - bash와 zsh는 기능이 많아 dash보다 무겁지만, 보통의 자동화 스크립트에서는 체감 차이가 크지 않습니다.
- PowerShell은 .NET 런타임을 띄우므로 시작 시간이 가장 길고, 대신 객체 파이프라인과 .NET 라이브러리를 그대로 쓸 수 있습니다.
루프 안에서 grep·sed·awk를 행마다 호출하는 스크립트는 셸 종류와 상관없이 느립니다. 프로세스 생성 비용이 반복되기 때문이며, 한 번의 awk 호출로 처리하거나 셸 내장 기능(${var#prefix} 같은 매개변수 확장)을 쓰는 편이 훨씬 효과적입니다. 대화형 사용에서는 zsh와 PowerShell이 자동 완성·플러그인 설정에 손이 더 많이 가지만 그만큼 편의 기능이 많고, dash는 대화형보다 스크립트용에 가깝습니다.
기능 비교
| 기능 | Bash | Zsh | PowerShell |
|---|---|---|---|
| 자동 완성 | 기본 | 강력 | 강력 |
| 플러그인 | 제한적 | 풍부 | 풍부 |
| 객체 파이프 | 없음 | 없음 | 있음 |
| 크로스 플랫폼 | Linux/Mac | Linux/Mac | All |
| 스크립트 속도 | 빠름 | 보통 | 느림 |
| 학습 곡선 | 낮음 | 중간 | 높음 |
| .NET 통합 | 없음 | 없음 | 있음 |
개발 환경 설정
.bashrc / .zshrc 추천 설정
# ~/.bashrc 또는 ~/.zshrc
# 별칭 (Alias)
alias ll='ls -lah'
alias la='ls -A'
alias l='ls -CF'
alias ..='cd ..'
alias ...='cd ../..'
alias gs='git status'
alias ga='git add'
alias gc='git commit'
alias gp='git push'
alias gpl='git pull'
alias gd='git diff'
alias gl='git log --oneline --graph --decorate'
# 함수
mkcd() {
mkdir -p "$1" && cd "$1"
}
extract() {
if [ -f "$1" ]; then
case "$1" in
*.tar.gz) tar xzf "$1" ;;
*.tar.bz2) tar xjf "$1" ;;
*.zip) unzip "$1" ;;
*.rar) unrar x "$1" ;;
*) echo "Unknown format" ;;
esac
fi
}
# 환경 변수
export EDITOR=vim
export VISUAL=vim
export LANG=en_US.UTF-8
# PATH 추가
export PATH="$HOME/bin:$PATH"
export PATH="/usr/local/bin:$PATH"
# 히스토리 설정
export HISTSIZE=10000
export HISTFILESIZE=20000
export HISTCONTROL=ignoredups:erasedups
# 프롬프트 커스터마이징
PS1='\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
# 자동 완성
if [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
PowerShell 프로필
# $PROFILE 파일 편집
notepad $PROFILE
# 별칭
Set-Alias ll Get-ChildItem
Set-Alias g git
# 함수
function mkcd {
param($Path)
New-Item -ItemType Directory -Path $Path -Force
Set-Location $Path
}
function gs { git status }
function ga { git add $args }
function gc { git commit -m $args }
function gp { git push }
# 프롬프트 커스터마이징
function prompt {
$location = Get-Location
Write-Host "PS " -NoNewline -ForegroundColor Green
Write-Host "$location" -NoNewline -ForegroundColor Blue
return "> "
}
# 모듈 자동 로드
Import-Module posh-git # Git 통합
Import-Module PSReadLine # 향상된 입력
# PSReadLine 설정
Set-PSReadLineOption -PredictionSource History
Set-PSReadLineOption -PredictionViewStyle ListView
디버깅
Bash 디버깅
# 디버그 모드 실행
bash -x script.sh
# 스크립트 내에서 활성화
set -x # 디버그 시작
# ... 코드 ...
set +x # 디버그 종료
# 출력 예시:
# + echo 'Hello'
# Hello
# + name=Alice
# + echo 'Name: Alice'
# Name: Alice
디버깅 옵션:
#!/bin/bash
set -e # 에러 시 즉시 종료
set -u # 미정의 변수 사용 시 에러
set -o pipefail # 파이프라인 에러 감지
set -x # 디버그 모드
# 또는 한 줄로
set -euxo pipefail
PowerShell 디버깅
# 디버그 모드
Set-PSDebug -Trace 1 # 명령어 추적
Set-PSDebug -Trace 2 # 변수 할당도 추적
# 중단점
Set-PSBreakpoint -Script script.ps1 -Line 10
# 단계별 실행
Set-PSDebug -Step
# 디버그 해제
Set-PSDebug -Off
크로스 플랫폼 스크립트
플랫폼 감지
#!/bin/bash
# OS 감지
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
echo "Linux"
package_manager="apt"
elif [[ "$OSTYPE" == "darwin"* ]]; then
echo "macOS"
package_manager="brew"
elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then
echo "Windows"
package_manager="choco"
fi
# 패키지 설치
case "$package_manager" in
apt)
sudo apt update
sudo apt install -y git
;;
brew)
brew install git
;;
choco)
choco install git -y
;;
esac
PowerShell 크로스 플랫폼
# OS 감지
if ($IsLinux) {
Write-Host "Running on Linux"
$packageManager = "apt"
} elseif ($IsMacOS) {
Write-Host "Running on macOS"
$packageManager = "brew"
} elseif ($IsWindows) {
Write-Host "Running on Windows"
$packageManager = "choco"
}
# 경로 구분자
$separator = [System.IO.Path]::DirectorySeparatorChar
$path = "folder$($separator)file.txt"
보안 모범 사례
안전한 스크립트 작성
#!/bin/bash
# 1. 엄격 모드
set -euo pipefail
# 2. 입력 검증
if [ $# -ne 1 ]; then
echo "Usage: $0 <filename>"
exit 1
fi
filename="$1"
# 3. 경로 검증
if [[ "$filename" != *.txt ]]; then
echo "Only .txt files allowed"
exit 1
fi
# 4. 변수 인용 (공백 처리)
if [ -f "$filename" ]; then # 인용 필수!
cat "$filename"
fi
# 5. 임시 파일 안전하게 생성
temp_file=$(mktemp)
trap 'rm -f "$temp_file"' EXIT # 작은따옴표: trap 실행 시점에 확장, 공백 경로도 안전
# 6. 비밀번호 입력 (숨김)
read -s -p "Password: " password
echo
비밀 정보 관리
# ❌ 스크립트에 하드코딩
DB_PASSWORD="secret123"
# ✅ 환경 변수
export DB_PASSWORD="secret123"
./script.sh
# ✅ .env 파일
# .env
DB_PASSWORD=secret123
# script.sh
source .env
echo $DB_PASSWORD
# ⚠️ .env를 .gitignore에 추가!
# ✅ 안전한 입력
read -s -p "Database password: " DB_PASSWORD
echo
성능 최적화
빠른 스크립트 작성
# ❌ 느림 (외부 명령어 반복 호출)
for file in *.txt; do
lines=$(wc -l < "$file")
echo "$file: $lines lines"
done
# ✅ 빠름 (한 번에 처리)
wc -l *.txt
# ❌ 느림 (불필요한 cat과 wc)
cat file.txt | grep "error" | wc -l
# ✅ grep이 파일을 직접 읽고 줄 수까지 셈
grep -c "error" file.txt
# 주의: grep "error" | grep "critical"(두 단어가 모두 있는 줄)을
# grep "error.*critical"로 바꾸면 순서가 고정되어 "critical ... error" 줄을 놓칩니다
병렬 처리
# 순차 처리 (느림)
for file in *.jpg; do
convert "$file" -resize 800x600 "resized_$file"
done
# 병렬 처리 (빠름)
for file in *.jpg; do
convert "$file" -resize 800x600 "resized_$file" &
done
wait # 모든 백그라운드 작업 완료 대기
# GNU parallel 사용 (가장 빠름)
parallel convert {} -resize 800x600 resized_{} ::: *.jpg
실전 도구
fzf (퍼지 파인더)
# 설치
# macOS
brew install fzf
# Ubuntu
sudo apt install fzf
# 사용
# Ctrl+R: 명령어 히스토리 검색
# Ctrl+T: 파일 검색
# Alt+C: 디렉토리 검색
# 스크립트에서 사용
selected_file=$(find . -type f | fzf)
echo "Selected: $selected_file"
ripgrep (빠른 검색)
# 설치
brew install ripgrep # macOS
sudo apt install ripgrep # Ubuntu
# 사용 (grep보다 훨씬 빠름)
rg "error" ./logs
rg -i "warning" # 대소문자 무시
rg -t js "function" # JavaScript 파일만
rg "TODO" --stats # 통계 포함
bat (cat 개선)
# 설치
brew install bat # macOS
sudo apt install bat # Ubuntu
# 사용 (문법 하이라이팅)
bat file.js
bat --style=numbers,changes file.js
exa (ls 개선)
# 설치
brew install exa # macOS
sudo apt install exa # Ubuntu
# 사용
exa -l # 자세히
exa -T # 트리 구조
exa -l --git # Git 상태 포함
CI/CD에서 Shell 활용
GitHub Actions
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: |
npm ci
npm run build
- name: Deploy
run: |
#!/bin/bash
set -e
echo "Deploying to production..."
# SSH로 서버 접속 및 배포
ssh user@server << 'EOF'
cd /var/www/app
git pull
npm ci --production
npm run build
pm2 restart app
EOF
echo "Deploy complete!"
GitLab CI
deploy:
stage: deploy
script:
- |
#!/bin/bash
set -euxo pipefail
# Docker 이미지 빌드
docker build -t myapp:$CI_COMMIT_SHA .
# Docker Hub 푸시
docker push myapp:$CI_COMMIT_SHA
# Kubernetes 배포
kubectl set image deployment/myapp \
myapp=myapp:$CI_COMMIT_SHA
only:
- main
Shell 스크립트 테스트
ShellCheck (정적 분석)
# 설치
brew install shellcheck # macOS
sudo apt install shellcheck # Ubuntu
# 사용
shellcheck script.sh
# 출력 예시:
# In script.sh line 5:
# if [ $name = "Alice" ]; then
# ^-- SC2086: Double quote to prevent globbing
Bats (테스트 프레임워크)
# 설치
npm install -g bats
# test.bats
#!/usr/bin/env bats
@test "addition" {
result=$(echo $((2 + 2)))
[ "$result" -eq 4 ]
}
@test "file exists" {
touch /tmp/test_file
[ -f /tmp/test_file ]
rm /tmp/test_file
}
# 실행
bats test.bats
색상·진행 표시는 터미널일 때만 켜기
셸 스크립트에 ANSI 색상 코드([0;32m 같은)나 로 한 줄을 덮어쓰는 진행 바를 넣는 것 자체는 흔합니다. 문제는 같은 스크립트를 ./build.sh > build.log로 리다이렉트하거나 CI에서 돌릴 때입니다. 출력이 터미널이 아니어도 이스케이프 시퀀스는 그대로 기록되기 때문에 로그 파일에 ^[[0;32m 같은 문자열이 섞이고, grep "Success" build.log처럼 로그를 다시 검색할 때 색상 코드가 단어 사이에 끼어 매칭이 어긋나기도 합니다. 진행 바는 CI 로그 뷰어에서 줄마다 새로 찍혀 수백 줄로 불어납니다.
그래서 출력 대상이 터미널인지 -t 테스트로 확인하고, 아니면 색상 변수를 빈 문자열로 두는 방식이 안전합니다. 많은 CLI 도구가 따르는 NO_COLOR 환경 변수도 함께 존중하면 사용자가 직접 끌 수 있습니다. 또 echo -e는 Bash에서만 이스케이프를 해석하고 sh(dash)나 macOS의 /bin/sh에서는 동작이 다르므로, 이식성이 필요하면 printf를 씁니다.
#!/bin/bash
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
GREEN='[0;32m'; RED='[0;31m'; NC='[0m'
else
GREEN=''; RED=''; NC=''
fi
success() { printf '%b%s%b
' "$GREEN" "$1" "$NC"; }
error() { printf '%b%s%b
' "$RED" "$1" "$NC" >&2; }
success "Build complete"
error "Build failed" # 에러는 stderr로 보내야 파이프 뒤 명령이 결과와 섞지 않습니다
한 번 쓰고 말 명령을 ~/.bashrc 함수로 옮기는 기준도 비슷합니다. 같은 파이프라인을 세 번째 타이핑하고 있다면 함수나 alias로 만들 때가 된 것이고, 인자를 받거나 여러 줄이 되면 alias가 아니라 함수로 만드는 편이 인용 문제를 덜 겪습니다.
실전 프로젝트
프로젝트 1: 개발 환경 셋업 스크립트
#!/bin/bash
set -e
echo "🚀 개발 환경 셋업 시작..."
# OS 감지
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
OS="linux"
elif [[ "$OSTYPE" == "darwin"* ]]; then
OS="macos"
else
echo "Unsupported OS"
exit 1
fi
# 패키지 매니저 업데이트
echo "📦 패키지 매니저 업데이트..."
if [ "$OS" = "linux" ]; then
sudo apt update && sudo apt upgrade -y
elif [ "$OS" = "macos" ]; then
brew update && brew upgrade
fi
# Git 설치
echo "📥 Git 설치..."
if ! command -v git &> /dev/null; then
if [ "$OS" = "linux" ]; then
sudo apt install -y git
elif [ "$OS" = "macos" ]; then
brew install git
fi
fi
# Node.js 설치
echo "📥 Node.js 설치..."
if ! command -v node &> /dev/null; then
if [ "$OS" = "linux" ]; then
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs
elif [ "$OS" = "macos" ]; then
brew install node
fi
fi
# Docker 설치
echo "🐳 Docker 설치..."
if ! command -v docker &> /dev/null; then
if [ "$OS" = "linux" ]; then
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
elif [ "$OS" = "macos" ]; then
echo "Please install Docker Desktop manually"
fi
fi
# VSCode 설치
echo "💻 VSCode 설치..."
if ! command -v code &> /dev/null; then
if [ "$OS" = "linux" ]; then
sudo snap install code --classic
elif [ "$OS" = "macos" ]; then
brew install --cask visual-studio-code
fi
fi
# Git 설정
echo "⚙️ Git 설정..."
read -p "Git username: " git_username
read -p "Git email: " git_email
git config --global user.name "$git_username"
git config --global user.email "$git_email"
git config --global init.defaultBranch main
# SSH 키 생성
if [ ! -f ~/.ssh/id_ed25519 ]; then
echo "🔑 SSH 키 생성..."
ssh-keygen -t ed25519 -C "$git_email" -f ~/.ssh/id_ed25519 -N ""
echo "SSH 공개 키:"
cat ~/.ssh/id_ed25519.pub
fi
echo
echo "✅ 개발 환경 셋업 완료!"
echo
echo "설치된 도구:"
echo "- Git: $(git --version)"
echo "- Node.js: $(node --version)"
echo "- npm: $(npm --version)"
echo "- Docker: $(docker --version 2>/dev/null || echo 'Not installed')"
echo "- VSCode: $(code --version 2>/dev/null | head -1 || echo 'Not installed')"
프로젝트 2: 로그 모니터링
#!/bin/bash
LOG_FILE="/var/log/nginx/access.log"
ALERT_EMAIL="[email protected]"
ERROR_THRESHOLD=100
# 실시간 로그 모니터링
tail -f "$LOG_FILE" | while read line; do
# 에러 감지
if echo "$line" | grep -q "500\|502\|503"; then
echo "⚠️ Error detected: $line"
# 에러 카운트
error_count=$(grep -c "50[0-3]" "$LOG_FILE")
# 임계값 초과 시 알림
if [ "$error_count" -gt "$ERROR_THRESHOLD" ]; then
echo "🚨 Too many errors! Sending alert..."
# 이메일 알림
echo "Error count: $error_count" | mail -s "Server Alert" "$ALERT_EMAIL"
# Slack 알림
curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
-H 'Content-Type: application/json' \
-d "{\"text\":\"🚨 Server errors: $error_count\"}"
fi
fi
done
프로젝트 3: 서버 배포 스크립트
#!/bin/bash
set -euo pipefail
# 색상
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# 함수
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# 설정
APP_NAME="myapp"
APP_DIR="/var/www/$APP_NAME"
BRANCH="main"
BACKUP_DIR="/backup/$APP_NAME"
# 백업
backup() {
log_info "Creating backup..."
local backup_file="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).tar.gz"
mkdir -p "$BACKUP_DIR"
tar -czf "$backup_file" -C "$APP_DIR" .
log_info "Backup created: $backup_file"
}
# 배포
deploy() {
log_info "Starting deployment..."
cd "$APP_DIR"
# 1. 백업
backup
# 2. Git Pull
log_info "Pulling latest code..."
git fetch origin
git reset --hard origin/$BRANCH
# 3. 의존성 설치
log_info "Installing dependencies..."
npm ci --production
# 4. 빌드
log_info "Building..."
npm run build
# 5. 데이터베이스 마이그레이션
log_info "Running migrations..."
npm run migrate
# 6. 서비스 재시작
log_info "Restarting service..."
pm2 restart "$APP_NAME"
# 7. 헬스 체크
log_info "Health check..."
sleep 5
local response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health)
if [ "$response" = "200" ]; then
log_info "✅ Deployment successful!"
else
log_error "❌ Health check failed (HTTP $response)"
rollback
exit 1
fi
}
# 롤백
rollback() {
log_warn "Rolling back..."
# 최신 백업 찾기
local latest_backup=$(ls -t "$BACKUP_DIR"/backup_*.tar.gz | head -1)
if [ -z "$latest_backup" ]; then
log_error "No backup found!"
exit 1
fi
log_info "Restoring from: $latest_backup"
cd "$APP_DIR"
tar -xzf "$latest_backup"
npm ci --production
pm2 restart "$APP_NAME"
log_info "Rollback complete"
}
# 메인
case "${1:-}" in
deploy)
deploy
;;
rollback)
rollback
;;
backup)
backup
;;
*)
echo "Usage: $0 {deploy|rollback|backup}"
exit 1
;;
esac
PowerShell 고급 기능
객체 파이프라인
# 프로세스 정보 (객체)
Get-Process |
Where-Object { $_.CPU -gt 10 } |
Sort-Object CPU -Descending |
Select-Object -First 10 Name, CPU, WorkingSet |
Format-Table -AutoSize
# 파일 크기 합계
Get-ChildItem -Recurse |
Measure-Object -Property Length -Sum |
Select-Object @{Name="TotalGB";Expression={[math]::Round($_.Sum / 1GB, 2)}}
# CSV 처리
Import-Csv users.csv |
Where-Object { $_.Age -gt 18 } |
Select-Object Name, Email |
Export-Csv adults.csv -NoTypeInformation
원격 실행
# 원격 서버에서 명령어 실행
Invoke-Command -ComputerName Server01 -ScriptBlock {
Get-Service | Where-Object { $_.Status -eq "Running" }
}
# 여러 서버
$servers = @("Server01", "Server02", "Server03")
Invoke-Command -ComputerName $servers -ScriptBlock {
Get-Process | Sort-Object CPU -Descending | Select-Object -First 5
}
# 자격 증명
$cred = Get-Credential
Invoke-Command -ComputerName Server01 -Credential $cred -ScriptBlock {
Restart-Service IIS
}
모듈 및 패키지
# 모듈 검색
Find-Module -Name "*Azure*"
# 모듈 설치
Install-Module -Name Az -Scope CurrentUser
# 모듈 가져오기
Import-Module Az
# 설치된 모듈 목록
Get-Module -ListAvailable
# 명령어 검색
Get-Command -Module Az
트러블슈팅
Bash 문제 해결
1) “Permission denied”
# 원인: 실행 권한 없음
# 해결:
chmod +x script.sh
2) “command not found”
# 원인: PATH에 없음
# 해결:
export PATH="/usr/local/bin:$PATH"
# 또는 전체 경로 사용
/usr/local/bin/mycommand
3) “No such file or directory”
# 원인: 공백 처리 안 됨
# ❌ 잘못된 코드
file=my file.txt # 대입이 아니라 "file=my" 환경에서 file.txt 명령 실행 → command not found
file="my file.txt"
cat $file # cat이 "my"와 "file.txt"를 별도 파일로 인식 → No such file or directory
# ✅ 올바른 코드
file="my file.txt"
cat "$file" # 인용 필수!
4) /bin/bash^M: bad interpreter 또는 $'\r': command not found
Windows에서 작성하거나 core.autocrlf=true인 Git으로 체크아웃한 스크립트는 줄 끝이 CRLF(\r\n)가 됩니다. Bash는 \r을 명령의 일부로 읽기 때문에 첫 줄의 인터프리터 경로가 /bin/bash\r이 되어 “그런 파일이 없다”는 에러가 나고, 파일 내용은 멀쩡해 보여서 원인을 찾기 어렵습니다. file script.sh로 with CRLF line terminators가 표시되는지 확인하고, dos2unix script.sh나 sed -i 's/\r$//' script.sh로 고친 뒤, 저장소에 *.sh text eol=lf를 담은 .gitattributes를 두면 재발하지 않습니다.
PowerShell 문제 해결
1) “실행 정책” 에러
# 원인: 스크립트 실행 정책
# 해결:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
# 확인
Get-ExecutionPolicy
2) “명령어를 찾을 수 없습니다”
# 원인: 모듈 미설치
# 해결:
Install-Module -Name ModuleName
# 또는 PATH 추가
$env:PATH += ";C:\MyTools"
FAQ
Q1. Bash와 Zsh 중 무엇을 써야 하나요? 서버 스크립트와 CI는 Bash가 기본값인 경우가 많습니다. 로컬 터미널에서 자동 완성과 플러그인을 쓰고 싶다면 Zsh를 많이 씁니다. macOS는 Catalina부터 기본 셸이 Zsh입니다.
Q2. PowerShell을 Linux에서도 쓸 수 있나요?
PowerShell 7(pwsh)은 크로스 플랫폼입니다. 다만 Linux 서버의 문서와 예제는 여전히 Bash 기준이 대부분입니다.
Q3. Shell과 Python은 어떻게 나눠 쓰나요? 짧은 파이프라인, 배포 스크립트, 크론 작업은 셸이 가볍습니다. 데이터 가공, 복잡한 분기, 외부 라이브러리가 필요하면 Python이 나은 경우가 많습니다.
Q4. 무엇부터 익혀야 하나요? Linux를 다룬다면 Bash는 피하기 어렵습니다. 그다음 로컬 환경에 Zsh를 얹거나, Windows 관리를 한다면 PowerShell을 함께 익히면 됩니다.
같이 보면 좋은 글
- Bun Shell로 크로스 플랫폼 스크립팅하기
- I/O 멀티플렉싱: select·epoll·IOCP·kqueue
- 개발자를 위한 리눅스·맥 명령어 실전 가이드 | 네트워크·파일·프로세스·디버깅