Зачем скрипту аргументы
Скрипт без аргументов — это hardcoded. Чтобы сделать его reusable, нужно принимать input снаружи. Bash предоставляет два механизма:
- Command-line arguments:
./script.sh arg1 arg2 arg3. Доступны через$1,$2, и т.д. - Stdin:
cat file | ./script.sh. Читается черезread.
Эти два паттерна — основа всей bash-кулинарии. ETL-скрипт принимает дату через ./run.sh 2026-05-13. Cleanup-скрипт читает список файлов из stdin: find ... | ./cleanup.sh.
Positional parameters: 2, …
Каждый аргумент при запуске скрипта попадает в $1, $2, $3, … — это positional parameters.
#!/usr/bin/env bash
# args.sh
echo "Script name: $0"
echo "First arg: $1"
echo "Second arg: $2"
echo "Total args: $#"
echo "All args: $@"
$ ./args.sh hello world
Script name: ./args.sh
First arg: hello
Second arg: world
Total args: 2
All args: hello world
Каждая отвечает на свой вопрос про argv.
”*” — критическое отличие
$ cat ./pass.sh
#!/usr/bin/env bash
echo "Count: $#"
for arg; do
echo " arg: [$arg]"
done
$ ./pass.sh "hello world" foo
Count: 2
arg: [hello world]
arg: [foo]
for arg без in ... итерирует по "$@" неявно — каждый arg остаётся отдельным.
Что если использовать "$*"?
$ cat ./pass2.sh
#!/usr/bin/env bash
echo "Count: $#"
for arg in "$*"; do
echo " arg: [$arg]"
done
$ ./pass2.sh "hello world" foo
Count: 2
arg: [hello world foo]
"$*" склеил всё в одну строку. Это редко то, что нужно.
Правило: всегда используй "$@" для перебора и передачи аргументов дальше. "$*" — только когда явно нужна склеенная строка.
Валидация: проверка количества аргументов
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 2 ]]; then
echo "Usage: $0 <source-dir> <dest-dir>" >&2
echo "Example: $0 /data/raw /data/processed" >&2
exit 1
fi
SOURCE="$1"
DEST="$2"
echo "Syncing $SOURCE -> $DEST"
rsync -av "$SOURCE/" "$DEST/"
Это обязательный паттерн для production: на старте проверять, что аргументы переданы, печатать usage если нет, exit с ненулевым кодом. Подробнее об условных операторах в уроке 04-conditionals.
shift: пройти по аргументам
#!/usr/bin/env bash
echo "Total args: $#"
while [[ $# -gt 0 ]]; do
echo "Processing: $1"
shift # $1 уходит, $2 становится новым $1, и т.д.
done
echo "Remaining: $#" # 0
$ ./shift-demo.sh apple banana cherry
Total args: 3
Processing: apple
Processing: banana
Processing: cherry
Remaining: 0
Это базовый паттерн ручного разбора аргументов. Подходит для несложных случаев.
10` — почему фигурные нужны для двузначных
$ ./many.sh a b c d e f g h i j k
# Внутри скрипта:
$10 # неправильно! bash интерпретирует как $1 (= a), потом литерал 0
${10} # правильно — k
Историческое наследие от Bourne shell, который поддерживал только $1..$9.
Stdin: read для чтения
read — встроенная команда для чтения строки из stdin:
#!/usr/bin/env bash
echo "What's your name?"
read name
echo "Hello, $name!"
$ ./greet.sh
What's your name?
Lev
Hello, Lev!
Опции read
read -p "Prompt: " name # -p PROMPT — печатает prompt сама, не нужен echo
read -s -p "Password: " pwd # -s — silent (не отображать ввод; для паролей)
read -t 10 name # -t SECONDS — timeout
read -r line # -r — НЕ обрабатывать backslashes (raw mode); **всегда используй -r!**
read -n 1 ch # -n N — прочитать N символов и stop (не ждать enter)
-r — обязателен для production. Без него read обрабатывает \ как escape, что ломает чтение файлов с backslashes (например, Windows-paths или JSON).
IFS: разделитель полей
Bash split-ит прочитанную строку по IFS (Internal Field Separator, по дефолту — whitespace).
$ echo "alice 25 admin" | (read user age role; echo "$user, $age, $role")
alice, 25, admin
Удобно для парсинга CSV или TSV:
$ echo "alice,25,admin" | (IFS=, read user age role; echo "$user, $age, $role")
alice, 25, admin
IFS=, read устанавливает разделитель , только для этой команды.
while read: обработка файла построчно
Самый частый stdin-паттерн в DE — обработать каждую строку файла:
#!/usr/bin/env bash
while IFS= read -r line; do
echo "Got line: $line"
done < input.txt
Магия здесь:
IFS=(пустой) — не split-ить по whitespace. Сохраняем leading/trailing spaces.-r— raw mode, не обрабатывать\.< input.txt— перенаправление stdin.readвозвращает non-zero при EOF — цикл закончится.
Чтение из pipe
find /data -name '*.csv' | while IFS= read -r file; do
echo "Processing: $file"
process_csv "$file"
done
В pipe-варианте есть subtle проблема: тело while выполняется в subshell (из-за pipe). Переменные, заданные внутри while, не сохраняются после цикла.
count=0
find /data -name '*.csv' | while read file; do
count=$((count + 1))
done
echo "Files: $count" # 0 — count внутри subshell, потерянРешение: process substitution < <(...):
count=0
while read file; do
count=$((count + 1))
done < <(find /data -name '*.csv')
echo "Files: $count" # реальное числоРазбор CSV-файла
#!/usr/bin/env bash
set -euo pipefail
# CSV: name,age,role
while IFS=, read -r name age role; do
echo "User $name is $age yo, role: $role"
done < users.csv
IFS=, — split по запятой. Этот скрипт не справится с quoting ("value,with,commas") — для production CSV используй csvkit, mlr, awk -F,, или Python. Но для простых ТSV/CSV while IFS=, read работает.
STDOUT vs STDERR при подаче в pipe
$ echo "info" | ./script.sh # подаёт "info\n" на stdin script.sh
stdin внутри скрипта доступен через read, или через cat, или просто как /dev/stdin:
#!/usr/bin/env bash
# Подсчитать строки в stdin:
lines=$(wc -l < /dev/stdin)
echo "Read $lines lines"
# Альтернатива:
lines=$(cat | wc -l)
Аргументы vs stdin: что когда
Базовое правило: маленькие имена/опции — аргументы; большие данные — stdin.
DE-сценарий: parquet sync с двумя источниками args
#!/usr/bin/env bash
#
# sync-parquet.sh — синхронизация parquet-файлов
#
# Usage:
# sync-parquet.sh <source-pattern> <dest-prefix>
#
# Example:
# sync-parquet.sh '/data/orders/*.parquet' s3://lake/orders/
#
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <source-pattern> <dest-prefix>" >&2
exit 1
fi
SOURCE_PATTERN="$1"
DEST_PREFIX="$2"
count=0
for file in $SOURCE_PATTERN; do
[[ -f "$file" ]] || continue # пропускать если glob не сматчился
echo "Uploading $file..."
aws s3 cp "$file" "$DEST_PREFIX$(basename "$file")"
count=$((count + 1))
done
echo "Uploaded $count files"
DE-сценарий: обработка stdin-списка файлов
#!/usr/bin/env bash
# compress-old.sh — gzip старые лог-файлы, читая их список из stdin
#
# Usage:
# find /var/log -name '*.log' -mtime +14 | ./compress-old.sh
set -euo pipefail
count=0
while IFS= read -r file; do
if [[ -f "$file" && ! "$file" =~ \.gz$ ]]; then
gzip "$file"
count=$((count + 1))
echo " $(date -Iseconds) compressed: $file"
fi
done
echo "Compressed $count files"
Это типовая «filter»-команда: input -> process -> output.
Знакомство с getopts
Для скриптов с флагами (типа --verbose, -o output.txt) ручной разбор через $1/shift неудобен. Bash имеет встроенный getopts:
#!/usr/bin/env bash
set -euo pipefail
VERBOSE=0
OUTPUT="output.txt"
while getopts ":vo:h" opt; do
case "$opt" in
v) VERBOSE=1 ;;
o) OUTPUT="$OPTARG" ;;
h) echo "Usage: $0 [-v] [-o OUTPUT] [-h]"; exit 0 ;;
\?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
:) echo "Option -$OPTARG requires an argument" >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1)) # сдвинуть, чтобы оставить только non-option args
echo "VERBOSE=$VERBOSE, OUTPUT=$OUTPUT, args: $@"
Запуск:
$ ./script.sh -v -o result.txt file1 file2
VERBOSE=1, OUTPUT=result.txt, args: file1 file2
getopts поддерживает только короткие флаги (-v, -o VALUE). Для long-options (--verbose, --output=...) — нужен GNU getopt (другая утилита) или сторонние решения. Подробнее в модуле 17-bash-scripting-advanced.
Для большинства DE-скриптов достаточно $1/shift или getopts. Сложный CLI с подкомандами — Python click/typer.
Попробуй сам
- Простой echo-скрипт:
cat > greet.sh <<'EOF' #!/usr/bin/env bash echo "Привет, $1! Сегодня $(date +%A)." EOF chmod +x greet.sh ./greet.sh Лев - Validate args:
cat > validate.sh <<'EOF' #!/usr/bin/env bash if [[ $# -lt 2 ]]; then echo "Usage: $0 <name> <age>" >&2 exit 1 fi echo "name=$1, age=$2" EOF chmod +x validate.sh ./validate.sh # покажет usage ./validate.sh Лев 30 - Read line by line:
cat > count.sh <<'EOF' #!/usr/bin/env bash count=0 while IFS= read -r line; do count=$((count + 1)) echo "Line $count: $line" done echo "Total: $count" EOF chmod +x count.sh printf "hello\nworld\nfoo\n" | ./count.sh - Process substitution для preserve counter:
count=0 while read line; do count=$((count + 1)); done < <(seq 5) echo "$count" # 5
Главное
$1,$2, …,$9— первые 9 positional аргументов. Для 10+ —${10}.$0— имя скрипта.$#— argc (число аргументов)."$@"— все args как массив (предпочтительно)."$*"— все как одна строка (редко надо).shiftсдвигает positional на 1 (или N сshift N). Удобно для ручного разбора аргументов.read -r var— читает строку из stdin.-rобязателен, чтобы не обрабатывать\.while IFS= read -r line; do ... done < file— стандарт обработки файла построчно.- В pipe (
find | while read) тело цикла в subshell — переменные не сохраняются. Используй process substitution:while ... done < <(find ...). - Validate args в начале:
if [[ $# -lt N ]]; then echo usage; exit 1; fi. getopts— для коротких флагов (-v,-o VALUE). Long-options (--verbose) — GNU getopt или другие инструменты.- Аргументы — для имён/флагов. Stdin — для данных/потоков.
- DE-паттерны: scripts с одним-двумя args + остальное stdin, либо filter-комбинации с
find ... | script.