Пошук речей
Огляд
Викладання: 25 хв
Вправи: 20 хвПитання
Як я можу знайти файли?
Як знайти щось у файлах?
Цілі
Використати
grep
для виділення рядків з текстових файлів, які відповідають простим шаблонам.Використати
find
для пошуку файлів і каталогів, назви яких відповідають простим шаблонам.Використати вихідні дані однієї команди як аргумент(и) командного рядка для іншої команди.
Пояснити, що мається на увазі під ‘текстовими’ та ‘бінарними’ файлами, і чому багато поширених інструментів погано працюють з останніми.
Так само, як багато хто з нас зараз використовує ‘Google’ як дієслово, що означає ‘шукати’, Unix-програмісти часто використовують слово ‘grep’. ‘grep’ - це скорочення від ‘global/regular expression/print’ (з англ. ‘глобальний/регулярний вираз/друк’), поширеної послідовності операцій у ранніх текстових редакторах Unix. Це також назва дуже корисної програми командного рядка.
grep
шукає і виводить рядки у файлах, які відповідають шаблону.
У нашому прикладі
ми використаємо файл, який містить три хайку, взяті з
конкурсу 1998 року
в журналі Salon (авторство належить Біллу Торкасо (Bill Torcaso), Говарду Кордеру (Howard Korder) та
Маргарет Сігал (Margaret Segall), відповідно. Див.
Haiku Error Messages в архіві
[Сторінка 1] (https://web.archive.org/web/20000310061355/http://www.salon.com/21st/chal/1998/02/10chal2.html)
та
Сторінка 2
.). Для цього набору прикладів
ми будемо працювати у підкаталозі writing:
$ cd
$ cd Desktop/shell-lesson-data/exercise-data/writing
$ cat haiku.txt
The Tao that is seen
Is not the true Tao, until
You bring fresh toner.
With searching comes loss
and the presence of absence:
"My Thesis" not found.
Yesterday it worked
Today it is not working
Software is like that.
Знайдемо рядки, які містять слово ‘not’:
$ grep not haiku.txt
Is not the true Tao, until
"My Thesis" not found
Today it is not working
Тут not
- це шаблон, який ми шукаємо.
Команда grep шукає у файлі збіги із заданим шаблоном.
Щоб скористатися нею, введіть grep
, потім шаблон, який ми шукаємо, і нарешті
ім’я файлу (або файлів), у якому (у яких) ми шукаємо.
У вихідний файл виводяться три рядки, які містять літери ‘not’.
За замовчуванням grep шукає шаблон з урахуванням регістру. Крім того, вибраний нами шаблон пошуку не обов’язково має бути повним словом, як ми побачимо у наступному прикладі.
Давайте відшукаємо шаблон: ‘The’.
$ grep The haiku.txt
The Tao that is seen
"My Thesis" not found.
Цього разу буде виведено два рядки, що містять літери ‘The’, один з яких містить наш шаблон пошуку у більшому слові ‘Thesis’.
Щоб обмежити збіги до рядків, що містять слово ‘The’ як таке,
ми можемо надати grep
опцію -w
.
Це обмежить збіги межами слів.
Пізніше у цьому уроці ми також побачимо, як можна змінити поведінку пошуку grep з урахуванням чутливості до регістру.
$ grep -w The haiku.txt
The Tao that is seen
Зверніть увагу, що ‘межа слова’ включає початок і кінець рядка, тобто не
лише літери, оточені пробілами.
Іноді ми хочемо
шукати не окреме слово, а фразу. Це також легко зробити за допомогою
grep
, взявши фразу в лапки.
$ grep -w "is not" haiku.txt
Today it is not working
Ми вже бачили, що не обов’язково брати в лапки окремі слова, але корисно використовувати лапки під час пошуку кількох слів. Це також допомагає легше відрізнити пошуковий термін або фразу від файлу, в якому відбувається пошук. Ми будемо використовувати лапки в решті прикладів.
Ще одна корисна опція - -n
, яка нумерує рядки, що збігаються:
$ grep -n "it" haiku.txt
5:With searching comes loss
9:Yesterday it worked
10:Today it is not working
Ми бачимо, що рядки 5, 9 і 10 містять літери ‘it’.
Ми можемо комбінувати опції (тобто прапорці) так само, як і в інших командах Unix.
Наприклад, давайте знайдемо рядки, які містять слово ‘the’.
Ми можемо комбінувати опцію -w
, щоб знайти рядки, які містять слово ‘the’
і -n
, щоб пронумерувати рядки, що збігаються:
$ grep -n -w "the" haiku.txt
2:Is not the true Tao, until
6:and the presence of absence:
Тепер ми хочемо використати опцію -i
, щоб зробити наш пошук нечутливим до регістру:
$ grep -n -w -i "the" haiku.txt
1:The Tao that is seen
2:Is not the true Tao, until
6:and the presence of absence:
Тепер ми хочемо використати опцію -v
для інвертування пошуку, тобто вивести
рядки, які не містять слова ‘the’.
$ grep -n -w -v "the" haiku.txt
1:The Tao that is seen
3:You bring fresh toner.
4:
5:With searching comes loss
7:"My Thesis" not found.
8:
9:Yesterday it worked
10:Today it is not working
11:Software is like that.
Якщо ми використовуємо опцію -r
(recursive, з англ. - рекурсивний),
grep
може шукати шаблон рекурсивно через набір файлів у підкаталогах.
Давайте виконаємо рекурсивний пошук Yesterday
у каталозі shell-lesson-data/exercise-data/writing
:
$ grep -r Yesterday .
./LittleWomen.txt:"Yesterday, when Aunt was asleep and I was trying to be as still as a
./LittleWomen.txt:Yesterday at dinner, when an Austrian officer stared at us and then
./LittleWomen.txt:Yesterday was a quiet day spent in teaching, sewing, and writing in my
./haiku.txt:Yesterday it worked
У grep
є багато інших варіантів. Щоб дізнатися, які саме, ми можемо набрати їх:
$ grep --help
Використання: grep [ПАРАМЕТР]… ШАБЛОНИ [ФАЙЛ]… Шукати ШАБЛОНИ у кожному ФАЙЛі. Приклад: grep -i ‘hello world’ menu.h main.c Запис ШАБЛОНИ може містити декілька шаблонів, які відокремлено символами нового рядка.
Вибір за взірцем та інтерпретація:
-E, –extended-regexp ШАБЛОНИ є розширеним формальним виразом
-F, –fixed-strings ШАБЛОНИ є набором рядків
-G, –basic-regexp ШАБЛОНИ є звичайними формальними виразами
-P, –perl-regexp ШАБЛОНИ є формальними виразами Perl
-e, –regexp=ШАБЛОНИ використовувати ШАБЛОНИ для встановлення відповіднос ті
-f, –file=ФАЙЛ взяти ШАБЛОНИ із ФАЙЛа
-i, –ignore-case ігнорувати регістр літер у шаблонах і даних
–no-ignore-case не ігнорувати регістр літер (типова поведінка)
-w, –word-regexp шукати лише цілі слова
-x, –line-regexp шукати лише цілі рядки
-z, –null-data рядки даних закінчуються байтом “0”, а не символом
кінця рядка (
)
Використання
grep
Яка команда призведе до наступного виводу:
and the presence of absence:
grep "of" haiku.txt
grep -E "of" haiku.txt
grep -w "of" haiku.txt
grep -i "of" haiku.txt
Розв’язання
Правильна відповідь 3, тому що опція
-w
шукає збіги лише між цілими словами. Інші варіанти також шукатимуть збіги зі словом ‘of’, якщо воно є частиною іншого слова.
Символи підстановки
Проте справжня сила
grep
полягає не у його опціях, а у тому, що шаблони можуть містити підстановочні символи. (Технічна назва для цього - регулярні вирази, для чого скорочення ‘re’ у ‘grep’). Регулярні вирази є складними і потужніми; якщо ви хочете робити складні пошуки, будь ласка, перегляньте урок на [нашому сайті] (http://v4.software-carpentry.org/regexp/index.html). Для початку ми можемо знайти рядки, які містять “o” у другій позиції, наприклад, так:$ grep -E "^.o" haiku.txt
You bring fresh toner. Today it is not working Software is like that.
Ми використовуємо опцію
-E
і беремо шаблон у лапки, щоб запобігти спробам терміналу інтерпретувати його. (Якщо шаблон містить*
, наприклад, оболонка спробує розгорнути його перед запускомgrep
). Символ^
у шаблоні прив’язує збіг до початку рядка. Символ.
відповідає одному символу (подібно до?
у командному рядку), тоді якo
відповідає власне ‘o’.
Відстеження видів
Лея має кілька сотень файлів даних, збережених в одному каталозі, кожен з яких відформатовано таким чином:
2012-11-05,deer,5 2012-11-05,rabbit,22 2012-11-05,raccoon,7 2012-11-06,rabbit,19 2012-11-06,deer,2 2012-11-06,fox,4 2012-11-07,rabbit,16 2012-11-07,bear,1
Вона хоче написати командний скрипт, який приймає вид як перший аргумент командного рядка і каталог як другий аргумент. Скрипт повинен повернути один файл з назвою
<вид>.txt', який містить список дат і кількість особин цього виду, які були помічені у кожну дату. Наприклад, використовуючи дані, показані вище,
rabbit.txt` буде містити:2012-11-05,22 2012-11-06,19 2012-11-07,16
Нижче кожен рядок містить окрему команду або канал. Впорядкуйте їх послідовність в одну команду, щоб досягти мети Леї:
cut -d : -f 2 > | grep -w $1 -r $2 | $1.txt cut -d , -f 1,3
Підказка: використовуйте
man grep
для пошуку того, як виконувати рекурсивний пошук тексту у каталозі іman cut
для виділення декількох полів у рядку.Приклад такого файлу наведено в
shell-lesson-data/exercise-data/animal-counts/animals.csv
Розв’язання
grep -w $1 -r $2 | cut -d : -f 2 | cut -d , -f 1,3 > $1.txt
Насправді, ви можете поміняти місцями порядок двох команд вирізання, і це все одно буде працювати. У командному рядку спробуйте змінити порядок команд вирізання і подивіться на вивід на кожному кроці, щоб зрозуміти, чому це відбувається.
Ви можете визвати наведений вище скрипт так:
$ bash count-species.sh bear .
Маленькі жінки
Ви з другом щойно закінчили читати “Маленьких жінок” Луїзи Мей Елкотт і сперечаєтеся. З чотирьох сестер у книзі, Джо, Мег, Бет та Емі, ваш друг вважає, що про Джо найчастіше згадується. Ви, однак, впевнені, що це була Емі. На щастя, у вас є файл
LittleWomen.txt
, який містить повний текст роману (shell-lesson-data/exercise-data/writing/LittleWomen.txt
). Використовуючи циклfor
, як ви виведете в таблицю кількість разів, коли кожна з чотирьох сестер згадується?Підказка: одне рішення може використовувати команди
grep
,wc
та|
, а інше може використовувати опціїgrep
. Часто існує більше ніж один спосіб розв’язання задачі програмування, тому конкретний спосіб зазвичай обирається на основі комбінації отримання правильного результату, елегантності, читабельності та швидкості.Розв’язання
for sis in Jo Meg Beth Amy do echo $sis: grep -ow $sis LittleWomen.txt | wc -l done
Альтернативне, трохи гірше рішення:
for sis in Jo Meg Beth Amy do echo $sis: grep -ocw $sis LittleWomen.txt done
Це рішення є гіршим, оскільки
grep -c
повідомляє лише про кількість знайдених рядків. Загальна кількість збігів, отриманих за допомогою цього методу, буде меншою, якщо на один рядок припадає більше, ніж один збіг.Уважні спостерігачі могли помітити, що імена персонажів іноді з’являються з великої літери у назвах розділів (наприклад, “MEG GOES TO VANITY FAIR”). Якщо ви хочете врахувати і ці випадки, ви можете додати опцію
-i
для нечутливості до регістру. (хоча в цьому випадку це не впливає на відповідь, яка сестра згадується найчастіше).
У той час як grep
знаходить рядки у файлах,
команда find
знаходить самі файли.
Знову ж таки,
у неї є багато опцій;
щоб показати, як працюють найпростіші з них, ми скористаємося деревом каталогів shell-lesson-data/exercise-data
,
що наведено нижче.
.
├── animal-counts/
│ └── animals.csv
├── creatures/
│ ├── basilisk.dat
│ ├── minotaur.dat
│ └── unicorn.dat
├── numbers.txt
├── proteins/
│ ├── cubane.pdb
│ ├── ethane.pdb
│ ├── methane.pdb
│ ├── octane.pdb
│ ├── pentane.pdb
│ └── propane.pdb
└── writing/
├── haiku.txt
└── LittleWomen.txt
Каталог exercise-data
містить один файл numbers.txt
та чотири каталоги:
animal-counts
, creatures
, proteins
і writing
, які містять різні файли.
Для нашої першої команди
давайте виконаємо find .
(не забудьте запустити цю команду з каталогу shell-lesson-data/exercise-data
).
$ find .
.
./writing
./writing/LittleWomen.txt
./writing/haiku.txt
./creatures
./creatures/basilisk.dat
./creatures/unicorn.dat
./creatures/minotaur.dat
./animal-counts
./animal-counts/animals.csv
./numbers.txt
./proteins
./proteins/ethane.pdb
./proteins/propane.pdb
./proteins/octane.pdb
./proteins/pentane.pdb
./proteins/methane.pdb
./proteins/cubane.pdb
Як завжди, символ .
сам по собі означає поточний робочий каталог,
з якого ми хочемо почати пошук.
Результатом роботи find
буде перелік імен усіх файлів та каталогів
у поточному робочому каталозі.
Спочатку це може здатися марним, але find
має багато можливостей
для фільтрації результатів, і у цьому уроці ми розглянемо деякі
з них.
Перший варіант у нашому списку
-type d
, що означає ‘елементи, які є каталогами’.
Звісно, команда find
виведе назви п’яти каталогів (включно з .
):
$ find . -type d
.
./writing
./creatures
./animal-counts
./proteins
Зверніть увагу, що об’єкти, які знаходить find
, не перераховано у певному порядку.
Якщо ми змінимо -type d
на -type f
,
ми отримаємо список усіх файлів:
$ find . -type f
./writing/LittleWomen.txt
./writing/haiku.txt
./creatures/basilisk.dat
./creatures/unicorn.dat
./creatures/minotaur.dat
./animal-counts/animals.csv
./numbers.txt
./proteins/ethane.pdb
./proteins/propane.pdb
./proteins/octane.pdb
./proteins/pentane.pdb
./proteins/methane.pdb
./proteins/cubane.pdb
Тепер спробуємо зіставити за іменами:
$ find . -name *.txt
./numbers.txt
Ми очікували, що будуть знайдені усі текстові файли,
але було виведено лише ./numbers.txt
.
Проблема полягає у тому, що перед виконанням команд оболонка розширює символи підстановки, такі як *
.
Оскільки *.txt
у поточному каталозі розширюється до ./numbers.txt
,
команда, яку ми виконали, була такою:
$ find . -name numbers.txt
Команда find
зробила те, що ми просили; ми просто просили не те, що треба.
Щоб отримати те, що ми хочемо,
давайте зробимо те, що ми зробили з grep
:
візьмемо *.txt
у лапки, щоб оболонка не змогла розкрити шаблон *
.
Таким чином,
find
отримає шаблон *.txt
, а не розширене ім’я файлу numbers.txt
:
$ find . -name "*.txt"
./writing/LittleWomen.txt
./writing/haiku.txt
./numbers.txt
Порівняння результатів
ls
таfind
ls
іfind
можна змусити робити подібні речі за допомогою правильних параметрів, але за звичайних обставин,ls
перелічує все, що може, тоді якfind
шукає речі з певними властивостями і показує їх.
Як ми вже говорили раніше,
сила командного рядка полягає у комбінуванні інструментів.
Ми бачили, як це можна зробити з каналами;
давайте подивимося на іншу техніку.
Як ми щойно бачили,
find . -name "*.txt"
дає нам список усіх текстових файлів у поточному каталозі або нижче.
Як ми можемо поєднати це з wc -l
, щоб порахувати рядки у всіх цих файлах?
Найпростіший спосіб - помістити команду find
всередину $()
:
$ wc -l $(find . -name "*.txt")
21022 ./writing/LittleWomen.txt
11 ./writing/haiku.txt
5 ./numbers.txt
21038 total
Коли термінал виконуватиме цю команду,
перше, що він зробить, це виконає все, що міститься у виразі $()
.
Потім він замінить вираз $()
на результати виконання цієї команди.
Оскільки результатом команди find
є три файли ./writing/LittleWomen.txt
,
./writing/haiku.txt
і ./numbers.txt
, термінал сконструює команду:
$ wc -l ./writing/LittleWomen.txt ./writing/haiku.txt ./numbers.txt
що ми і хотіли.
Таке розширення виконує саме те, що робить термінал, коли розширює такі символи, як *
та ?
,
але дозволяє нам використовувати будь-яку команду як власну “шаблонну”.
Дуже часто find
і grep
використовують разом.
Перша знаходить файли, які відповідають певному шаблону;
другий шукає рядки всередині цих файлів, які відповідають іншому шаблону.
Наприклад, ми можемо знайти txt-файли, які містять слово “searching”
шляхом пошуку рядка ‘searching’ у всіх файлах .txt
у поточному каталозі:
$ grep "searching" $(find . -name "*.txt")
./writing/LittleWomen.txt:sitting on the top step, affected to be searching for her book, but was
./writing/haiku.txt:With searching comes loss
Порівняння та віднімання
Параметр
-v
доgrep
інвертує відповідність шаблону, щоб виводилися лише рядки, які не збігаються з шаблоном. Враховуючи це, яка з наступних команд знайде усі файли .dat уcreatures
окрімunicorn.dat
? Після того, як ви обміркуєте свою відповідь, ви можете протестувати команди у каталогуshell-lesson-data/exercise-data
.
find creatures -name "*.dat" | grep -v unicorn
.find creatures -name *.dat | grep -v unicorn
grep -v "unicorn" $(find creatures -name "*.dat")
- Нічого з перерахованого вище.
Розв’язання
Варіант 1 правильний. Взяття виразу збігу у лапки запобігає тому, щоб термінал розгортав його перед передачею команді
find
.Варіант 2 також працює у цьому випадку, оскільки термінал намагається розгорнути
*.dat
. але у поточному каталозі немає файлів*.dat
, тому вираз підстановки буде передано доfind
. Вперше ми зіткнулися з цим у [епізоді 3] (../03-create/index.html/#підстановочні-символи).Варіант 3 є неправильним, оскільки він шукає у вмісті файлів рядки, які не збігаються зі словом ‘unicorn’, а не імена файлів.
Двійкові файли
Ми зосередилися виключно на пошуку шаблонів у текстових файлах. Що робити, якщо ваші дані зберігаються у вигляді зображень, баз даних або в іншому форматі?
Кілька утиліт розширюють
grep
для роботи з деякими нетекстовими форматами. Але більш узагальненим підходом є перетворення даних у текст, або вилучення текстових елементів з даних. З одного боку, це робить прості речі легкими для виконання. З іншого боку, складні речі, як правило, неможливі. Наприклад, досить легко написати програму, яка буде витягувати розміри X та Y з файлів зображень, щобgrep
міг погратися з ними, але як би ви написати щось для пошуку значень у електронній таблиці, клітинки якої містять формули?Останній варіант - визнати, що термінал і обробка тексту мають свої межі, і використовувати іншу мову програмування. Коли прийде час це зробити, не будьте надто суворими до терміналу: багато сучасних мов програмування запозичили багато ідей з нього, а наслідування також є найщирішою формою похвали.
Термінал Unix старіший за більшість людей, які нею користуються. Він проіснував так довго, тому що це одне з найпродуктивніших середовищ для програмування, коли-небудь створених - можливо, навіть найпродуктивнішим. Його синтаксис може бути незрозумілим, але люди, які його опанували, можуть експериментувати з різними командами в інтерактивному режимі, а потім використовувати те, чого вони навчилися, для автоматизації своєї роботи. Графічні інтерфейси користувача можуть бути простішими у використанні спочатку, але після того, як після опанування терміналу, продуктивність роботи в ньому буде неперевершеною. І як писав Альфред Норт Уайтхед у 1911 році: “Цивілізація розвивається шляхом збільшення кількості важливих операцій, які ми можемо виконувати, не замислюючись про них”.
Розуміння читання конвеєру
find
Напишіть короткий пояснювальний коментар до наступного скрипту термінала:
wc -l $(find . -name "*.dat") | sort -n
Розв’язання
- Рекурсивно знайти всі файли з розширенням
.dat
у поточному каталозі- Підрахувати кількість рядків у кожному з цих файлів
- Відсортувати вивід з пункту 2. за числовим значенням
[workshop-repo]: [yaml]: http://yaml.org/
Ключові моменти
find
шукає файли з певними властивостями, які відповідають шаблонам.
grep
вибирає рядки з файлів, які відповідають шаблонам.
--help
- це опція, яка підтримується багатьма командами bash і програмами, які можна запустити з bash, для відображення додаткової інформації про те, як користуватися цими командами або програмами.
man [команда]
показує сторінку посібника для даної команди.
$([команда])
вставляє вивід команди на місце.