File Coverage

blib/lib/Octets/To/Unicode.pm
Criterion Covered Total %
statement 42 90 46.6
branch 12 44 27.2
condition 1 6 16.6
subroutine 8 18 44.4
pod 11 12 91.6
total 74 170 43.5


line stmt bran cond sub pod time code
1             use 5.008001;
2 2     2   1329 use utf8;
  2         6  
3 2     2   10 use strict;
  2         3  
  2         12  
4 2     2   37 use warnings FATAL => 'all';
  2         3  
  2         65  
5 2     2   11  
  2         2  
  2         228  
6             our $VERSION = "0.06";
7              
8             require Exporter;
9             our @ISA = qw(Exporter);
10             our @EXPORT =
11             grep { *{ $Octets::To::Unicode::{$_} }{CODE} } keys %Octets::To::Unicode::;
12              
13             use Encode qw//;
14 2     2   486  
  2         9043  
  2         153  
15             #=========================================
16             #@category Кодировка
17              
18             # Определяет кодировку.
19             my ($s) = @_;
20             my $c = 0;
21 16     16 1 20 while ( $s =~ m![а-яё]+!gi ) {
22 16         16 my $len = length $&;
23 2     2   12 if ( $& =~ /^[А-ЯЁ][а-яё]+$/ ) { $c += $len }
  2         3  
  2         32  
  16         54  
24 52         82 elsif ( $& =~ /^[А-ЯЁ]+$/ ) { $c += $len / 3 }
25 52 100       162 elsif ( $& =~ /^[а-яё]+$/ ) { $c += $len / 3 }
  6 100       12  
    100          
26 16         42 else { $c -= $len }
27 24         53 }
28 6         16 $c;
29             }
30 16         40  
31             # Определить кодировку и декодировать
32             my ( $octets, $encodings ) = @_;
33              
34             return if !length $octets;
35 4     4 1 3076  
36             utf8::encode($octets) if utf8::is_utf8($octets);
37 4 50       10  
38             $encodings //= [qw/utf-8 cp1251 koi8-r/];
39 4 100       11  
40             my @x = grep length $_->[0], map {
41 4   50     24  
42             # В случае ошибки Encode::decode помещает пустую строку в свой второй аргумент. Какой-то баг.
43             my $save = $octets;
44             eval { [ Encode::decode( $_, $save, Encode::FB_CROAK ), $_ ] };
45             } @$encodings;
46 4         8  
  12         462  
47 12         13 my ( $unicode, $mem_encoding );
  12         21  
48             ( $unicode, $mem_encoding ) = @{ $x[0] } if @x == 1;
49              
50 4         131 if ( @x > 1 ) {
51 4 50       9 ( $unicode, $mem_encoding ) =
  0         0  
52             @{ ( sort { bohemy( $b->[0] ) <=> bohemy( $a->[0] ) } @x )[0] };
53 4 50       8 }
54              
55 4         4 wantarray ? ( $unicode, $mem_encoding ) : $unicode;
  4         24  
  8         16  
