json-csv 0.5.1 → 0.5.2
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/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: []
|