Цикли
Останнє оновлення 2025-11-05 | Редагувати цю сторінку
Огляд
Питання
- Як виконати одні й ті ж дії над різними файлами?
Цілі
- Написати цикл, який застосовує одну або декілька команд окремо до кожного файлу в наборі файлів.
- Простежити, яких значень набуває змінна циклу під час виконання циклу.
- Пояснити різницю між ім’ям змінної та її значенням.
- Пояснити, чому в іменах файлів не можна використовувати пробіли та деякі розділові знаки.
- Продемонструвати, як побачити, які команди були виконані останнім часом.
- Перезапустити нещодавно виконані команди без повторного введення.
Цикли - це конструкції програмування, які дозволяють повторити команду або набір команд для кожного елемента у списку. Таким чином, автоматизація виконання повторюваних дій суттєво підвищує ефективність. Подібно до шаблонів і автодоповнення, цикли допомагають зменшити кількість вручну набраного тексту (а отже, зменшують кількість помилок).
Припустимо, у нас є кілька сотень файлів даних, які містять
інформацію про геноми та мають імена на кшталт
basilisk.dat, minotaur.dat та
unicorn.dat. Для наступного прикладу ми використаємо
каталог exercise-data/creatures, який містить лише три
зразкові файли, але ті ж самі методи можна застосувати до значно більшої
кількості файлів одночасно.
Ці файли мають однакову структуру: перші три рядки містять назву виду, його класифікацію та дату оновлення, а у наступних рядках наведені послідовності ДНК. Погляньмо, що містять ці файли:
Для кожного виду ми хотіли б надрукувати його класифікацію, яка
наведена у другому рядку відповідного файлу. Для кожного файлу нам
потрібно виконати команду head -n 2 і передати її результат
через канал до команди tail -n 1. Скористаймося циклом, щоб
уникнути цю проблему, але спочатку розгляньмо загальну форму циклу,
використовуючи наведений нижче псевдокод:
BASH
# Слово "for" вказує на початок команди для виконання циклу "For"
for thing in list_of_things
# Слово "do" вказує на початок списку завдань для виконання
do
# Відступи всередині циклу не є обов'язковими, але сприяють розбірливості
operation_using/command $thing
# Слово "done" вказує на кінець циклу
done
У такому разі, ми можемо застосувати це до нашого прикладу наступним чином:
BASH
$ for filename in basilisk.dat minotaur.dat unicorn.dat
> do
> echo $filename
> head -n 2 $filename | tail -n 1
> done
ВИХІД
basilisk.dat
CLASSIFICATION: basiliscus vulgaris
minotaur.dat
CLASSIFICATION: bos hominus
unicorn.dat
CLASSIFICATION: equus monoceros
Слідкуйте за підказками командного рядка
Під час введення нашого циклу запрошення термінала змінювалося з
$ на > та назад. Друге запрошення
(>) відрізняється, щоб нагадати нам, що ми ще не
завершили введення повної команди. Крапка з комою ;
використовується для розділення двох команд, написаних в одному
рядку.
Коли термінал бачить ключове слово for, він розуміє, що
потрібно повторити команду (або групу команд) для кожного елемента зі
списку. Кожного разу, коли цикл виконується (цей процес називається
ітерацією), елемент списку послідовно присвоюється
змінній та виконуються команди всередині циклу, після
чого цикл переходить до наступного елементу списку. Усередині циклу ми
звертаємося до значення змінної, додаючи $ перед її іменем.
Символ $ повідомляє інтерпретатор командного рядка, що далі
йде назва змінної, тож слід підставити її значення, а не сприймати запис
як текст чи назву команди.
У цьому прикладі список складається з трьох файлів:
basilisk.dat, minotaur.dat та
unicorn.dat. Кожного разу, коли цикл повторюється, ми
спочатку використовуємо echo для друку значення, яке наразі
зберігає змінна $filename. Це не обов’язково робити, але
допомагає нам слідкувати за виконанням програми. Далі ми виконаємо
команду head для файлу, на який зараз посилається
$filename. При першому проходженні циклу
$filename має значення basilisk.dat.
Інтерпретатор виконує команду head над
basilisk.dat і передає перші два рядки команді
tail, яка виводить другий рядок цього файлу. Для другої
ітерації $filename стає minotaur.dat. Цього
разу термінал виконує команду head над
minotaur.dat і передає перші два рядки команді
tail, яка виводить другий рядок minotaur.dat.
На третій ітерації $filename стає unicorn.dat,
тому термінал виконує команду head для цього файлу, і
tail обробляє результат. Оскільки список містив лише три
елементи, оболонка закінчує цикл for.
Однакові символи, різні значення
Тут ми бачимо, що символ > використовується як
запрошення командного рядка, але > також застосовується
для перенаправлення виводу. Аналогічно, символ $ діє як
запрошення оболонки, але, як ми бачили раніше, його функція теж може
полягати в отриманні значення змінної.
Якщо термінал друкує > або $,
то він очікує від вас введення команди й цей символ є підказкою.
Якщо ви вводите > або $
самостійно, це означає, що ви даєте команду оболонці перенаправити вивід
або отримати значення змінної.
При використанні змінних також можна брати їхні імена у фігурні
дужки, щоб чітко відокремити імена змінних: $filename
еквівалентно ${filename}, але відрізняється від
${file}name. Ви можете побачити таку форму запису в інших
програмах.
Ми назвали змінну у цьому циклі filename (ім’я файлу),
щоб її призначення було зрозуміліше для читачів. Самій оболонці байдуже,
як називається змінна; якби ми написали цей цикл так:
або:
BASH
$ 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?
Змінні в циклах
Ця вправа стосується каталогу
shell-lesson-data/exercise-data/alkanes. Команда
ls *.pdb дає такий результат:
ВИХІД
cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb
Що виведе наступний код?
А цей?
Чому ці два цикли дають різні результати?
Перший блок коду дає однаковий результат на кожній ітерації циклу.
Bash розгортає шаблон *.pdb в тілі циклу (а також перед
початком циклу), щоб знайти всі файли, що закінчуються на
.pdb, а потім виводить їх список за допомогою
ls. Розширений цикл матиме такий вигляд:
BASH
$ 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/alkanes?
- Жодної назви файлу не буде виведено.
- Будуть перелічені всі файли.
- Будуть перелічені лише
cubane.pdb,octane.pdbтаpentane.pdb. - Буде виведено лише
cubane.pdb.
4 - правильна відповідь. Символ * відповідає нулю або
більшій кількості символів, тому будь-яке ім’я файлу, що починається з
літери ‘c’, за якою йдуть нуль або більша кількість символів, буде
відповідати шаблону c*.
Обмеження наборів файлів (continued)
Як зміниться результат, якщо замість цього скористатися ось цією командою?
- Будуть перелічені ті ж самі файли.
- Цього разу будуть перелічені всі файли.
- Цього разу не буде виведено жодного файлу.
- Будуть перелічені файли
cubane.pdbтаoctane.pdb. - Буде перелічено лише файл
octane.pdb.
4 - правильна відповідь. Символ * відповідає нулю або
більшій кількості символів, тому всі імена файлів з нулем або більшою
кількістю символів перед літерою ‘c’ або після літери ‘c’ будуть
відповідати шаблону *c*.
Як зберігати результати в файл під час виконання циклу - частина перша
В каталозі shell-lesson-data/exercise-data/alkanes, яким
буде результат роботи цього циклу?
- Буде виведено
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/alkanes, що буде виведено у
наступному циклі?
- Весь текст з файлів
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. Тут цикл трохи
складніший:
Термінал розпочинає роботу з розгортання *.dat, щоб
створити список файлів для подальшої обробки. Тіло
циклу виконує дві команди для кожного з них. Перша команда,
echo, виводить свої аргументи на стандартний вивід (тобто,
на standard output). Наприклад:
друкує:
ВИХІД
hello there
У цьому випадку, оскільки термінал підставить до
$filename імʼя файлу, echo $filename виведе
ім’я файлу. Зауважте, що ми не можемо написати це як:
тому що під час першої ітерації циклу, коли $filename
буде замінено на basilisk.dat, термінал спробує запустити
basilisk.dat як програму. Нарешті, комбінація
head і tail виділить рядки 81-100 з будь-якого
файлу, що наразі обробляється (за умови, що у відповідному файлі є
принаймні 100 рядків).
Пробіли в іменах
Пробіли використовуються для відокремлення елементів списку, які ми будемо перебирати у циклі. Якщо один з цих елементів містить пробіл, нам потрібно взяти його в лапки та зробити те ж саме зі змінною циклу. Припустимо, що наші файли даних мають імена:
red dragon.dat
purple unicorn.dat
Щоб переглянути ці файли у циклі, нам потрібно додати подвійні лапки, ось так:
BASH
$ 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: target `original-*.dat' is not a directory
Ця проблема виникає, коли команда cp отримує більше ніж
два вхідних аргументи. Коли це відбувається, вона очікує, що останнім
вхідним параметром буде каталог, куди вона зможе скопіювати всі файли,
які їй було передано. Оскільки у каталозі creatures немає
каталогу з назвою original-*.dat, ми отримаємо помилку.
Замість цього ми можемо використати цикл:
Цей цикл виконує команду cp один раз для кожного імені
файлу. Перший раз, коли змінна $filename має значення
basilisk.dat, термінал виконує:
Вдруге, буде виконана наступна команда:
В останній раз, команда буде такою:
Оскільки команда cp зазвичай не виводить жодного
результату, важко перевірити що цикл працює правильно. Однак ми
дізналися, як виводити рядки за допомогою echo. Це допоможе
нам перевірити, які команди виконувалися б у циклі без їх фактичного
виконання.
Наступна діаграма показує, що відбувається при виконанні зміненого
циклу, і демонструє, як доречне використання echo може
допомагати у програмуванні.
Конвеєр Неллі: Обробка файлів
Тепер Неллі готова обробити свої файли даних, використовуючи
goostats.sh — скрипт командної оболонки, який був написаний
її керівником. Він розраховує деякі статистичні параметри для зразка
білка, і приймає два аргументи:
- вхідний файл (що містить необроблені дані)
- вихідний файл (для збереження обчисленої статистики)
Оскільки вона все ще вчиться користуватися терміналом, вона вирішує
будувати потрібну послідовність команд поступово. Спершу потрібно
впевнитися, що було обрано правильні вхідні файли — ті, назви яких
закінчуються на ‘A’ або ‘B’, але не на ‘Z’. Переходячи до каталогу
north-pacific-gyre, Неллі вводить:
BASH
$ cd
$ cd Desktop/shell-lesson-data/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’ до назви
кожного вхідного файлу здається простим рішенням, тому вона модифікує
свій цикл відповідним чином:
ВИХІД
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, але тепер
переконалася, що її скрипт зможе обрати потрібні файли та створити
правильні вихідні імена для результатів.
Постійне повторення одних і тих самих команд уже починає набридати, і Неллі боїться помилитися, тому замість цього вона натискає клавішу ↑. У результаті оболонка повторно показує весь цикл в один рядок (використовуючи крапку з комою для розділення його частин):
Використовуючи ←, Неллі переходить до команди
echo та змінює її на bash goostats.sh:
Коли вона натискає Enter, термінал виконує змінену команду. Однак, здається, нічого не відбувається — немає жодного виводу. Через деякий час Неллі розуміє, що оскільки її скрипт більше нічого не виводить на екран, вона не має жодного уявлення як швидко він виконується і чи працює взагалі. Вона перериває виконання команди, натискаючи Ctrl+C, та за допомогою клавіші↑ повторно викликає її та редагує, щоб вона виглядала так:
BASH
$ 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
NENE01736A.txt
NENE01751A.txt
...
Помножив 1518 файлів на 5 секунд і поділивши результат на 60, Неллі
підраховує що її скрипт буде виконуватися близько двох годин. Для
завершення перевірки вона відкриває нове вікно терміналу, переходить до
каталогу north-pacific-gyre та використовує команду
cat stats-NENE01729B.txt. для перегляду одного зі створених
файлів. Оскільки все працює як слід, Неллі задоволено йде зробити каву
та провести час із книжкою.
Хто знає історію, той може її повторити
Ще один спосіб відтворити попередні дії — це команда
history, яка показує перелік останніх кількох сотень
виконаних команд. Після цього можна ввести !123 (де ‘123’
замінено на номер відповідної команди), щоб запустити її знову.
Наприклад, якщо Неллі набере наступне:
ВИХІД
456 for datafile in NENE*A.txt NENE*B.txt; do echo $datafile stats-$datafile; done
457 for datafile in NENE*A.txt NENE*B.txt; do echo $datafile stats-$datafile; done
458 for datafile in NENE*A.txt NENE*B.txt; do bash goostats.sh $datafile stats-$datafile; done
459 for datafile in NENE*A.txt NENE*B.txt; do echo $datafile; bash goostats.sh $datafile
stats-$datafile; done
460 history | tail -n 5
то вона може перезапустити goostats.sh просто набравши
!459.
Інші корисні команди для роботи з історією
Окрім history, існує низка скорочень, які дозволяють
швидше переглядати та викликати попередні команди.
- Ctrl+R переходить у режим ‘зворотного пошуку’, який дозволяє знайти останню команду завдяки частині тексту. Натисніть Ctrl+R ще один або кілька разів для перегляду більш ранніх збігів. Після цього можна за допомогою стрілок вліво та вправо переміститися по знайденому рядку, відредагувати його та натиснути Return, щоб виконати команду.
-
!!повертає безпосередньо попередню команду (деякі з вас можуть знайти це більш зручним, ніж використання ↑) -
!$повертає останнє слово останньої команди. Ця можливість корисна частіше, ніж здається: післяbash goostats.sh NENE01729B.txt stats-NENE01729B.txtможна просто набратиless !$для перегляду файлуstats-NENE01729B.txt, що швидше, ніж шукати попередню команду зі ↑ та змінювати її вручну.
Пробний запуск
Цикл — це спосіб виконати багато дій одночасно — або зробити багато
помилок одразу, якщо він робить щось не те. Один зі способів перевірити
роботу циклу - замінити фактичне виконання команд на
echo.
Припустимо, ми хочемо переглянути команди, які виконає наступний цикл, без виконання цих команд:
У чому різниця між двома наведеними нижче циклами, і який із них слід запустити?
Нам потрібен саме другий варіант циклу. Він виводить на екран увесь
текст у лапках, підставивши назву змінної циклу, оскільки перед нею
стоїть знак долара. Крім того, ця команда не створює і не змінює файл
all.pdb, оскільки оператор >>
розглядається як частина рядка, а не як інструкція перенаправлення
виводу.
Перша версія додає вивід команди echo cat $datafile до
файлу all.pdb. Цей файл міститиме лише список команд типу
cat cubane.pdb, cat ethane.pdb,
cat methane.pdb тощо.
Спробуйте обидві версії самостійно, щоб побачити результат!
Обов’язково відкрийте файл all.pdb, щоб переглянути його
вміст.
Вкладені цикли
Припустімо, що ми хочемо створити систему каталогів для впорядкування певних експериментів, у яких досліджується швидкість реакцій із різними хімічними сполуками та температурами. Яким буде результат виконання наступного коду:
Ми маємо вкладений цикл, тобто такий, що міститься в іншому циклі,
тому для кожного значення змінної species у зовнішньому
циклі внутрішній цикл (вкладений цикл) перебирає список температур і
створює новий каталог для кожної комбінації.
Спробуйте запустити цей код самостійно, щоб побачити, які каталоги буде створено!
- Цикл
forповторює команди один раз для кожного елемента списку. - У кожному циклі
forвикористовується змінна, що вказує на поточний об’єкт, з яким він зараз працює. - Використовуйте
$nameдля підстановки змінної (тобто отримання її значення). Також можна використовувати${name}. - Не варто використовувати пробіли, лапки чи символи підстановки, такі як ‘*’ або ‘?’, у назвах файлів, адже це може призвести до помилок під час роботи зі змінними.
- Надавайте файлам послідовні імена, які можна легко описати за допомогою шаблонів, щоб полегшити їх вибір для циклів.
- Щоб швидко знайти й повторити попередню команду, скористайтеся клавішею зі стрілкою вгору — це дозволяє редагувати та виконувати її без повторного введення.
- Використовуйте Ctrl+R для пошуку попередньо введених команд.
- Використовуйте команду
history, щоб побачити перелік останніх команд; також застосовуйте![номер]для повторення команди за її номером.