Скрипти командної оболонки

Останнє оновлення 2025-11-26 | Редагувати цю сторінку

Огляд

Питання

  • Як я можу зберігати та повторно використовувати команди?

Цілі

  • Написати скрипт командної оболонки, який виконує одну або декілька команд для заздалегідь визначеного набору файлів.
  • Запустити скрипт командної оболонки з термінала.
  • Написати скрипт командної оболонки, який обробляє файли, вказані користувачем у командному рядку.
  • Створити конвеєри, що використовують скрипти оболонки, створені вами та іншими користувачами.

Нарешті ми готові дізнатися, чому оболонка є таким потужним середовищем програмування. Ми збираємося зібрати та зберегти у файлах часто використовувані команди, щоб пізніше можна було виконати всі ці дії одночасно, набравши лише одну команду. З історичних причин скопійовані в файл команди зазвичай називають командним скриптом, скриптом командної оболонки, або скриптом терміналу, але не помиляйтеся: це насправді невеликі програми.

Написання командних скриптів не тільки прискорить вашу роботу, а й дозволить уникнути постійного повторного введення тих самих команд. Крім того, це підвищить якість вашої роботи (зменшить ризик друкарських помилок) і полегшить її відтворення. Якщо ви повернетеся до своєї роботи пізніше (або якщо хтось знайде вашу роботу і захоче її використати), відтворити ті ж результати можна буде просто запустивши скрипт, без потреби пригадувати та повторно вводити довгий перелік команд.

Спершу повернемося до каталогу alkanes/ і створимо новий файл middle.sh, який стане нашим скриптом терміналу:

BASH

$ cd alkanes
$ nano middle.sh

Команда nano middle.sh відкриває файл middle.sh у текстовому редакторі ‘nano’ (який запускається у терміналі). Якщо файл не існує, його буде створено. Ми можемо скористатися текстовим редактором для безпосереднього редагування файлу, додавши до нього наступний рядок:

head -n 15 octane.pdb | tail -n 5

Це варіант каналу, який ми побудували раніше: він вибирає рядки 11-15 файлу octane.pdb. Пам’ятайте, ми поки не запускаємо його як команду: ми лише записуємо команди у файл.

Потім ми зберігаємо файл (Ctrl-O у nano) і виходимо з текстового редактора (Ctrl-X у nano). Переконайтеся, що в каталозі alkanes тепер міститься файл з назвою middle.sh.

Після того, як ми зберегли файл, ми можемо дати оболонці команду виконати його вміст. Оскільки термінал називається bash, ми виконаємо наступну команду:

BASH

$ bash middle.sh

ВИХІД

ATOM      9  H           1      -4.502   0.681   0.785  1.00  0.00
ATOM     10  H           1      -5.254  -0.243  -0.537  1.00  0.00
ATOM     11  H           1      -4.357   1.252  -0.895  1.00  0.00
ATOM     12  H           1      -3.009  -0.741  -1.467  1.00  0.00
ATOM     13  H           1      -3.172  -1.337   0.206  1.00  0.00

Дійсно, результат роботи скрипту збігається з тим, що ми отримали б, запустивши конвеєр напряму у терміналі.

Виноска

Текст або будь-що інше?

Зазвичай ми називаємо “текстовими редакторами” програми на кшталт Microsoft Word або LibreOffice Writer, але коли мова йде про програмування, потрібно бути трохи обережнішими. За замовчуванням, Microsoft Word зберігає у файлах .docx не лише текст, але й інформацію про форматування: шрифти, заголовки тощо. Ця додаткова інформація не зберігається у вигляді звичайних символів і є незрозумілою для програм на кшталт head, яка очікує на те, що у файлі будуть тільки літери, числа та пунктуація зі стандартної комп’ютерної клавіатури. Отже, редагуючи програми, вам слід користуватися текстовим редактором, який працює зі звичайним текстом, або подбати про те, щоб файли зберігалися у форматі звичайного тексту.

Що робити, якщо потрібно вибрати рядки з будь-якого файлу? Ми могли б щоразу редагувати middle.sh для зміни імені файлу, але це, ймовірно, зайняло б більше часу, ніж повторне введення й виконання команди у терміналі з новим ім’ям. Натомість відредагуймо middle.sh і зробимо його більш універсальним:

BASH

$ nano middle.sh

