Пошук речей

Огляд

Викладання: 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:
  1. grep "of" haiku.txt
  2. grep -E "of" haiku.txt
  3. grep -w "of" haiku.txt
  4. 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.

  1. find creatures -name "*.dat" | grep -v unicorn.
  2. find creatures -name *.dat | grep -v unicorn
  3. grep -v "unicorn" $(find creatures -name "*.dat")
  4. Нічого з перерахованого вище.

Розв’язання

Варіант 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

Розв’язання

  1. Рекурсивно знайти всі файли з розширенням .dat у поточному каталозі
  2. Підрахувати кількість рядків у кожному з цих файлів
  3. Відсортувати вивід з пункту 2. за числовим значенням

[workshop-repo]: [yaml]: http://yaml.org/

Ключові моменти

  • find шукає файли з певними властивостями, які відповідають шаблонам.

  • grep вибирає рядки з файлів, які відповідають шаблонам.

  • --help - це опція, яка підтримується багатьма командами bash і програмами, які можна запустити з bash, для відображення додаткової інформації про те, як користуватися цими командами або програмами.

  • man [команда] показує сторінку посібника для даної команди.

  • $([команда]) вставляє вивід команди на місце.