abbu 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +120 -8
- data/bin/abbu +185 -54
- data/docs/ABBU.md +226 -7
- data/docs/CHANGELOG.md +126 -0
- data/docs/CONTRIBUTING.md +13 -3
- data/docs/TODO.md +70 -62
- data/lib/abbu/archive.rb +66 -6
- data/lib/abbu/contact.rb +3 -1
- data/lib/abbu/diagnostic.rb +27 -0
- data/lib/abbu/exporters/csv_exporter.rb +2 -2
- data/lib/abbu/exporters/json_exporter.rb +7 -1
- data/lib/abbu/exporters/vcard_exporter.rb +19 -1
- data/lib/abbu/image_extractor.rb +134 -0
- data/lib/abbu/live_store.rb +104 -0
- data/lib/abbu/parse_error.rb +13 -0
- data/lib/abbu/parsers/plist_parser.rb +41 -11
- data/lib/abbu/parsers/sqlite_parser.rb +134 -63
- data/lib/abbu/query.rb +73 -0
- data/lib/abbu/schema_inspector.rb +133 -0
- data/lib/abbu/utils/contact_identity.rb +97 -0
- data/lib/abbu/utils/deduplicator.rb +130 -2
- data/lib/abbu/utils/image_resolver.rb +59 -0
- data/lib/abbu/utils/label_normalizer.rb +15 -0
- data/lib/abbu/utils/source_descriptor.rb +35 -0
- data/lib/abbu/version.rb +1 -1
- data/lib/abbu.rb +14 -2
- data/tasks/abbu.rake +1 -1
- metadata +19 -15
- data/bin/cleanse +0 -7
- data/bin/console +0 -10
- data/bin/dev +0 -7
- data/bin/lint +0 -7
- data/bin/outdated +0 -7
- data/bin/test +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e93fb2c992d75b467a430083bd474aef632c6f759ae858653e9b3ed44e261cc9
|
|
4
|
+
data.tar.gz: f346d67e1789116120ec804881b0f9660d6853c55102557e084d8be78184a0cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae3d1565ac2203067a1e2da4cb304eba8b112bfbd94b78f65c0a39a58f6a007835da1929fcfbc58cea13fdde43ac23eae1338b77fcf4466b31a2a2f40f31a3a6
|
|
7
|
+
data.tar.gz: 6a904be665a31a2254c139aced555dfcfd92af6612bec463ad776e5e99f939210954625c5141b17ee095518b0bd85d29f9dbfa795ec0f0ac34a9b8dd8231c6b0
|
data/README.md
CHANGED
|
@@ -2,18 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
# abbu
|
|
4
4
|
|
|
5
|
-
Read
|
|
5
|
+
Read-only Apple Contacts toolkit for `.abbu` archives and opt-in live macOS stores.
|
|
6
|
+
Version 0.4.0 adds querying, diagnostics, identity evidence, and safe image extraction
|
|
7
|
+
alongside CSV, JSON, and vCard export. The public API remains pre-1.0.
|
|
6
8
|
|
|
7
9
|
## Features
|
|
8
10
|
|
|
9
11
|
- Parse ABBU (Apple Contacts export) bundles
|
|
12
|
+
- Opt-in, read-only access to a local macOS Contacts store
|
|
10
13
|
- SQLite-backed contact extraction (modern macOS)
|
|
11
14
|
- Legacy plist `.abcdp` parsing (older macOS)
|
|
12
|
-
-
|
|
15
|
+
- Evidence-backed Apple Contacts fields: names, nicknames, prefix/suffix, job title, department, phonetics, pronouns, and more
|
|
13
16
|
- Rich relational data: addresses, URLs, notes, related names, social profiles
|
|
14
17
|
- Export to CSV, JSON, vCard 3.0
|
|
15
18
|
- CLI + Ruby API
|
|
16
|
-
-
|
|
19
|
+
- Source provenance and creation/modification timestamps
|
|
20
|
+
- Lossless raw labels alongside human-friendly normalization
|
|
21
|
+
- Schema introspection, tolerant diagnostics, and strict mode
|
|
22
|
+
- Chainable archive queries, exact identifier lookup, and TSV/JSON CLI search
|
|
23
|
+
- Provenance-aware duplicate suggestions without automatic merging
|
|
24
|
+
- Safe photo extraction with content detection and no destination overwrites
|
|
17
25
|
|
|
18
26
|
## Installation
|
|
19
27
|
|
|
@@ -36,13 +44,66 @@ require "abbu"
|
|
|
36
44
|
|
|
37
45
|
archive = Abbu.open("Contacts.abbu")
|
|
38
46
|
contacts = archive.contacts
|
|
47
|
+
schema = archive.schema_report # Evidence-only SQLite schema diagnostics
|
|
39
48
|
|
|
40
49
|
contacts.first.full_name # => "Honorable Stan \"Stretch\" Carver II"
|
|
41
|
-
contacts.first.emails # => [{ address: "stan@example.com", label: "Work" }]
|
|
42
|
-
contacts.first.phones # => [{ number: "555-1234", label: "Mobile" }]
|
|
50
|
+
contacts.first.emails # => [{ address: "stan@example.com", label: "Work", raw_label: "_$!<Work>!$_" }]
|
|
51
|
+
contacts.first.phones # => [{ number: "555-1234", label: "Mobile", raw_label: "Mobile" }]
|
|
43
52
|
contacts.first.job_title # => "Engineer"
|
|
53
|
+
|
|
54
|
+
# Copy resolved photos using safe, content-derived filenames.
|
|
55
|
+
result = archive.extract_images("exported-photos")
|
|
56
|
+
result.files # copied-file metadata, including source_path and media_type
|
|
57
|
+
result.diagnostics # image errors or destination_exists; may contain contact identifiers
|
|
58
|
+
|
|
59
|
+
# Recover safe records and inspect non-fatal data loss.
|
|
60
|
+
archive.diagnostics.each { |diagnostic| warn diagnostic.to_h }
|
|
61
|
+
|
|
62
|
+
# Or fail on the first corrupt/unsupported optional input.
|
|
63
|
+
strict_contacts = Abbu.open("Contacts.abbu", strict: true).contacts
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Live-store access is a separate, explicit API and never changes `Abbu.open` archive
|
|
67
|
+
validation:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# Auto-discover the current macOS user's AddressBook directory
|
|
71
|
+
live_contacts = Abbu.open_live.contacts
|
|
72
|
+
|
|
73
|
+
# Or supply a directory for automation and platform-independent testing
|
|
74
|
+
live_contacts = Abbu.open_live("/path/to/AddressBook").contacts
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Live databases are opened with SQLite's read-only mode. The process may require
|
|
78
|
+
Full Disk Access under **System Settings → Privacy & Security → Full Disk Access**.
|
|
79
|
+
Live inputs expose parser `diagnostics` and accept `strict: true` (CLI `--strict`).
|
|
80
|
+
The live CLI supports stats, deduplication, and exports; archive-only search, schema,
|
|
81
|
+
and image-extraction options are rejected explicitly.
|
|
82
|
+
|
|
83
|
+
Live access does not write Contacts databases. WAL/concurrent-writer consistency
|
|
84
|
+
remains unverified ([#28](https://github.com/scarver2/abbu/issues/28)).
|
|
85
|
+
|
|
86
|
+
Labeled values expose a normalized `label` for display and retain the source
|
|
87
|
+
value in `raw_label`. For example, `_$!<Mobile>!$_` becomes `Mobile` while the
|
|
88
|
+
original wrapper remains available in `raw_label`.
|
|
89
|
+
|
|
90
|
+
### Search and identifier lookup
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# Exact lookup normalizes email case/whitespace and phone punctuation.
|
|
94
|
+
archive.find_by_email("STAN@EXAMPLE.COM").each { |contact| puts contact.full_name }
|
|
95
|
+
archive.find_by_phone("(555) 123-4567").each { |contact| puts contact.full_name }
|
|
96
|
+
|
|
97
|
+
# Name and email search is case-insensitive and can be chained with `where`.
|
|
98
|
+
archive.where(company: "Acme Corp").search("stan").each do |contact|
|
|
99
|
+
puts [contact.full_name, contact.source[:relative_path]].join("\t")
|
|
100
|
+
end
|
|
44
101
|
```
|
|
45
102
|
|
|
103
|
+
Lookup methods return every match as an `Abbu::Query`; they never silently pick
|
|
104
|
+
one contact when the same identifier appears in multiple sources. Returned
|
|
105
|
+
contacts retain their parser-provided source provenance.
|
|
106
|
+
|
|
46
107
|
### Export
|
|
47
108
|
|
|
48
109
|
```ruby
|
|
@@ -66,6 +127,23 @@ dupes.each do |email, contacts|
|
|
|
66
127
|
end
|
|
67
128
|
```
|
|
68
129
|
|
|
130
|
+
For provenance-aware suggestions, use `#matches`. Results preserve both contacts,
|
|
131
|
+
their source records, raw and normalized evidence, confidence, and ambiguity:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
matches = Abbu::Utils::Deduplicator.new(archive.contacts).matches
|
|
135
|
+
matches.each do |match|
|
|
136
|
+
puts "#{match.confidence}: #{match.left.full_name} / #{match.right.full_name}"
|
|
137
|
+
pp match.sources
|
|
138
|
+
pp match.evidence
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Matching never mutates or collapses contacts. Merging requires a caller policy:
|
|
142
|
+
merged = matches.first.merge(policy: ->(left, right, evidence:) {
|
|
143
|
+
MyContactMerge.call(left, right, evidence: evidence)
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
69
147
|
## CLI
|
|
70
148
|
|
|
71
149
|
```bash
|
|
@@ -78,13 +156,41 @@ abbu Contacts.abbu -f json | jq .
|
|
|
78
156
|
# vCard export
|
|
79
157
|
abbu Contacts.abbu -f vcard -o contacts.vcf
|
|
80
158
|
|
|
159
|
+
# Copy contact photos to a selected directory
|
|
160
|
+
abbu Contacts.abbu --extract-images exported-photos
|
|
161
|
+
|
|
81
162
|
# Stats
|
|
82
163
|
abbu Contacts.abbu --stats
|
|
83
164
|
|
|
84
165
|
# Find duplicates
|
|
85
166
|
abbu Contacts.abbu --dedupe
|
|
167
|
+
|
|
168
|
+
# Read the current macOS user's live Contacts store
|
|
169
|
+
abbu --live --stats
|
|
170
|
+
|
|
171
|
+
# Read a caller-supplied AddressBook directory
|
|
172
|
+
abbu --live-path /path/to/AddressBook -f json
|
|
173
|
+
|
|
174
|
+
# Fail on the first corrupt or unsupported optional record/table.
|
|
175
|
+
abbu Contacts.abbu --stats --strict
|
|
176
|
+
|
|
177
|
+
# Inspect each SQLite schema without inferring undocumented semantics
|
|
178
|
+
abbu Contacts.abbu --schema
|
|
179
|
+
|
|
180
|
+
# Tab-separated search output: name, emails, phones, source-relative path
|
|
181
|
+
abbu Contacts.abbu --search stan
|
|
182
|
+
abbu Contacts.abbu --email stan@example.com
|
|
183
|
+
abbu Contacts.abbu --phone '(555) 123-4567'
|
|
184
|
+
|
|
185
|
+
# Stable structured search output using the regular contact JSON schema
|
|
186
|
+
abbu Contacts.abbu --search stan --json | jq .
|
|
86
187
|
```
|
|
87
188
|
|
|
189
|
+
CLI search defaults to tab-separated output and exits successfully when at least
|
|
190
|
+
one contact matches. A search with no matches exits with status 1; TSV mode emits
|
|
191
|
+
no output, while `--json` emits a valid empty array. This makes both modes
|
|
192
|
+
suitable for shell conditionals, pipelines, and agent integrations.
|
|
193
|
+
|
|
88
194
|
## Rake Tasks
|
|
89
195
|
|
|
90
196
|
```ruby
|
|
@@ -107,13 +213,19 @@ SQLite table schema, and format history.
|
|
|
107
213
|
|
|
108
214
|
See [`docs/TODO.md`](docs/TODO.md) for the full release schedule and feature checklist.
|
|
109
215
|
|
|
216
|
+
## Ruby Compatibility
|
|
217
|
+
|
|
218
|
+
`abbu` supports Ruby 3.3 and newer. CI exercises Ruby 3.3, 3.4, and 4.0;
|
|
219
|
+
Ruby 3.3 is the compatibility floor and designated lint/tooling job.
|
|
220
|
+
|
|
110
221
|
## Development
|
|
111
222
|
|
|
112
223
|
```bash
|
|
113
224
|
mise exec -- bundle install
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
225
|
+
bin/dev # Guard feedback loop
|
|
226
|
+
bin/spec # RSpec with the 100% coverage gate
|
|
227
|
+
bin/lint # RuboCop
|
|
228
|
+
bin/package # build and verify the gem in isolation
|
|
117
229
|
```
|
|
118
230
|
|
|
119
231
|
## Contributing
|
data/bin/abbu
CHANGED
|
@@ -4,90 +4,221 @@
|
|
|
4
4
|
|
|
5
5
|
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
6
6
|
|
|
7
|
+
require 'json'
|
|
7
8
|
require 'optparse'
|
|
8
9
|
require 'abbu'
|
|
9
10
|
|
|
10
11
|
options = {
|
|
11
12
|
format: nil,
|
|
12
13
|
output: nil,
|
|
14
|
+
extract_images: nil,
|
|
13
15
|
dedupe: false,
|
|
14
|
-
|
|
16
|
+
email: nil,
|
|
17
|
+
json: false,
|
|
18
|
+
live: nil,
|
|
19
|
+
live_path: nil,
|
|
20
|
+
phone: nil,
|
|
21
|
+
search: nil,
|
|
22
|
+
schema: false,
|
|
23
|
+
stats: false,
|
|
24
|
+
strict: false
|
|
15
25
|
}
|
|
16
26
|
|
|
17
|
-
parser = OptionParser.new
|
|
18
|
-
|
|
27
|
+
parser = OptionParser.new
|
|
28
|
+
parser.banner = 'Usage: abbu <file.abbu> [options] | abbu --live [options] | abbu --live-path PATH [options]'
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
parser.on('-f', '--format FORMAT', %w[csv json vcard], 'Export format (csv, json, vcard)') do |f|
|
|
31
|
+
options[:format] = f
|
|
32
|
+
end
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
parser.on('-o', '--output FILE', 'Output file (default: stdout)') do |o|
|
|
35
|
+
options[:output] = o
|
|
36
|
+
end
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
parser.on('--extract-images DIR', 'Copy contact images to DIR') do |dir|
|
|
39
|
+
options[:extract_images] = dir
|
|
40
|
+
end
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
parser.on('--stats', 'Print contact statistics') do
|
|
43
|
+
options[:stats] = true
|
|
44
|
+
end
|
|
35
45
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
end
|
|
46
|
+
parser.on('--dedupe', 'Find and print duplicate contacts') do
|
|
47
|
+
options[:dedupe] = true
|
|
48
|
+
end
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
parser.on('--search TERM', 'Find contacts by partial name or email') do |term|
|
|
51
|
+
options[:search] = term
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
parser.on('--strict', 'Raise on unsupported or corrupt optional data') do
|
|
55
|
+
options[:strict] = true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
parser.on('--schema', 'Print SQLite schema diagnostics as JSON') do
|
|
59
|
+
options[:schema] = true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
parser.on('--email ADDRESS', 'Find contacts by exact normalized email') do |address|
|
|
63
|
+
options[:email] = address
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
parser.on('--phone NUMBER', 'Find contacts by exact normalized phone number') do |number|
|
|
67
|
+
options[:phone] = number
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
parser.on('--json', 'Emit search results as structured JSON instead of TSV') do
|
|
71
|
+
options[:json] = true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
parser.on('-v', '--version', 'Print version') do
|
|
75
|
+
puts "abbu #{Abbu::VERSION}"
|
|
76
|
+
exit
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
parser.on('-h', '--help', 'Print this help') do
|
|
80
|
+
puts parser
|
|
81
|
+
exit
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
parser.on('--live', 'Auto-discover the current macOS Contacts store') do
|
|
85
|
+
options[:live] = true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
parser.on('--live-path PATH', 'Read the specified live AddressBook directory') do |path|
|
|
89
|
+
options[:live_path] = path
|
|
45
90
|
end
|
|
46
91
|
|
|
47
92
|
parser.parse!
|
|
48
93
|
|
|
94
|
+
live_mode = options[:live] || !options[:live_path].nil?
|
|
95
|
+
live_path = options[:live_path]
|
|
96
|
+
|
|
97
|
+
if options[:live] && live_path
|
|
98
|
+
warn 'Use only one of --live or --live-path PATH.'
|
|
99
|
+
exit 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if live_mode && ARGV.any?
|
|
103
|
+
warn 'Unexpected positional arguments in live mode; use --live-path PATH for an explicit store.'
|
|
104
|
+
exit 1
|
|
105
|
+
end
|
|
106
|
+
|
|
49
107
|
file = ARGV.shift
|
|
50
108
|
|
|
51
|
-
if file.nil?
|
|
109
|
+
if file.nil? && !live_mode
|
|
52
110
|
puts parser
|
|
53
111
|
exit 1
|
|
54
112
|
end
|
|
55
113
|
|
|
56
|
-
archive =
|
|
114
|
+
archive = nil
|
|
57
115
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
116
|
+
at_exit do
|
|
117
|
+
next if archive.nil? || archive.diagnostics.empty?
|
|
118
|
+
|
|
119
|
+
warn "Diagnostics: #{archive.diagnostics.count}"
|
|
120
|
+
archive.diagnostics.each do |diagnostic|
|
|
121
|
+
warn "- #{diagnostic.category}: #{diagnostic.message} [#{diagnostic.source}]"
|
|
122
|
+
end
|
|
64
123
|
end
|
|
65
124
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
125
|
+
begin
|
|
126
|
+
if live_mode && options.values_at(:extract_images, :schema, :search, :email, :phone, :json).any?
|
|
127
|
+
warn '--live supports --stats, --dedupe, --format, and --strict; archive-only options are not supported.'
|
|
128
|
+
exit 1
|
|
129
|
+
end
|
|
130
|
+
archive = live_mode ? Abbu.open_live(live_path, strict: options[:strict]) : Abbu.open(file, strict: options[:strict])
|
|
131
|
+
|
|
132
|
+
if options[:extract_images]
|
|
133
|
+
result = archive.extract_images(options[:extract_images])
|
|
134
|
+
puts "Extracted #{result.files.count} image(s) to #{File.expand_path(options[:extract_images])}"
|
|
135
|
+
result.diagnostics.each do |diagnostic|
|
|
136
|
+
warn "Image #{diagnostic[:code]}: #{diagnostic[:contact_name]} (#{diagnostic[:image_uri]})"
|
|
75
137
|
end
|
|
138
|
+
exit unless options[:format] || options[:stats] || options[:dedupe]
|
|
76
139
|
end
|
|
77
|
-
exit
|
|
78
|
-
end
|
|
79
140
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
141
|
+
if options[:schema]
|
|
142
|
+
puts JSON.pretty_generate(archive.schema_report)
|
|
143
|
+
exit
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
search_options = options.values_at(:search, :email, :phone).compact
|
|
147
|
+
if search_options.length > 1
|
|
148
|
+
warn 'Use only one of --search, --email, or --phone.'
|
|
149
|
+
exit 1
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
if options[:json] && search_options.empty?
|
|
153
|
+
warn '--json requires --search, --email, or --phone.'
|
|
154
|
+
exit 1
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
if search_options.any?
|
|
158
|
+
results = if options[:search]
|
|
159
|
+
archive.search(options[:search])
|
|
160
|
+
elsif options[:email]
|
|
161
|
+
archive.find_by_email(options[:email])
|
|
162
|
+
else
|
|
163
|
+
archive.find_by_phone(options[:phone])
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if options[:json]
|
|
167
|
+
Abbu::Exporters::JsonExporter.new(results).to_stdout
|
|
168
|
+
else
|
|
169
|
+
results.each do |contact|
|
|
170
|
+
fields = [
|
|
171
|
+
contact.full_name,
|
|
172
|
+
contact.emails.filter_map { |email| email[:address] }.join(','),
|
|
173
|
+
contact.phones.filter_map { |phone| phone[:number] }.join(','),
|
|
174
|
+
contact.source&.fetch(:relative_path, nil)
|
|
175
|
+
]
|
|
176
|
+
puts fields.map { |field| field.to_s.gsub(/[\t\r\n]/, ' ') }.join("\t")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
exit(results.any? ? 0 : 1)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if options[:stats]
|
|
183
|
+
contacts = archive.contacts
|
|
184
|
+
puts "Total contacts : #{contacts.count}"
|
|
185
|
+
puts "With email : #{contacts.count { |c| c.emails.any? }}"
|
|
186
|
+
puts "With phone : #{contacts.count { |c| c.phones.any? }}"
|
|
187
|
+
exit
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
if options[:dedupe]
|
|
191
|
+
require 'abbu/utils/deduplicator'
|
|
192
|
+
dupes = Abbu::Utils::Deduplicator.new(archive.contacts).duplicates
|
|
193
|
+
if dupes.empty?
|
|
194
|
+
puts 'No duplicates found.'
|
|
195
|
+
else
|
|
196
|
+
dupes.each do |email, contacts|
|
|
197
|
+
puts "Duplicate: #{email}"
|
|
198
|
+
contacts.each { |c| puts " - #{c.full_name}" }
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
exit
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
case options[:format]
|
|
205
|
+
when 'csv'
|
|
206
|
+
exporter = Abbu::Exporters::CsvExporter.new(archive.contacts)
|
|
207
|
+
options[:output] ? exporter.to_file(options[:output]) : exporter.to_stdout
|
|
208
|
+
when 'json'
|
|
209
|
+
exporter = Abbu::Exporters::JsonExporter.new(archive.contacts)
|
|
210
|
+
options[:output] ? exporter.to_file(options[:output]) : exporter.to_stdout
|
|
211
|
+
when 'vcard'
|
|
212
|
+
exporter = Abbu::Exporters::VcardExporter.new(archive.contacts)
|
|
213
|
+
options[:output] ? exporter.to_file(options[:output]) : exporter.to_stdout
|
|
214
|
+
else
|
|
215
|
+
puts parser
|
|
216
|
+
exit 1
|
|
217
|
+
end
|
|
218
|
+
rescue Abbu::LiveStore::Error => e
|
|
219
|
+
warn "abbu: #{e.message}"
|
|
92
220
|
exit 1
|
|
221
|
+
rescue Abbu::ParseError => e
|
|
222
|
+
warn "abbu: #{e.message}"
|
|
223
|
+
exit 2
|
|
93
224
|
end
|