Огляд
Викладання: хв
Вправи: хвПитання
Цілі
Цикли - це конструкція програмування, яка дозволяє повторювати команду або набір команд для кожного елемента у списку. Таким чином, вони є ключем до підвищення продуктивності за рахунок автоматизації. Подібно до підстановочних символів і завершення клавішею табуляції, використання циклів також зменшує кількість необхідного набору тексту (а отже, зменшує кількість помилок).
Припустимо, у нас є кілька сотень файлів геномних даних з іменами basilisk.dat
, minotaur.dat
та
unicorn.dat
.
Для цього прикладу ми використаємо каталог exercise-data/creatures
, який містить лише три
файли з прикладами,
але ці принципи можна застосувати до набагато більшої кількості файлів одночасно.
Структура цих файлів однакова: загальна назва, класифікація та дата оновлення у перших трьох рядках, а в наступних - послідовності ДНК. Давайте подивимось на файли:
$ head -n 5 basilisk.dat minotaur.dat unicorn.dat
Ми хотіли б роздрукувати класифікацію для кожного виду, яка наведена у другому
рядку кожного файлу.
Для кожного файлу нам потрібно виконати команду head -n 2
і з’єднати її каналом з командою tail -n 1
.
Для вирішення цієї задачі ми скористаємося циклом, але спочатку розглянемо загальну форму циклу,
використовуючи псевдокод нижче:
for thing in list_of_things
do
operation_using $thing # Відступ усередині циклу не є обов'язковим, але полегшує читабельність
done
і ми можемо застосувати це до нашого прикладу наступним чином:
$ for filename in basilisk.dat minotaur.dat unicorn.dat
> do
> head -n 2 $filename | tail -n 1
> done
CLASSIFICATION: basiliscus vulgaris
CLASSIFICATION: bos hominus
CLASSIFICATION: equus monoceros
Слідкуйте за підказкою
Запрошення до введення в терміналі змінюється з
$
на>
і назад, коли ми вводили команди всередині нашого циклу. Друге запрошення до введення,>
, відрізняється, щоб нагадати нам, що ми ще не закінчили введення повної команди. Крапка з комою;
використовується для розділення двох команд, написаних в одному рядку.
Коли термінал бачить ключове слово for
,
він знає, що потрібно повторити команду (або групу команд) один раз для кожного елемента списку.
Кожного разу, коли вміст циклу виконується (одне виконання команд всередині циклу називається ітерацією), елемент списку послідовно присвоюється
змінній, і команди всередині циклу виконуються, перш ніж перейти
до наступного елементу списку.
Усередині циклу
ми звертаємося до значення змінної, ставлячи $
перед ї іменем.
Символ $
вказує інтерпретатору командного рядка розглядати
змінну як ім’я змінної і підставити замість неї її значення,
замість того, щоб розглядати її як текст або зовнішню команду.
У цьому прикладі список складається з трьох файлів: basilisk.dat
, minotaur.dat
та unicorn.dat
.
Кожного разу, коли виконанується цикл, він присвоює чергове ім’я файлу змінній filename
і виконає команду head
.
При першому проходженні циклу
$filename
дорівнює basilisk.dat
.
Інтерпретатор виконує команду head
на basilisk.dat
і передає перші два рядки команді tail
,
яка виводить другий рядок файлу basilisk.dat
.
Для другої ітерації $filename
стає
minotaur.dat
. Цього разу термінал виконує команду head
на minotaur.dat
і передає перші два рядки команді tail
,
яка виводить другий рядок minotaur.dat
.
На третій ітерації $filename
стає
unicorn.dat
, тому термінал виконує команду head
для цього файлу,
і tail
на виході цього.
Оскільки у списку було лише три елементи, термінал вийде з циклу for
.
Однакові символи, різні значення
Тут ми бачимо, що символ
>
використовується як запрошення командного рядка, тоді як>
також використовується для перенаправлення виводу. Аналогічно, символ$
використовується як запрошення до командного рядка, але, як ми бачили раніше, він також використовується для запиту до оболонки про значення змінної.Якщо термінал виводить
>
або$
, то він очікує, що ви щось введете, і цей символ є підказкою.Якщо ви вводите
>
або$
самостійно, це є вашою вказівкою про те, що оболонці перенаправити вивід або отримати значення змінної.
При використанні змінних також
можна брати імена у фігурні дужки, щоб чітко розмежувати імена змінних:
$filename
еквівалентно ${filename}
, але відрізняється від
${file}name
. Ви можете зустріти таку форму запису у програмах інших людей.
Ми назвали змінну у цьому циклі filename
(ім’я файлу)
для того, щоб зробити її призначення більш зрозумілим для читачів-людей.
Самій оболонці байдуже, як називається змінна;
якщо ми напишемо цей цикл наступним чином:
$ for x in basilisk.dat minotaur.dat unicorn.dat
> do
> head -n 2 $x | tail -n 1
> done
чи:
$ for temperature in basilisk.dat minotaur.dat unicorn.dat
> do
> head -n 2 $temperature | tail -n 1
> done
це спрацювало б точно так само.
Не роби цього.
Програми корисні лише тоді, коли люди можуть їх розуміти,
тому беззмістовні назви (наприклад, x
) або назви, що вводять в оману (наприклад, temperature
)
збільшують ймовірність того, що програма не буде робити те, що читачі думають, що вона робить.
У наведених вище прикладах змінним (thing
, filename
, x
та temperature
)
можна було б назвати будь-якою іншою назвою, аби вона була зрозумілою як для того,
хто пише код, так і для того, хто його читає.
Зауважте також, що цикли можна використовувати для інших речей, окрім імен файлів, наприклад, для списку чисел або підмножини даних.
Напишіть власний цикл
Як би ви написали цикл, який виводить всі 10 чисел від 0 до 9?
Розв’язання
$ for loop_variable in 0 1 2 3 4 5 6 7 8 9 > do > echo $loop_variable > done
0 1 2 3 4 5 6 7 8 9
Змінні в циклах
Ця вправа звертається до каталогу
shell-lesson-data/exercise-data/proteins
.ls *.pdb
видає наступний результат:cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
Що виводить наступний код?
$ for datafile in *.pdb > do > ls *.pdb > done
Тепер, що виводить наступний код?
$ for datafile in *.pdb > do > ls $datafile > done
Чому ці два цикли дають різні результати?
Розв’язання
Перший блок коду дає однаковий результат на кожній ітерації циклу. Bash розширює шаблон
*.pdb
в тілі циклу (а також перед початком циклу), щоб знайти всі файли, що закінчуються на.pdb
. а потім перераховує їх за допомогоюls
. Розширений цикл матиме такий вигляд:$ for datafile in cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb > do > ls cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb > done
cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
Другий блок коду перераховує різні файли на кожній ітерації циклу. Значення змінної
datafile
обчислюється за допомогою$datafile
, а потім перераховується за допомогоюls
.cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
Обмеження набору файлів
Що буде виведено у результаті виконання наступного циклу в каталозі
shell-lesson-data/exercise-data/proteins
?>$ for filename in c* > do > ls $filename > done
- Жодної назви файлу не буде виведено.
- Будуть перелічені всі файли.
- Будуть перелічені лише
cubane.pdb
,octane.pdb
таpentane.pdb
.- Буде виведено лише
cubane.pdb
.Розв’язання
4 - правильна відповідь. Підстановочний символ
*
відповідає нулю або більшій кількості символів, тому будь-яке ім’я файлу, що починається з літери ‘c’, за якою йдуть нуль або більша кількість символів, підійде.Як зміниться результат, якщо замість неї використати цю команду?
$ for filename in *c* > do > ls $filename > done
- Будуть перераховані ті ж файли.
- Цього разу перераховані всі файли.
- Цього разу не виведено жодного файла.
- Будуть перераховані файли
cubane.pdb
іoctane.pdb
.- Буде перераховано лише файл
octane.pdb
.Розв’язання
4 - правильна відповідь. Підстановочний символ
*
відповідає нулю або більшій кількості символів, тому всі імена файлів з нулем або більшою кількістю символів перед літерою ‘c’ і нулем або більшою кількістю символів після літери ‘c’ підійдуть.
Збереження у файл в циклі - Частина перша
В каталозі
shell-lesson-data/exercise-data/proteins
, який результат роботи цього циклу?for alkanes in *.pdb do echo $alkanes cat $alkanes > alkanes.pdb done
- Буде виведено
cubane.pdb
,ethane.pdb
,methane.pdb
,octane.pdb
,pentane.pdb
іpropane.pdb
, а текст зpropane.pdb
буде збережено у файлі з назвоюalkanes.pdb
.- Буде виведено
cubane.pdb
,ethane.pdb
іmethane.pdb
, і текст з усіх трьох файлів буде об’єднано і збережено у файл з назвоюalkanes.pdb
.- Буде виведено
cubane.pdb
,ethane.pdb
,methane.pdb
,octane.pdb
іpentane.pdb
, а текст зpropane.pdb
буде збережено до файлу з назвоюalkanes.pdb
.- Нічого з перерахованого вище.
Розв’язання
- Текст з кожного файлу по черзі записується у файл
alkanes.pdb
. Однак, файл перезаписується на кожній ітерації циклу, тому кінцевий вмістalkanes.pdb' буде дорівнювати тексту з файлу
propane.pdb`.
Збереження у файл в циклі - Частина друга
Також у каталозі
shell-lesson-data/exercise-data/proteins
, що буде виведено у наступному циклі?>for datafile in *.pdb do cat $datafile >> all.pdb done
- Весь текст з файлів
cubane.pdb
,ethane.pdb
,methane.pdb
,octane.pdb
таpentane.pdb
буде об’єднано і збережено у файлі з назвоюall.pdb
.- Текст з файлу
ethane.pdb
буде збережено до файлу з назвоюall.pdb
.- Весь текст з файлів
cubane.pdb
,ethane.pdb
,methane.pdb
,octane.pdb
,pentane.pdb
таpropane.pdb
буде об’єднано та збережено у файл з назвоюall.pdb
.- Весь текст з файлів
cubane.pdb
,ethane.pdb
,methane.pdb
,octane.pdb
,pentane.pdb
іpropane.pdb
буде виведено на екран і збережено у файлі з назвоюall.pdb
.Розв’язання
3 - правильна відповідь. Оператор
>>
додає вміст до файлу, а не перезаписує його перенаправленим виводом команди. Оскільки вивід командиcat
було перенаправлено, на екран нічого не буде виведено.
Давайте продовжимо наш приклад у каталозі shell-lesson-data/exercise-data/creatures
.
Тут цикл трохи складніший:
$ for filename in *.dat
> do
> echo $filename
> head -n 100 $filename | tail -n 20
> done
Термінал розпочинає роботу з розгортання *.dat
для створення списку файлів, які він буде обробляти.
Тіло циклу
виконує дві команди для кожного з цих файлів.
Перша команда, echo
, виводить аргументи командного рядка у стандартний вивід.
Наприклад:
$ echo hello there
виводить:
hello there
У цьому випадку,
оскільки термінал розширює $filename
до імені файлу,
echo $filename
виводить ім’я файлу.
Зауважте, що ми не можемо написати це як:
$ for filename in *.dat
> do
> $filename
> head -n 100 $filename | tail -n 20
> done
тому що при першому проходженні через цикл,
коли $filename
розшириться до basilisk.dat
, оболонка спробує запустити basilisk.dat
як програму.
Нарешті,
комбінація head
і tail
виділить рядки 81-100
з будь-якого файлу, який обробляється
(за умови, що у відповідному файлі є принаймні 100 рядків).
Пробіли в іменах
Пробіли використовуються для відокремлення елементів списку які ми будемо перебирати у циклі. Якщо один з цих елементів містить пробіл, нам потрібно взяти його в лапки і зробити те ж саме зі змінною циклу. Припустимо, що наші файли даних мають імена:
red dragon.dat purple unicorn.dat
Щоб виконати цикл над цими файлами, нам потрібно додати подвійні лапки, ось так:
$ for filename in "red dragon.dat" "purple unicorn.dat" > do > head -n 100 "$filename" | tail -n 20 > done
Простіше уникати використання пробілів (або інших спеціальних символів) у назвах файлів.
Вищевказані файли не існують, тому якщо ми виконаємо вищенаведений код, команда
head
не зможе знайти їх, однак у повідомленні про помилку буде показано назви цих файлів, що очікувалися:head: cannot open ‘red dragon.dat’ for reading: No such file or directory head: cannot open ‘purple unicorn.dat’ for reading: No such file or directory
Спробуйте видалити лапки навколо
$filename
у наведеному вище циклі, щоб побачити ефект лапок позначки на пробілах. Зверніть увагу, що ми отримуємо результат команди циклу для unicorn.dat коли ми запускаємо цей код у каталозіcreatures
:head: cannot open ‘red’ for reading: No such file or directory head: cannot open ‘dragon.dat’ for reading: No such file or directory head: cannot open ‘purple’ for reading: No such file or directory CGGTACCGAA AAGGGTCGCG CAAGTGTTCC ...
Ми б хотіли змінити кожен з файлів у shell-lesson-data/exercise-data/creatures
,
але також зберегти версію
оригінальних файлів, назвавши копії original-basilisk.dat
і original-unicorn.dat
.
Ми не можемо використовувати:
$ cp *.dat original-*.dat
тому що це буде розширено до:
$ cp basilisk.dat minotaur.dat unicorn.dat original-*.dat
Це не створить резервну копію наших файлів, натомість ми отримаємо помилку:
cp: target `original-*.dat' is not a directory
Ця проблема виникає, коли команда cp
отримує більше двох входів. Коли це відбувається, вона
очікує, що останнім вхідним параметром буде каталог, куди вона зможе скопіювати всі файли, які їй було передано.
Оскільки у каталозі creatures
немає каталогу з назвою original-*.dat
, ми отримаємо
помилку.
Замість цього ми можемо використати цикл:
$ for filename in *.dat
> do
> cp $filename original-$filename
> done
Цей цикл виконує команду cp
один раз для кожного імені файлу.
Перший раз,
коли змінна $filename
має значення до basilisk.dat
,
термінал виконає:
cp basilisk.dat original-basilisk.dat
У другий раз команда наступна:
cp minotaur.dat original-minotaur.dat
В третій, останній раз, команда буде такою:
cp unicorn.dat original-unicorn.dat
Оскільки команда cp
зазвичай не виводить нічого, важко перевірити
що цикл працює правильно.
Однак раніше ми дізналися, як виводити рядки за допомогою echo
, і ми можемо модифікувати цикл
щоб використовувати echo
для виведення наших команд без їхнього виконання.
Таким чином, ми можемо перевірити, які команди виконувалися би у немодифікованому циклі.
Наступна діаграма
показує, що відбувається при виконанні модифікованого циклу, і демонструє, як
розумне використання echo
є гарною технікою зневадження.
Конвеєр Неллі: Обробка файлів
Тепер Неллі готова обробити свої файли даних за допомогою goostats.sh
—
скрипта терміналу, написаного її керівником.
Він обчислює деякі статистичні дані з файлу зразка білка і приймає два аргументи:
- вхідний файл (що містить вихідні дані)
- вихідний файл (для збереження розрахованої статистики)
Оскільки вона все ще вчиться користуватися терміналом, вона вирішує створювати необхідні команди поетапно. Першим кроком буде переконатися, що вона може вибирати правильні вхідні файли - запам’ятайте, це ті, назви яких закінчуються на ‘A’ або ‘B’, а не на ‘Z’. Починаючи з домашнього каталогу, Неллі набирає:
$ cd north-pacific-gyre
$ for datafile in NENE*A.txt NENE*B.txt
> do
> echo $datafile
> done
NENE01729A.txt
NENE01729B.txt
NENE01736A.txt
...
NENE02043A.txt
NENE02043B.txt
Наступним кроком буде вирішити
як назвати файли, які створить програма аналізу goostats.sh
.
Додавання до імені кожного вхідного файлу префікса “stats” здається простим,
тому вона модифікує свій цикл для цього:
$ for datafile in NENE*A.txt NENE*B.txt
> do
> echo $datafile stats-$datafile
> done
NENE01729A.txt stats-NENE01729A.txt
NENE01729B.txt stats-NENE01729B.txt
NENE01736A.txt stats-NENE01736A.txt
...
NENE02043A.txt stats-NENE02043A.txt
NENE02043B.txt stats-NENE02043B.txt
Насправді, вона ще не запускала goostats.sh
,
але тепер вона впевнена, що може вибрати правильні файли і згенерувати правильні назви вихідних файлів.
Введення команд знову і знову стає нудним, і Неллі хвилюється через можливі помилки, тож замість того, щоб перенабирати свій цикл, вона натискає ↑. У відповідь оболонка відобразить весь цикл в одному рядку (використовуючи крапку з комою для розділення частин):
$ for datafile in NENE*A.txt NENE*B.txt; do echo $datafile stats-$datafile; done
За допомогою клавіші зі стрілкою ліворуч,
Неллі створює резервну копію і змінює команду echo
на bash goostats.sh
:
$ for datafile in NENE*A.txt NENE*B.txt; do bash goostats.sh $datafile stats-$datafile; done
Коли вона натискає Enter, термінал виконає змінену команду. Однак, здається, нічого не відбувається - немає ніякого виводу. За мить Неллі розуміє, що оскільки її скрипт більше нічого не виводить на екран, вона не має жодного уявлення про те, чи виконується він, а тим паче, як швидко. Вона перериває команду виконання, набравши Ctrl+C, використовує ↑ для повтору команди, і редагує її, щоб читати:
$ for datafile in NENE*A.txt NENE*B.txt; do echo $datafile;
bash goostats.sh $datafile stats-$datafile; done
Початок і кінець
Перехід на початок рядка в оболонці здійснюється за допомогою комбінації клавіш Ctrl+A і в кінець рядка - за допомогою Ctrl+E.
Коли вона запускає свою програму зараз, програма виводить один рядок кожні п’ять секунд або близько того:
NENE01729A.txt
NENE01729B.txt
NENE01736A.txt
...
Значення 1518, помножене на 5 секунд,
поділене на 60,
каже їй, що її сценарій буде виконуватися близько двох годин.
Для остаточної перевірки
вона відкриває інше вікно терміналу,
переходить в north-pacific-gyre
,
і використовує cat stats-NENE01729B.txt
.
для перевірки одного з вихідних файлів.
Виглядає добре,
тож вона вирішує випити кави і продовжити читання.
Хто знає історію, той може її повторити
Інший спосіб повторити попередню роботу - скористатися командою
history
, щоб отримати список останніх кількох сотень команд, які було виконано, і потім скористатися командою!123
(де “123” замінено на номер команди), щоб повторити одну з цих команд. Наприклад, якщо Неллі набере наступне:$ history | tail -n 5
456 ls -l NENE0*.txt 457 rm stats-NENE01729B.txt.txt 458 bash goostats.sh NENE01729B.txt stats-NENE01729B.txt 459 ls -l NENE0*.txt 460 history
тоді вона може перезапустити
goostats.sh
наNENE01729B.txt
, просто набравши!458
.
Інші команди історії
Існує ряд інших команд швидкого доступу до історії.
- Ctrl+R переходить у режим “зворотного пошуку” в історіїі і знаходить останню команду у вашому журналі, яка відповідає тексту, що ви введете далі. НатиснітьCtrl+R ще один або кілька додаткових разів для пошуку більш ранніх збігів. За допомогою клавіш зі стрілками вліво і вправо виберіть цей рядок і відредагуйте його, потім натисніть Return щоб виконати команду.
!!
повертає безпосередньо попередню команду (ви можете знайти це більш зручним, ніж використання ↑)!$
повертає останнє слово останньої команди. Це корисно частіше, ніж ви можете собі уявити: післяbash goostats.sh NENE01729B.txt stats-NENE01729B.txt
, ви можете ввестиless !$
для перегляду файлуstats-NENE01729B.txt
, що швидше, ніж набирати ↑ і редагувати командний рядок.
Виконання пробного запуску
Цикл - це спосіб зробити багато речей одночасно — або зробити багато помилок одночасно, якщо він робить неправильні речі. Один зі способів перевірити, що робив би цикл це за допомогою
echo
виводити команди, які він виконуватиме, замість того, щоб виконувати їх насправді.Припустимо, ми хочемо переглянути команди, які виконає наступний цикл без виконання цих команд:
$ for datafile in *.pdb > do > cat $datafile >> all.pdb > done
У чому різниця між двома наведеними нижче циклами, і який з них ми хочемо запустити?
# Варіант 1 $ for datafile in *.pdb > do > echo cat $datafile >> all.pdb > done
# Варіант 2 $ for datafile in *.pdb > do > echo "cat $datafile >> all.pdb" > done
Розв’язання
Друга версія - це та, яку ми хочемо запустити. Вона виводить на екран усе, що укладено у лапки, розширюючи назву змінної циклу, оскільки ми додали до неї знак долара. Він також не змінює і не створює файл
all.pdb
, оскільки оператор>>
розглядається буквально як частина рядка, а не як інструкція перенаправлення.Перша версія додає вивід команди
echo cat $datafile
до файлуall.pdb
. Цей файл міститиме лише списокcat cubane.pdb
,cat ethane.pdb
,cat methane.pdb
тощо.Спробуйте обидві версії, щоб побачити результат! Обов’язково відкрийте файл
all.pdb
, щоб переглянути його вміст.
Вкладені цикли
Припустімо, що ми хочемо створити структуру каталогів для організації певних експериментів з вимірювання констант швидкості реакції з різними сполуками та різними температурами. Яким буде результат виконання наступного коду:
$ for species in cubane ethane methane > do > for temperature in 25 30 37 40 > do > mkdir $species-$temperature > done > done
Розв’язання
Ми маємо вкладений цикл, тобто такий, що міститься в іншому циклі, тому для кожного значення змінної species у зовнішньому циклі внутрішній цикл (вкладений цикл) перебирає список температур і створює новий каталог для кожної комбінації.
Спробуйте запустити код самостійно, щоб побачити, які каталоги буде створено!
[workshop-repo]: [yaml]: http://yaml.org/
Ключові моменти