Learning Platform
Глоссарий Troubleshooting
Урок 18.03 · 22 мин
Начальный
bashargumentspositional parametersstdinreadwhile loop

Зачем скрипту аргументы

Скрипт без аргументов — это hardcoded. Чтобы сделать его reusable, нужно принимать input снаружи. Bash предоставляет два механизма:

  1. Command-line arguments: ./script.sh arg1 arg2 arg3. Доступны через $1, $2, и т.д.
  2. Stdin: cat file | ./script.sh. Читается через read.

Эти два паттерна — основа всей bash-кулинарии. ETL-скрипт принимает дату через ./run.sh 2026-05-13. Cleanup-скрипт читает список файлов из stdin: find ... | ./cleanup.sh.

Positional parameters: 1,1, 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.

$0имя скрипта
$1, $2, ..., `${10}`аргументы по порядку
$#argc (число аргументов)
"$@"все аргументы как массив
"$*"все аргументы как одна строка
shiftсдвинуть positional

@"vs"@" vs "*” — критическое отличие

$ 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

Это базовый паттерн ручного разбора аргументов. Подходит для несложных случаев.

1через1 через `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
WARNING

В 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?

Базовое правило: маленькие имена/опции — аргументы; большие данные — stdin.

Аргументыимена, флаги, ID
Stdinданные, потоки, списки
Combinedрежим в args + данные в 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.

Попробуй сам

  1. Простой echo-скрипт:
    cat > greet.sh <<'EOF'
    #!/usr/bin/env bash
    echo "Привет, $1! Сегодня $(date +%A)."
    EOF
    chmod +x greet.sh
    ./greet.sh Лев
  2. 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
  3. 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
  4. 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.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Как обратиться к 10-му аргументу скрипта?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5