Скрипти терміналу

Огляд

Викладання: 30 хв
Вправи: 15 хв
Питання
  • Як я можу зберігати і повторно використовувати команди?

Цілі
  • Написати сценарій терміналу, який виконує команду або серію команд для фіксованого набору файлів.

  • Запустити сценарій терміналу з командного рядка.

  • Написати сценарій терміналу, який оперує набором файлів, заданих користувачем у командному рядку.

  • Створити конвеєри, які включають написані вами та іншими користувачами скрипти терміналу.

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

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

Давайте почнемо з того, що повернемося до каталогу proteins/ і створимо новий файл middle.sh, який стане нашим скриптом терміналу:

$ cd proteins
$ 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). Переконайтеся, що в каталозі proteins тепер міститься файл з назвою middle.sh.

Після того, як ми зберегли файл, ми можемо попросити термінал виконати команди, які у ньому містяться. Наша термінал називається 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 і зробимо його більш універсальним:

$ nano middle.sh

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

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

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

$ 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 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 відповідно:

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

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

$ 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 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, може знадобитися деякий час, щоб зрозуміти, що він робить. Ми можемо покращити наш скрипт, додавши деякі коментарі зверху:

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

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

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

$ wc -l *.pdb | sort -n

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

Ось приклад:

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

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

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

Розв’язання

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

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

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

$ 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, ми отримаємо абсолютно точний запис того, як ми створили цю фігуру.

Навіщо записувати команди в історію перед виконанням?

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

$ history | tail -n 5 > recent.sh

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

Розв’язання

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

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

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

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

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

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

Вона створює файл з використанням nano

$ nano do-stats.sh

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

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

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

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

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

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

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

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

# Обчисліть статистику для файлів даних 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, якщо нічого не було передано. Звичайно, це ще один компроміс між гнучкістю і складністю.

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

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

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

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

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

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

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

Розв’язання

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

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

$ 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 longest.sh shell-lesson-data/exercise-data/proteins pdb

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

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

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

Розв’язання

# Скрипт терміналу, який приймає два аргументи:
#    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 відповідно.

# Скрипт 1
echo *.*
# Скрипт 2
for filename in $1 $2 $3
do
    cat $filename
done
# Скрипт 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:>

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

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

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

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

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

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

Розв’язання

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

[workshop-repo]: [yaml]: http://yaml.org/

Ключові моменти

  • Зберігайте команди у файлах (зазвичай їх називають скриптами терміналу) для повторного використання.

  • bash [ім'я файлу] виконує команди, збережені у відповідному файлі.

  • $@ посилається на всі аргументи командного рядка скрипта терміналу.

  • $1, $2, і т.д., посилаються на перший аргумент командного рядка, другий аргумент командного рядка і т.д.

  • Беріть змінні в лапки, якщо значення можуть містити пробіли.

  • Дозвіл користувачам вирішувати, які файли обробляти, є більш гнучким і більш узгодженим з вбудованими командами Unix.