Цикли
Останнє оновлення 2025-10-29 | Редагувати цю сторінку
Огляд
Питання
- Як виконати одні й ті ж дії над різними файлами?
 
Цілі
- Написати цикл, який застосовує одну або декілька команд окремо до кожного файлу в наборі файлів.
 - Простежити, яких значень набуває змінна циклу під час виконання циклу.
 - Пояснити різницю між ім’ям змінної та її значенням.
 - Пояснити, чому в іменах файлів не можна використовувати пробіли та деякі розділові знаки.
 - Продемонструвати, як побачити, які команди були виконані останнім часом.
 - Перезапустити нещодавно виконані команди без повторного введення.
 
Цикли - це конструкції програмування, які дозволяють повторити команду або набір команд для кожного елемента у списку. Таким чином, автоматизація виконання повторюваних дій суттєво підвищує ефективність. Подібно до шаблонів і автодоповнення, цикли допомагають зменшити кількість вручну набраного тексту (а отже, зменшують кількість помилок).
Припустимо, у нас є кілька сотень файлів даних, які містять
інформацію про геноми та мають імена на кшталт
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 — скрипт командної оболонки, який був написаний
її керівником. Він розраховує деякі статистичні параметри для зразка
білка, і приймає два аргументи:
- an input file (containing the raw data)
 - an output file (to store the calculated statistics)
 
Since she’s still learning how to use the shell, she decides to build
up the required commands in stages. Her first step is to make sure that
she can select the right input files — remember, these are ones whose
names end in ‘A’ or ‘B’, rather than ‘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
Her next step is to decide what to call the files that the
goostats.sh analysis program will create. Prefixing each
input file’s name with ‘stats’ seems simple, so she modifies her loop to
do that:
ВИХІД
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
She hasn’t actually run goostats.sh yet, but now she’s
sure she can select the right files and generate the right output
filenames.
Typing in commands over and over again is becoming tedious, though, and Nelle is worried about making mistakes, so instead of re-entering her loop, she presses ↑. In response, the shell redisplays the whole loop on one line (using semi-colons to separate the pieces):
Using the ←, Nelle navigates to the echo
command and changes it to bash goostats.sh:
When she presses Enter, the shell runs the modified command. However, nothing appears to happen — there is no output. After a moment, Nelle realizes that since her script doesn’t print anything to the screen any longer, she has no idea whether it is running, much less how quickly. She kills the running command by typing Ctrl+C, uses ↑ to repeat the command, and edits it to read:
BASH
$ for datafile in NENE*A.txt NENE*B.txt; do echo $datafile;
bash goostats.sh $datafile stats-$datafile; done
Beginning and End
We can move to the beginning of a line in the shell by typing Ctrl+A and to the end using Ctrl+E.
When she runs her program now, it produces one line of output every five seconds or so:
ВИХІД
NENE01729A.txt
NENE01736A.txt
NENE01751A.txt
...
1518 times 5 seconds, divided by 60, tells her that her script will
take about two hours to run. As a final check, she opens another
terminal window, goes into north-pacific-gyre, and uses
cat stats-NENE01729B.txt to examine one of the output
files. It looks good, so she decides to get some coffee and catch up on
her reading.
Those Who Know History Can Choose to Repeat It
Another way to repeat previous work is to use the
history command to get a list of the last few hundred
commands that have been executed, and then to use !123
(where ‘123’ is replaced by the command number) to repeat one of those
commands. Наприклад, якщо Неллі набере наступне:
ВИХІД
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
then she can re-run goostats.sh on the files simply by
typing !459.
Other History Commands
There are a number of other shortcut commands for getting at the history.
- Ctrl+R enters a history search mode ‘reverse-i-search’ and finds the most recent command in your history that matches the text you enter next. Press Ctrl+R one or more additional times to search for earlier matches. You can then use the left and right arrow keys to choose that line and edit it then hit Return to run the command.
 - 
!!повертає безпосередньо попередню команду (ви можете знайти це більш зручним, ніж використання ↑) - 
!$повертає останнє слово останньої команди. That’s useful more often than you might expect: afterbash goostats.sh NENE01729B.txt stats-NENE01729B.txt, you can typeless !$to look at the filestats-NENE01729B.txt, which is quicker than doing ↑ and editing the command-line. 
Doing a Dry Run
A loop is a way to do many things at once — or to make many mistakes
at once if it does the wrong thing. One way to check what a loop
would do is to echo the commands it would run
instead of actually running them.
Suppose we want to preview the commands the following loop will execute without actually running those commands:
What is the difference between the two loops below, and which one would we want to run?
The second version is the one we want to run. This prints to screen
everything enclosed in the quote marks, expanding the loop variable name
because we have prefixed it with a dollar sign. It also does
not modify nor create the file all.pdb, as the
>> is treated literally as part of a string rather
than as a redirection instruction.
The first version appends the output from the command
echo cat $datafile to the file, all.pdb. This
file will just contain the list; cat cubane.pdb,
cat ethane.pdb, cat methane.pdb etc.
Try both versions for yourself to see the output! Be sure to open the
all.pdb file to view its contents.
Nested Loops
Suppose we want to set up a directory structure to organize some experiments measuring reaction rate constants with different compounds and different temperatures. Яким буде результат виконання наступного коду:
Ми маємо вкладений цикл, тобто такий, що міститься в іншому циклі,
тому для кожного значення змінної species у зовнішньому
циклі внутрішній цикл (вкладений цикл) перебирає список температур і
створює новий каталог для кожної комбінації.
Try running the code for yourself to see which directories are created!
- Цикл 
forповторює команди один раз для кожного елемента списку. - Every 
forloop needs a variable to refer to the thing it is currently operating on. - Use 
$nameto expand a variable (i.e., get its value). Також можна використовувати${name}. - Do not use spaces, quotes, or wildcard characters such as ‘*’ or ‘?’ in filenames, as it complicates variable expansion.
 - Give files consistent names that are easy to match with wildcard patterns to make it easy to select them for looping.
 - Use the up-arrow key to scroll up through previous commands to edit and repeat them.
 - Використовуйте Ctrl+R для пошуку попередньо введених команд.
 - Use 
historyto display recent commands, and![number]to repeat a command by number.