Lesson 11: 인수 파싱 및 CLI 인터페이스
Lesson 11: 인수 파싱 및 CLI 인터페이스¶
난이도: ⭐⭐⭐
이전: 10_Error_Handling.md | 다음: 12_Portability_and_Best_Practices.md
1. 수동 인수 파싱¶
수동 파싱은 인수 처리에 대한 완전한 제어를 제공합니다.
기본 인수 루프¶
#!/bin/bash
# Parse arguments manually
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
echo "Usage: $0 [OPTIONS]"
exit 0
;;
-v|--verbose)
VERBOSE=1
shift
;;
-o|--output)
OUTPUT="$2"
shift 2
;;
--)
shift
break
;;
-*)
echo "Unknown option: $1" >&2
exit 1
;;
*)
# Positional argument
ARGS+=("$1")
shift
;;
esac
done
# Remaining arguments (after --)
REMAINING_ARGS=("$@")
echo "VERBOSE: ${VERBOSE:-0}"
echo "OUTPUT: ${OUTPUT:-none}"
echo "ARGS: ${ARGS[*]}"
echo "REMAINING: ${REMAINING_ARGS[*]}"
값을 가진 옵션 처리¶
#!/bin/bash
# Parse options that take values
parse_args() {
local verbose=0
local output=""
local count=1
local files=()
while [ $# -gt 0 ]; do
case "$1" in
-v|--verbose)
verbose=1
shift
;;
-o|--output)
if [ -z "$2" ] || [[ "$2" == -* ]]; then
echo "Error: --output requires a value" >&2
return 1
fi
output="$2"
shift 2
;;
--output=*)
output="${1#*=}"
shift
;;
-n|--count)
if [ -z "$2" ] || [[ "$2" == -* ]]; then
echo "Error: --count requires a value" >&2
return 1
fi
count="$2"
shift 2
;;
--count=*)
count="${1#*=}"
shift
;;
--)
shift
files=("$@")
break
;;
-*)
echo "Unknown option: $1" >&2
return 1
;;
*)
files+=("$1")
shift
;;
esac
done
# Export parsed values
echo "verbose=$verbose"
echo "output=$output"
echo "count=$count"
echo "files=(${files[*]})"
}
# Test
parse_args -v --output=result.txt --count 5 file1.txt file2.txt
echo "---"
parse_args --verbose -o result.txt -n 3 -- file1.txt file2.txt -special-file
고급 수동 파싱¶
#!/bin/bash
# Complete argument parser
declare -A OPTIONS
declare -a POSITIONAL
parse_arguments() {
local expecting_value=""
local option_name=""
while [ $# -gt 0 ]; do
# Handle value for previous option
if [ -n "$expecting_value" ]; then
OPTIONS["$option_name"]="$1"
expecting_value=""
option_name=""
shift
continue
fi
case "$1" in
# Long option with value: --option=value
--*=*)
option_name="${1%%=*}"
option_name="${option_name#--}"
OPTIONS["$option_name"]="${1#*=}"
shift
;;
# Long option without value: --option
--*)
option_name="${1#--}"
# Check if next arg is a value or another option
if [ $# -gt 1 ] && [[ ! "$2" =~ ^- ]]; then
expecting_value=1
else
OPTIONS["$option_name"]=1
fi
shift
;;
# Short option: -o
-[!-])
option_name="${1#-}"
# Check if next arg is a value
if [ $# -gt 1 ] && [[ ! "$2" =~ ^- ]]; then
expecting_value=1
else
OPTIONS["$option_name"]=1
fi
shift
;;
# Combined short options: -abc
-[!-]*)
local opts="${1#-}"
for (( i=0; i<${#opts}; i++ )); do
OPTIONS["${opts:$i:1}"]=1
done
shift
;;
# End of options
--)
shift
POSITIONAL+=("$@")
break
;;
# Positional argument
*)
POSITIONAL+=("$1")
shift
;;
esac
done
# Check if we're still expecting a value
if [ -n "$expecting_value" ]; then
echo "Error: Option --$option_name requires a value" >&2
return 1
fi
}
# Usage
parse_arguments -abc --verbose --output=file.txt --count 5 input1.txt input2.txt
# Display results
echo "Options:"
for key in "${!OPTIONS[@]}"; do
echo " $key = ${OPTIONS[$key]}"
done
echo "Positional arguments:"
for arg in "${POSITIONAL[@]}"; do
echo " $arg"
done
2. getopts (POSIX)¶
getopts는 옵션 파싱을 위한 POSIX 내장 명령어입니다.
기본 getopts 사용법¶
#!/bin/bash
# Parse options with getopts
usage() {
echo "Usage: $0 [-v] [-o OUTPUT] [-n COUNT] FILE..."
exit 1
}
verbose=0
output=""
count=1
while getopts "vo:n:h" opt; do
case "$opt" in
v)
verbose=1
;;
o)
output="$OPTARG"
;;
n)
count="$OPTARG"
;;
h)
usage
;;
\?)
echo "Invalid option: -$OPTARG" >&2
usage
;;
:)
echo "Option -$OPTARG requires an argument" >&2
usage
;;
esac
done
# Shift processed options
shift $((OPTIND - 1))
# Remaining arguments are positional
files=("$@")
echo "verbose=$verbose"
echo "output=$output"
echo "count=$count"
echo "files=(${files[*]})"
getopts 에러 처리¶
#!/bin/bash
# Two error handling modes:
# 1. Default (verbose): getopts prints errors
# 2. Silent mode: prepend option string with ":"
# Silent mode (recommended)
while getopts ":vho:n:" opt; do
case "$opt" in
v)
VERBOSE=1
;;
o)
OUTPUT="$OPTARG"
;;
n)
COUNT="$OPTARG"
# Validate it's a number
if ! [[ "$COUNT" =~ ^[0-9]+$ ]]; then
echo "Error: -n requires a number" >&2
exit 1
fi
;;
h)
echo "Help message"
exit 0
;;
:)
# Option requires argument but none provided
echo "Error: -$OPTARG requires an argument" >&2
exit 1
;;
\?)
# Invalid option
echo "Error: Invalid option -$OPTARG" >&2
exit 1
;;
esac
done
shift $((OPTIND - 1))
echo "Parsed successfully"
echo "Remaining args: $*"
함수와 함께 getopts 사용¶
#!/bin/bash
# Parse options in a function
parse_options() {
local OPTIND opt
local verbose=0
local output=""
while getopts ":vo:" opt; do
case "$opt" in
v) verbose=1 ;;
o) output="$OPTARG" ;;
\?) echo "Invalid option: -$OPTARG" >&2; return 1 ;;
:) echo "Option -$OPTARG requires an argument" >&2; return 1 ;;
esac
done
shift $((OPTIND - 1))
# Return parsed values (using global variables or output)
PARSED_VERBOSE=$verbose
PARSED_OUTPUT=$output
PARSED_ARGS=("$@")
}
# Call parser
parse_options -v -o output.txt file1 file2
echo "verbose=$PARSED_VERBOSE"
echo "output=$PARSED_OUTPUT"
echo "args=${PARSED_ARGS[*]}"
완전한 getopts 예제¶
#!/bin/bash
set -euo pipefail
# Script configuration
SCRIPT_NAME=$(basename "$0")
VERSION="1.0.0"
# Default values
VERBOSE=0
DRY_RUN=0
OUTPUT_FILE=""
INPUT_FILES=()
# Usage message
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] FILE...
Process files with various options.
OPTIONS:
-v Verbose mode
-n Dry run (don't make changes)
-o FILE Output file
-h Show this help message
-V Show version
EXAMPLES:
$SCRIPT_NAME -v input.txt
$SCRIPT_NAME -o output.txt -n input1.txt input2.txt
EOF
exit 0
}
# Version message
version() {
echo "$SCRIPT_NAME version $VERSION"
exit 0
}
# Parse options
while getopts ":vno:hV" opt; do
case "$opt" in
v)
VERBOSE=1
;;
n)
DRY_RUN=1
;;
o)
OUTPUT_FILE="$OPTARG"
;;
h)
usage
;;
V)
version
;;
:)
echo "Error: Option -$OPTARG requires an argument" >&2
echo "Try '$SCRIPT_NAME -h' for more information." >&2
exit 1
;;
\?)
echo "Error: Invalid option -$OPTARG" >&2
echo "Try '$SCRIPT_NAME -h' for more information." >&2
exit 1
;;
esac
done
shift $((OPTIND - 1))
# Validate arguments
if [ $# -eq 0 ]; then
echo "Error: No input files specified" >&2
echo "Try '$SCRIPT_NAME -h' for more information." >&2
exit 1
fi
INPUT_FILES=("$@")
# Process files
[ $VERBOSE -eq 1 ] && echo "Processing ${#INPUT_FILES[@]} files..."
[ $DRY_RUN -eq 1 ] && echo "DRY RUN MODE"
for file in "${INPUT_FILES[@]}"; do
[ $VERBOSE -eq 1 ] && echo "Processing: $file"
# Process file here
done
[ -n "$OUTPUT_FILE" ] && echo "Output: $OUTPUT_FILE"
3. getopt (GNU)¶
GNU getopt는 긴 옵션과 더 고급 파싱을 지원합니다.
기본 getopt 사용법¶
#!/bin/bash
# Note: This requires GNU getopt (not available on macOS by default)
# macOS users: brew install gnu-getopt
# Parse with getopt
OPTS=$(getopt -o "vo:n:" --long "verbose,output:,count:" -n "$0" -- "$@")
if [ $? -ne 0 ]; then
echo "Failed to parse options" >&2
exit 1
fi
# Reset positional parameters
eval set -- "$OPTS"
# Parse options
verbose=0
output=""
count=1
while true; do
case "$1" in
-v|--verbose)
verbose=1
shift
;;
-o|--output)
output="$2"
shift 2
;;
-n|--count)
count="$2"
shift 2
;;
--)
shift
break
;;
*)
echo "Internal error!" >&2
exit 1
;;
esac
done
# Remaining arguments
files=("$@")
echo "verbose=$verbose"
echo "output=$output"
echo "count=$count"
echo "files=(${files[*]})"
긴 옵션만 사용하는 getopt¶
#!/bin/bash
# Long options only
OPTS=$(getopt --long "help,version,verbose,output:,dry-run" -n "$0" -- "$@")
if [ $? -ne 0 ]; then
exit 1
fi
eval set -- "$OPTS"
while true; do
case "$1" in
--help)
echo "Help message"
exit 0
;;
--version)
echo "Version 1.0.0"
exit 0
;;
--verbose)
VERBOSE=1
shift
;;
--output)
OUTPUT="$2"
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--)
shift
break
;;
*)
echo "Internal error!" >&2
exit 1
;;
esac
done
echo "Parsed options successfully"
getopt vs getopts 비교¶
| 기능 | getopts (POSIX) | getopt (GNU) |
|---|---|---|
| 이식성 | POSIX (모든 시스템) | GNU (Linux, 설치된 macOS) |
| 긴 옵션 | 아니오 | 예 |
| 옵션 묶기 | 제한적 | 완전 지원 |
-- 구분자 |
수동 처리 | 내장 |
| 에러 메시지 | 기본 | 상세 |
| 옵션 재정렬 | 아니오 | 예 |
| 복잡도 | 간단 | 더 복잡 |
| 사용 사례 | 간단한 스크립트 | 복잡한 CLI 도구 |
완전한 getopt 예제¶
#!/bin/bash
set -euo pipefail
SCRIPT_NAME=$(basename "$0")
# Check if GNU getopt is available
if ! getopt --test > /dev/null 2>&1; then
if [ $? -ne 4 ]; then
echo "Error: GNU getopt not available" >&2
exit 1
fi
fi
# Parse options
SHORT_OPTS="vno:h"
LONG_OPTS="verbose,dry-run,output:,help,version,config:"
OPTS=$(getopt -o "$SHORT_OPTS" --long "$LONG_OPTS" -n "$SCRIPT_NAME" -- "$@")
if [ $? -ne 0 ]; then
echo "Run '$SCRIPT_NAME --help' for usage" >&2
exit 1
fi
eval set -- "$OPTS"
# Default values
VERBOSE=0
DRY_RUN=0
OUTPUT=""
CONFIG=""
# Parse
while true; do
case "$1" in
-v|--verbose)
VERBOSE=1
shift
;;
-n|--dry-run)
DRY_RUN=1
shift
;;
-o|--output)
OUTPUT="$2"
shift 2
;;
--config)
CONFIG="$2"
if [ ! -f "$CONFIG" ]; then
echo "Error: Config file not found: $CONFIG" >&2
exit 1
fi
shift 2
;;
-h|--help)
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] FILE...
OPTIONS:
-v, --verbose Verbose output
-n, --dry-run Dry run mode
-o, --output FILE Output file
--config FILE Configuration file
-h, --help Show this help
--version Show version
EOF
exit 0
;;
--version)
echo "$SCRIPT_NAME 1.0.0"
exit 0
;;
--)
shift
break
;;
*)
echo "Internal error!" >&2
exit 1
;;
esac
done
# Remaining arguments
FILES=("$@")
if [ ${#FILES[@]} -eq 0 ]; then
echo "Error: No input files specified" >&2
exit 1
fi
# Execute
[ $VERBOSE -eq 1 ] && echo "Processing ${#FILES[@]} files"
[ $DRY_RUN -eq 1 ] && echo "DRY RUN MODE"
for file in "${FILES[@]}"; do
[ $VERBOSE -eq 1 ] && echo "Processing: $file"
done
4. 자체 문서화 도움말¶
좋은 도움말 메시지는 CLI 도구를 사용자 친화적으로 만듭니다.
도움말 메시지 템플릿¶
#!/bin/bash
show_help() {
cat << EOF
NAME
$(basename "$0") - Brief description of what the script does
SYNOPSIS
$(basename "$0") [OPTIONS] COMMAND [ARGUMENTS]
DESCRIPTION
Detailed description of what this script does.
Can span multiple lines and include examples.
OPTIONS
-v, --verbose
Enable verbose output
-o, --output FILE
Specify output file (default: stdout)
-n, --count NUMBER
Number of iterations (default: 1)
-h, --help
Show this help message and exit
-V, --version
Show version information and exit
COMMANDS
start Start the service
stop Stop the service
restart Restart the service
status Show service status
EXAMPLES
# Basic usage
$(basename "$0") start
# With options
$(basename "$0") -v --output=log.txt start
# Multiple operations
$(basename "$0") -n 5 process file1.txt file2.txt
EXIT STATUS
0 Success
1 General error
2 Invalid arguments
66 Input file not found
77 Permission denied
AUTHOR
Written by Your Name
REPORTING BUGS
Report bugs to: bugs@example.com
SEE ALSO
Full documentation at: https://example.com/docs
EOF
}
# Call with -h or --help
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
show_help
exit 0
fi
주석에서 도움말 추출¶
#!/bin/bash
### NAME
### myscript - Does something useful
###
### SYNOPSIS
### myscript [OPTIONS] FILE...
###
### DESCRIPTION
### This script processes files in various ways.
###
### OPTIONS
### -v, --verbose Verbose output
### -h, --help Show this help
show_help() {
sed -n 's/^### \?//p' "$0"
}
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
show_help
exit 0
fi
echo "Script running..."
버전 정보¶
#!/bin/bash
SCRIPT_NAME=$(basename "$0")
VERSION="1.2.3"
AUTHOR="John Doe"
COPYRIGHT="Copyright (c) 2024"
LICENSE="MIT License"
show_version() {
cat << EOF
$SCRIPT_NAME version $VERSION
$COPYRIGHT $AUTHOR
License: $LICENSE
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Written by $AUTHOR
EOF
}
if [ "${1:-}" = "--version" ] || [ "${1:-}" = "-V" ]; then
show_version
exit 0
fi
동적 도움말 생성¶
#!/bin/bash
# Define options structure
declare -A OPTIONS_HELP=(
["-v|--verbose"]="Enable verbose output"
["-o|--output FILE"]="Specify output file"
["-n|--count NUM"]="Number of iterations"
["-h|--help"]="Show this help message"
)
generate_help() {
echo "Usage: $(basename "$0") [OPTIONS] FILE..."
echo ""
echo "OPTIONS:"
for key in $(echo "${!OPTIONS_HELP[@]}" | tr ' ' '\n' | sort); do
printf " %-25s %s\n" "$key" "${OPTIONS_HELP[$key]}"
done
}
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
generate_help
exit 0
fi
5. 컬러 출력¶
색상은 CLI 출력의 가독성을 향상시킵니다.
ANSI 컬러 코드¶
#!/bin/bash
# Standard colors
BLACK='\033[0;30m'
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[0;37m'
# Bold colors
BOLD_BLACK='\033[1;30m'
BOLD_RED='\033[1;31m'
BOLD_GREEN='\033[1;32m'
BOLD_YELLOW='\033[1;33m'
BOLD_BLUE='\033[1;34m'
BOLD_MAGENTA='\033[1;35m'
BOLD_CYAN='\033[1;36m'
BOLD_WHITE='\033[1;37m'
# Background colors
BG_BLACK='\033[40m'
BG_RED='\033[41m'
BG_GREEN='\033[42m'
BG_YELLOW='\033[43m'
BG_BLUE='\033[44m'
BG_MAGENTA='\033[45m'
BG_CYAN='\033[46m'
BG_WHITE='\033[47m'
# Text styles
BOLD='\033[1m'
DIM='\033[2m'
UNDERLINE='\033[4m'
BLINK='\033[5m'
REVERSE='\033[7m'
HIDDEN='\033[8m'
# Reset
NC='\033[0m' # No Color
# Usage
echo -e "${RED}Error message${NC}"
echo -e "${GREEN}Success message${NC}"
echo -e "${YELLOW}Warning message${NC}"
echo -e "${BLUE}Info message${NC}"
echo -e "${BOLD}${WHITE}Important${NC}"
echo -e "${UNDERLINE}Underlined text${NC}"
echo -e "${BG_RED}${WHITE}Alert${NC}"
완전한 컬러 테이블¶
| 코드 | 색상 | 굵게 코드 | 굵은 색상 |
|---|---|---|---|
\033[0;30m |
검정 | \033[1;30m |
굵은 검정 |
\033[0;31m |
빨강 | \033[1;31m |
굵은 빨강 |
\033[0;32m |
초록 | \033[1;32m |
굵은 초록 |
\033[0;33m |
노랑 | \033[1;33m |
굵은 노랑 |
\033[0;34m |
파랑 | \033[1;34m |
굵은 파랑 |
\033[0;35m |
마젠타 | \033[1;35m |
굵은 마젠타 |
\033[0;36m |
시안 | \033[1;36m |
굵은 시안 |
\033[0;37m |
흰색 | \033[1;37m |
굵은 흰색 |
tput 명령어¶
#!/bin/bash
# Using tput (more portable)
tput_setup() {
# Check if terminal supports colors
if [ -t 1 ] && [ $(tput colors) -ge 8 ]; then
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4)
MAGENTA=$(tput setaf 5)
CYAN=$(tput setaf 6)
WHITE=$(tput setaf 7)
BOLD=$(tput bold)
UNDERLINE=$(tput smul)
RESET=$(tput sgr0)
else
RED=""
GREEN=""
YELLOW=""
BLUE=""
MAGENTA=""
CYAN=""
WHITE=""
BOLD=""
UNDERLINE=""
RESET=""
fi
}
tput_setup
echo "${RED}Red text${RESET}"
echo "${GREEN}Green text${RESET}"
echo "${BOLD}${YELLOW}Bold yellow${RESET}"
조건부 컬러링¶
#!/bin/bash
# Detect if output is to terminal
if [ -t 1 ]; then
# Terminal detected, use colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
else
# Not a terminal (pipe, file, etc.), no colors
RED=''
GREEN=''
YELLOW=''
NC=''
fi
# Respect NO_COLOR environment variable
if [ -n "${NO_COLOR:-}" ]; then
RED=''
GREEN=''
YELLOW=''
NC=''
fi
echo -e "${GREEN}This is green in terminal${NC}"
echo -e "${RED}This is red in terminal${NC}"
# Test: ./script.sh (colored)
# ./script.sh | cat (not colored)
# NO_COLOR=1 ./script.sh (not colored)
컬러 헬퍼 함수¶
#!/bin/bash
# Setup colors
setup_colors() {
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4)
BOLD=$(tput bold)
RESET=$(tput sgr0)
else
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
fi
}
# Helper functions
error() {
echo "${RED}ERROR: $*${RESET}" >&2
}
success() {
echo "${GREEN}SUCCESS: $*${RESET}"
}
warning() {
echo "${YELLOW}WARNING: $*${RESET}" >&2
}
info() {
echo "${BLUE}INFO: $*${RESET}"
}
bold() {
echo "${BOLD}$*${RESET}"
}
setup_colors
# Usage
error "Something went wrong"
success "Operation completed"
warning "This might be a problem"
info "FYI: Some information"
bold "Important message"
6. 진행 표시기¶
장시간 실행되는 작업의 진행 상황을 표시합니다.
스피너 애니메이션¶
#!/bin/bash
# Spinner animation
spinner() {
local pid=$1
local delay=0.1
local spinstr='|/-\'
while kill -0 "$pid" 2>/dev/null; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr"
spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b"
done
printf " \b\b\b\b"
}
# Usage
(sleep 5) &
echo -n "Processing..."
spinner $!
echo "Done!"
# Alternative spinner with more frames
spinner_fancy() {
local pid=$1
local delay=0.1
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
while kill -0 "$pid" 2>/dev/null; do
for frame in "${frames[@]}"; do
printf "\r%s Processing..." "$frame"
sleep $delay
if ! kill -0 "$pid" 2>/dev/null; then
break 2
fi
done
done
printf "\r✓ Done! \n"
}
# Test fancy spinner
(sleep 3) &
spinner_fancy $!
진행 막대¶
#!/bin/bash
# Progress bar
progress_bar() {
local current=$1
local total=$2
local width=${3:-50}
local percent=$((current * 100 / total))
local completed=$((width * current / total))
local remaining=$((width - completed))
printf "\r["
printf "%${completed}s" | tr ' ' '='
printf "%${remaining}s" | tr ' ' ' '
printf "] %3d%%" "$percent"
if [ "$current" -eq "$total" ]; then
echo ""
fi
}
# Usage
total=100
for i in $(seq 1 $total); do
progress_bar $i $total
sleep 0.05
done
# Percentage-based progress
show_progress() {
local percent=$1
local width=50
local completed=$((width * percent / 100))
local remaining=$((width - completed))
printf "\rProgress: ["
printf "%${completed}s" | tr ' ' '█'
printf "%${remaining}s" | tr ' ' '░'
printf "] %3d%%" "$percent"
}
# Test
for i in $(seq 0 5 100); do
show_progress $i
sleep 0.2
done
echo ""
파일 다운로드 진행 상황¶
#!/bin/bash
# Simulate file download with progress
download_with_progress() {
local url=$1
local output=$2
local total_size=${3:-1000000} # Bytes
echo "Downloading: $url"
local downloaded=0
local chunk_size=10000
while [ $downloaded -lt $total_size ]; do
# Simulate download
sleep 0.1
downloaded=$((downloaded + chunk_size))
if [ $downloaded -gt $total_size ]; then
downloaded=$total_size
fi
# Calculate progress
local percent=$((downloaded * 100 / total_size))
local mb_downloaded=$((downloaded / 1024 / 1024))
local mb_total=$((total_size / 1024 / 1024))
# Show progress
printf "\r[%-50s] %d%% (%dMB/%dMB)" \
$(printf '%*s' $((percent / 2)) | tr ' ' '=') \
"$percent" \
"$mb_downloaded" \
"$mb_total"
done
echo ""
echo "Download complete: $output"
}
# Test
download_with_progress "https://example.com/file.zip" "file.zip" 5000000
다중 라인 진행 표시¶
#!/bin/bash
# Multi-line progress (useful for parallel tasks)
show_multi_progress() {
local -n tasks=$1
# Save cursor position
tput sc
while true; do
local all_done=1
# Restore cursor position
tput rc
for i in "${!tasks[@]}"; do
local task="${tasks[$i]}"
local status=$(get_task_status "$task")
local percent=$(get_task_percent "$task")
printf "Task %d: [%-30s] %3d%%\n" \
"$i" \
"$(printf '%*s' $((percent * 30 / 100)) | tr ' ' '=')" \
"$percent"
if [ "$percent" -lt 100 ]; then
all_done=0
fi
done
[ $all_done -eq 1 ] && break
sleep 0.5
done
}
# Simpler version for demonstration
demo_multi_progress() {
local tasks=("Task 1" "Task 2" "Task 3")
local progress=(0 0 0)
while true; do
clear
echo "=== Progress Dashboard ==="
echo ""
local all_done=1
for i in "${!tasks[@]}"; do
printf "%s: [%-30s] %3d%%\n" \
"${tasks[$i]}" \
"$(printf '%*s' $((progress[$i] * 30 / 100)) | tr ' ' '#')" \
"${progress[$i]}"
if [ ${progress[$i]} -lt 100 ]; then
all_done=0
progress[$i]=$((progress[$i] + RANDOM % 20))
if [ ${progress[$i]} -gt 100 ]; then
progress[$i]=100
fi
fi
done
[ $all_done -eq 1 ] && break
sleep 0.5
done
echo ""
echo "All tasks completed!"
}
demo_multi_progress
7. 대화형 입력¶
사용자 입력을 효과적으로 수집합니다.
기본 입력¶
#!/bin/bash
# Simple input
read -p "Enter your name: " name
echo "Hello, $name!"
# Input with default value
read -p "Enter filename [default.txt]: " filename
filename=${filename:-default.txt}
echo "Using: $filename"
# Input with timeout
if read -t 5 -p "Enter something (5s timeout): " input; then
echo "You entered: $input"
else
echo -e "\nTimeout!"
fi
비밀번호 입력¶
#!/bin/bash
# Hidden input (for passwords)
read -sp "Enter password: " password
echo ""
echo "Password length: ${#password}"
# Password with confirmation
read_password() {
local password
local password_confirm
while true; do
read -sp "Enter password: " password
echo ""
read -sp "Confirm password: " password_confirm
echo ""
if [ "$password" = "$password_confirm" ]; then
echo "$password"
return 0
else
echo "Passwords don't match. Try again."
fi
done
}
# Usage
user_password=$(read_password)
echo "Password set successfully"
예/아니오 확인¶
#!/bin/bash
# Simple yes/no
ask_yes_no() {
local prompt=$1
local default=${2:-}
if [ "$default" = "y" ]; then
prompt="$prompt [Y/n]: "
elif [ "$default" = "n" ]; then
prompt="$prompt [y/N]: "
else
prompt="$prompt [y/n]: "
fi
while true; do
read -p "$prompt" response
# Use default if no response
if [ -z "$response" ] && [ -n "$default" ]; then
response=$default
fi
case "$response" in
[Yy]*) return 0 ;;
[Nn]*) return 1 ;;
*) echo "Please answer yes or no." ;;
esac
done
}
# Usage
if ask_yes_no "Do you want to continue?" "y"; then
echo "Continuing..."
else
echo "Aborted"
exit 1
fi
메뉴 선택¶
#!/bin/bash
# Menu selection
show_menu() {
local prompt=$1
shift
local options=("$@")
echo "$prompt"
echo ""
for i in "${!options[@]}"; do
echo " $((i + 1)). ${options[$i]}"
done
echo ""
while true; do
read -p "Enter choice [1-${#options[@]}]: " choice
if [[ "$choice" =~ ^[0-9]+$ ]] && \
[ "$choice" -ge 1 ] && \
[ "$choice" -le "${#options[@]}" ]; then
echo "$((choice - 1))"
return 0
else
echo "Invalid choice. Please try again."
fi
done
}
# Usage
options=("Option A" "Option B" "Option C" "Quit")
selected=$(show_menu "Please select an option:" "${options[@]}")
echo "You selected: ${options[$selected]}"
검증을 포함한 고급 입력¶
#!/bin/bash
# Input with validation
read_validated() {
local prompt=$1
local validator=$2
local error_msg=$3
while true; do
read -p "$prompt" input
if eval "$validator"; then
echo "$input"
return 0
else
echo "$error_msg" >&2
fi
done
}
# Validators
is_number() { [[ "$input" =~ ^[0-9]+$ ]]; }
is_email() { [[ "$input" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; }
is_not_empty() { [ -n "$input" ]; }
# Usage
name=$(read_validated "Enter your name: " "is_not_empty" "Name cannot be empty")
age=$(read_validated "Enter your age: " "is_number" "Age must be a number")
email=$(read_validated "Enter email: " "is_email" "Invalid email format")
echo "Name: $name"
echo "Age: $age"
echo "Email: $email"
8. 완전한 CLI 도구 예제¶
모든 것을 전문적인 CLI 도구로 통합합니다.
#!/bin/bash
set -euo pipefail
# ============================================================================
# CONFIGURATION
# ============================================================================
SCRIPT_NAME=$(basename "$0")
VERSION="1.0.0"
AUTHOR="Your Name"
# Colors
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4)
BOLD=$(tput bold)
RESET=$(tput sgr0)
else
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
fi
# Default values
VERBOSE=0
DRY_RUN=0
OUTPUT_FILE=""
LOG_FILE="/tmp/${SCRIPT_NAME}.log"
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
error() { echo "${RED}ERROR: $*${RESET}" >&2; }
success() { echo "${GREEN}SUCCESS: $*${RESET}"; }
warning() { echo "${YELLOW}WARNING: $*${RESET}" >&2; }
info() { echo "${BLUE}INFO: $*${RESET}"; }
verbose() { [ $VERBOSE -eq 1 ] && echo "${BLUE}VERBOSE: $*${RESET}"; }
die() {
local code=$1
shift
error "$*"
exit "$code"
}
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}
progress_bar() {
local current=$1
local total=$2
local width=40
local percent=$((current * 100 / total))
local completed=$((width * current / total))
printf "\r${BLUE}Progress:${RESET} ["
printf "%${completed}s" | tr ' ' '='
printf "%$((width - completed))s" | tr ' ' ' '
printf "] %3d%%" "$percent"
[ "$current" -eq "$total" ] && echo ""
}
# ============================================================================
# USAGE AND VERSION
# ============================================================================
show_version() {
cat << EOF
$SCRIPT_NAME version $VERSION
Written by $AUTHOR
EOF
exit 0
}
show_help() {
cat << EOF
${BOLD}NAME${RESET}
$SCRIPT_NAME - Process files with various options
${BOLD}SYNOPSIS${RESET}
$SCRIPT_NAME [OPTIONS] FILE...
${BOLD}DESCRIPTION${RESET}
This tool processes files with configurable options.
It demonstrates best practices for CLI argument parsing.
${BOLD}OPTIONS${RESET}
-v, --verbose
Enable verbose output
-n, --dry-run
Perform a dry run without making changes
-o, --output FILE
Specify output file (default: stdout)
-l, --log FILE
Specify log file (default: /tmp/$SCRIPT_NAME.log)
-h, --help
Show this help message
-V, --version
Show version information
${BOLD}EXAMPLES${RESET}
# Basic usage
$SCRIPT_NAME file1.txt file2.txt
# Verbose mode with output file
$SCRIPT_NAME -v --output=result.txt input.txt
# Dry run
$SCRIPT_NAME -n *.txt
${BOLD}EXIT STATUS${RESET}
0 Success
1 General error
2 Invalid arguments
${BOLD}AUTHOR${RESET}
Written by $AUTHOR
EOF
exit 0
}
# ============================================================================
# ARGUMENT PARSING
# ============================================================================
parse_arguments() {
local files=()
while [ $# -gt 0 ]; do
case "$1" in
-v|--verbose)
VERBOSE=1
shift
;;
-n|--dry-run)
DRY_RUN=1
shift
;;
-o|--output)
OUTPUT_FILE="$2"
shift 2
;;
--output=*)
OUTPUT_FILE="${1#*=}"
shift
;;
-l|--log)
LOG_FILE="$2"
shift 2
;;
--log=*)
LOG_FILE="${1#*=}"
shift
;;
-h|--help)
show_help
;;
-V|--version)
show_version
;;
--)
shift
files+=("$@")
break
;;
-*)
die 2 "Unknown option: $1\nRun '$SCRIPT_NAME --help' for usage"
;;
*)
files+=("$1")
shift
;;
esac
done
# Validate
if [ ${#files[@]} -eq 0 ]; then
die 2 "No input files specified\nRun '$SCRIPT_NAME --help' for usage"
fi
# Check files exist
for file in "${files[@]}"; do
if [ ! -f "$file" ]; then
die 1 "File not found: $file"
fi
done
echo "${files[@]}"
}
# ============================================================================
# MAIN LOGIC
# ============================================================================
process_file() {
local file=$1
verbose "Processing file: $file"
log "Processing: $file"
# Simulate work
sleep 0.5
verbose "Completed: $file"
log "Completed: $file"
}
main() {
log "Script started"
verbose "Verbose mode enabled"
[ $DRY_RUN -eq 1 ] && warning "DRY RUN MODE"
# Parse arguments
local files
IFS=' ' read -ra files <<< "$(parse_arguments "$@")"
info "Processing ${#files[@]} file(s)..."
# Process files
local count=0
local total=${#files[@]}
for file in "${files[@]}"; do
((count++))
progress_bar $count $total
if [ $DRY_RUN -eq 0 ]; then
process_file "$file"
fi
done
success "All files processed successfully"
log "Script completed"
# Save output
if [ -n "$OUTPUT_FILE" ]; then
echo "Results saved to: $OUTPUT_FILE" > "$OUTPUT_FILE"
info "Output saved to: $OUTPUT_FILE"
fi
}
# ============================================================================
# ENTRY POINT
# ============================================================================
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
main "$@"
fi
9. 연습 문제¶
문제 1: 고급 옵션 파서¶
다음을 지원하는 유연한 옵션 파서를 생성하세요:
- 짧은 옵션(-v, -o file)
- 긴 옵션(--verbose, --output=file)
- 결합된 짧은 옵션(-vxf)
- 선택적 vs 필수 옵션 인수
- 불린 플래그와 값 옵션
- 위치 인수
- -- 구분자
- 각 옵션 유형에 대한 검증
- 옵션 정의에서 자동 생성된 도움말
문제 2: 설정 파일 통합¶
다음을 수행하는 CLI 도구를 구축하세요:
- 명령줄, 설정 파일, 환경 변수에서 옵션 받기
- 우선순위 사용: CLI > 환경 > 설정 파일 > 기본값
- 여러 설정 파일 형식 지원(INI, JSON, YAML)
- 모든 설정 값 검증
- 현재 유효한 설정 출력 가능
- 설정 파일 경로를 지정하는 --config 옵션 포함
문제 3: 대화형 설정 마법사¶
다음을 수행하는 대화형 설정 마법사를 생성하세요: - 설정을 통해 사용자 안내 - 각 입력 검증 - 다중 선택 옵션에 대한 메뉴 표시 - 이전 단계로 돌아갈 수 있음 - 저장 전 확인 - 설정 파일 생성 - 대화형 및 비대화형 모드 모두 보유(자동화용) - 컬러 출력 및 진행 표시기 포함
문제 4: Git 스타일 하위 명령 인터페이스¶
git 스타일 하위 명령을 가진 CLI 도구를 구현하세요:
- 메인 명령: mytool <subcommand> [options]
- 여러 하위 명령(init, add, remove, list 등)
- 각 하위 명령은 자체 옵션과 도움말을 가짐
- 공유 전역 옵션(--verbose, --config)
- 탭 완성 지원(bash-completion 스크립트)
- 도움말 텍스트에서 맨 페이지 생성
- 모든 하위 명령에 걸친 일관된 에러 처리
문제 5: CLI 대시보드¶
다음을 수행하는 대화형 CLI 대시보드를 구축하세요: - 여러 프로세스의 실시간 상태 표시 - 스크롤 없이 매초 디스플레이 업데이트 - 시각적 매력을 위해 색상 및 유니코드 문자 사용 - 키보드 명령 받기(q=종료, r=새로고침, p=일시정지) - 실행 중인 작업의 진행 막대 표시 - 모든 이벤트를 파일에 로그 - 스크립트용 비대화형 모드에서 실행 가능
이전: 10_Error_Handling.md | 다음: 12_Portability_and_Best_Practices.md