Тепер у “nano” замініть текст octane.pdb на спеціальну змінну з назвою $1:

head -n 15 "$1" | tail -n 5

У командному скрипті змінна $1 позначає перший аргумент командного рядка – перше ім’я файлу (або інший аргумент). Тепер ми можемо запустити наш скрипт наступним чином для того ж самого файлу:

BASH

$ bash middle.sh octane.pdb

ВИХІД

ATOM      9  H           1      -4.502   0.681   0.785  1.00  0.00
ATOM     10  H           1      -5.254  -0.243  -0.537  1.00  0.00
ATOM     11  H           1      -4.357   1.252  -0.895  1.00  0.00
ATOM     12  H           1      -3.009  -0.741  -1.467  1.00  0.00
ATOM     13  H           1      -3.172  -1.337   0.206  1.00  0.00

або ж запустити з іншим файлом ось так, вказавши його імʼя подібним чином:

BASH

$ bash middle.sh pentane.pdb

ВИХІД

ATOM      9  H           1       1.324   0.350  -1.332  1.00  0.00
ATOM     10  H           1       1.271   1.378   0.122  1.00  0.00
ATOM     11  H           1      -0.074  -0.384   1.288  1.00  0.00
ATOM     12  H           1      -0.048  -1.362  -0.205  1.00  0.00
ATOM     13  H           1      -1.183   0.500  -1.412  1.00  0.00
Виноска

Подвійні лапки навколо аргументів

Як і у випадку зі змінною циклу, $1 потрібно брати в подвійні лапки, оскільки назва файлу містить пробіли.

Наразі нам доводиться редагувати middle.sh щоразу, коли ми хочемо змінити діапазон рядків, які повертаються. Ми виправимо це, налаштувавши наш скрипт для використання трьох аргументів командного рядка. Після першого аргументу командного рядка ($1), наступні надані аргументи будуть зберігатися у спеціальних змінних $1, $2, $3, які відповідно посилаються на перший, другий і третій аргумент командного рядка.

Знаючи це, ми можемо використовувати додаткові аргументи для визначення діапазону рядків, які треба передати до head та tail:

BASH

$ nano middle.sh
head -n "$2" "$1" | tail -n "$3"

Тепер ми можемо запустити:

BASH

$ bash middle.sh pentane.pdb 15 5

ВИХІД

ATOM      9  H           1       1.324   0.350  -1.332  1.00  0.00
ATOM     10  H           1       1.271   1.378   0.122  1.00  0.00
ATOM     11  H           1      -0.074  -0.384   1.288  1.00  0.00
ATOM     12  H           1      -0.048  -1.362  -0.205  1.00  0.00
ATOM     13  H           1      -1.183   0.500  -1.412  1.00  0.00

Достатньо змінити аргументи команди й скрипт працюватиме по-іншому:

BASH

$ bash middle.sh pentane.pdb 20 5

ВИХІД

ATOM     14  H           1      -1.259   1.420   0.112  1.00  0.00
ATOM     15  H           1      -2.608  -0.407   1.130  1.00  0.00
ATOM     16  H           1      -2.540  -1.303  -0.404  1.00  0.00
ATOM     17  H           1      -3.393   0.254  -0.321  1.00  0.00
TER      18              1

Такий варіант працює, але іншому користувачеві може знадобитися певний час, щоб розібратися, як саме працює скрипт middle.sh. Щоб зробити скрипт зрозумілішим, додамо на його початку кілька коментарів:

BASH

$ nano middle.sh
# Виділення рядків з середини файлу.
# Використання: bash middle.sh filename end_line num_lines
head -n "$2" "$1" | tail -n "$3"

Коментар починається зі символу # і триває до кінця рядка. Коментарі не впливають на виконання коду, але вони допомагають користувачам (і вам самим у майбутньому) швидко зрозуміти та використовувати скрипти. Єдине застереження полягає у тому, що кожного разу, коли ви змінюєте скрипт, ви повинні перевіряти, що коментар все ще правильний. Пояснення, яке спрямовує читача в неправильному напрямку, гірше, ніж його відсутність.

Що робити, якщо ми хочемо обробити багато файлів в одному конвеєрі? Наприклад, якщо ми хочемо відсортувати наші .pdb-файли за довжиною, ми введемо:

BASH

$ wc -l *.pdb | sort -n

