Скрипти командної оболонки
Останнє оновлення 2025-11-21 | Редагувати цю сторінку
Огляд
Питання
- Як я можу зберігати та повторно використовувати команди?
Цілі
- Написати скрипт командної оболонки, який виконує одну або декілька команд для заздалегідь визначеного набору файлів.
- Запустити скрипт командної оболонки з термінала.
- Написати скрипт командної оболонки, який обробляє файли, вказані користувачем у командному рядку.
- Створити конвеєри, що використовують скрипти оболонки, створені вами та іншими користувачами.
Нарешті ми готові дізнатися, чому оболонка є таким потужним середовищем програмування. Ми збираємося зібрати та зберегти у файлах часто використовувані команди, щоб пізніше можна було виконати всі ці дії одночасно, набравши лише одну команду. З історичних причин скопійовані в файл команди зазвичай називають командним скриптом, скриптом командної оболонки, або скриптом терміналу, але не помиляйтеся: це насправді невеликі програми.
Написання командних скриптів не тільки прискорить вашу роботу, а й дозволить уникнути постійного повторного введення тих самих команд. Крім того, це підвищить якість вашої роботи (зменшить ризик друкарських помилок) і полегшить її відтворення. Якщо ви повернетеся до своєї роботи пізніше (або якщо хтось знайде вашу роботу і захоче її використати), відтворити ті ж результати можна буде просто запустивши скрипт, без потреби пригадувати та повторно вводити довгий перелік команд.
Спершу повернемося до каталогу alkanes/ і створимо новий
файл 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, ми
виконаємо наступну команду:
ВИХІД
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” замініть текст octane.pdb на спеціальну
змінну з назвою $1:
head -n 15 "$1" | tail -n 5
У командному скрипті змінна $1 позначає перший аргумент
командного рядка – перше ім’я файлу (або інший аргумент). Тепер ми
можемо запустити наш скрипт наступним чином для того ж самого файлу:
ВИХІД
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
або ж запустити з іншим файлом ось так, вказавши його імʼя подібним чином:
ВИХІД
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:
head -n "$2" "$1" | tail -n "$3"
Тепер ми можемо запустити:
ВИХІД
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
Достатньо змінити аргументи команди й скрипт працюватиме по-іншому:
ВИХІД
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 middle.sh filename end_line num_lines
head -n "$2" "$1" | tail -n "$3"
Коментар починається зі символу # і триває до кінця
рядка. Коментарі не впливають на виконання коду, але вони допомагають
користувачам (і вам самим у майбутньому) швидко зрозуміти та
використовувати скрипти. Єдине застереження полягає у тому, що кожного
разу, коли ви змінюєте скрипт, ви повинні перевіряти, що коментар все ще
правильний. Пояснення, яке спрямовує читача в неправильному напрямку,
гірше, ніж його відсутність.
Що робити, якщо ми хочемо обробити багато файлів в одному конвеєрі?
Наприклад, якщо ми хочемо відсортувати наші .pdb-файли за
довжиною, ми введемо:
оскільки wc -l виводить кількість рядків у файлах
(нагадаємо, що wc означає ‘підрахунок слів’ (word count), а
додавання опції -l натомість означає ‘підрахунок рядків’
(lines)) та sort -n використовує числове сортування. Ми
можемо записати цей конвеєр у файл, але тоді він сортуватиме лише список
.pdb файлів у поточному каталозі. Якщо ми хочемо отримати
відсортований список інших типів файлів, нам потрібно передати всі ці
імена у скрипт. У цьому випадку не можна скористатися змінними
$1, $2 тощо, бо ми не знаємо наперед, скільки
файлів потрібно обробити. Натомість ми використовуємо спеціальну змінну
$@, що означає, “Всі аргументи командного рядка передані
скрипту”. Необхідно взяти $@ у подвійні лапки, щоб
правильно обробляти аргументи з пробілами ("$@" є
спеціальним синтаксисом еквівалентним "$1"
"$2" …).
Ось приклад:
# Сортування файлів за їх розміром.
# Використання: bash sorted.sh one_or_more_filenames
wc -l "$@" | sort -n
ВИХІД
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, який сприймає
довільну кількість імен файлів за аргументи командного рядка. Він має
використовувати змінену версію попередньої команди для виведення списку
унікальних видів, які зустрічаються в кожному з цих файлів окремо.
Припустимо, ми щойно виконали низку команд, які зробили щось корисне — наприклад, створили графік, який ми плануємо використати у публікації. Ми хотіли б мати змогу відтворити графік пізніше, якщо знадобиться, тому збережемо команди у файл. Замість повторного введення (і ризику помилок), ми можемо зробити ось так:
Файл 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
After a moment’s work in an editor to remove the serial numbers on
the commands, and to remove the final line where we called the
history command, we have a completely accurate record of
how we created that figure.
Чому варто записувати команди в історію перед їх виконанням?
Якщо виконати команду:
останньою командою у файлі є сама команда history,
тобто, термінал додав history до журналу команд перед тим,
як фактично її виконав. Насправді термінал завжди додає команди
до журналу перед їх виконанням. Як ви гадаєте, чому він поводиться саме
так?
Якщо якась команда призводить до збою або зависання, знання того, яка саме команда це спричинила, допоможе з’ясувати причину проблеми. Якби команда записувалася лише після її виконання, ми б втратили запис останньої команди у разі збою.
На практиці, більшість людей створюють скрипти терміналу, запускаючи
команди в командному рядку кілька разів, щоб переконатися, що вони
роблять все правильно, а потім зберігають їх у файлі для подальшого
використання. This style of work allows people to recycle what they
discover about their data and their workflow with one call to
history and a bit of editing to clean up the output and
save it as a shell script.
Nelle’s Pipeline: Creating a Script
Nelle’s supervisor insisted that all her analytics must be reproducible. The easiest way to capture all the steps is in a script.
First we return to Nelle’s project directory:
She creates a file using nano …
…which contains the following:
BASH
# Calculate stats for data files.
for datafile in "$@"
do
echo $datafile
bash goostats.sh $datafile stats-$datafile
done
She saves this in a file called do-stats.sh so that she
can now re-do the first stage of her analysis by typing:
She can also do this:
so that the output is just the number of files processed rather than the names of the files that were processed.
One thing to note about Nelle’s script is that it lets the person running it decide what files to process. She could have written it as:
BASH
# Calculate stats for Site A and Site B data files.
for datafile in NENE*A.txt NENE*B.txt
do
echo $datafile
bash goostats.sh $datafile stats-$datafile
done
The advantage is that this always selects the right files: she
doesn’t have to remember to exclude the ‘Z’ files. The disadvantage is
that it always selects just those files — she can’t run it on
all files (including the ‘Z’ files), or on the ‘G’ or ‘H’ files her
colleagues in Antarctica are producing, without editing the script. If
she wanted to be more adventurous, she could modify her script to check
for command-line arguments, and use NENE*A.txt NENE*B.txt
if none were provided. Of course, this introduces another tradeoff
between flexibility and complexity.
Variables in Shell Scripts
Уявіть, що у каталозі alkanes у вас є скрипт з назвою
script.sh, який містить наступні команди:
Перебуваючи у каталозі alkanes, ви вводите наступну
команду:
Які з наведених нижче результатів ви очікуєте побачити?
- Усі рядки між першим та останнім рядками кожного файлу, що
закінчується на
.pdbу каталозіalkanes - Перший та останній рядок кожного файлу, що закінчується на
.pdbу каталозіalkanes - Перший та останній рядок кожного файлу в каталозі
alkanes - Помилку через лапки навколо
*.pdb
The correct answer is 2.
The special variables $1, $2 and
$3 represent the command line arguments given to the
script, such that the commands run are:
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
The shell does not expand '*.pdb' because it is enclosed
by quote marks. Таким чином, першим аргументом скрипту є
'*.pdb', який буде розгорнуто у скрипті за допомогою
head і tail.
Find the Longest File With a Given Extension
Напишіть сценарій терміналу з назвою longest.sh, який
отримує в якості аргументів ім’я каталогу і розширення імені файлу як
аргументи, і виводить назву файлу з найбільшою кількістю рядків у цьому
каталозі з цим розширенням. For example:
виведе назву файлу .pdb у каталозі
shell-lesson-data/exercise-data/proteins, який має
найбільшу кількість рядків.
Feel free to test your script on another directory e.g.
BASH
# Shell script which takes two arguments:
# 1. a directory name
# 2. a file extension
# and prints the name of the file in that directory
# with the most lines which matches the file extension.
wc -l $1/*.$2 | sort -n | tail -n 2 | head -n 1
The first part of the pipeline, wc -l $1/*.$2 | sort -n,
counts the lines in each file and sorts them numerically (largest last).
When there’s more than one file, wc also outputs a final
summary line, giving the total number of lines across all
files. Ми використовуємо tail -n 2 | head -n 1, щоб
відкинути цей останній рядок.
With wc -l $1/*.$2 | sort -n | tail -n 1 we’ll see the
final summary line: we can build our pipeline up in pieces to be sure we
understand the output.
Script Reading Comprehension
For this question, consider the
shell-lesson-data/exercise-data/alkanes directory once
again. This contains a number of .pdb files in addition to
any other files you may have created. Explain what each of the following
three scripts would do when run as bash script1.sh *.pdb,
bash script2.sh *.pdb, and
bash script3.sh *.pdb respectively.
In each case, the shell expands the wildcard in *.pdb
before passing the resulting list of file names as arguments to the
script.
Script 1 would print out a list of all files containing a dot in their name. The arguments passed to the script are not actually used anywhere in the script.
Script 2 would print the contents of the first 3 files with a
.pdb file extension. $1, $2, and
$3 refer to the first, second, and third argument
respectively.
Script 3 would print all the arguments to the script (i.e. all the
.pdb files), followed by .pdb. $@
refers to all the arguments given to a shell script.
ВИХІД
cubane.pdb ethane.pdb methane.pdb octane.pdb pentane.pdb propane.pdb.pdb
Debugging Scripts
Suppose you have saved the following script in a file called
do-errors.sh in Nelle’s north-pacific-gyre
directory:
BASH
# Calculate stats for data files.
for datafile in "$@"
do
echo $datfile
bash goostats.sh $datafile stats-$datafile
done
Якщо ви запускаєте його з каталогу
north-pacific-gyre:
програма нічого не виводить. Щоб з’ясувати причину, перезапустіть
скрипт з опцією -x:
Що показує вивід? Which line is responsible for the error?
The -x option causes bash to run in debug
mode. This prints out each command as it is run, which will help you to
locate errors. In this example, we can see that echo isn’t
printing anything. We have made a typo in the loop variable name, and
the variable datfile doesn’t exist, hence returning an
empty string.
- Save commands in files (usually called shell scripts) for re-use.
-
bash [ім'я файлу]виконує команди, збережені у відповідному файлі. -
$@refers to all of a shell script’s command-line arguments. -
$1,$2, etc., refer to the first command-line argument, the second command-line argument, etc. - Place variables in quotes if the values might have spaces in them.
- Letting users decide what files to process is more flexible and more consistent with built-in Unix commands.