File Coverage

bin/vcardtidy
Criterion Covered Total %
statement 68 115 59.1
branch 13 36 36.1
condition 4 14 28.5
subroutine 10 12 83.3
pod n/a
total 95 177 53.6


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2 1     1   144185 use strict;
  1         1  
  1         50  
3 1     1   4 use warnings;
  1         1  
  1         44  
4 1     1   531 use OptArgs2;
  1         32777  
  1         8  
5 1     1   1205 use Path::Tiny;
  1         19710  
  1         108  
6 1     1   775 use Text::vCard::Addressbook;
  1         51157  
  1         12  
7 1     1   857 use Time::Piece;
  1         17112  
  1         6  
8              
9             our $VERSION = 'v1.1.0';
10              
11             my $opts = optargs(
12             comment => 'tidy (normalize) VCARD contact files',
13             optargs => [
14             files => {
15             isa => 'ArrayRef',
16             default => sub {
17             [ ( -t STDIN ) ? ( die OptArgs2::usage(__PACKAGE__) ) : '-' ]
18             },
19             greedy => 1,
20             comment => 'file to tidy (default is stdin)',
21             },
22             debug => {
23             isa => '--Flag',
24             alias => 'd',
25             comment => 'output debugging information to STDERR',
26             },
27             regex => {
28             isa => '--ArrayRef',
29             isa_name => 'REGEX',
30             alias => 'r',
31             comment => 'Regular expression to run against $_ first',
32             default => sub { [] },
33             },
34             force => {
35             isa => '--Flag',
36             alias => 'f',
37             comment => 'insert missing fields where required',
38             },
39             no_rev => {
40             isa => '--Flag',
41             alias => 'R',
42             comment => 'do not update REV value'
43             },
44             nothing => {
45             isa => '--Flag',
46             alias => 'n',
47             comment => q{don't modify files, only report errors},
48             },
49             vcard_version => {
50             isa => '--Str',
51             alias => 'v',
52             default => '4.0',
53             comment => 'value for vCard VERSION field'
54             },
55             version => {
56             isa => '--Flag',
57             alias => 'V',
58             comment => 'print version information and exit',
59             trigger => sub {
60             require File::Basename;
61             die File::Basename::basename($0)
62             . ' version '
63             . $VERSION . "\n";
64             },
65             },
66             ],
67             );
68              
69             my $dtstamp = localtime->strftime('%Y-%m-%dT%H%M%SZ');
70              
71             my $badcount = 0;
72             foreach my $f ( @{ $opts->{files} } ) {
73             $opts->{input} = $f;
74             vcardtidy($opts) || $badcount++;
75             }
76              
77             die "vcardtidy failure count: $badcount\n" if $badcount;
78              
79             sub vcardtidy {
80 1     1   3 my $opts = shift;
81              
82 1         2 my $data;
83             my $file;
84              
85 1 50       5 if ( $opts->{input} eq '-' ) {
86 0         0 local $/;
87 0         0 binmode STDIN, ':raw:encoding(UTF-8)';
88 0         0 $data = ;
89             }
90             else {
91 1         6 $file = path( $opts->{input} );
92 1         81 $data = $file->slurp( { binmode => ':raw:encoding(UTF-8)' } );
93             }
94              
95 1         3058 my $filtered = $data;
96 1         3 foreach my $user_re ( @{ $opts->{regex} } ) {
  1         6  
97 1     1   1530 use experimental 're_strict';
  1         2383  
  1         8  
98 1     1   198 use re 'strict';
  1         2  
  1         697  
99              
100 0 0       0 print STDERR "Regex: $user_re\n" if $opts->{debug};
101 0         0 my $old = $filtered;
102 0         0 eval {
103 0     0   0 local $SIG{ALRM} = sub { die "Timeout\n" };
  0         0  
104 0         0 alarm 1;
105 0         0 eval "\$filtered =~ $user_re";
106 0 0       0 die $@ if $@;
107 0         0 alarm 0;
108             };
109              
110 0 0       0 die "Regex failed: $user_re:" . $@ if $@;
111 0 0       0 _diff( $old, $filtered ) if $opts->{debug};
112             }
113              
114             my $ab =
115 1         4 eval { Text::vCard::Addressbook->new( { 'source_text' => $filtered } ); };
  1         14  
116              
117 1 50       6176 if ($@) {
118 0         0 warn "$opts->{input}: $@";
119 0         0 return 0;
120             }
121              
122 1         5 my @vcards = $ab->vcards;
123 1 50       11 if ( 0 == @vcards ) {
124 0         0 warn "$opts->{input}: No cards to tidy!\n";
125 0         0 return 1; # not considered an error. TODO If '-' then print?
126             }
127              
128 1         3 foreach my $vcard (@vcards) {
129 1         2 my $i = 0;
130 1   33     5 my $u = $vcard->get_simple_type('UID') // do {
131 1     1   9 use feature 'state';
  1         2  
  1         1796  
132 0         0 state @c = ( 'a' .. 'f', 0 .. 9 );
133 0         0 state $len_c = scalar @c;
134             my $uid = join '-', map {
135 0         0 join '',
136 0         0 map { $c[ rand($len_c) ] }
  0         0  
137             @$_
138             } [ 1 .. 8 ], [ 1 .. 4 ], [ 1 .. 4 ], [ 1 .. 4 ], [ 1 .. 12 ];
139              
140 0         0 $vcard->add_node( { 'node_type' => 'UID', } )->value($uid);
141 0         0 warn qq/$opts->{input}: VCARD $i missing UID (set to "$uid")\n/;
142 0         0 $uid;
143             };
144 1         73 $u =~ s/-.*//;
145              
146 1 50       6 if ( not $vcard->get('FN') ) {
147 0 0       0 if ( $opts->{force} ) {
148 0         0 $vcard->fn($u);
149 0         0 warn
150             qq/$opts->{input}: VCARD $i missing FN field (set to "$u")\n/;
151             }
152             else {
153 0         0 warn qq/$opts->{input}: VCARD $i missing FN field!\n/;
154 0         0 return 0;
155             }
156             }
157              
158 1 50       25 if ( not $vcard->get('N') ) {
159 0 0       0 if ( $opts->{force} ) {
160 0         0 $vcard->add_node(
161             {
162             'node_type' => 'N',
163             data => [ { value => $u . ';;;;' } ]
164             }
165             );
166 0         0 warn
167             qq/$opts->{input}: VCARD $i missing N type (set to "$u")\n/;
168             }
169             else {
170 0         0 warn qq/$opts->{input}: VCARD $i missing N field!\n/;
171 0         0 return 0;
172             }
173             }
174              
175 1         20 my $v = $vcard->version;
176 1 50 33     64 if ( not length $v ) {
    50          
177 0         0 $vcard->version( $opts->{vcard_version} );
178 0         0 warn qq/$opts->{input}: VCARD $i missing VERSION /
179             . qq/(set to "$opts->{vcard_version}")\n/;
180             }
181             elsif ( $v ne $opts->{vcard_version} and $opts->{force} ) {
182 0         0 $vcard->version( $opts->{vcard_version} );
183 0         0 warn qq/$opts->{input}: forcing VCARD $i VERSION /
184             . qq/to $opts->{vcard_version}\n/;
185             }
186              
187 1 50       7 $vcard->REV($dtstamp) unless $opts->{no_rev};
188 1         295 $vcard->PRODID("vcardtidy $VERSION");
189              
190 1         168 $i++;
191             }
192              
193             # Remove duplicate fields
194 1         2 my $prev = '';
195 1         2 my $seen;
196             my $tidy = join '', map {
197 1         8 $seen = $_ eq $prev;
  14         6551  
198 14         20 $prev = $_;
199 14 100       39 $seen ? () : $_ . "\x0D\x0A"
200             } split "\x0D\x0A", $ab->export;
201              
202             # Fix for multiple categories
203 1         27 while ( $tidy =~ s/^(CATEGORIES:.*?)\\,/$1,/mg ) { }
204              
205 1 50       20 if ( $opts->{debug} ) {
206 0         0 print STDERR "vcardtidy:\n";
207 0         0 _diff( $data, $tidy );
208             }
209              
210 1 50       5 if ( $opts->{input} eq '-' ) {
211 0         0 binmode STDOUT, ':raw:encoding(UTF-8)';
212 0         0 print $tidy;
213             }
214             else {
215 1         3 my $data2 = $data;
216 1         7 $data2 =~ s/^REV:.*\015\012//m;
217 1         32 $data2 =~ s/^PRODID:.*\015\012//m;
218              
219 1         3 my $tidy2 = $tidy;
220 1         11 $tidy2 =~ s/^REV:.*\015\012//m;
221 1         7 $tidy2 =~ s/^PRODID:.*\015\012//m;
222              
223             $file->spew( { binmode => ':raw:encoding(UTF-8)' }, $tidy )
224             unless $opts->{nothing}
225 1 50 33     20 or ( $data2 eq $tidy2 and not $opts->{force} );
      33        
226             }
227              
228 1         9490 1;
229             }
230              
231             sub _diff {
232 0     0     my ( $t1, $t2 ) = @_;
233 0           require Text::Diff;
234 0   0       print STDERR ( Text::Diff::diff( \$t1, \$t2 ) || "(No change)\n" ) =~
235             s/^/ /gmr;
236             }
237              
238             __END__