json-csv 0.5.1 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/json-csv +74 -30
- data/lib/json-csv.rb +74 -30
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c1924dbb8fd7b4a477a13c6cfef62d737d25cb3
|
4
|
+
data.tar.gz: 828aebf03a83c06d86b9dd2d5412b0f0e6345b1b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86959ae536415a661d3cd586590e281509f09cddccf91f54a6f9bb4f8b282a4922bd092ece930295b87a907afb0bdfaa766481964a23f6bee8d196d70d0d2979
|
7
|
+
data.tar.gz: a1ee53fd352df70a2ba8a13fd7dd74efb6c66582af6fd7a3755fbba470de2e6247cab51e103a58b31cac3300630a47cb7643a703c55295abc0d7e07afc4e7f3d
|
data/bin/json-csv
CHANGED
@@ -15,7 +15,7 @@ require 'optparse'
|
|
15
15
|
require 'json'
|
16
16
|
|
17
17
|
class JsonCsv
|
18
|
-
VERSION = "0.5.
|
18
|
+
VERSION = "0.5.2"
|
19
19
|
VERSION_DATE = "2017-06-25"
|
20
20
|
|
21
21
|
DEFAULT_OPTS = {
|
@@ -29,13 +29,13 @@ class JsonCsv
|
|
29
29
|
}
|
30
30
|
|
31
31
|
class << self
|
32
|
-
def
|
32
|
+
def parse_argv(argv)
|
33
33
|
opts = DEFAULT_OPTS
|
34
34
|
|
35
|
-
OptionParser.new do |op|
|
35
|
+
argv = OptionParser.new do |op|
|
36
36
|
op.banner = <<-EOT
|
37
|
-
|
38
|
-
|
37
|
+
Converts JSON to CSV, and vice versa.
|
38
|
+
Usage: #{$0} [options] [--] [input-file [output-file]]
|
39
39
|
EOT
|
40
40
|
|
41
41
|
op.on("-i input-file", "--input input-file", "Input file (default STDIN)") do |input_file|
|
@@ -52,7 +52,6 @@ class JsonCsv
|
|
52
52
|
|
53
53
|
op.on("-d depth", "--depth depth", "Maximum depth of JSON-to-CSV conversion (default -1, unlimited)") do |depth|
|
54
54
|
opts[:depth] = depth.to_i
|
55
|
-
opts[:depth] += 1 if opts[:depth] > 0 # this is a fudge to use -1 as infinity
|
56
55
|
end
|
57
56
|
|
58
57
|
op.on("-e crlf|cr|lf", "--line-ending crlf|cr|lf", "Line endings for output file (default crlf).") do |ending|
|
@@ -78,12 +77,16 @@ class JsonCsv
|
|
78
77
|
exit
|
79
78
|
end
|
80
79
|
|
81
|
-
end.parse
|
82
|
-
|
80
|
+
end.parse(argv)
|
83
81
|
|
84
82
|
opts[:input_file] = argv.shift if argv.count > 0
|
85
83
|
opts[:output_file] = argv.shift if argv.count > 0
|
86
84
|
|
85
|
+
opts
|
86
|
+
end
|
87
|
+
|
88
|
+
def new_from_argv(argv)
|
89
|
+
opts = parse_argv(argv)
|
87
90
|
self.new(opts)
|
88
91
|
end
|
89
92
|
|
@@ -96,10 +99,14 @@ class JsonCsv
|
|
96
99
|
end
|
97
100
|
end
|
98
101
|
|
102
|
+
|
99
103
|
def initialize(opts)
|
100
104
|
@opts = DEFAULT_OPTS.merge(opts)
|
101
105
|
end
|
102
106
|
|
107
|
+
|
108
|
+
# Performs the JSON-to-CSV or CSV-to-JSON conversion, as specified in
|
109
|
+
# `opts` and the options passed in during `JsonCsv.new`.
|
103
110
|
def run(opts = {})
|
104
111
|
opts = @opts.merge(opts)
|
105
112
|
enc = opts[:source_encoding]
|
@@ -113,6 +120,7 @@ class JsonCsv
|
|
113
120
|
end
|
114
121
|
end
|
115
122
|
|
123
|
+
|
116
124
|
def convert_json_to_csv(opts = {})
|
117
125
|
opts = @opts.merge(opts)
|
118
126
|
|
@@ -133,7 +141,9 @@ class JsonCsv
|
|
133
141
|
end
|
134
142
|
|
135
143
|
debug(opts, "Getting headers from JSON data.")
|
136
|
-
|
144
|
+
depth = opts[:depth]
|
145
|
+
depth += 1 if depth > 0 # a fudge, in order to use -1 as infinity
|
146
|
+
headers = get_headers_from_json(input_fh, tmp_fh, depth)
|
137
147
|
|
138
148
|
input_fh.close
|
139
149
|
tmp_fh.close if tmp_fh
|
@@ -150,7 +160,7 @@ class JsonCsv
|
|
150
160
|
end
|
151
161
|
|
152
162
|
debug(opts, "Writing CSV output.")
|
153
|
-
output_csv(headers, data_fh, output_fh)
|
163
|
+
output_csv(headers, data_fh, output_fh, opts[:line_ending])
|
154
164
|
data_fh.close
|
155
165
|
output_fh.close
|
156
166
|
|
@@ -169,6 +179,10 @@ private
|
|
169
179
|
STDERR.puts("#{Time.now}\t#{msg}") if opts[:debug]
|
170
180
|
end
|
171
181
|
|
182
|
+
|
183
|
+
|
184
|
+
# Scans a JSON file at `input_fh` to determine the headers
|
185
|
+
# to use when writing CSV data.
|
172
186
|
# Returns a hash of `'header' => index` pairs, sorted.
|
173
187
|
def get_headers_from_json(input_fh, tmp_fh, depth)
|
174
188
|
headers = {}
|
@@ -182,6 +196,7 @@ private
|
|
182
196
|
sort_keys(headers)
|
183
197
|
end
|
184
198
|
|
199
|
+
# Helper function to get_headers_from_json --
|
185
200
|
# Sorts a hash with string keys by number of dots in the string,
|
186
201
|
# then alphabetically.
|
187
202
|
# Returns a hash of `'key' => index` pairs, in order of index.
|
@@ -197,22 +212,31 @@ private
|
|
197
212
|
sorted
|
198
213
|
end
|
199
214
|
|
215
|
+
# Helper function to sort_keys --
|
216
|
+
# Counts the number of dots in a string.
|
200
217
|
def count_dots(str)
|
201
218
|
str.chars.select{|c| c == "."}.count
|
202
219
|
end
|
203
220
|
|
204
|
-
def flat_assign(dest, key, value, depth)
|
205
|
-
flat_value = flatten_json(value, depth - 1)
|
206
|
-
if flat_value.is_a?(Hash)
|
207
|
-
flat_value.each do |k,v|
|
208
|
-
dest["#{key}.#{k}"] = v
|
209
|
-
end
|
210
|
-
else
|
211
|
-
dest["#{key}"] = flat_value
|
212
|
-
end
|
213
|
-
dest
|
214
|
-
end
|
215
221
|
|
222
|
+
|
223
|
+
# Returns a flattened representation of the given JSON-encodable
|
224
|
+
# data (that is: hashes, arrays, numbers, strings, and `nil`).
|
225
|
+
# Dot-separated string keys are used to encode nested hash and
|
226
|
+
# array structures.
|
227
|
+
#
|
228
|
+
# Hashes get flattened like so:
|
229
|
+
#
|
230
|
+
# flatten_json({a: {b: {c: 1, d: "x"}, c: nil}})
|
231
|
+
# #=> {"a.b.c" => 1, "a.b.d" => "x", "a.c" => nil}
|
232
|
+
#
|
233
|
+
# Arrays are turned into hashes like:
|
234
|
+
#
|
235
|
+
# flatten_json([0, 1, 2, {a: "x"])
|
236
|
+
# #=> {"0" => 0, "1" => 1, "2" => 2, "3.a" => "x"}
|
237
|
+
#
|
238
|
+
# Simple data (numbers, strings, nil) passes through unchanged.
|
239
|
+
#
|
216
240
|
def flatten_json(json, depth = -1)
|
217
241
|
return {} if depth == 0
|
218
242
|
|
@@ -230,23 +254,31 @@ private
|
|
230
254
|
end
|
231
255
|
flat
|
232
256
|
|
233
|
-
else # number or string
|
257
|
+
else # number or string or nil
|
234
258
|
json
|
235
259
|
end
|
236
260
|
end
|
237
261
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
262
|
+
## Helper function to flatten_json --
|
263
|
+
## Assigns a flattened value at the current key.
|
264
|
+
def flat_assign(dest, key, value, depth)
|
265
|
+
flat_value = flatten_json(value, depth - 1)
|
266
|
+
if flat_value.is_a?(Hash)
|
267
|
+
flat_value.each do |k,v|
|
268
|
+
dest["#{key}.#{k}"] = v
|
269
|
+
end
|
242
270
|
else
|
243
|
-
|
271
|
+
dest["#{key}"] = flat_value
|
244
272
|
end
|
273
|
+
dest
|
245
274
|
end
|
246
275
|
|
276
|
+
|
277
|
+
|
278
|
+
## Reads JSON data from data_fh, and writes CSV data (with header) to
|
279
|
+
## output_fh.
|
247
280
|
def output_csv(headers, data_fh, output_fh, line_ending)
|
248
|
-
|
249
|
-
output_fh.write(headers.map{|h| armor(h[0])}.join(","))
|
281
|
+
output_fh.write(headers.map{|h| csv_armor(h[0])}.join(","))
|
250
282
|
output_fh.write(line_ending)
|
251
283
|
|
252
284
|
header_count = headers.count
|
@@ -257,10 +289,22 @@ private
|
|
257
289
|
flat.each do |key, value|
|
258
290
|
output[headers[key]] = value if headers[key]
|
259
291
|
end
|
260
|
-
output_fh.write(output.map{|x|
|
292
|
+
output_fh.write(output.map{|x| csv_armor(x)}.join(","))
|
261
293
|
output_fh.write(line_ending)
|
262
294
|
end
|
263
295
|
end
|
296
|
+
|
297
|
+
## Helper function to output_csv --
|
298
|
+
## Returns a CSV-armored version of `val`.
|
299
|
+
## Escapes special characters and adds double-quotes if necessary.
|
300
|
+
def csv_armor(val)
|
301
|
+
str = val.to_s.gsub('"', '""')
|
302
|
+
if str.match(/[",\n]/)
|
303
|
+
'"' + str + '"'
|
304
|
+
else
|
305
|
+
str
|
306
|
+
end
|
307
|
+
end
|
264
308
|
end
|
265
309
|
|
266
310
|
|
data/lib/json-csv.rb
CHANGED
@@ -15,7 +15,7 @@ require 'optparse'
|
|
15
15
|
require 'json'
|
16
16
|
|
17
17
|
class JsonCsv
|
18
|
-
VERSION = "0.5.
|
18
|
+
VERSION = "0.5.2"
|
19
19
|
VERSION_DATE = "2017-06-25"
|
20
20
|
|
21
21
|
DEFAULT_OPTS = {
|
@@ -29,13 +29,13 @@ class JsonCsv
|
|
29
29
|
}
|
30
30
|
|
31
31
|
class << self
|
32
|
-
def
|
32
|
+
def parse_argv(argv)
|
33
33
|
opts = DEFAULT_OPTS
|
34
34
|
|
35
|
-
OptionParser.new do |op|
|
35
|
+
argv = OptionParser.new do |op|
|
36
36
|
op.banner = <<-EOT
|
37
|
-
|
38
|
-
|
37
|
+
Converts JSON to CSV, and vice versa.
|
38
|
+
Usage: #{$0} [options] [--] [input-file [output-file]]
|
39
39
|
EOT
|
40
40
|
|
41
41
|
op.on("-i input-file", "--input input-file", "Input file (default STDIN)") do |input_file|
|
@@ -52,7 +52,6 @@ class JsonCsv
|
|
52
52
|
|
53
53
|
op.on("-d depth", "--depth depth", "Maximum depth of JSON-to-CSV conversion (default -1, unlimited)") do |depth|
|
54
54
|
opts[:depth] = depth.to_i
|
55
|
-
opts[:depth] += 1 if opts[:depth] > 0 # this is a fudge to use -1 as infinity
|
56
55
|
end
|
57
56
|
|
58
57
|
op.on("-e crlf|cr|lf", "--line-ending crlf|cr|lf", "Line endings for output file (default crlf).") do |ending|
|
@@ -78,12 +77,16 @@ class JsonCsv
|
|
78
77
|
exit
|
79
78
|
end
|
80
79
|
|
81
|
-
end.parse
|
82
|
-
|
80
|
+
end.parse(argv)
|
83
81
|
|
84
82
|
opts[:input_file] = argv.shift if argv.count > 0
|
85
83
|
opts[:output_file] = argv.shift if argv.count > 0
|
86
84
|
|
85
|
+
opts
|
86
|
+
end
|
87
|
+
|
88
|
+
def new_from_argv(argv)
|
89
|
+
opts = parse_argv(argv)
|
87
90
|
self.new(opts)
|
88
91
|
end
|
89
92
|
|
@@ -96,10 +99,14 @@ class JsonCsv
|
|
96
99
|
end
|
97
100
|
end
|
98
101
|
|
102
|
+
|
99
103
|
def initialize(opts)
|
100
104
|
@opts = DEFAULT_OPTS.merge(opts)
|
101
105
|
end
|
102
106
|
|
107
|
+
|
108
|
+
# Performs the JSON-to-CSV or CSV-to-JSON conversion, as specified in
|
109
|
+
# `opts` and the options passed in during `JsonCsv.new`.
|
103
110
|
def run(opts = {})
|
104
111
|
opts = @opts.merge(opts)
|
105
112
|
enc = opts[:source_encoding]
|
@@ -113,6 +120,7 @@ class JsonCsv
|
|
113
120
|
end
|
114
121
|
end
|
115
122
|
|
123
|
+
|
116
124
|
def convert_json_to_csv(opts = {})
|
117
125
|
opts = @opts.merge(opts)
|
118
126
|
|
@@ -133,7 +141,9 @@ class JsonCsv
|
|
133
141
|
end
|
134
142
|
|
135
143
|
debug(opts, "Getting headers from JSON data.")
|
136
|
-
|
144
|
+
depth = opts[:depth]
|
145
|
+
depth += 1 if depth > 0 # a fudge, in order to use -1 as infinity
|
146
|
+
headers = get_headers_from_json(input_fh, tmp_fh, depth)
|
137
147
|
|
138
148
|
input_fh.close
|
139
149
|
tmp_fh.close if tmp_fh
|
@@ -150,7 +160,7 @@ class JsonCsv
|
|
150
160
|
end
|
151
161
|
|
152
162
|
debug(opts, "Writing CSV output.")
|
153
|
-
output_csv(headers, data_fh, output_fh)
|
163
|
+
output_csv(headers, data_fh, output_fh, opts[:line_ending])
|
154
164
|
data_fh.close
|
155
165
|
output_fh.close
|
156
166
|
|
@@ -169,6 +179,10 @@ private
|
|
169
179
|
STDERR.puts("#{Time.now}\t#{msg}") if opts[:debug]
|
170
180
|
end
|
171
181
|
|
182
|
+
|
183
|
+
|
184
|
+
# Scans a JSON file at `input_fh` to determine the headers
|
185
|
+
# to use when writing CSV data.
|
172
186
|
# Returns a hash of `'header' => index` pairs, sorted.
|
173
187
|
def get_headers_from_json(input_fh, tmp_fh, depth)
|
174
188
|
headers = {}
|
@@ -182,6 +196,7 @@ private
|
|
182
196
|
sort_keys(headers)
|
183
197
|
end
|
184
198
|
|
199
|
+
# Helper function to get_headers_from_json --
|
185
200
|
# Sorts a hash with string keys by number of dots in the string,
|
186
201
|
# then alphabetically.
|
187
202
|
# Returns a hash of `'key' => index` pairs, in order of index.
|
@@ -197,22 +212,31 @@ private
|
|
197
212
|
sorted
|
198
213
|
end
|
199
214
|
|
215
|
+
# Helper function to sort_keys --
|
216
|
+
# Counts the number of dots in a string.
|
200
217
|
def count_dots(str)
|
201
218
|
str.chars.select{|c| c == "."}.count
|
202
219
|
end
|
203
220
|
|
204
|
-
def flat_assign(dest, key, value, depth)
|
205
|
-
flat_value = flatten_json(value, depth - 1)
|
206
|
-
if flat_value.is_a?(Hash)
|
207
|
-
flat_value.each do |k,v|
|
208
|
-
dest["#{key}.#{k}"] = v
|
209
|
-
end
|
210
|
-
else
|
211
|
-
dest["#{key}"] = flat_value
|
212
|
-
end
|
213
|
-
dest
|
214
|
-
end
|
215
221
|
|
222
|
+
|
223
|
+
# Returns a flattened representation of the given JSON-encodable
|
224
|
+
# data (that is: hashes, arrays, numbers, strings, and `nil`).
|
225
|
+
# Dot-separated string keys are used to encode nested hash and
|
226
|
+
# array structures.
|
227
|
+
#
|
228
|
+
# Hashes get flattened like so:
|
229
|
+
#
|
230
|
+
# flatten_json({a: {b: {c: 1, d: "x"}, c: nil}})
|
231
|
+
# #=> {"a.b.c" => 1, "a.b.d" => "x", "a.c" => nil}
|
232
|
+
#
|
233
|
+
# Arrays are turned into hashes like:
|
234
|
+
#
|
235
|
+
# flatten_json([0, 1, 2, {a: "x"])
|
236
|
+
# #=> {"0" => 0, "1" => 1, "2" => 2, "3.a" => "x"}
|
237
|
+
#
|
238
|
+
# Simple data (numbers, strings, nil) passes through unchanged.
|
239
|
+
#
|
216
240
|
def flatten_json(json, depth = -1)
|
217
241
|
return {} if depth == 0
|
218
242
|
|
@@ -230,23 +254,31 @@ private
|
|
230
254
|
end
|
231
255
|
flat
|
232
256
|
|
233
|
-
else # number or string
|
257
|
+
else # number or string or nil
|
234
258
|
json
|
235
259
|
end
|
236
260
|
end
|
237
261
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
262
|
+
## Helper function to flatten_json --
|
263
|
+
## Assigns a flattened value at the current key.
|
264
|
+
def flat_assign(dest, key, value, depth)
|
265
|
+
flat_value = flatten_json(value, depth - 1)
|
266
|
+
if flat_value.is_a?(Hash)
|
267
|
+
flat_value.each do |k,v|
|
268
|
+
dest["#{key}.#{k}"] = v
|
269
|
+
end
|
242
270
|
else
|
243
|
-
|
271
|
+
dest["#{key}"] = flat_value
|
244
272
|
end
|
273
|
+
dest
|
245
274
|
end
|
246
275
|
|
276
|
+
|
277
|
+
|
278
|
+
## Reads JSON data from data_fh, and writes CSV data (with header) to
|
279
|
+
## output_fh.
|
247
280
|
def output_csv(headers, data_fh, output_fh, line_ending)
|
248
|
-
|
249
|
-
output_fh.write(headers.map{|h| armor(h[0])}.join(","))
|
281
|
+
output_fh.write(headers.map{|h| csv_armor(h[0])}.join(","))
|
250
282
|
output_fh.write(line_ending)
|
251
283
|
|
252
284
|
header_count = headers.count
|
@@ -257,10 +289,22 @@ private
|
|
257
289
|
flat.each do |key, value|
|
258
290
|
output[headers[key]] = value if headers[key]
|
259
291
|
end
|
260
|
-
output_fh.write(output.map{|x|
|
292
|
+
output_fh.write(output.map{|x| csv_armor(x)}.join(","))
|
261
293
|
output_fh.write(line_ending)
|
262
294
|
end
|
263
295
|
end
|
296
|
+
|
297
|
+
## Helper function to output_csv --
|
298
|
+
## Returns a CSV-armored version of `val`.
|
299
|
+
## Escapes special characters and adds double-quotes if necessary.
|
300
|
+
def csv_armor(val)
|
301
|
+
str = val.to_s.gsub('"', '""')
|
302
|
+
if str.match(/[",\n]/)
|
303
|
+
'"' + str + '"'
|
304
|
+
else
|
305
|
+
str
|
306
|
+
end
|
307
|
+
end
|
264
308
|
end
|
265
309
|
|
266
310
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: json-csv
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- pete gamache
|
@@ -42,5 +42,5 @@ rubyforge_project:
|
|
42
42
|
rubygems_version: 2.4.8
|
43
43
|
signing_key:
|
44
44
|
specification_version: 4
|
45
|
-
summary: A command-line JSON/CSV converter
|
45
|
+
summary: A customizable command-line JSON/CSV converter
|
46
46
|
test_files: []
|