csv_plus_plus 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +8 -3
- data/docs/CHANGELOG.md +16 -0
- data/lib/csv_plus_plus/a1_reference.rb +202 -0
- data/lib/csv_plus_plus/benchmarked_compiler.rb +3 -3
- data/lib/csv_plus_plus/cell.rb +1 -35
- data/lib/csv_plus_plus/cli.rb +43 -80
- data/lib/csv_plus_plus/cli_flag.rb +71 -70
- data/lib/csv_plus_plus/color.rb +1 -1
- data/lib/csv_plus_plus/compiler.rb +31 -21
- data/lib/csv_plus_plus/entities/ast_builder.rb +11 -4
- data/lib/csv_plus_plus/entities/boolean.rb +16 -9
- data/lib/csv_plus_plus/entities/builtins.rb +68 -40
- data/lib/csv_plus_plus/entities/date.rb +14 -11
- data/lib/csv_plus_plus/entities/entity.rb +11 -29
- data/lib/csv_plus_plus/entities/entity_with_arguments.rb +18 -31
- data/lib/csv_plus_plus/entities/function.rb +22 -11
- data/lib/csv_plus_plus/entities/function_call.rb +35 -11
- data/lib/csv_plus_plus/entities/has_identifier.rb +19 -0
- data/lib/csv_plus_plus/entities/number.rb +15 -10
- data/lib/csv_plus_plus/entities/reference.rb +77 -0
- data/lib/csv_plus_plus/entities/runtime_value.rb +36 -23
- data/lib/csv_plus_plus/entities/string.rb +13 -10
- data/lib/csv_plus_plus/entities.rb +2 -18
- data/lib/csv_plus_plus/error/cli_error.rb +17 -0
- data/lib/csv_plus_plus/error/compiler_error.rb +17 -0
- data/lib/csv_plus_plus/error/error.rb +18 -5
- data/lib/csv_plus_plus/error/formula_syntax_error.rb +12 -13
- data/lib/csv_plus_plus/error/modifier_syntax_error.rb +10 -36
- data/lib/csv_plus_plus/error/modifier_validation_error.rb +6 -32
- data/lib/csv_plus_plus/error/positional_error.rb +15 -0
- data/lib/csv_plus_plus/error/writer_error.rb +1 -1
- data/lib/csv_plus_plus/error.rb +4 -1
- data/lib/csv_plus_plus/error_formatter.rb +111 -0
- data/lib/csv_plus_plus/google_api_client.rb +18 -8
- data/lib/csv_plus_plus/lexer/racc_lexer.rb +144 -0
- data/lib/csv_plus_plus/lexer/tokenizer.rb +53 -17
- data/lib/csv_plus_plus/lexer.rb +40 -1
- data/lib/csv_plus_plus/modifier/data_validation.rb +1 -1
- data/lib/csv_plus_plus/modifier/expand.rb +17 -0
- data/lib/csv_plus_plus/modifier.rb +6 -1
- data/lib/csv_plus_plus/options/file_options.rb +49 -0
- data/lib/csv_plus_plus/options/google_sheets_options.rb +42 -0
- data/lib/csv_plus_plus/options/options.rb +97 -0
- data/lib/csv_plus_plus/options.rb +22 -110
- data/lib/csv_plus_plus/parser/cell_value.tab.rb +65 -66
- data/lib/csv_plus_plus/parser/code_section.tab.rb +92 -84
- data/lib/csv_plus_plus/parser/modifier.tab.rb +40 -30
- data/lib/csv_plus_plus/reader/csv.rb +50 -0
- data/lib/csv_plus_plus/reader/google_sheets.rb +129 -0
- data/lib/csv_plus_plus/reader/reader.rb +27 -0
- data/lib/csv_plus_plus/reader/rubyxl.rb +37 -0
- data/lib/csv_plus_plus/reader.rb +14 -0
- data/lib/csv_plus_plus/runtime/graph.rb +6 -6
- data/lib/csv_plus_plus/runtime/{position_tracker.rb → position.rb} +16 -5
- data/lib/csv_plus_plus/runtime/references.rb +32 -27
- data/lib/csv_plus_plus/runtime/runtime.rb +73 -67
- data/lib/csv_plus_plus/runtime/scope.rb +280 -0
- data/lib/csv_plus_plus/runtime.rb +9 -9
- data/lib/csv_plus_plus/source_code.rb +14 -9
- data/lib/csv_plus_plus/template.rb +17 -12
- data/lib/csv_plus_plus/version.rb +1 -1
- data/lib/csv_plus_plus/writer/csv.rb +32 -5
- data/lib/csv_plus_plus/writer/excel.rb +19 -6
- data/lib/csv_plus_plus/writer/file_backer_upper.rb +27 -14
- data/lib/csv_plus_plus/writer/google_sheets.rb +23 -129
- data/lib/csv_plus_plus/writer/{google_sheet_builder.rb → google_sheets_builder.rb} +39 -55
- data/lib/csv_plus_plus/writer/merger.rb +31 -0
- data/lib/csv_plus_plus/writer/open_document.rb +16 -2
- data/lib/csv_plus_plus/writer/rubyxl_builder.rb +68 -43
- data/lib/csv_plus_plus/writer/writer.rb +42 -0
- data/lib/csv_plus_plus/writer.rb +58 -19
- data/lib/csv_plus_plus.rb +26 -14
- metadata +37 -12
- data/lib/csv_plus_plus/entities/cell_reference.rb +0 -231
- data/lib/csv_plus_plus/entities/variable.rb +0 -37
- data/lib/csv_plus_plus/error/syntax_error.rb +0 -71
- data/lib/csv_plus_plus/google_options.rb +0 -32
- data/lib/csv_plus_plus/lexer/lexer.rb +0 -89
- data/lib/csv_plus_plus/runtime/can_define_references.rb +0 -87
- data/lib/csv_plus_plus/runtime/can_resolve_references.rb +0 -209
- data/lib/csv_plus_plus/writer/base_writer.rb +0 -45
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4462a2a82490271a95479970e10246ed7d8b1e8a62d21928a5f2b710b2bd511
|
4
|
+
data.tar.gz: 31ce301945c5cc4d3154395dfa62271637baba4af78d2efed9b13d779036e015
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a968bc15a47a6ac129b0a4a13df3dfb1997991548f13af9ac28598091b5562be8d7d1943a03f86a1a501533ff2eb974d1e2cc788f43fca27f09be455edcdba9f
|
7
|
+
data.tar.gz: f091403bede1fb2e751fad9b6937a836f0676e5ab284690fa2c8983fa746d90e9780c701d3a7ca1161fa9aca04b84263a1b7e7789dda866d6fe1caf975593287
|
data/README.md
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
![main](https://github.com/patrickomatic/csv-plus-plus/actions/workflows/rspec.yml/badge.svg)
|
1
2
|
[![Ruby Style Guide](https://img.shields.io/badge/code_style-community-brightgreen.svg)](https://rubystyle.guide)
|
2
3
|
[![Gem Version](https://badge.fury.io/rb/csv_plus_plus.svg)](https://badge.fury.io/rb/csv_plus_plus)
|
3
4
|
|
@@ -19,8 +20,8 @@ quantity := celladjacent(D)
|
|
19
20
|
def profit() (price * quantity) - fees
|
20
21
|
|
21
22
|
---
|
22
|
-
![[format=bold/align=center]]Date,Ticker,Price,Quantity,
|
23
|
-
![[expand]],[[format=
|
23
|
+
![[format=bold/align=center]]Date ,Ticker ,Price ,Quantity ,Profit ,Fees
|
24
|
+
![[expand]] ,[[format=italic]] , , ,"=profit()" ,=fees
|
24
25
|
```
|
25
26
|
|
26
27
|
And can be compiled into a `.xlsx` file by:
|
@@ -45,7 +46,7 @@ or if you want the very latest changes, clone this repository and run:
|
|
45
46
|
|
46
47
|
## Examples
|
47
48
|
|
48
|
-
Take a look at the [examples](
|
49
|
+
Take a look at the [repository of examples](https://github.com/patrickomatic/csvpp-examples) repository for a bunch of example `.csvpp` files.
|
49
50
|
|
50
51
|
## CLI Arguments
|
51
52
|
|
@@ -62,3 +63,7 @@ Usage: csv++ [options]
|
|
62
63
|
-x, --offset-columns OFFSET Apply the template offset by OFFSET cells
|
63
64
|
-y, --offset-rows OFFSET Apply the template offset by OFFSET rows
|
64
65
|
```
|
66
|
+
|
67
|
+
## See Also:
|
68
|
+
|
69
|
+
* [Supported features by output format](./docs/feature_matrix.csvpp)
|
data/docs/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
## main/upcoming
|
2
|
+
|
3
|
+
## v0.2.0
|
4
|
+
|
5
|
+
### **Breaking Changes**
|
6
|
+
|
7
|
+
- Removal of the $$ operator - to dereference variables you can just reference them by name and they will be resolved if they are defined. Otherwise they will be left alone in the output
|
8
|
+
|
9
|
+
### Non-breaking Changes
|
10
|
+
|
11
|
+
- Excel: fix the merging of existing values
|
12
|
+
- CSV: fix the merging of existing values
|
13
|
+
- Support merging in values from CSV (previously it would ignore/overwrite them)
|
14
|
+
- Allow for more generous spacing in the csv section (and reflect this in the examples)
|
15
|
+
- More type coverage
|
16
|
+
|
1
17
|
## v0.1.3
|
2
18
|
|
3
19
|
- Proper scoping of variables defined within an expand modifier
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module CSVPlusPlus
|
5
|
+
# A reference to a cell. Internally it is represented by a simple +cell_index+ and +row_index+ but there are
|
6
|
+
# functions for converting to and from A1-style formats. Supported formats are:
|
7
|
+
#
|
8
|
+
# * `1` - A reference to the entire first row
|
9
|
+
# * `A` - A reference to the entire first column
|
10
|
+
# * `A1` - A reference to the first cell (top left)
|
11
|
+
# * `A1:D10` - The range defined between A1 and D10
|
12
|
+
# * `Sheet1!B2` - Cell B2 on the sheet "Sheet1"
|
13
|
+
#
|
14
|
+
# @attr sheet_name [String, nil] The name of the sheet reference
|
15
|
+
# @attr_reader cell_index [Integer, nil] The cell index of the cell being referenced
|
16
|
+
# @attr_reader row_index [Integer, nil] The row index of the cell being referenced
|
17
|
+
# @attr_reader upper_cell_index [Integer, nil] If set, the cell reference is a range and this is the upper cell
|
18
|
+
# index of it
|
19
|
+
# @attr_reader upper_row_index [Integer, nil] If set, the cell reference is a range and this is the upper row index
|
20
|
+
# of in
|
21
|
+
# rubocop:disable Metrics/ClassLength
|
22
|
+
class A1Reference
|
23
|
+
extend ::T::Sig
|
24
|
+
|
25
|
+
sig { returns(::T.nilable(::String)) }
|
26
|
+
attr_accessor :sheet_name
|
27
|
+
|
28
|
+
sig { returns(::T.nilable(::Integer)) }
|
29
|
+
attr_reader :cell_index
|
30
|
+
|
31
|
+
sig { returns(::T.nilable(::Integer)) }
|
32
|
+
attr_reader :row_index
|
33
|
+
|
34
|
+
sig { returns(::T.nilable(::Integer)) }
|
35
|
+
attr_reader :upper_cell_index
|
36
|
+
|
37
|
+
sig { returns(::T.nilable(::Integer)) }
|
38
|
+
attr_reader :upper_row_index
|
39
|
+
|
40
|
+
sig { returns(::T.nilable(::CSVPlusPlus::Modifier::Expand)) }
|
41
|
+
attr_reader :scoped_to_expand
|
42
|
+
|
43
|
+
# TODO: this is getting gross, maybe define an actual parser
|
44
|
+
A1_NOTATION_REGEXP = /
|
45
|
+
^
|
46
|
+
(?:
|
47
|
+
(?:
|
48
|
+
(?:'([^'\\]|\\.)*') # allow for a single-quoted sheet name
|
49
|
+
|
|
50
|
+
(\w+) # or if it's not quoted, just allow \w+
|
51
|
+
)
|
52
|
+
! # if a sheet name is specified, it's always followed by a !
|
53
|
+
)?
|
54
|
+
([a-zA-Z0-9]+) # the only part required - something alphanumeric
|
55
|
+
(?: :([a-zA-Z0-9]+))? # and they might make it a range
|
56
|
+
$
|
57
|
+
/x
|
58
|
+
public_constant :A1_NOTATION_REGEXP
|
59
|
+
|
60
|
+
ALPHA = ::T.let(('A'..'Z').to_a.freeze, ::T::Array[::String])
|
61
|
+
private_constant :ALPHA
|
62
|
+
|
63
|
+
sig { params(cell_reference_string: ::String).returns(::T::Boolean) }
|
64
|
+
# Does the given +cell_reference_string+ conform to a valid cell reference?
|
65
|
+
#
|
66
|
+
# {https://developers.google.com/sheets/api/guides/concepts}
|
67
|
+
#
|
68
|
+
# @param cell_reference_string [::String] The string to check if it is a valid cell reference (we assume it's in
|
69
|
+
# A1 notation but maybe can support R1C1)
|
70
|
+
#
|
71
|
+
# @return [::T::Boolean]
|
72
|
+
def self.valid_cell_reference?(cell_reference_string)
|
73
|
+
!(cell_reference_string =~ ::CSVPlusPlus::A1Reference::A1_NOTATION_REGEXP).nil?
|
74
|
+
end
|
75
|
+
|
76
|
+
sig do
|
77
|
+
params(
|
78
|
+
ref: ::T.nilable(::String),
|
79
|
+
cell_index: ::T.nilable(::Integer),
|
80
|
+
row_index: ::T.nilable(::Integer),
|
81
|
+
scoped_to_expand: ::T.nilable(::CSVPlusPlus::Modifier::Expand)
|
82
|
+
).void
|
83
|
+
end
|
84
|
+
# Either +ref+, +cell_index+ or +row_index+ must be specified. If +ref+ is supplied it will be parsed to calculate
|
85
|
+
# +row_index+ and +cell_index.
|
86
|
+
#
|
87
|
+
# @param ref [String, nil] A raw user-inputted reference
|
88
|
+
# @param cell_index [Integer, nil] The index of the cell being referenced.
|
89
|
+
# @param row_index [Integer, nil] The index of the row being referenced.
|
90
|
+
# @param scoped_to_expand [Expand] The [[expand]] that this cell reference will be scoped to. In other words, it
|
91
|
+
# will only be able to be resolved if the position is within the bounds of the expand (it can't be referenced
|
92
|
+
# outside of the expand.)
|
93
|
+
def initialize(ref: nil, cell_index: nil, row_index: nil, scoped_to_expand: nil)
|
94
|
+
raise(::ArgumentError, 'Must specify :cell_index or :row_index') unless ref || cell_index || row_index
|
95
|
+
|
96
|
+
@scoped_to_expand = scoped_to_expand
|
97
|
+
|
98
|
+
if ref
|
99
|
+
from_a1_ref!(ref)
|
100
|
+
else
|
101
|
+
@cell_index = ::T.let(cell_index, ::T.nilable(::Integer))
|
102
|
+
@row_index = ::T.let(row_index, ::T.nilable(::Integer))
|
103
|
+
|
104
|
+
@upper_cell_index = ::T.let(nil, ::T.nilable(::Integer))
|
105
|
+
@upper_row_index = ::T.let(nil, ::T.nilable(::Integer))
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
sig { override.params(other: ::BasicObject).returns(::T::Boolean) }
|
110
|
+
# @param other [BasicObject]
|
111
|
+
#
|
112
|
+
# @return [boolean]
|
113
|
+
def ==(other)
|
114
|
+
case other
|
115
|
+
when self.class
|
116
|
+
@cell_index == other.cell_index && @row_index == other.row_index && @sheet_name == other.sheet_name \
|
117
|
+
&& @upper_cell_index == other.upper_cell_index && @upper_row_index == other.upper_row_index
|
118
|
+
else
|
119
|
+
false
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
sig { params(position: ::CSVPlusPlus::Runtime::Position).returns(::T.nilable(::String)) }
|
124
|
+
# Turns index-based/X,Y coordinates into a A1 format
|
125
|
+
#
|
126
|
+
# @param position [Position]
|
127
|
+
#
|
128
|
+
# @return [::String, nil]
|
129
|
+
def to_a1_ref(position)
|
130
|
+
row_index = position_row_index(position)
|
131
|
+
return unless row_index || @cell_index
|
132
|
+
|
133
|
+
rowref = row_index ? (row_index + 1).to_s : ''
|
134
|
+
cellref = @cell_index ? to_a1_cell_ref : ''
|
135
|
+
[cellref, rowref].join
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
sig { params(position: ::CSVPlusPlus::Runtime::Position).returns(::T.nilable(::Integer)) }
|
141
|
+
def position_row_index(position)
|
142
|
+
@scoped_to_expand ? position.row_index : @row_index
|
143
|
+
end
|
144
|
+
|
145
|
+
sig { returns(::String) }
|
146
|
+
# Turns a cell index into an A1 reference (just the "A" part - for example 0 == 'A', 1 == 'B', 2 == 'C', etc.)
|
147
|
+
#
|
148
|
+
# @return [::String]
|
149
|
+
def to_a1_cell_ref
|
150
|
+
c = @cell_index.dup
|
151
|
+
ref = ''
|
152
|
+
|
153
|
+
while c >= 0
|
154
|
+
# rubocop:disable Lint/ConstantResolution
|
155
|
+
ref += ::T.must(ALPHA[c % 26])
|
156
|
+
# rubocop:enable Lint/ConstantResolution
|
157
|
+
c = (c / 26).floor - 1
|
158
|
+
end
|
159
|
+
|
160
|
+
ref.reverse
|
161
|
+
end
|
162
|
+
|
163
|
+
sig { params(ref: ::String).void }
|
164
|
+
def from_a1_ref!(ref)
|
165
|
+
quoted_sheet_name, unquoted_sheet_name, lower_range, upper_range = ::T.must(
|
166
|
+
ref.strip.match(
|
167
|
+
::CSVPlusPlus::A1Reference::A1_NOTATION_REGEXP
|
168
|
+
)
|
169
|
+
).captures
|
170
|
+
|
171
|
+
@sheet_name = quoted_sheet_name || unquoted_sheet_name
|
172
|
+
|
173
|
+
parse_lower_range!(lower_range) if lower_range
|
174
|
+
parse_upper_range!(upper_range) if upper_range
|
175
|
+
end
|
176
|
+
|
177
|
+
sig { params(lower_range: ::String).void }
|
178
|
+
def parse_lower_range!(lower_range)
|
179
|
+
cell_ref, row_ref = ::T.must(lower_range.match(/^([a-zA-Z]+)?(\d+)?$/)).captures
|
180
|
+
@cell_index = from_a1_cell_ref!(cell_ref) if cell_ref
|
181
|
+
@row_index = Integer(row_ref, 10) - 1 if row_ref
|
182
|
+
end
|
183
|
+
|
184
|
+
sig { params(upper_range: ::String).void }
|
185
|
+
# TODO: make this less redundant with the above function
|
186
|
+
def parse_upper_range!(upper_range)
|
187
|
+
cell_ref, row_ref = ::T.must(upper_range.match(/^([a-zA-Z]+)?(\d+)?$/)).captures
|
188
|
+
@upper_cell_index = from_a1_cell_ref!(cell_ref) if cell_ref
|
189
|
+
@upper_row_index = Integer(row_ref, 10) - 1 if row_ref
|
190
|
+
end
|
191
|
+
|
192
|
+
sig { params(cell_ref: ::String).returns(::Integer) }
|
193
|
+
def from_a1_cell_ref!(cell_ref)
|
194
|
+
(cell_ref.upcase.chars.reduce(0) do |cell_index, letter|
|
195
|
+
# rubocop:disable Lint/ConstantResolution
|
196
|
+
(cell_index * 26) + ::T.must(ALPHA.find_index(letter)) + 1
|
197
|
+
# rubocop:enable Lint/ConstantResolution
|
198
|
+
end) - 1
|
199
|
+
end
|
200
|
+
end
|
201
|
+
# rubocop:enable Metrics/ClassLength
|
202
|
+
end
|
@@ -18,7 +18,7 @@ module CSVPlusPlus
|
|
18
18
|
|
19
19
|
sig do
|
20
20
|
params(
|
21
|
-
options: ::CSVPlusPlus::Options,
|
21
|
+
options: ::CSVPlusPlus::Options::Options,
|
22
22
|
runtime: ::CSVPlusPlus::Runtime::Runtime,
|
23
23
|
block: ::T.proc.params(compiler: ::CSVPlusPlus::Compiler).void
|
24
24
|
).void
|
@@ -40,7 +40,7 @@ module CSVPlusPlus
|
|
40
40
|
sig do
|
41
41
|
params(
|
42
42
|
benchmark: ::Benchmark::Report,
|
43
|
-
options: ::CSVPlusPlus::Options,
|
43
|
+
options: ::CSVPlusPlus::Options::Options,
|
44
44
|
runtime: ::CSVPlusPlus::Runtime::Runtime
|
45
45
|
).void
|
46
46
|
end
|
@@ -52,7 +52,7 @@ module CSVPlusPlus
|
|
52
52
|
@timings = ::T.let([], ::T::Array[::Benchmark::Tms])
|
53
53
|
end
|
54
54
|
|
55
|
-
sig {
|
55
|
+
sig { params(block: ::T.proc.params(position: ::CSVPlusPlus::Runtime::Position).void).void }
|
56
56
|
# Time the Compiler#outputting! stage
|
57
57
|
# rubocop:disable Naming/BlockForwarding
|
58
58
|
def outputting!(&block)
|
data/lib/csv_plus_plus/cell.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
module CSVPlusPlus
|
5
5
|
# A cell of a template
|
6
6
|
#
|
7
|
-
# @attr ast [Entity]
|
7
|
+
# @attr ast [Entity, nil] The AST of the formula in the cell (if there is one)
|
8
8
|
# @attr row_index [Integer] The cell's row index (starts at 0)
|
9
9
|
# @attr_reader index [Integer] The cell's index (starts at 0)
|
10
10
|
# @attr_reader modifier [Modifier] The modifier for this cell
|
@@ -23,26 +23,6 @@ module CSVPlusPlus
|
|
23
23
|
sig { returns(::CSVPlusPlus::Modifier::Modifier) }
|
24
24
|
attr_reader :modifier
|
25
25
|
|
26
|
-
sig do
|
27
|
-
params(
|
28
|
-
value: ::T.nilable(::String),
|
29
|
-
runtime: ::CSVPlusPlus::Runtime::Runtime,
|
30
|
-
modifier: ::CSVPlusPlus::Modifier::Modifier
|
31
|
-
).returns(::CSVPlusPlus::Cell)
|
32
|
-
end
|
33
|
-
# Parse a +value+ into a Cell object.
|
34
|
-
#
|
35
|
-
# @param value [String] A string value which should already have been processed through a CSV parser
|
36
|
-
# @param runtime [Runtime]
|
37
|
-
# @param modifier [Modifier]
|
38
|
-
#
|
39
|
-
# @return [Cell]
|
40
|
-
def self.parse(value, runtime:, modifier:)
|
41
|
-
new(value:, row_index: runtime.row_index, index: runtime.cell_index, modifier:).tap do |c|
|
42
|
-
c.ast = ::T.unsafe(::CSVPlusPlus::Parser::CellValue.new).parse(value, runtime)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
26
|
sig do
|
47
27
|
params(
|
48
28
|
index: ::Integer,
|
@@ -66,24 +46,10 @@ module CSVPlusPlus
|
|
66
46
|
# The +@value+ (cleaned up some)
|
67
47
|
#
|
68
48
|
# @return [::String]
|
69
|
-
# TODO: is this used?
|
70
49
|
def value
|
71
50
|
stripped = @value&.strip
|
72
51
|
|
73
52
|
stripped&.empty? ? nil : stripped
|
74
53
|
end
|
75
|
-
|
76
|
-
sig { params(runtime: ::CSVPlusPlus::Runtime::Runtime).returns(::T.nilable(::String)) }
|
77
|
-
# A compiled final representation of the cell. This can only happen after all cell have had variables and functions
|
78
|
-
# resolved.
|
79
|
-
#
|
80
|
-
# @param runtime [Runtime]
|
81
|
-
#
|
82
|
-
# @return [::String]
|
83
|
-
def evaluate(runtime)
|
84
|
-
return value unless @ast
|
85
|
-
|
86
|
-
"=#{@ast.evaluate(runtime)}"
|
87
|
-
end
|
88
54
|
end
|
89
55
|
end
|
data/lib/csv_plus_plus/cli.rb
CHANGED
@@ -2,111 +2,46 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module CSVPlusPlus
|
5
|
-
# Handle running the application with the
|
5
|
+
# Handle running the application with the supported +CLIFlag+s
|
6
6
|
#
|
7
|
-
# @attr options [Options
|
7
|
+
# @attr options [Options] The parsed CLI options
|
8
8
|
class CLI
|
9
9
|
extend ::T::Sig
|
10
10
|
|
11
|
-
sig { returns(::CSVPlusPlus::Options) }
|
11
|
+
sig { returns(::CSVPlusPlus::Options::Options) }
|
12
12
|
attr_accessor :options
|
13
13
|
|
14
|
+
sig { returns(::CSVPlusPlus::SourceCode) }
|
15
|
+
attr_accessor :source_code
|
16
|
+
|
14
17
|
sig { void }
|
15
18
|
# Handle CLI flags and launch the compiler
|
16
19
|
#
|
17
20
|
# @return [CLI]
|
18
21
|
def self.launch_compiler!
|
19
|
-
|
20
|
-
cli.main
|
22
|
+
new.main
|
21
23
|
rescue ::StandardError => e
|
22
|
-
|
24
|
+
warn(e.message)
|
23
25
|
exit(1)
|
24
26
|
end
|
25
27
|
|
26
28
|
sig { void }
|
27
29
|
# Initialize and parse the CLI flags provided to the program
|
28
30
|
def initialize
|
29
|
-
|
30
|
-
|
31
|
+
opts = parse_options
|
32
|
+
|
33
|
+
@source_code = ::T.let(::CSVPlusPlus::SourceCode.new(source_code_filename), ::CSVPlusPlus::SourceCode)
|
34
|
+
@options = ::T.let(apply_options(opts), ::CSVPlusPlus::Options::Options)
|
31
35
|
end
|
32
36
|
|
33
37
|
sig { void }
|
34
38
|
# Compile the given template using the given CLI flags
|
35
39
|
def main
|
36
|
-
::CSVPlusPlus.
|
37
|
-
end
|
38
|
-
|
39
|
-
sig { params(error: ::StandardError).void }
|
40
|
-
# Nicely handle a given error. How it's handled depends on if it's our error and if @options.verbose
|
41
|
-
#
|
42
|
-
# @param error [CSVPlusPlus::Error, Google::Apis::ClientError, StandardError]
|
43
|
-
def handle_error(error)
|
44
|
-
# make sure that we're on a newline (verbose mode might be in the middle of printing a benchmark)
|
45
|
-
puts("\n\n") if @options.verbose
|
46
|
-
|
47
|
-
case error
|
48
|
-
when ::CSVPlusPlus::Error::Error
|
49
|
-
handle_internal_error(error)
|
50
|
-
when ::Google::Apis::ClientError
|
51
|
-
handle_google_error(error)
|
52
|
-
else
|
53
|
-
unhandled_error(error)
|
54
|
-
end
|
40
|
+
::CSVPlusPlus.cli_compile(source_code, options)
|
55
41
|
end
|
56
42
|
|
57
43
|
private
|
58
44
|
|
59
|
-
sig { void }
|
60
|
-
# Handle the supplied command line options, setting +@options+ or throw an error if anything is invalid
|
61
|
-
def parse_options!
|
62
|
-
option_parser.parse!
|
63
|
-
validate_options
|
64
|
-
rescue ::OptionParser::InvalidOption => e
|
65
|
-
raise(::CSVPlusPlus::Error::Error, e.message)
|
66
|
-
end
|
67
|
-
|
68
|
-
sig { params(error: ::StandardError).void }
|
69
|
-
# An error was thrown that we weren't planning on
|
70
|
-
def unhandled_error(error)
|
71
|
-
warn(
|
72
|
-
<<~ERROR_MESSAGE)
|
73
|
-
An unexpected error was encountered. Please try running again with --verbose and
|
74
|
-
reporting the error at: https://github.com/patrickomatic/csv-plus-plus/issues/new'
|
75
|
-
ERROR_MESSAGE
|
76
|
-
|
77
|
-
return unless @options.verbose
|
78
|
-
|
79
|
-
warn(error.full_message)
|
80
|
-
warn("Cause: #{error.cause}") if error.cause
|
81
|
-
end
|
82
|
-
|
83
|
-
sig { params(error: ::CSVPlusPlus::Error::Error).void }
|
84
|
-
def handle_internal_error(error)
|
85
|
-
case error
|
86
|
-
when ::CSVPlusPlus::Error::SyntaxError
|
87
|
-
warn(@options.verbose ? error.to_verbose_trace : error.to_trace)
|
88
|
-
else
|
89
|
-
warn(error.message)
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
sig { params(error: ::Google::Apis::ClientError).void }
|
94
|
-
def handle_google_error(error)
|
95
|
-
warn("Error making Google Sheets API request: #{error.message}")
|
96
|
-
return unless @options.verbose
|
97
|
-
|
98
|
-
warn("#{error.status_code} Error making Google API request [#{error.message}]: #{error.body}")
|
99
|
-
end
|
100
|
-
|
101
|
-
sig { void }
|
102
|
-
def validate_options
|
103
|
-
error_message = @options.validate
|
104
|
-
return if error_message.nil?
|
105
|
-
|
106
|
-
puts(option_parser)
|
107
|
-
raise(::CSVPlusPlus::Error::Error, error_message)
|
108
|
-
end
|
109
|
-
|
110
45
|
sig { returns(::OptionParser) }
|
111
46
|
def option_parser
|
112
47
|
::OptionParser.new do |parser|
|
@@ -115,10 +50,38 @@ module CSVPlusPlus
|
|
115
50
|
exit
|
116
51
|
end
|
117
52
|
|
118
|
-
::SUPPORTED_CSVPP_FLAGS.each do |f|
|
119
|
-
parser.on(f.short_flag, f.long_flag, f.description)
|
53
|
+
::CSVPlusPlus::SUPPORTED_CSVPP_FLAGS.each do |f|
|
54
|
+
parser.on(f.short_flag, f.long_flag, f.description)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { params(opts: ::T::Hash[::Symbol, ::String]).returns(::CSVPlusPlus::Options::Options) }
|
60
|
+
def apply_options(opts)
|
61
|
+
::CSVPlusPlus::Options.from_cli_flags(opts, source_code.filename).tap do |options|
|
62
|
+
opts.each do |key, value|
|
63
|
+
::T.must(::CSVPlusPlus::FLAG_HANDLERS[key]).call(options, value) if ::CSVPlusPlus::FLAG_HANDLERS.key?(key)
|
120
64
|
end
|
121
65
|
end
|
122
66
|
end
|
67
|
+
|
68
|
+
sig { returns(::T::Hash[::Symbol, ::String]) }
|
69
|
+
def parse_options
|
70
|
+
{}.tap do |opts|
|
71
|
+
option_parser.parse!(into: opts)
|
72
|
+
end
|
73
|
+
rescue ::OptionParser::InvalidOption => e
|
74
|
+
puts(option_parser)
|
75
|
+
raise(::CSVPlusPlus::Error::CLIError, e.message)
|
76
|
+
end
|
77
|
+
|
78
|
+
sig { returns(::String) }
|
79
|
+
# NOTE: this must be called after #parse_options, since #parse_options modifiers +ARGV+
|
80
|
+
def source_code_filename
|
81
|
+
::ARGV.pop || raise(
|
82
|
+
::CSVPlusPlus::Error::CLIError,
|
83
|
+
'You must specify a source (.csvpp) file to compile as the last argument'
|
84
|
+
)
|
85
|
+
end
|
123
86
|
end
|
124
87
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
module CSVPlusPlus
|
@@ -8,83 +8,84 @@ module CSVPlusPlus
|
|
8
8
|
# @attr_reader long_flag [String] A definition of the long/word-based flag
|
9
9
|
# @attr_reader description [String] A description of what the flag does
|
10
10
|
# @attr_reader handler [Proc(Options, String)] A proc which is called to handle when this flag is seen
|
11
|
-
class
|
12
|
-
|
11
|
+
class CLIFlag
|
12
|
+
extend ::T::Sig
|
13
13
|
|
14
|
+
sig { returns(::String) }
|
15
|
+
attr_reader :description
|
16
|
+
|
17
|
+
sig { returns(::String) }
|
18
|
+
attr_reader :long_flag
|
19
|
+
|
20
|
+
sig { returns(::String) }
|
21
|
+
attr_reader :short_flag
|
22
|
+
|
23
|
+
sig { params(short_flag: ::String, long_flag: ::String, description: ::String).void }
|
14
24
|
# @param short_flag [String] A definition of the short/single-character flag
|
15
25
|
# @param long_flag [String] A definition of the long/word-based flag
|
16
26
|
# @param description [String] A description of what the flag does
|
17
|
-
|
18
|
-
def initialize(short_flag, long_flag, description, handler)
|
27
|
+
def initialize(short_flag, long_flag, description)
|
19
28
|
@short_flag = short_flag
|
20
29
|
@long_flag = long_flag
|
21
30
|
@description = description
|
22
|
-
@handler = handler
|
23
|
-
end
|
24
|
-
|
25
|
-
# @return [String]
|
26
|
-
def to_s
|
27
|
-
"#{@short_flag}, #{@long_flag} #{@description}"
|
28
31
|
end
|
29
32
|
end
|
30
|
-
end
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
->(options, v) { options.google_sheet_id = v }
|
51
|
-
),
|
52
|
-
::CSVPlusPlus::CliFlag.new(
|
53
|
-
'-k',
|
54
|
-
'--key-values KEY_VALUES',
|
55
|
-
'A comma-separated list of key=values which will be made available to the template',
|
56
|
-
lambda do |options, v|
|
57
|
-
options.key_values =
|
58
|
-
begin
|
59
|
-
[v.split('=')].to_h
|
60
|
-
rescue ::StandardError
|
61
|
-
{}
|
62
|
-
end
|
63
|
-
end
|
64
|
-
),
|
65
|
-
::CSVPlusPlus::CliFlag.new(
|
66
|
-
'-n SHEET_NAME',
|
67
|
-
'--sheet-name SHEET_NAME',
|
68
|
-
'The name of the sheet to apply the template to',
|
69
|
-
->(options, v) { options.sheet_name = v }
|
70
|
-
),
|
71
|
-
::CSVPlusPlus::CliFlag.new(
|
72
|
-
'-o OUTPUT_FILE',
|
73
|
-
'--output OUTPUT_FILE',
|
74
|
-
'The file to write to (must be .csv, .ods, .xls)',
|
75
|
-
->(options, v) { options.output_filename = v }
|
76
|
-
),
|
77
|
-
::CSVPlusPlus::CliFlag.new('-v', '--verbose', 'Enable verbose output', ->(options, _v) { options.verbose = true }),
|
78
|
-
::CSVPlusPlus::CliFlag.new(
|
79
|
-
'-x OFFSET',
|
80
|
-
'--offset-columns OFFSET',
|
81
|
-
'Apply the template offset by OFFSET cells',
|
82
|
-
->(options, v) { options.offset[0] = v }
|
83
|
-
),
|
84
|
-
::CSVPlusPlus::CliFlag.new(
|
85
|
-
'-y OFFSET',
|
86
|
-
'--offset-rows OFFSET',
|
87
|
-
'Apply the template offset by OFFSET rows',
|
88
|
-
->(options, v) { options.offset[1] = v }
|
34
|
+
FLAG_HANDLERS = ::T.let(
|
35
|
+
{
|
36
|
+
backup: ->(options, _v) { options.backup = true },
|
37
|
+
create: ->(options, _v) { options.create_if_not_exists = true },
|
38
|
+
'key-values': lambda { |options, v|
|
39
|
+
options.key_values =
|
40
|
+
begin
|
41
|
+
[v.split('=')].to_h
|
42
|
+
rescue ::StandardError
|
43
|
+
{}
|
44
|
+
end
|
45
|
+
},
|
46
|
+
'offset-columns': ->(options, v) { options.offset[0] = v },
|
47
|
+
'offset-rows': ->(options, v) { options.offset[1] = v },
|
48
|
+
output: ->(options, v) { options.output_filename = ::Pathname.new(v) },
|
49
|
+
verbose: ->(options, _v) { options.verbose = true }
|
50
|
+
},
|
51
|
+
::T::Hash[::Symbol, ::T.proc.params(options: ::CSVPlusPlus::Options::Options, v: ::String).void]
|
89
52
|
)
|
90
|
-
|
53
|
+
public_constant :FLAG_HANDLERS
|
54
|
+
|
55
|
+
SUPPORTED_CSVPP_FLAGS = ::T.let(
|
56
|
+
[
|
57
|
+
::CSVPlusPlus::CLIFlag.new('-b', '--backup', 'Create a backup of the spreadsheet before applying changes.'),
|
58
|
+
::CSVPlusPlus::CLIFlag.new(
|
59
|
+
'-c',
|
60
|
+
'--create',
|
61
|
+
"Create the sheet if it doesn't exist. It will use --sheet-name if specified"
|
62
|
+
),
|
63
|
+
::CSVPlusPlus::CLIFlag.new(
|
64
|
+
'-g SHEET_ID',
|
65
|
+
'--google-sheet-id SHEET_ID',
|
66
|
+
'The id of the sheet - you can extract this from the URL: ' \
|
67
|
+
'https://docs.google.com/spreadsheets/d/< ... SHEET_ID ... >/edit#gid=0'
|
68
|
+
),
|
69
|
+
::CSVPlusPlus::CLIFlag.new(
|
70
|
+
'-k',
|
71
|
+
'--key-values KEY_VALUES',
|
72
|
+
'A comma-separated list of key=values which will be made available to the template'
|
73
|
+
),
|
74
|
+
::CSVPlusPlus::CLIFlag.new(
|
75
|
+
'-n SHEET_NAME',
|
76
|
+
'--sheet-name SHEET_NAME',
|
77
|
+
'The name of the sheet to apply the template to'
|
78
|
+
),
|
79
|
+
::CSVPlusPlus::CLIFlag.new(
|
80
|
+
'-o OUTPUT_FILE',
|
81
|
+
'--output OUTPUT_FILE',
|
82
|
+
'The file to write to (must be .csv, .ods, .xls)'
|
83
|
+
),
|
84
|
+
::CSVPlusPlus::CLIFlag.new('-v', '--verbose', 'Enable verbose output'),
|
85
|
+
::CSVPlusPlus::CLIFlag.new('-x OFFSET', '--offset-columns OFFSET', 'Apply the template offset by OFFSET cells'),
|
86
|
+
::CSVPlusPlus::CLIFlag.new('-y OFFSET', '--offset-rows OFFSET', 'Apply the template offset by OFFSET rows')
|
87
|
+
].freeze,
|
88
|
+
::T::Array[::CSVPlusPlus::CLIFlag]
|
89
|
+
)
|
90
|
+
public_constant :SUPPORTED_CSVPP_FLAGS
|
91
|
+
end
|