оскільки wc -l виводить кількість рядків у файлах (нагадаємо, що wc означає ‘підрахунок слів’ (word count), а додавання опції -l натомість означає ‘підрахунок рядків’ (lines)) та sort -n використовує числове сортування. Ми можемо записати цей конвеєр у файл, але тоді він сортуватиме лише список .pdb файлів у поточному каталозі. Якщо ми хочемо отримати відсортований список інших типів файлів, нам потрібно передати всі ці імена у скрипт. У цьому випадку не можна скористатися змінними $1, $2 тощо, бо ми не знаємо наперед, скільки файлів потрібно обробити. Натомість ми використовуємо спеціальну змінну $@, що означає, “Всі аргументи командного рядка передані скрипту”. Необхідно взяти $@ у подвійні лапки, щоб правильно обробляти аргументи з пробілами ("$@" є спеціальним синтаксисом еквівалентним "$1" "$2" …).

Ось приклад:

BASH

$ nano sorted.sh
# Сортування файлів за їх розміром.
# Використання: bash sorted.sh one_or_more_filenames
wc -l "$@" | sort -n

BASH

$ bash sorted.sh *.pdb ../creatures/*.dat

ВИХІД

9 methane.pdb
12 ethane.pdb
15 propane.pdb
20 cubane.pdb
21 pentane.pdb
30 octane.pdb
163 ../creatures/basilisk.dat
163 ../creatures/minotaur.dat
163 ../creatures/unicorn.dat
596 total
Вправа

Перелік унікальних видів тварин

Лія має кілька сотень файлів даних, кожен з яких відформатований наступним чином:

2013-11-05,deer,5
2013-11-05,rabbit,22
2013-11-05,raccoon,7
2013-11-06,rabbit,19
2013-11-06,deer,2
2013-11-06,fox,1
2013-11-07,rabbit,18
2013-11-07,bear,1

Приклад файлу такого типу наведено у shell-lesson-data/exercise-data/animal-counts/animals.сsv.

Ми можемо скористатися командою cut -d , -f 2 animals.csv | sort | uniq, щоб отримати унікальні види тварин з файлу animals.csv. Щоб заощадити час і не повторювати введення команд, науковець може замість цього написати скрипт командної оболонки.

Створіть скрипт із назвою species.sh, який сприймає довільну кількість імен файлів за аргументи командного рядка. Він має використовувати змінену версію попередньої команди для виведення списку унікальних видів, які зустрічаються в кожному з цих файлів окремо.

BASH

# Скрипт для пошуку унікальних видів у csv-файлах, де другий стовпець містить назви видів
# Цей скрипт приймає будь-яку кількість імен файлів як аргументи командного рядка 

# Перебір всіх файлів
do
    echo "Unique species in $file:"
    # Вилучити назви видів
    cut -d , -f 2 $file | sort | uniq
done

Припустимо, ми щойно виконали низку команд, які зробили щось корисне — наприклад, створили графік, який ми плануємо використати у публікації. Ми хотіли б мати змогу відтворити графік пізніше, якщо знадобиться, тому збережемо команди у файл. Замість повторного введення (і ризику помилок), ми можемо зробити ось так:

BASH

$ history | tail -n 5 > redo-figure-3.sh

Файл redo-figure-3.sh тепер містить наступне:

297 bash goostats.sh NENE01729B.txt stats-NENE01729B.txt
298 bash goodiff.sh stats-NENE01729B.txt /data/validated/01729.txt > 01729-differences.txt
299 cut -d ',' -f 2-3 01729-differences.txt > 01729-time-series.txt
300 ygraph --format scatter --color bw --borders none 01729-time-series.txt figure-3.png
301 history | tail -n 5 > redo-figure-3.sh

Після невеликого редагування для видалення номерів команд і останнього рядка з командою history, ми отримаємо абсолютно точний запис того, як було створено цей графік.

Вправа

Чому варто записувати команди в історію перед їх виконанням?

Якщо виконати команду:

BASH

$ history | tail -n 5 > recent.sh

останньою командою у файлі є сама команда history, тобто, термінал додав history до журналу команд перед тим, як фактично її виконав. Насправді термінал завжди додає команди до журналу перед їх виконанням. Як ви гадаєте, чому він поводиться саме так?

Якщо якась команда призводить до збою або зависання, знання того, яка саме команда це спричинила, допоможе з’ясувати причину проблеми. Якби команда записувалася лише після її виконання, ми б втратили запис останньої команди у разі збою.

На практиці, більшість людей створюють скрипти терміналу, запускаючи команди в командному рядку кілька разів, щоб переконатися, що вони роблять все правильно, а потім зберігають їх у файлі для подальшого використання. Такий підхід дозволяє повторно відтворити робочий процес та дослідження даних, за допомогою одного виклику history і невеликого редагування для впорядкування команд та їх збереження як скрипт терміналу.

Конвеєр Неллі: створення скрипту


Науковий керівник Неллі наполягав на тому, що вся її аналітика має бути відтворюваною. Найпростіший спосіб зберегти всі кроки - записати їх у скрипт.

Спочатку повернемося до каталогу проєкту Неллі:

BASH

$ cd ../../north-pacific-gyre/

За допомогою nano вона створює файл …

BASH

$ nano do-stats.sh

…який містить наступне:

BASH

# Розрахунок статистики для файлів даних.
for datafile in "$@"
do
    echo $datafile
    bash goostats.sh $datafile stats-$datafile
done

Вона зберігає цей код у файлі з назвою do-stats.sh, щоб тепер мати змогу повторно виконати перший етап аналізу, набравши:

BASH

$ bash do-stats.sh NENE*A.txt NENE*B.txt

Вона також може зробити наступне:

BASH

$ bash do-stats.sh NENE*A.txt NENE*B.txt | wc -l

щоб вивести лише кількість оброблених файлів, а не їхні назви.

Одна з важливих особливостей скрипту Неллі полягає в тому, що він дозволяє користувачеві самостійно вибирати, які файли потрібно обробляти. Вона могла б також написати його так:

BASH

# Розрахунок статистики для файлів з локацій A та B. 
for datafile in NENE*A.txt NENE*B.txt
do
    echo $datafile
    bash goostats.sh $datafile stats-$datafile
done

Перевага цього буде полягати в тому, що цей код завжди вибирає правильні файли, і Неллі не потрібно пам’ятати про виключення файлів із літерою ‘Z’. Недолік полягає в тому, що скрипт завжди обробляє лише ці файли. Не редагуючи скрипт, Неллі не може застосувати його до всіх файлів (у тому числі до файлів ‘Z’) або до файлів ‘G’ чи ‘H’, які створюють її колеги в Антарктиді. Якщо вона хотіла б піти далі, то могла б модифікувати свій скрипт для перевірки аргументів командного рядка та за замовчуванням використовував NENE_A.txt NENE_B.txt, якщо жодних аргументів не передано. Звичайно, це створює інший компроміс між гнучкістю і складністю.

Вправа

Змінні в скриптах терміналу

Уявіть, що у каталозі alkanes у вас є скрипт з назвою script.sh, який містить наступні команди:

BASH

head -n $2 $1
tail -n $3 $1

Перебуваючи у каталозі alkanes, ви вводите наступну команду:

BASH

$ bash script.sh '*.pdb' 1 1

Які з наведених нижче результатів ви очікуєте побачити?

  1. Усі рядки між першим та останнім рядками кожного файлу, що закінчується на .pdb у каталозі alkanes
  2. Перший та останній рядок кожного файлу, що закінчується на .pdb у каталозі alkanes
  3. Перший та останній рядок кожного файлу в каталозі alkanes
  4. Помилку через лапки навколо *.pdb

Правильною є відповідь 2.

Спеціальні змінні $1, $2 та $3 відповідають аргументам командного рядка, що передаються скрипту, тому виконуються наступні команди:

BASH

$ head -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb
$ tail -n 1 cubane.pdb ethane.pdb octane.pdb pentane.pdb propane.pdb

Термінал не розгортає '*.pdb', оскільки символи взято у лапки. Таким чином, першим аргументом скрипту є '*.pdb', який буде розгорнуто у скрипті за допомогою head і tail.

Вправа

Пошук найдовшого файлу із заданим розширенням

Напишіть сценарій терміналу з назвою longest.sh, який отримує в якості аргументів ім’я каталогу і розширення імені файлу як аргументи, і виводить назву файлу з найбільшою кількістю рядків у цьому каталозі з цим розширенням. Наприклад:

BASH

$ bash longest.sh shell-lesson-data/exercise-data/alkanes pdb

виведе назву файлу .pdb у каталозі shell-lesson-data/exercise-data/proteins, який має найбільшу кількість рядків.

Ви можете протестувати свій скрипт в іншому каталозі, наприклад

BASH

$ bash longest.sh shell-lesson-data/exercise-data/writing txt

BASH

# Скрипт терміналу, який приймає два аргументи:
#    1. ім'я каталогу
#    2. розширення файлу
# і виводить ім'я файлу з даним розширенням в цьому каталозі 
# який має найбільшу кількість рядків

wc -l $1/*.$2 | sort -n | tail -n 2 | head -n 1

Перша частина конвеєра, wc -l $1/*.$2 | sort -n, рахує кількість рядків у кожному файлі та сортує їх у числовому порядку (найбільший файл буде останнім). Коли надано кілька файлів, wc також виведе останній підсумковий рядок, який покаже загальну кількість рядків у всіх файлах. Ми використовуємо tail -n 2 | head -n 1, щоб відкинути цей останній рядок.

Використовуючи wc -l $1/*.$2 | sort -n | tail -n 1, ми побачимо останній підсумковий рядок. Ми також можемо будувати конвеєр крок за кроком, щоб краще зрозуміти результат.

Вправа

Читання і розуміння скриптів

Для цього завдання ще раз розглянемо каталог shell-lesson-data/exercise-data/proteins. У ньому міститься низка файлів .pdb разом з іншими файлами, які ви могли створити. Опишіть, що відбудеться при послідовному виконанні кожного з трьох скриптів із командами bash script1.sh *.pdb, bash script2.sh *.pdb та bash script3.sh *.pdb.

BASH

# Скрипт 1
echo *.*

BASH

# Скрипт 2
for filename in $1 $2 $3
do
    cat $filename
done

BASH

# Скрипт 3
echo $@.pdb

У кожному випадку термінал розгортає символ підстановки у *.pdb, а потім передає отриманий список файлів як аргументи скрипту.

Скрипт 1 виведе список усіх файлів, що містять крапку в їх назві. Передані скрипту аргументи взагалі не використовуються.

Скрипт 2 виведе вміст перших 3 файлів з розширенням .pdb. $1, $2 і $3 відповідають першому, другому та третьому аргументам відповідно.

Скрипт 3 виведе всі аргументи скрипту (тобто назви всіх файлів з розширенням .pdb), і додасть до них .pdb. Змінна $@ зазначає усі аргументи, що були передані скрипту.

ВИХІД

cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb.pdb
Вправа

Налагодження скриптів

Припустимо, ви зберегли наступний скрипт у файлі з назвою do-errors.sh у каталозі north-pacific-gyre/scripts:

BASH

# Статистичні розрахунки для файлів даних.
for datafile in "$@"
do
    echo $datfile
    bash goostats.sh $datafile stats-$datafile
done

Якщо ви запускаєте його з каталогу north-pacific-gyre:

BASH

$ bash do-errors.sh NENE*A.txt NENE*B.txt

програма нічого не виводить. Щоб з’ясувати причину, перезапустіть скрипт з опцією -x:

BASH

$ bash -x do-errors.sh NENE*A.txt NENE*B.txt

Що показує вивід? Який рядок призводить до помилки?

Параметр -x призводить до запуску скрипту у режимі налагодження (відлагодження). Він виводить кожну команду під час її виконання, допомагаючи вам локалізувати помилки. У цьому прикладі ми таким чином можемо побачити, що команда echo нічого не виводить. Ми допустили друкарську помилку у назві змінної циклу, і оскільки змінної datfile не існує, вона повертає порожній рядок.

Ключові моменти
  • Зберігайте команди у файлах (які зазвичай називають скриптами оболонки або скриптами терміналу) для їх повторного використання.
  • bash [ім'я файлу] виконує команди, збережені у відповідному файлі.
  • $@ посилається на всі аргументи командного рядка, передані скрипту оболонки.
  • $1, $2 і так далі представляють перший, другий та наступні аргументи командного рядка.
  • Беріть змінні в лапки, якщо їхні значення можуть містити пробіли.
  • Надання користувачам можливості самим обирати файли для обробки робить скрипт гнучкішим і більш узгодженим із вбудованими командами Unix.