forked from emulek/sed
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ch04s04.html
300 lines (286 loc) · 38.8 KB
/
ch04s04.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><title>Просмотр логов (базы данных в sed).</title><link rel="stylesheet" href="chs/default.css" type="text/css"><meta name="generator" content="DocBook XSL Stylesheets V1.75.1"><link rel="home" href="index.html" title="Краткий учебник по sed."><link rel="up" href="ch04.html" title="Глава 4. Примеры скриптов."><link rel="prev" href="ch04s03.html" title="Поиск одинаковых подстрок."><link rel="next" href="ch04s05.html" title="Подсчёт количества вхождений каждого символа в тексте."></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">Просмотр логов (базы данных в sed).</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="ch04s03.html">Пред.</a> </td><th width="60%" align="center">Глава 4. Примеры скриптов.</th><td width="20%" align="right"> <a accesskey="n" href="ch04s05.html">След.</a></td></tr></table><hr></div><div class="section" title="Просмотр логов (базы данных в sed)."><div class="titlepage"><div><div><h3 class="title"><a name="id2519065"></a>Просмотр логов (базы данных в sed).</h3></div></div></div><p>
Просматривать логи лениво и сильно утомляет, но - надо. Утомляет это всё тем, что там одна и та-же фигня, которая повторяется десять тысяч раз, и отличается только датой.
Неплохо-бы было отфильтровать одинаковые строчки, так что-бы печатались только первые, а остальные такие-же можно и не печатать - если это нас заинтересует, мы можем просмотреть только их командой <span class="command"><strong>grep</strong></span>, или той-же <span class="command"><strong>sed</strong></span>. Для решения этой задачи нам потребуется хранить все уникальные строчки, и перед выводом каждой проверять, не выводили-ли мы её прежде.
Наверное можно извратится используя утилиты <span class="command"><strong>sort</strong></span>, затем <span class="command"><strong>uniq</strong></span> для фильтрации одинаковых строк, ну а потом ещё раз <span class="command"><strong>sort</strong></span> для вывода всего этого в хронологическом порядке.
Думаю такой изврат будет работать ещё медленнее чем <span class="command"><strong>sed</strong></span> (хотя <span class="command"><strong>sed</strong></span> и не рассчитана на такое применение, фактически это работа с довольно большой базой данных!), впрочем, гуру линукса могут это и проверить и подтвердить (или опровергнуть) правильность моего решения.
Я конечно буду благодарен тому, кто это сделает ;)
</p><p>
При использовании <span class="command"><strong>sed</strong></span> у нас не возникает вопроса <span class="quote">«<span class="quote">где хранить временные данные</span>»</span>, у нас на всё про всё всего одна переменная, буфер удержания (hold space), и выбора особого нет, в данном случае и с форматом нашей БД не должно быть вопросов - команда загрузки в ОУ (<span class="command"><strong>H</strong></span>) пишет в конец области разделяя строчки символом <span class="quote">«<span class="quote">\n</span>»</span>.
я возьму кусочек своего <code class="filename">/var/log/syslog</code>, для примера:
</p><pre class="programlisting">Nov 8 13:08:29 localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov 8 13:08:29 localhost kernel: Current sd0b:00: sns = 70 3
Nov 8 13:08:29 localhost kernel: ASC=11 ASCQ= 0
Nov 8 13:08:29 localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov 8 13:08:29 localhost kernel: I/O error: dev 0b:00, sector 4046908
Nov 8 13:11:40 localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov 8 13:11:40 localhost kernel: Current sd0b:00: sns = 70 3
Nov 8 13:11:40 localhost kernel: ASC=11 ASCQ= 0
Nov 8 13:11:40 localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov 8 13:11:40 localhost kernel: I/O error: dev 0b:00, sector 3681164
Nov 8 13:12:10 localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov 8 13:12:10 localhost kernel: Current sd0b:00: sns = 70 3
Nov 8 13:12:10 localhost kernel: ASC=11 ASCQ= 0
Nov 8 13:12:10 localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov 8 13:12:10 localhost kernel: I/O error: dev 0b:00, sector 3690236
Nov 8 13:12:13 localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov 8 13:12:13 localhost kernel: Current sd0b:00: sns = 70 3
Nov 8 13:12:13 localhost kernel: ASC=11 ASCQ= 0
Nov 8 13:12:13 localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00</pre><p>
Там такой фигни много, это я царапанные CD пытался прочитать... Для начала загрузим нашу строчку в БД командой <span class="command"><strong>H</strong></span>, и извлечём БД в буфер командой <span class="command"><strong>x</strong></span>, а затем найдём новую строчку среди старых, с помощью выражения
</p><pre class="programlisting">/.*([^\n]+)\n.*\1$/</pre><p>
</p><p>
...Ага...
я тут подумал, и решил сделать по другому: записывать новую строчку не в конец базы, а в её начало.
У этого подхода есть три преимущества: во-первых <span class="command"><strong>sed</strong></span> просматривает строки слева-направо, и ей будет намного удобнее, если она <span class="emphasis"><em>УЖЕ</em></span> знает что искать - она сразу найдёт первую строчку (а что её искать? Понятно-же что она вначале, мы её туда сами записали!), и будет просто проверять все остальные на совпадение, найти две одинаковые строки, не зная ни то, где начало и той и другой, ни то, какой они длинны значительно медленнее.
Во-вторых, если записывать строки в начало базы, тогда старые строчки будут в конце базы, а известно, что строки чаще совпадают с более новыми записями (например, когда-то давно я поменял CPU, в логе это отражено конечно, и каждый раз, когда я запускаю комп, в лог пишется тип процессора, если проверять с конца, то сначала <span class="command"><strong>sed</strong></span> проверит новый со старым, и пойдёт проверять далее по базе, пока не найдёт новый тип, ежели начать с новых записей, то <span class="command"><strong>sed</strong></span> найдёт новую запись, и на этом успокоится).
В третьих, если строчка добавляется командой <span class="command"><strong>G</strong></span>, то при нахождении новой строки в базе вообще ничего делать: ведь печатать не надо да и базу менять - то-же, команда <span class="command"><strong>G</strong></span> как раз и не меняет базу, что нельзя сказать о команде <span class="command"><strong>H</strong></span>. С таким временем поиска скорость падает пропорционально квадрату уникальных строчек, что довольно быстро, потому скорость работы этого скрипта чрезвычайно критична.
</p><p>
<a name="t06_2"></a>
Кроме того, выражение для поиска в базе получилось намного сложнее выше приведённого: дело в том, что сравнивать надо не всю строку целиком, а только хвост после даты, а формат даты не так прост:
</p><pre class="programlisting">/(\S+\s+){3}/</pre><p>
я выкрутился так: я использую не одну, а две <span class="command"><strong>sed</strong></span>, конвейером. Первая форматирует дату таким образом:
</p><pre class="screen">$ sed -r 's/^(\S+\s+){3}/\0~/' sys.txt
Nov 8 13:08:29 ~localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov 8 13:08:29 ~localhost kernel: Current sd0b:00: sns = 70 3
Nov 8 13:08:29 ~localhost kernel: ASC=11 ASCQ= 0
Nov 8 13:08:29 ~localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov 8 13:08:29 ~localhost kernel: I/O error: dev 0b:00, sector 4046908
Nov 8 13:11:40 ~localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov 8 13:11:40 ~localhost kernel: Current sd0b:00: sns = 70 3
Nov 8 13:11:40 ~localhost kernel: ASC=11 ASCQ= 0
Nov 8 13:11:40 ~localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov 8 13:11:40 ~localhost kernel: I/O error: dev 0b:00, sector 3681164
Nov 8 13:12:10 ~localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov 8 13:12:10 ~localhost kernel: Current sd0b:00: sns = 70 3
Nov 8 13:12:10 ~localhost kernel: ASC=11 ASCQ= 0
Nov 8 13:12:10 ~localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov 8 13:12:10 ~localhost kernel: I/O error: dev 0b:00, sector 3690236
Nov 8 13:12:13 ~localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov 8 13:12:13 ~localhost kernel: Current sd0b:00: sns = 70 3
Nov 8 13:12:13 ~localhost kernel: ASC=11 ASCQ= 0
Nov 8 13:12:13 ~localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00</pre><p>
Теперь я могу отфильтровать дату намного более простым выражением:
</p><pre class="programlisting">/^[^~]+~/</pre><p>
Это то-же <a class="link" href="ch04s02.html#mark">использование маркёров</a>, в качестве маркёра можно использовать не только ~, но и любой другой символ, которого не бывает в дате.
</p><p>
теперь можно писать сам скрипт:
</p><pre class="screen">$ sed -r 's/^(\S+\s+){3}/\0~/' sys.txt | sed -rn 'G;s/^[^~]+~([^\n]+)\n.*[^~]+~\1.*/\0/;t;h;s/\n.*//p'
Nov 8 13:08:29 ~localhost kernel: scsi0: ERROR on channel 0, id 0, lun 0, CDB: 0x03 00 00 00 40 00
Nov 8 13:08:29 ~localhost kernel: Current sd0b:00: sns = 70 3
Nov 8 13:08:29 ~localhost kernel: ASC=11 ASCQ= 0
Nov 8 13:08:29 ~localhost kernel: Raw sense data:0x70 0x00 0x03 0x00 0x00 0x00 0x00 0x0a 0x00 0x00
0x00 0x00 0x11 0x00 0x00 0x00 0x00 0x00
Nov 8 13:08:29 ~localhost kernel: I/O error: dev 0b:00, sector 4046908
Nov 8 13:11:40 ~localhost kernel: I/O error: dev 0b:00, sector 3681164
Nov 8 13:12:10 ~localhost kernel: I/O error: dev 0b:00, sector 3690236</pre><p>
Если строчка уже есть в базе - я завершаю скрипт командой <span class="command"><strong>t</strong></span> (и перехожу к новой строке), а вот если строчка новая, то я сначала сохраняю новую базу (команда <span class="command"><strong>h</strong></span>), а затем выделяю и печатаю эту строку.
</p><p>
В качестве "домашнего задания" предлагаю немного доделать этот скрипт: во первых, маркёр неплохо-бы убрать из вывода, делается это достаточно просто: надо добавить ещё одну команду <span class="command"><strong>s</strong></span> в самый конец однострока. Во вторых, сильно тормозят работу строчки
</p><pre class="programlisting">Nov 8 13:08:29 ~localhost kernel: I/O error: dev 0b:00, sector 4046908</pre><p>
Их очень много, и они все уникальные. Я вынес номер сектора (который всегда разный и который мне не слишком интересен) в дату, перед ~, потому у меня эти строки не считаются уникальными. Конечно было-бы неплохо засунуть номер сектора на своё место. Лог-файл из 2362 строк обработался за 410 секунд, что вполне приемлемо для моего CPU на 400MHz. Это конечно после исключения номера сектора, с номером время работы как минимум в 10 раз больше! При этом первая команда <span class="command"><strong>sed</strong></span> практически не потребляет ресурсов.В итоге получилось всего 145 строк, что конечно очень полезно - теперь не надо читать огромные логи!
</p><p>
В заключение, хочу заметить, что этот однострок можно использовать для многих разных целей, к примеру можно посчитать все уникальные слова в тексте, для этого достаточно убрать всё кроме слов, и каждое слово записать в новой строке.
</p><p>
ну примерно таким образом:
<a name="t06_1"></a>
</p><div class="example"><a name="id2519558"></a><p class="title"><b>Пример 4.1. Скрипт для подсчёта слов.</b></p><div class="example-contents"><a class="ulink" href="examples/wcx.sh" target="_top">wcx.sh</a><pre class="programlisting">#!/bin/sh
# Это вроде как shell, но на самом деле, это 2 скрипта на sed.
sed -r '
# если в строке нет ни одной буквы - то нет и слов, исключаем
/\w/!d
# небуквы в конце строки удаляем
s/\W+$//
# вставка перевода строки и маркёра в дыры между словами
# дырка - это много небукв после правой границы слова
# правых границ столько-же сколько слов, но мы меняем
# не всё - после последней границы не может быть небукв
# мы их удалили прошлой командой
s/\>\W+/\na~/g
# вставка маркёра перед первым словом
s/^\W*/a~/
' $@ |\
sed -rn '
G
# на эту строчку приходится основная нагрузка:
# первое подвыражение в скобках захватывает полностью анализируемое слово
# второе подвыражение расположено в начале базы до найденного совпадения
# третье - это старый номер в позиционной системе счисления, тут a означает 1,
# aa == 2, aaa == 3 ... b == 10, ba == 11, baa = 12 ... bbbaa == 32, dccbaaaa == 1214
# четвёртое подвыражение - это хвост, то, что расположено за совпадением
# в случае совпадения слово с номером убирается из базы, и записывается в начало,
# так-же к номеру добавляется a, т.о. счётчик увеличивается на 1
# кроме того, часто используемые слова группируются в начале базы для увеличения
# быстродействия.
s/^a~([^\n]+)(.*)\n([a-f]+)~\1\n(.*)/\3a~\1\2\n\4/
t old_word
# новое слово
h
# что-бы не было очень скучно выводим новые слова в stderr
s/\n.*//
s/^a~//
w /dev/stderr
b ctrl_end
:old_word
# Старое слово, мы его нашли, и вытащили в начало базы
# и ещё добавили букву а в конец. Если букв a больше 9,
# нам следует сменить их на b, и т.д.
s/^([b-f]*)a{10}/\1b/; T done
s/^([c-f]*)b{10}/\1c/; T done
s/^([d-f]*)c{10}/\1d/; T done
s/^([ef]*)d{10}/\1e/; T done
s/^(f*)e{10}/\1f/; T done
s/^f{10}//
# максимум 999999 единиц - вполне хватит ИМХО
:done
x
:ctrl_end
$!b
#следующие команды выполняются в конце текста
s/.*/ ----- Найденные слова -----/p
x
# перевод позиционной системы счисления в обычную
:begin_convert
# проверяем, надо-ли ещё переводить?
/^[0-9 ]/ b end_convert
# сохраняем, что-бы не конвертить всю базу
h
s/~.*//
t lx_convert
:lx_convert
s/a{9}/9/; t dx_convert
s/a{8}/8/; t dx_convert
s/a{7}/7/; t dx_convert
s/a{6}/6/; t dx_convert
s/a{5}/5/; t dx_convert
s/a{4}/4/; t dx_convert
s/a{3}/3/; t dx_convert
s/a{2}/2/; t dx_convert
s/a/1/; t dx_convert
s/$/0/; t dx_convert
:dx_convert
y/bcdef/abcde/
s/[a-f]/\0/
t lx_convert
# теперь мы отконвертировали наш счётчик
# выравнивание: слева от числа добавляется 5 пробелов
s/^/ /
# затем берутся 6 последних символов
s/.*(......)/\1/
# теперь мы можем добавить базу
G
# ротация базы. первая запись переносится на последнее место
# кроме того, мы затираем позиционный счётчик.
s/([^\n]+)\n[a-f]+~([^\n]+\n)(.*)/\3\1 \2/
b begin_convert
:end_convert
p
'</pre></div></div><p><br class="example-break">
Думаю, пояснений вполне достаточно и в комментариях, впрочем ещё чуть-чуть: ну тут я активно использую жадность, всегда следует учитывать, что первый квантификатор захватывает всё что может, а второй - тоже всё, из того, что осталось, потому, часто нет смысла использовать <span class="emphasis"><em>якоря</em></span> (это <span class="quote">«<span class="quote">^</span>»</span> и <span class="quote">«<span class="quote">$</span>»</span>, которые означают начало и конец строки соответственно),
к примеру шаблон /.*/ уже и так неявно заякорен, и применение якорей ничего не изменят.
Хотя в некоторых случаях применять якоря желательно для повышения быстродействия (если нам надо искать что-то, и мы уверены что это что-то в самом конце строки, нужно сообщить об этом <span class="command"><strong>sed</strong></span>, возможно уже сейчас или в будущих версиях появятся оптимизации, которые используют эту информацию).
Привязывать к началу строки без необходимости нет смысла - <span class="command"><strong>sed</strong></span> и так просматривает строку с начала.
</p><p>
Для фильтрации слов я использовал спец-символы <span class="quote">«<span class="quote">\w</span>»</span> и <span class="quote">«<span class="quote">\W</span>»</span>, это более предпочтительней чем <span class="quote">«<span class="quote">[a-zA-Z0-9_]</span>»</span>, потому-что работает во всех кодировках и на всех языках.
Кроме того, я использовал границу <span class="quote">«<span class="quote">\></span>»</span>.
Этот спец-символ совпадает с пустой строкой, которая лежит между <span class="quote">«<span class="quote">\w\W</span>»</span>, или между <span class="quote">«<span class="quote">\w$</span>»</span>, конечно есть и левая граница слова.
</p><p>
Тема записи в файл в этом скрипте не раскрыта, хотя я и применил команду <span class="command"><strong>w</strong></span>, но это просто для вывода в поток ошибок - вполне естественно сохранить список найденных слов, и при этом скучно глядеть как этот скрипт работает (а работает он долго - sed это вам не MySQL).
</p><p>
В скрипте имеются циклы, в том числе и вложенные, обратите внимание на сброс флага перехода, после выполнения команды <span class="command"><strong>s</strong></span>, которая всегда находит своё RE, необходимо перейти командой <span class="command"><strong>t</strong></span> на следующую команду <span class="command"><strong>s</strong></span>, результат работы которой мы будем анализировать. Если этого не сделать, то даже если вторая команда s ничего не найдёт и не поменяет, команда перехода <span class="command"><strong>t</strong></span> всё равно передаст управление. Для сброса команды перехода я использовал так-же команду <span class="command"><strong>b</strong></span>, однако в примерах из info sed почему-то используется именно <span class="command"><strong>t</strong></span>.
</p><p><a name="cy"></a>
Есть в скрипте и команда <span class="command"><strong>y</strong></span> - это достаточно редкая команда у меня служит для деления на 10 позиционного счётчика, например если счётчик равен </p><pre class="programlisting">ddcccccbbbbbbb</pre><p>
(это 2570 по человечески), то после выполнения
</p><pre class="programlisting">y/bcdef/abcde/</pre><p>
мы получим
</p><pre class="programlisting">ccbbbbbaaaaaaa</pre><p>
что как раз и составляет необходимые нам 257.
</p><div class="note" title="Замечание" style="margin-left: 0.5in; margin-right: 0.5in;"><h3 class="title">Замечание</h3>
Смысл работы команды <span class="command"><strong>y</strong></span> довольно прост: ищутся все символы из первого списка, и заменяются на символы из второго.
В нашем случае все b меняются на a, c на b, d на c, и так далее.
Невозможно использовать в этой команде списки, классы и прочее, потому <span class="quote">«<span class="quote">одностроки на перле</span>»</span>, которые удаляют всё содержимое на всех дисках здесь невозможны.
Не знаю, хорошо это или плохо...
</div><p>
</p><p>
А вообще я думаю, что для ускорения работы следует применить например пятеричную систему счисления, хотя её и сложнее переводить в десятичную, но сам перевод нужно сделать единожды для каждого слова, а эти счётчики сильно тормозят поиск... Не знаю... Попробуйте если есть желание...
</p><p>
В целом использовать <span class="command"><strong>sed</strong></span> как базу данных можно.
Если конечно число уникальных элементов не слишком велико.
Если счёт идёт на десятки-сотни тысяч - поднимайте MySQL сервер - без вариантов!
Впрочем, управлять этим сервером можно и с помощью <span class="command"><strong>sed</strong></span>.
Кроме того, иногда в качестве БД можно использовать файловую систему - насоздавать тыщу файлов, и пусть ось сама ищет нужное нам.
Например, учитывая что у нас слова, можно сделать для каждого слова отдельный файл, в котором будет лежать счётчик, а имя файла - само слово.
Я не знаю, будет-ли такой подход более эффективным, возможно это будет намного быстрее, кроме того, можно сегодня добавить в наш словарь один текст, а завтра - другой.
Кроме того, в файлах можно хранить не только счётчик, но и даже информацию о том, в каком тексте(текстах) скрывается данное слово.
</p><p><a name="exa_crc"></a>
Недавно у меня возникла задача скачать и сохранить около 7000 файлов.
При этом, каждый файл идентифицировался уникальной строкой, <span class="quote">«<span class="quote">темой</span>»</span>, мне пришлось просматривать каждые 2 часа определённый сервер, и выкачивать оттуда файлы с новой <span class="quote">«<span class="quote">темой</span>»</span>. Эту самую <span class="quote">«<span class="quote">тему</span>»</span> я свернул в CRC-32 командой <span class="command"><strong>cksum</strong></span>, а затем перевёл в восьмеричную систему командой <span class="command"><strong>bc</strong></span> Конечно всё это делалось внутри <span class="quote">«<span class="quote">sed</span>»</span>, вот например перевод числа в восьмеричный вид:
</p><pre class="programlisting">s/^[0-9]+$/echo \x27obase=8; &\x27 | bc/e
s/.*/0000000000&/
s/.*([0-7]{11})$/\1/</pre><p>
Узнав crc32 я могу узнать, есть-ли у меня этот файл, к примеру, для темы <span class="quote">«<span class="quote">test</span>»</span> сумма будет 06757644257, и узнать есть-ли такой файл можно просто проверить наличие файла 06757644257.html, в реальности, я упаковал все html файлы в архивы, но сделал короткие файлы с самой темой, например 06757644257.subj.
К примеру, если в буфере лежит crc, то наличие нужного файла внутри sed-скрипта узнаётся так:
</p><pre class="programlisting">s/^[0-7]+$/test -f &.subj; echo $?/e
/^0$/ b переход_если_файл_существует
# а здесь обрабатывается случай когда такого файла ещё нет</pre><p>
Такой подход естественно намного быстрее чем хранение базы в области удержания (в моём случае, если файлов ~7000).
</p><p>
</p><div class="warning" title="Внимание" style="margin-left: 0.5in; margin-right: 0.5in;"><h3 class="title">Внимание</h3>
Обратите внимание, как я задаю выражение при использовании модификатора <code class="option">e</code>:
я мог-бы написать /.*/, однако я задаю <span class="command"><strong>/^[0-7]+$/</strong></span> - это связано с безопасностью, злоумышленник может подсунуть в мой скрипт какую-нибудь кривую <span class="quote">«<span class="quote">тему</span>»</span> с вредоносным кодом.
Этот код будет выполнен командой <span class="command"><strong>s/.*//e</strong></span>. Однако у меня это невозможно - <span class="command"><strong>s/^[0-7]+$/.../e</strong></span> выполняется только если в буфере лежит восьмеричное число (если враг мне подсунет какую-нибудь каку, то <span class="command"><strong>s</strong></span> просто не выполнится).
ИМХО лучше так перестраховаться, чем потом кусать локти - например сейчас этот скрипт работает в пяти километрах от меня, и я не имею никакой возможности остановить его, или как-то изменить логику его работы.
</div><p>
</p><p>
Продолжим рассмотрение найденной уязвимости (см. <a class="link" href="ch02s04.html#bugs_utf">здесь</a>).
</p><p>
Как я уже показал, злоумышленник может протолкнуть любой код сквозь фильтр.
Однако, что в этом такого? Подумаешь...
Ан нет - рассмотрим простой пример: требуется выделить первые буквы(все, что есть), и отправить их в команду shell (выше я выполнил shell-команду test).
Для примера я возьму команду echo (она безвредная). Итак:
</p><pre class="screen">$ echo -e "--ф; ls" | sed -r 's/\W*(\w*).*/\1/;s/.*/echo "&"/e'
ф</pre><p>
тут мы имеем 2 команды <span class="command"><strong>s</strong></span>, первая из них служит фильтром, который отфильтровывает буквы.
Используется RE <span class="command"><strong>/\W*(\w*).*/</strong></span>, оно отфильтровывает только первые буквы.
Ну а затем отфильтрованные буквы передаются в команду <span class="command"><strong>echo</strong></span>, т.к. там только буквы, то их можно распечатать и так, но на всякий случай программер заключил их в кавычки.
Вроде всё хорошо, и даже кавычки есть.
Тут для простоты двойные, но можно и одиночные, это без разницы.
</p><p>
Подразумевается, что на вход <span class="command"><strong>sed</strong></span> может поступить всё что угодно, и вроде как всё нормально, отфильтруются только буквы, а даже если их и не будет, то <span class="quote">«<span class="quote">\w*</span>»</span> вполне себе совпадает ни с чем, и этот однострок отлично работает.
Но вот приходит враг, и подставляет <span class="emphasis"><em>несимвол</em></span>:
</p><pre class="screen">$ echo -e "--ф\xD1; ls" | sed -r 's/\W*(\w*).*/\1/;s/.*/echo "&"/e'
ф?
Makefile
Makefile.bak
current_commit.sh
main.c
main.c.bak
main.o
regex</pre><p>
О бл*!!! Злоумышленник выполнил свой вредоносный код!!!
</p><p>
В данном случае просто просмотрел каталог, но команда могла быть любой другой.
</p><p>
И что делать? Для начала разберёмся: тут после <span class="quote">«<span class="quote">ф</span>»</span> враг вставил несимвол, а затем, через точку с запятой и пробел, нужную команду.
Это всё прошло сквозь фильтр, и отправилось в пайпе к нашей оболочке.
Оболочка получила <span class="command"><strong>echo</strong></span>, за которой буква <span class="quote">«<span class="quote">ф</span>»</span> в кавычках, а затем несимвол.
Это она посчитала параметром <span class="command"><strong>echo</strong></span>, и распечатала.
После чего оболочка обнаружила точку с запятой, и перешла к выполнению команды злоумышленника.
</p><p>
Что-бы исключить такую атаку, необходимо и достаточно выполнять shell команды тогда и только тогда, когда в буфере размещается <span class="emphasis"><em>целиком</em></span> допустимое значение.
Т.е. нужна вторая проверка, непосредственно в команде, которая вызывает shell (почему я кстати и не использую команду <span class="command"><strong>e</strong></span> - там такой проверки нет).
</p><pre class="screen">$ echo -e "--ф\xD1; ls" | sed -r 's/\W*(\w*).*/\1/;s/^\w+$/echo "&"/e'
ф?; ls</pre><p>
Вот - какая-то вражья фигня не совпадает с <span class="command"><strong>/^\w+$/</strong></span> (только буквы, с начала и до конца), и потому shell-команда просто не выполняется.
</p><div class="blockquote"><blockquote class="blockquote"><p>
Вы можете обсудить этот документ на <a class="ulink" href="http://emulek.tk/forum/viewtopic.php?f=19&t=5026" target="_top">форуме</a>. Текст предоставляется по лицензии <a class="ulink" href="http://www.gnu.org/licenses/fdl.html" target="_top">GNU Free Documentation License</a> (<a class="ulink" href="http://forum.lorcode.org/viewtopic.php?f=15&t=30" target="_top">Перевод лицензии GFDL</a>).
</p><p>
Вы можете пожертвовать небольшую сумму яндекс-денег на счёт <span class="command"><strong>41001666004238</strong></span> для оплаты хостинга, интернета, и прочего. Это конечно добровольно, однако это намного улучшит данный документ (у меня будет больше времени для его улучшения). На самом деле, проект часто находится на грани закрытия, ибо никаких денег никогда не приносил, и приносить не будет. Вы можете мне помочь. Спасибо.
</p></blockquote></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="ch04s03.html">Пред.</a> </td><td width="20%" align="center"><a accesskey="u" href="ch04.html">Уровень выше</a></td><td width="40%" align="right"> <a accesskey="n" href="ch04s05.html">След.</a></td></tr><tr><td width="40%" align="left" valign="top">Поиск одинаковых подстрок. </td><td width="20%" align="center"><a accesskey="h" href="index.html">Начало</a></td><td width="40%" align="right" valign="top"> Подсчёт количества вхождений каждого символа в тексте.</td></tr></table></div></body></html>