56             }
57              
58 4 50       26 # Определить кодировку
59             ( decode @_ )[1];
60             }
61              
62             #=========================================
63 0     0 1   #@category Вспомогательные функции
64              
65             # Ищет файлы в директориях рекурсивно
66             sub file_find($);
67              
68             my ($file) = @_;
69             if ( -d $file ) {
70             $file =~ s!/$!!;
71             map file_find($_), <$file/*>;
72             }
73 0     0 1   else {
74 0 0         $file;
75 0           }
76 0           }
77              
78             # Чтение бинарного файла
79 0           my ($file) = @_;
80             open my $f, "<", $file
81             or die "При открытии для чтения $file произошла ошибка: $!.";
82             read $f, my $buf, -s $f;
83             close $f;
84             $buf;
85 0     0 1   }
86 0 0          
87             # Запись в бинарный файл
88 0           my ( $file, $unicode ) = @_;
89 0            
90 0           utf8::encode($unicode) if utf8::is_utf8($unicode);
91              
92             open my $f, ">", $file
93             or die "При открытии для записи $file произошла ошибка: $!.";
94             print $f $unicode;
95 0     0 1   close $f;
96             return;
97 0 0         }
98              
99 0 0         # Определить кодировку и декодировать файл
100             my ( $file, $encodings ) = @_;
101 0           decode file_read $file, $encodings;
102 0           }
103 0            
104             # Кодировать в указанной кодировке и записать в файл
105             my ( $file, $encoding, $unicode ) = @_;
106              
107             utf8::decode($unicode) if !utf8::is_utf8($unicode);
108 0     0 1    
109 0           $unicode = Encode::encode( $encoding, $unicode ) if defined $encoding;
110              
111             file_write $file, $unicode;
112             }
113              
114 0     0 1   #=========================================
115             #@category Тестирование файлов
116 0 0          
117             # Если расширение отсутствует, то 1-я строка должна содержать #!(.*)$interpreter
118 0 0         # * $exts — расширения: [qw/pl pm py/]
119             # * $interpreters — интерпретаторы: [qw/perl python/]
120 0           # Примечание: $interpreters проверяются только для файлов без расширений и только если пустое расширение было в $exts
121             my ( $file, $exts, $interpreters ) = @_;
122              
123             $exts //= [];
124             $interpreters //= [];
125              
126             my ($ext) = $file =~ /\.([^.\/]*)$/;
127              
128             return @$exts ? ( 1 == grep { $ext eq $_ } @$exts ) : 1 if defined $ext;
129              
130             open my $f, "<", $file or die "$file: $!";
131 0     0 1   read $f, my $buf, 2;
132             close($f), return 0 if $buf ne "#!";
133 0   0        
134 0   0       return 1 if !@$interpreters;
135              
136 0           my $first_line = <$f>;
137             close $f;
138 0 0          
  0 0          
139             $interpreters = join "|", @$interpreters;
140 0 0          
141 0           $first_line =~ /.*\b($interpreters)\b/ ? 1 : 0;
142 0 0         }
143              
144 0 0         # Тестирует файлы на соответствия расширениям
145             my ( $files, $exts, $interpreters ) = @_;
146 0            
147 0           $exts = [ split /,/, $exts ] if !ref $exts;
148             $interpreters = [ split /,/, $interpreters ] if !ref $interpreters;
149 0            
150             grep test_file( $_, $exts, $interpreters ), @$files;
151 0 0         }
152              
153             # Возвращает изменённые файлы в репозитории git
154             map { file_find $_ }
155             map { s/^\s*[\w\?]+\s+//; $_ } grep { !/^\s*D / } split /\n/,
156 0     0 1   `git status -s`;
157             }
158 0 0          
159 0 0         # Возвращает изменённые файлы в ветке
160             grep length, split "\n",
161 0           `git diff --name-only --diff-filter=AM origin/master...`;
162             }
163              
164             1;
165              
166 0           =encoding utf-8
167 0     0 1    
  0            
  0            
  0            
168             =head1 NAME
169              
170             Octets::To::Unicode - модуль и утилиты ru-perltidy и ru-utf8 для распознавания кодировки текста (в том числе в файлах) и его декодирования.
171              
172             =head1 VERSION
173 0     0 0    
174             0.01
175              
176             =head1 SYNOPSIS
177              
178             use Octets::To::Unicode;
179            
180             my $unicode = decode "Стар Трек";
181             my ($unicode, $encoding) = decode "Стар Трек";
182             my $unicode = decode $octets_in_cp1251_or_maybe_in_utf8, [qw/cp1251 utf-8/];
183            
184             my $encoding = detect $octets;
185             my $encoding = detect $octets, [qw/cp1251 utf-8/];
186            
187             my ($file_text_in_unicode, $encoding) = file_decode "path/to/file", ["cp1251", "koi8-r"];
188             file_encode "path/to/file2", "koi8-r", $file_text_in_unicode;
189              
190             Использование утилит:
191              
192             # Отформатировать указанные файлы perltidy:
193             $ ru-perltidy file1 file2
194              
195             # Указать кодировку:
196             $ ru-perltidy file1 file2 -e utf-8,cp1251
197              
198             # Форматирует только изменённые файлы в репозитории git:
199             $ ru-perltidy
200              
201             # Форматирует изменённые файлы в ветке (на случай, если забыл отформатировать перед комитом):
202             $ ru-perltidy --in-branch
203              
204             # Указать расширения файлов:
205             $ ru-perltidy --ext 'pl,pm,'
206            
207             # Обработать файлы в директориях:
208             $ ru-perltidy --in-dir .,/tmp/mydir
209              
210             # Выполнить операцию с файлами:
211             $ ru-utf8 file1 file2 -c 'perltidy $f -st > $o'
212            
213             # Переменные, которые можно использовать:
214             $ ru-utf8 file1 file2 -c 'echo $f $o $e $x'
215             $ ru-utf8 file1 file2 -o -c 'echo $f1 $o1 $e1 $x1 - $f2 $o2 $e2 $x2'
216            
217             # Кроме команды шелла можно использовать ещё код perl:
218             $ ru-utf8 file1 file2 -s 'print "$f $o $e $x -- $unicode\n"'
219             $ ru-utf8 file1 file2 -o -s 'print "@f @o @e @x"'
220            
221             # Определить кодировку файлов и перекодировать их в koi8-r:
222             $ ru-encode -t koi8-r
223              
224             =head1 DESCRIPTION
225              
226             Пакет включает в себя утилиты:
227              
228             =over 4
229              
230             =item B<ru-perltidy> — форматирует файлы через perltidy c определением их кодировки;
231              
232             =item B<ru-utf8> — переводит файлы во временные (в кодировке utf-8), выполняет указанную команду и переписывает обратно в определённой кодировке;
233              
234             =item B<ru-encode> — перекодирует файлы в указанную кодировку.
235              
236             =back
237              
238             и модуль perl:
239              
240             =over 4
241              
242             =item B<Octets::To::Unicode> — модуль c функциями определения кодировки текста и его конвертирования между кодировками.
243              
244             =back
245              
246             B<Octets::To::Unicode> предоставляет необходимое множество утилит для определения кодировки текста и его декодирования, а так же — работы с файлами.
247              
248             В 2000-х определилась тенденция переводить проекты в национальных кодировках в utf-8. Однако не везде их перевели одним махом, а решили рубить собаке хвост постепенно. В результате во многих проектах часть файлов c кодом в utf-8, а часть — в национальной кодировке (cp1251, например).
249              
250             Ещё одной проблемой могут служить урлы с эскейп-последоваительностями. Например, https://ru.wikipedia.org/wiki/Молчание#Золото преобразуется в мессенджере, куда эту ссылку можно скопировать, в https://ru.wikipedia.org/wiki/%D0%9C%D0%BE%D0%BB%D1%87%D0%B0%D0%BD%D0%B8%D0%B5#%D0%97%D0%BE%D0%BB%D0%BE%D1%82%D0%BE. Причём один мессенджер переведёт русские символы в utf-8, другой — в cp1251, третий — в koi8-r.
251              
252             Чтобы решить эти две проблемы в приложениях и был написан этот модуль.
253              
254             =head1 SUBROUTINES/METHODS
255              
256             =head2 bohemy
257              
258             $num = bohemy $unicode;
259              
260             Возвращает числовую характеристику похожести текста на русский.
261              
262             Алгоритм основан на наблюдении, что в русском языке слово начинается на прописную или строчную букву, а затем состоит из строчных букв.
263              
264             Таким образом, числовая характеристика, это сумма длин русско-похожих слов с разницей суммы длин русско-непохожих.
265              
266             Принимает параметр:
267              
268             =over 4
269              
270             =item B<$unicode>
271              
272             Текст в юникоде (с взведённым флажком utf8).
273              
274             =back
275              
276             =head2 decode
277              
278             $unicode = decode $octets, $encodings;
279             ($unicode, $encoding) = decode $octets, $encodings;
280              
281             Возвращает декодированный текст в скалярном контексте, а в списочном, ещё и определённую кодировку.
282              
283             Если ни одна из кодировок не подошла, то вместо юникода в первом параметре возвращаются октеты, а вместо кодировки - C<undef>:
284              
285             ($octets, $encoding_is_undef) = decode $octets, [];
286              
287             Принимает параметры:
288              
289             =over 4
290              
291             =item B<$unicode>
292              
293             Текст в юникоде (с взведённым флажком utf8).
294              
295             =item B<$encodings>
296              
297             Cписок кодировок, которыми предлагается попробовать декодировать текст.
298              
299             Необязательный. Значение по умолчанию: C<[qw/utf-8 cp1251 koi8-r/]>.
300              
301             =back
302              
303             =head2 detect
304              
305             $encoding = detect $octets, $encodings;
306              
307             Возвращает определённую кодировку или C<undef>.
308              
309             Параметры такие же как у L</"decode">.
310              
311             =head2 file_find
312              
313             @files = file_find $path_to_directory;
314              
315             Ищет файлы в директориях рекурсивно и возвращает список путей к ним.
316              
317             Принимает параметр:
318              
319             =over 4
320              
321             =item B<$path_to_directory>
322              
323             Путь к файлу или директории. Если путь не ведёт к директории, то он просто возвращается в списке.
324              
325             =back
326              
327             =head2 file_read
328              
329             $octets = file_read $path;
330              
331             Считывает файл.
332              
333             Возвращает текст в октетах.
334              
335             Выбрасывает исключение, если открыть файл не удалось.
336              
337             Принимает параметр:
338              
339             =over 4
340              
341             =item B<$path>
342              
343             Путь к файлу.
344              
345             =back
346              
347             =head2 file_write
348              
349             file_write $path, $octets_or_unicode;
350              
351             Перезаписывает файл строкой.
352              
353             Ничего не возвращает.
354              
355             Выбрасывает исключение, если открыть файл не удалось.
356              
357             Принимает параметры:
358              
359             =over 4
360              
361             =item B<$path>
362              
363             Путь к файлу.
364              
365             =item B<$octets_or_unicode>
366              
367             Новое тело файла в октетах или юникоде.
368              
369             =back
370              
371             =head2 file_decode
372              
373             $unicode = file_decode $path, $encodings;
374             ($unicode, $encoding) = file_decode $path, $encodings;
375              
376             Возвращает декодированный текст из файла в скалярном контексте, а в списочном, ещё и определённую кодировку.
377              
378             Если ни одна из кодировок не подошла, то вместо юникода в первом параметре возвращаются октеты, а вместо кодировки - C<undef>:
379              
380             ($octets, $encoding_is_undef) = file_decode $path, [];
381              
382             Принимает параметры:
383              
384             =over 4
385              
386             =item B<$path>
387              
388             Путь к файлу.
389              
390             =item B<$encodings>
391              
392             Cписок кодировок, которыми предлагается попробовать декодировать текст.
393              
394             Необязательный. Значение по умолчанию: C<[qw/utf-8 cp1251 koi8-r/]>.
395              
396             =back
397              
398             =head2 file_encode
399              
400             file_encode $path, $encoding, $unicode;
401              
402             Переписывает текст в файле в указанной кодировке.
403              
404             Принимает параметры:
405              
406             =over 4
407              
408             =item B<$path>
409              
410             Путь к файлу.
411              
412             =item B<$encoding>
413              
414             Кодировка в которую следует перевести параметр C<unicode> перед записью в файл.
415              
416             =item B<$unicode>
417              
418             Новый текст файла в юникоде (с установленным флажком utf8).
419              
420             =back
421              
422             =head2 test_file
423              
424             $is_file = test_file $file, $exts, $interpreters;
425              
426             Тестирует файл на соответствие указанным расширениям, а если расширения нет, то на соответсвие интерпретаторов к указанному в первой строке файла начинающейся на C<#!>.
427              
428             Принимает параметры:
429              
430             =over 4
431              
432             =item B<$file>
433              
434             Путь к файлу.
435              
436             Обязательный.
437              
438             =item B<$exts>
439              
440             Список расширений для сопоставления, если он пуст, то подходит любое.
441              
442             Необязательный. Значение по умолчанию: C<[]>.
443              
444             =item B<$interpreters>
445              
446             Список интерпретаторов для сопоставления, если он пуст, то подходит любой, главное, чтобы строка начиналась на C<#!>.
447              
448             Необязательный. Значение по умолчанию: C<[]>.
449              
450             =back
451              
452             =head2 test_files
453              
454             @files = test_files $files, $exts, $interpreters;
455              
456             Тестирует файлы на соответствие указанным расширениям или интерпретаторам.
457              
458             Принимает параметры:
459              
460             =over 4
461              
462             =item B<$files>
463              
464             Список файлов.
465              
466             Обязательный.
467              
468             =item B<$exts>
469              
470             Такой же как в B<test_file>.
471              
472             =item B<$interpreters>
473              
474             Такой же как в B<test_file>.
475              
476             =back
477              
478             =head2 change_files
479              
480             @files = change_files();
481              
482             Возвращает изменённые файлы в репозитории git.
483              
484             =head2 change_files
485              
486             @files = change_files_in_branch();
487              
488             Возвращает изменённые файлы в ветке.
489              
490             =head1 INSTALL
491              
492             Установить можно любым менеджером C<perl> со B<CPAN>, например:
493              
494             $ sudo cpm install -g Octets::To::Unicode
495              
496             =head1 DEPENDENCIES
497              
498             Зависит от модулей:
499              
500             =over 4
501              
502             =item * Getopt::Long
503              
504             =item * Encode
505              
506             =item * List::Util
507              
508             =item * Pod::Usage
509              
510             =item * Term::ANSIColor
511              
512             =back
513              
514             и от B<perltidy> опционально:
515              
516             =over 4
517              
518             =item * Perl::Tidy
519              
520             =back
521              
522             =head1 RELEASE
523              
524             Релиз на B<CPAN> осуществляется так:
525              
526             =over 4
527              
528             =item Обновить исходники:
529              
530             $ git pull
531            
532             =item Отредактировать файл I<Changes>.
533              
534             В файле I<Changes> нужно написать список изменений, которые вошли в этот релиз.
535              
536             Изменения записываются в виде списка, одно изменение — один элемент списка. Элементы списка обозначаются символами тире `-`.
537              
538             Список с изменениями нужно разместить между строкой `{{$NEXT}}` и строкой с предыдущим релизом.
539              
540             Допустим, предыдущий релиз был 1.71. Тогда описание изменений нового релиза будет выглядеть так:
541              
542             {{$NEXT}}
543            
544             - RU-5 Какой-то тикет, который вошел в релиз.
545             - RU-6 Ещё один тикет, вошедший в релиз.
546            
547             1.71 2021-05-07T08:52:18Z
548            
549             - RU-4 Какой-то предыдущий тикет.
550              
551             Обратите внимание — у нового релиза пока нет версии. Версия будет вычислена Миниллой при выполнении релиза и автоматически вписана в файл I<Changes> вместо метки C<{{$NEXT}}>.
552              
553             =item Активировать локальную библиотеку:
554              
555             $ cpanm --local-lib=~/perl5 local::lib && eval $(perl -I ~/perl5/lib/perl5/ -Mlocal::lib)
556              
557             Это нужно, чтобы не выполнять релиз под рутом.
558              
559             =item Выполнить релиз:
560              
561             $ minil release
562            
563             В процессе Минилла задаст несколько вопросов, в частности предложит выбрать номер новой версии.
564              
565             Обычно на все вопросы нужно отвечать кнопкой "enter". Иначе лучше прервать процесс и внести изменения в конфигурационные файлы.
566              
567             =back
568              
569             =head1 LINKS
570              
571             =over 4
572              
573             =item * perltidy и cp1251 / L<https://habr.com/ru/post/664308/>.
574              
575             =back
576              
577             =head1 AUTHOR
578              
579             Yaroslav O. Kosmina E<lt>darviarush@mail.ruE<gt>
580              
581             =head1 LICENSE
582              
583             ⚖ B<GPLv3>
584              
585             =cut