레슨 08: Bash에서의 정규 표현식(Regular Expressions)
레슨 08: Bash에서의 정규 표현식(Regular Expressions)¶
난이도: ⭐⭐⭐
이전: 07_String_Processing.md | 다음: 09_Process_Management.md
1. Glob vs Regex¶
Bash 스크립팅에서 glob과 regex의 차이점을 이해하는 것은 매우 중요합니다.
1.1 근본적인 차이점¶
| 특징 | Glob | Regex |
|---|---|---|
| 목적 | 파일명 매칭 | 문자열 패턴 매칭 |
| 컨텍스트 | 파일 작업, case 문 | [[ =~ ]], grep, sed, awk |
* 의미 |
0개 이상의 문자 | 이전 문자의 0개 이상 반복 |
. 의미 |
리터럴 점 | 임의의 단일 문자 |
? 의미 |
정확히 하나의 문자 | 이전 문자의 0개 또는 1개 |
| 문자 클래스 | [abc] |
[abc] (동일) |
| 부정 | [!abc] |
[^abc] |
| 앵커 | 없음 (암묵적) | ^ (시작), $ (끝) |
| 그룹 | {a,b} (중괄호 확장) |
(a\|b) (선택) |
1.2 Glob 예제¶
#!/bin/bash
# Glob은 파일명 매칭에 사용됩니다
ls *.txt # .txt로 끝나는 모든 파일
ls test?.log # test1.log, test2.log 등
ls [abc]*.txt # a, b, c로 시작하는 파일
ls [!0-9]* # 숫자로 시작하지 않는 파일
ls file{1,2,3}.txt # file1.txt, file2.txt, file3.txt
# case 문에서
case $filename in
*.txt) echo "Text file" ;;
*.jpg|*.png) echo "Image file" ;;
test*) echo "Test file" ;;
esac
# 조건문에서
if [[ $filename == *.txt ]]; then
echo "Text file"
fi
1.3 Regex 예제¶
#!/bin/bash
# Regex는 문자열 매칭에 사용됩니다
[[ $string =~ ^[0-9]+$ ]] && echo "All digits"
[[ $email =~ ^[a-z]+@[a-z]+\.[a-z]+$ ]] && echo "Valid email pattern"
# grep과 함께
grep '^ERROR' logfile.txt # ERROR로 시작하는 라인
grep 'test.*done' logfile.txt # 'test' 다음에 'done'이 오는 라인
# sed와 함께
sed 's/[0-9]\+/NUM/g' file.txt # 숫자를 NUM으로 변경
1.4 일반적인 혼동 지점¶
#!/bin/bash
# 틀림: =~에 glob 패턴 사용
[[ $str =~ *.txt ]] # 이것은 리터럴 "*" 다음에 ".txt"를 매칭합니다!
# 올바름: ==에 glob 사용
[[ $str == *.txt ]] # .txt로 끝나는 문자열 매칭
# 올바름: =~에 regex 사용
[[ $str =~ .*\.txt$ ]] # Regex: .txt로 끝나는 모든 것
# Glob: *는 0개 이상의 문자를 의미
echo test* matches: test, test1, test123
# Regex: *는 이전 문자의 0개 이상 반복을 의미
[[ $str =~ test* ]] # 매칭: tes, test, testt, testtt
# Glob: ?는 정확히 하나의 문자를 의미
ls file?.txt # 매칭: file1.txt, fileA.txt
# Regex: ?는 이전 문자의 0개 또는 1개를 의미
[[ $str =~ tests? ]] # 매칭: test, tests
2. =~ 연산자¶
=~ 연산자는 Bash의 [[ ]] 구조에서 regex 매칭을 수행합니다.
2.1 기본 사용법¶
#!/bin/bash
# 간단한 패턴 매칭
string="hello123"
if [[ $string =~ [0-9] ]]; then
echo "Contains a digit"
fi
# 앵커가 있는 패턴
if [[ $string =~ ^hello ]]; then
echo "Starts with 'hello'"
fi
if [[ $string =~ [0-9]$ ]]; then
echo "Ends with a digit"
fi
# 전체 문자열 매칭
if [[ $string =~ ^[a-z]+[0-9]+$ ]]; then
echo "Letters followed by numbers"
fi
2.2 인용 동작¶
#!/bin/bash
string="test123"
# 틀림: 인용된 패턴은 리터럴이 됨
if [[ $string =~ "[0-9]+" ]]; then
echo "Never matches - looks for literal '[0-9]+'"
fi
# 올바름: 인용되지 않은 패턴
if [[ $string =~ [0-9]+ ]]; then
echo "Matches one or more digits"
fi
# 올바름: 패턴을 포함하는 변수 (인용됨)
pattern='[0-9]+'
if [[ $string =~ $pattern ]]; then
echo "Matches - variable expansion is NOT quoted"
fi
# 중요: 이식성과 특수 문자를 위해 변수를 사용하세요
2.3 반환 값¶
#!/bin/bash
string="test123"
# 반환 값
if [[ $string =~ [0-9]+ ]]; then
echo "Match found (returns 0)"
else
echo "No match (returns 1)"
fi
# 반환 값을 직접 사용
validate_email() {
[[ $1 =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]
# 매칭되면 0, 매칭되지 않으면 1 반환
}
if validate_email "user@example.com"; then
echo "Valid email"
fi
2.4 변수에 패턴 저장¶
#!/bin/bash
# 명확성과 재사용성을 위해 변수에 패턴 정의
readonly IP_PATTERN='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
readonly EMAIL_PATTERN='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
readonly URL_PATTERN='^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
validate_ip() {
[[ $1 =~ $IP_PATTERN ]]
}
validate_email() {
[[ $1 =~ $EMAIL_PATTERN ]]
}
validate_url() {
[[ $1 =~ $URL_PATTERN ]]
}
# 사용법
if validate_ip "192.168.1.1"; then
echo "Valid IP"
fi
3. BASH_REMATCH¶
BASH_REMATCH 배열은 regex 매칭에서 캡처된 그룹을 저장합니다.
3.1 기본 캡처 그룹¶
#!/bin/bash
string="John Doe, age 30"
pattern='([A-Z][a-z]+) ([A-Z][a-z]+), age ([0-9]+)'
if [[ $string =~ $pattern ]]; then
echo "Full match: ${BASH_REMATCH[0]}"
echo "First name: ${BASH_REMATCH[1]}"
echo "Last name: ${BASH_REMATCH[2]}"
echo "Age: ${BASH_REMATCH[3]}"
fi
# 출력:
# Full match: John Doe, age 30
# First name: John
# Last name: Doe
# Age: 30
3.2 구조화된 데이터 추출¶
#!/bin/bash
# 로그 항목 파싱
log_entry="2024-02-13 14:30:45 [ERROR] Database connection failed"
pattern='^([0-9-]+) ([0-9:]+) \[([A-Z]+)\] (.+)$'
if [[ $log_entry =~ $pattern ]]; then
date="${BASH_REMATCH[1]}"
time="${BASH_REMATCH[2]}"
level="${BASH_REMATCH[3]}"
message="${BASH_REMATCH[4]}"
echo "Date: $date"
echo "Time: $time"
echo "Level: $level"
echo "Message: $message"
fi
3.3 중첩 그룹¶
#!/bin/bash
# URL 파싱
url="https://user:pass@example.com:8080/path/to/resource?key=value"
pattern='^(https?)://([^:]+):([^@]+)@([^:]+):([0-9]+)(/[^?]*)(\?.*)?$'
if [[ $url =~ $pattern ]]; then
protocol="${BASH_REMATCH[1]}"
username="${BASH_REMATCH[2]}"
password="${BASH_REMATCH[3]}"
host="${BASH_REMATCH[4]}"
port="${BASH_REMATCH[5]}"
path="${BASH_REMATCH[6]}"
query="${BASH_REMATCH[7]}"
echo "Protocol: $protocol"
echo "Username: $username"
echo "Password: $password"
echo "Host: $host"
echo "Port: $port"
echo "Path: $path"
echo "Query: $query"
fi
3.4 루프에서 여러 매칭¶
#!/bin/bash
# 텍스트에서 모든 이메일 주소 추출
text="Contact us at support@example.com or sales@example.com for more info."
pattern='[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
while [[ $text =~ $pattern ]]; do
email="${BASH_REMATCH[0]}"
echo "Found: $email"
# 다음 매칭을 찾기 위해 매칭된 부분 제거
text="${text#*"$email"}"
done
# 출력:
# Found: support@example.com
# Found: sales@example.com
3.5 실용적인 추출 함수¶
#!/bin/bash
# 문자열에서 키-값 쌍 추출
parse_key_value() {
local text=$1
declare -gA parsed_data
local pattern='([a-zA-Z_][a-zA-Z0-9_]*)=([^,]+)'
while [[ $text =~ $pattern ]]; do
local key="${BASH_REMATCH[1]}"
local value="${BASH_REMATCH[2]}"
parsed_data[$key]="$value"
# 매칭된 부분 제거
text="${text#*"${BASH_REMATCH[0]}"}"
done
}
# 사용법
data="name=Alice,age=30,city=NYC,role=admin"
parse_key_value "$data"
for key in "${!parsed_data[@]}"; do
echo "$key: ${parsed_data[$key]}"
done
4. 확장 정규 표현식(Extended Regular Expressions)¶
ERE(확장 정규 표현식)는 더 강력한 패턴 매칭을 제공합니다.
4.1 문자 클래스¶
#!/bin/bash
# POSIX 문자 클래스
[[ $char =~ [[:alpha:]] ]] # 알파벳 문자
[[ $char =~ [[:digit:]] ]] # 숫자
[[ $char =~ [[:alnum:]] ]] # 영숫자
[[ $char =~ [[:space:]] ]] # 공백 문자
[[ $char =~ [[:punct:]] ]] # 구두점
[[ $char =~ [[:upper:]] ]] # 대문자
[[ $char =~ [[:lower:]] ]] # 소문자
[[ $char =~ [[:xdigit:]] ]] # 16진수 숫자
# 사용자 정의 문자 클래스
[[ $char =~ [aeiouAEIOU] ]] # 모음
[[ $char =~ [^aeiouAEIOU] ]] # 자음 (부정)
[[ $char =~ [0-9a-fA-F] ]] # 16진수 문자
4.2 수량자(Quantifiers)¶
#!/bin/bash
# 기본 수량자
[[ $str =~ a+ ]] # 하나 이상의 'a'
[[ $str =~ a* ]] # 0개 이상의 'a'
[[ $str =~ a? ]] # 0개 또는 1개의 'a'
# 경계 수량자
[[ $str =~ a{3} ]] # 정확히 3개의 'a'
[[ $str =~ a{3,} ]] # 3개 이상의 'a'
[[ $str =~ a{3,5} ]] # 3개에서 5개 사이의 'a'
# 실용적인 예제
[[ $str =~ ^[0-9]{3}-[0-9]{2}-[0-9]{4}$ ]] # SSN 형식: 123-45-6789
[[ $str =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] # IP 주소
[[ $str =~ ^[a-zA-Z]{2,20}$ ]] # 이름: 2-20개의 문자
4.3 선택(Alternation)¶
#!/bin/bash
# 선택 (OR)
[[ $str =~ ^(yes|no|maybe)$ ]]
[[ $str =~ \.(jpg|png|gif)$ ]]
[[ $str =~ ^(http|https|ftp):// ]]
# 그룹과 함께
[[ $str =~ ^(Mr|Mrs|Ms|Dr)\. [A-Z][a-z]+ [A-Z][a-z]+$ ]]
# 매칭: Mr. John Smith, Dr. Jane Doe 등
# 복잡한 선택
file_pattern='.*\.(txt|log|conf|cfg|ini|yaml|yml|json|xml)$'
[[ $filename =~ $file_pattern ]]
4.4 그룹화(Grouping)¶
#!/bin/bash
# 캡처를 위한 그룹
pattern='(https?)://([^/]+)(/.*)?'
url="https://example.com/path/to/page"
if [[ $url =~ $pattern ]]; then
protocol="${BASH_REMATCH[1]}"
domain="${BASH_REMATCH[2]}"
path="${BASH_REMATCH[3]}"
fi
# 수량자를 위한 그룹
[[ $str =~ ^(ab)+ ]] # 매칭: ab, abab, ababab
[[ $str =~ ^([0-9]{3}-){2}[0-9]{4}$ ]] # 전화번호: 555-123-4567
# 선택을 위한 그룹
[[ $str =~ ^(red|green|blue) (car|bike|boat)$ ]]
# 매칭: "red car", "green bike", "blue boat" 등
4.5 앵커(Anchors)¶
#!/bin/bash
# 시작과 끝 앵커
[[ $str =~ ^hello ]] # 'hello'로 시작
[[ $str =~ world$ ]] # 'world'로 끝남
[[ $str =~ ^test$ ]] # 정확히 'test'
# 단어 경계 (grep과 함께, =~에서 직접 사용 안 됨)
echo "hello world" | grep -E '\bhello\b' # 완전한 단어로 'hello' 매칭
# 실용적인 예제
[[ $str =~ ^# ]] # 주석 라인 (#으로 시작)
[[ $str =~ ;$ ]] # 세미콜론으로 끝남
[[ $str =~ ^$ ]] # 빈 라인
[[ $str =~ ^[[:space:]]*$ ]] # 공백 라인 (공백만 있음)
4.6 ERE vs BRE 비교¶
| 특징 | BRE (기본) | ERE (확장) |
|---|---|---|
| 그룹화 | \(\) |
() |
| 선택 | \| |
| |
수량자 +, ? |
지원 안 됨 | +, ? |
| 경계 수량자 | \{n,m\} |
{n,m} |
| 사용처 | grep, sed (기본) | grep -E, egrep, awk |
Bash =~ |
ERE 사용 | 네이티브 |
#!/bin/bash
# BRE (grep 기본)
echo "test123" | grep '\([a-z]\+\)[0-9]\+'
# ERE (grep -E 또는 egrep)
echo "test123" | grep -E '([a-z]+)[0-9]+'
# Bash =~는 ERE 사용
[[ "test123" =~ ^([a-z]+)([0-9]+)$ ]]
5. 실용적인 검증 함수¶
5.1 이메일 검증¶
#!/bin/bash
#
# 이메일 주소 검증
#
# 반환값: 유효하면 0, 유효하지 않으면 1
#
validate_email() {
local email=$1
local pattern='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
[[ $email =~ $pattern ]]
}
# 테스트 케이스
test_email() {
local email=$1
if validate_email "$email"; then
echo "✓ Valid: $email"
else
echo "✗ Invalid: $email"
fi
}
test_email "user@example.com" # ✓ Valid
test_email "user.name@example.co.uk" # ✓ Valid
test_email "user+tag@example.com" # ✓ Valid
test_email "invalid@" # ✗ Invalid
test_email "@example.com" # ✗ Invalid
test_email "user@example" # ✗ Invalid
5.2 IPv4 주소 검증¶
#!/bin/bash
#
# IPv4 주소 검증 (범위 확인 포함)
#
validate_ipv4() {
local ip=$1
local pattern='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
# 형식 확인
if ! [[ $ip =~ $pattern ]]; then
return 1
fi
# 각 옥텟이 0-255인지 확인
local IFS='.'
read -ra octets <<< "$ip"
for octet in "${octets[@]}"; do
if ((octet < 0 || octet > 255)); then
return 1
fi
done
return 0
}
# 테스트 케이스
test_ip() {
local ip=$1
if validate_ipv4 "$ip"; then
echo "✓ Valid: $ip"
else
echo "✗ Invalid: $ip"
fi
}
test_ip "192.168.1.1" # ✓ Valid
test_ip "10.0.0.1" # ✓ Valid
test_ip "255.255.255.255" # ✓ Valid
test_ip "256.1.1.1" # ✗ Invalid (256 > 255)
test_ip "192.168.1" # ✗ Invalid (불완전)
test_ip "192.168.1.1.1" # ✗ Invalid (옥텟 수 초과)
5.3 날짜 형식 검증¶
#!/bin/bash
#
# YYYY-MM-DD 형식의 날짜 검증
#
validate_date() {
local date=$1
local pattern='^([0-9]{4})-([0-9]{2})-([0-9]{2})$'
if ! [[ $date =~ $pattern ]]; then
return 1
fi
local year="${BASH_REMATCH[1]}"
local month="${BASH_REMATCH[2]}"
local day="${BASH_REMATCH[3]}"
# 월 검증
if ((month < 1 || month > 12)); then
return 1
fi
# 일 검증
if ((day < 1 || day > 31)); then
return 1
fi
# date 명령으로 추가 검증
if ! date -d "$date" > /dev/null 2>&1; then
return 1
fi
return 0
}
# 테스트 케이스
test_date() {
local date=$1
if validate_date "$date"; then
echo "✓ Valid: $date"
else
echo "✗ Invalid: $date"
fi
}
test_date "2024-02-13" # ✓ Valid
test_date "2024-12-31" # ✓ Valid
test_date "2024-02-30" # ✗ Invalid (2월 30일은 존재하지 않음)
test_date "2024-13-01" # ✗ Invalid (월 13)
test_date "24-02-13" # ✗ Invalid (잘못된 형식)
5.4 URL 검증¶
#!/bin/bash
#
# URL 검증
#
validate_url() {
local url=$1
local pattern='^(https?|ftp)://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/[^[:space:]]*)?$'
[[ $url =~ $pattern ]]
}
# 더 포괄적인 URL 검증
validate_url_detailed() {
local url=$1
local pattern='^(https?|ftp)://(([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|localhost|([0-9]{1,3}\.){3}[0-9]{1,3})(:[0-9]{1,5})?(/[^[:space:]]*)?$'
[[ $url =~ $pattern ]]
}
# 테스트 케이스
test_url() {
local url=$1
if validate_url_detailed "$url"; then
echo "✓ Valid: $url"
else
echo "✗ Invalid: $url"
fi
}
test_url "https://example.com" # ✓ Valid
test_url "http://example.com/path/to/page" # ✓ Valid
test_url "https://sub.example.com:8080/api" # ✓ Valid
test_url "ftp://files.example.com" # ✓ Valid
test_url "https://localhost:3000" # ✓ Valid
test_url "https://192.168.1.1:8080" # ✓ Valid
test_url "htp://example.com" # ✗ Invalid (프로토콜 오타)
test_url "https://example" # ✗ Invalid (TLD 없음)
5.5 시맨틱 버전 검증¶
#!/bin/bash
#
# 시맨틱 버전(semver) 검증
# 형식: MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]
#
validate_semver() {
local version=$1
local pattern='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$'
[[ $version =~ $pattern ]]
}
# semver 구성 요소 추출
parse_semver() {
local version=$1
local pattern='^([0-9]+)\.([0-9]+)\.([0-9]+)(-([a-zA-Z0-9.-]+))?(\+([a-zA-Z0-9.-]+))?$'
if [[ $version =~ $pattern ]]; then
echo "Major: ${BASH_REMATCH[1]}"
echo "Minor: ${BASH_REMATCH[2]}"
echo "Patch: ${BASH_REMATCH[3]}"
echo "Prerelease: ${BASH_REMATCH[5]}"
echo "Build: ${BASH_REMATCH[7]}"
return 0
fi
return 1
}
# 테스트 케이스
test_semver() {
local version=$1
if validate_semver "$version"; then
echo "✓ Valid: $version"
parse_semver "$version"
else
echo "✗ Invalid: $version"
fi
echo
}
test_semver "1.0.0" # ✓ Valid
test_semver "1.0.0-alpha" # ✓ Valid
test_semver "1.0.0-alpha.1" # ✓ Valid
test_semver "1.0.0+20240213" # ✓ Valid
test_semver "1.0.0-beta+exp.sha.5114f85" # ✓ Valid
test_semver "1.0" # ✗ Invalid (불완전)
test_semver "v1.0.0" # ✗ Invalid ('v' 접두사 있음)
5.6 포괄적인 검증 프레임워크¶
#!/bin/bash
# 검증 규칙
declare -A VALIDATORS=(
[email]='validate_email'
[ipv4]='validate_ipv4'
[date]='validate_date'
[url]='validate_url'
[semver]='validate_semver'
)
# 범용 검증 함수
validate() {
local type=$1
local value=$2
local validator="${VALIDATORS[$type]}"
if [[ -z $validator ]]; then
echo "Error: Unknown validator type: $type" >&2
return 2
fi
if ! type "$validator" > /dev/null 2>&1; then
echo "Error: Validator function not found: $validator" >&2
return 2
fi
"$validator" "$value"
}
# 사용 예제
validate_input() {
local field=$1
local value=$2
local type=$3
if validate "$type" "$value"; then
echo "✓ $field is valid"
return 0
else
echo "✗ $field is invalid: $value" >&2
return 1
fi
}
# 예제: 사용자 입력 검증
validate_input "Email" "user@example.com" "email"
validate_input "IP Address" "192.168.1.1" "ipv4"
validate_input "Version" "2.1.0-beta" "semver"
6. grep 및 sed와 함께 Regex 사용¶
6.1 확장 Regex를 사용한 grep¶
#!/bin/bash
# grep -E로 확장 regex 사용
grep -E '^[0-9]+$' file.txt # 숫자만 포함하는 라인
grep -E '(error|warning|critical)' log.txt # 여러 패턴
grep -E '\b[A-Z]{3,}\b' file.txt # 3개 이상의 대문자 단어
# 대소문자 구분 안 함
grep -iE 'error' log.txt
# 매칭 반전
grep -vE '^#' config.txt # 주석이 아닌 라인
# 매칭 카운트
grep -cE 'pattern' file.txt
# 컨텍스트 표시
grep -E -A 3 -B 3 'ERROR' log.txt # 전후 3줄
6.2 sed 패턴 매칭¶
#!/bin/bash
# regex를 사용한 기본 치환
sed 's/[0-9]\+/NUM/g' file.txt # 숫자 치환
# 앵커된 패턴
sed 's/^#.*//' file.txt # 주석 라인 제거
sed 's/[[:space:]]\+$//' file.txt # 후행 공백 제거
# 그룹과 역참조
sed 's/\([0-9]\{3\}\)-\([0-9]\{2\}\)-\([0-9]\{4\}\)/(\1) \2-\3/' # SSN 형식화
# 123-45-6789 → (123) 45-6789
# 조건부 처리
sed '/pattern/s/old/new/' file.txt # 매칭되는 라인에서만 치환
sed '/^#/d' file.txt # 주석 라인 삭제
6.3 다중 라인 매칭¶
#!/bin/bash
# grep 다중 라인 (GNU grep의 -Pzo와 함께)
grep -Pzo '(?s)function.*?\{.*?\}' code.js
# sed 다중 라인
sed -n '/start/,/end/p' file.txt # 범위 출력
# awk 다중 라인
awk '/start/,/end/' file.txt
7. 성능 고려 사항¶
7.1 Regex 컴파일¶
#!/bin/bash
# 비효율적: 매 반복마다 Regex 컴파일
for item in "${items[@]}"; do
if [[ $item =~ ^[0-9]+$ ]]; then
process "$item"
fi
done
# 효율적: 한 번 컴파일, 여러 번 사용
pattern='^[0-9]+$'
for item in "${items[@]}"; do
if [[ $item =~ $pattern ]]; then
process "$item"
fi
done
7.2 재앙적 역추적(Catastrophic Backtracking) 회피¶
#!/bin/bash
# 위험: 재앙적 역추적을 일으킬 수 있음
# 패턴: (a+)+b
# 문자열: "aaaaaaaaaaaaaaaaaaaaaaaac" (끝에 'b' 없음)
# 이것은 지수 시간이 걸립니다!
bad_pattern='(a+)+b'
# 같은 내용에 중첩된 수량자 피하기
# 안전: 원자 그룹이나 소유 수량자 사용 (지원되는 경우)
# 또는 역추적을 피하도록 재구성
good_pattern='a+b'
# 일반 규칙: (.*)*, (.+)+ 같은 패턴 피하기
7.3 간단한 연산 vs Regex¶
#!/bin/bash
# 간단한 문자열 연산이 regex보다 빠를 때
# 접두사 확인
# 느림:
[[ $str =~ ^prefix ]]
# 빠름:
[[ $str == prefix* ]]
# 접미사 확인
# 느림:
[[ $str =~ suffix$ ]]
# 빠름:
[[ $str == *suffix ]]
# 포함 확인
# 느림:
[[ $str =~ substring ]]
# 빠름:
[[ $str == *substring* ]]
# 패턴 매칭이 정말 필요할 때 regex 사용
# 리터럴 부분 문자열 확인에는 간단한 연산 사용
7.4 패턴 벤치마킹¶
#!/bin/bash
benchmark_regex() {
local pattern=$1
local test_string=$2
local iterations=10000
local start=$(date +%s%N)
for ((i=0; i<iterations; i++)); do
[[ $test_string =~ $pattern ]] > /dev/null
done
local end=$(date +%s%N)
local elapsed=$(( (end - start) / 1000000 ))
echo "Pattern: $pattern"
echo "Time: ${elapsed}ms for $iterations iterations"
echo "Avg: $((elapsed * 1000 / iterations))μs per match"
}
# 패턴 비교
benchmark_regex '^[0-9]+$' "12345"
benchmark_regex '[0-9]' "12345"
8. 일반 패턴 참조¶
8.1 패턴 라이브러리¶
| 패턴 | 설명 | 예제 |
|---|---|---|
^[0-9]+$ |
정수 | 123, 456 |
^[0-9]*\.[0-9]+$ |
소수 | 3.14, 0.5 |
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ |
이메일 | user@example.com |
^https?://[^\s]+$ |
URL | https://example.com |
^([0-9]{1,3}\.){3}[0-9]{1,3}$ |
IPv4 | 192.168.1.1 |
^[0-9]{4}-[0-9]{2}-[0-9]{2}$ |
날짜 YYYY-MM-DD | 2024-02-13 |
^([01][0-9]|2[0-3]):[0-5][0-9]$ |
시간 HH:MM | 14:30 |
^[0-9]{3}-[0-9]{2}-[0-9]{4}$ |
SSN | 123-45-6789 |
^\(\d{3}\) \d{3}-\d{4}$ |
전화번호 (미국) | (555) 123-4567 |
^/.*$ |
Unix 경로 | /path/to/file |
^[a-fA-F0-9]{32}$ |
MD5 해시 | 5d41402abc4b2a76b9719d911017c592 |
^[0-9a-f]{40}$ |
SHA-1 해시 | aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d |
^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$ |
IBAN | GB82WEST12345698765432 |
8.2 실용적인 패턴 예제¶
#!/bin/bash
# 신용카드 (간소화 - 체크섬 검증 안 함)
CC_PATTERN='^[0-9]{4}[[:space:]-]?[0-9]{4}[[:space:]-]?[0-9]{4}[[:space:]-]?[0-9]{4}$'
# 16진수 색상 코드
COLOR_PATTERN='^#[0-9a-fA-F]{6}$'
# MAC 주소
MAC_PATTERN='^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$'
# 사용자명 (영숫자, 밑줄, 하이픈, 3-16자)
USERNAME_PATTERN='^[a-zA-Z0-9_-]{3,16}$'
# 강력한 비밀번호 (최소 8자, 대문자, 소문자, 숫자, 특수문자)
PASSWORD_PATTERN='^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*]).{8,}$'
# 도메인명
DOMAIN_PATTERN='^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$'
# 파일 확장자
EXT_PATTERN='\.(txt|log|conf|json|yaml|xml)$'
연습 문제¶
문제 1: 고급 입력 검증기¶
포괄적인 입력 검증 라이브러리를 만드세요: - 필드당 여러 검증 규칙 지원 (예: 필수, 유형, 길이, 패턴) - 사용자 정의 검증기 구현 (예: 비밀번호 강도, 신용카드, IBAN) - 상세한 에러 메시지 반환 (무엇이 실패했고 왜 실패했는지) - 조건부 검증 지원 (필드 A에 값이 있으면 필드 B 필수) - 중첩된 데이터 구조 검증 (JSON과 같은 객체) - 모든 에러가 포함된 검증 리포트 생성
문제 2: Regex를 사용한 로그 파서¶
다음 기능을 가진 로그 파서를 만드세요: - 로그 형식 자동 감지 (Apache, Nginx, syslog, 사용자 정의) - regex를 사용하여 타임스탬프, 레벨, 소스, 메시지 추출 - 다중 라인 로그 항목 처리 (스택 트레이스 등) - 로그 형식 검증 및 잘못된 형식의 항목 보고 - 타임스탬프를 다른 형식으로 변환 - 복잡한 regex 패턴으로 로그 필터링 - 통계 생성 (상위 에러, 시간 분포)
문제 3: 데이터 정제기(Sanitizer)¶
데이터 정제 도구를 구현하세요: - 다양한 컨텍스트(SQL, HTML, shell)에 대한 특수 문자 제거/이스케이프 - 전화번호 검증 및 정규화 (여러 형식) - 이메일 주소 검증 및 정규화 - regex를 사용하여 민감한 데이터 제거 (SSN, 신용카드, API 키) - 날짜/시간 검증 및 형식화 - URL 처리 (파싱, 검증, 정규화) - 정제 리포트 생성
문제 4: 설정 파일 파서¶
범용 설정 파서를 만드세요: - INI, YAML과 유사한 형식, 사용자 정의 key=value 형식 파싱 - 섹션, 중첩 키, 배열 지원 - regex를 사용하여 구문 검증 - 타입 변환으로 값 추출 (string, int, bool, array) - 변수 치환 지원 (예: ${HOME}/path) - 패턴에 대한 값 검증 (예: 포트는 1-65535여야 함) - 포함 및 상속 지원
문제 5: 패턴 기반 라우터¶
URL/경로 라우터를 만드세요: - 패턴으로 경로 정의 (예: /user/:id, /posts/:year/:month/:slug) - regex와 BASH_REMATCH를 사용하여 경로 매개변수 추출 - 선택적 매개변수 지원 (/path/:id?, /path/:id*) - regex 제약 조건 지원 (/user/:id([0-9]+)) - 우선순위로 경로 매칭 - 패턴과 매개변수로부터 URL 생성 - 쿼리 문자열 파싱 지원