mixml 0.0.3 → 0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,169 @@
1
+ # mixml
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/mixml.png)](http://badge.fury.io/rb/mixml)
4
+ [![Build Status](https://travis-ci.org/jochenseeber/mixml.png?branch=master)](https://travis-ci.org/jochenseeber/mixml)
5
+ [![Coverage Status](https://coveralls.io/repos/jochenseeber/mixml/badge.png?branch=master)](https://coveralls.io/r/jochenseeber/mixml?branch=master)
6
+
7
+ Mixml is a small tool to greatly simplify the tedious task of changing multiple multiple XML files at once. Its main
8
+ purpose is to spare me from having to use XSLT ever again. You can use mixml to change XML files in the following ways:
9
+
10
+ * Pretty print
11
+ * Remove nodes
12
+ * Add nodes
13
+ * Replace nodes
14
+ * Rename nodes
15
+ * Change node values
16
+
17
+ For example, the following command will remove all attributes named `id` from the supplied XML files:
18
+
19
+ mixml remove --inplace --xpath '//@id' *.xml
20
+
21
+ Mixml also supports a simple DSL to perform scripted changes. To perform the same as above using a script, save the
22
+ following in `test.mixml`:
23
+
24
+ xpath '//@id' do
25
+ remove
26
+ end
27
+
28
+ and then call:
29
+
30
+ mixml execute --script test.mixml *.xml
31
+
32
+ You can also use mixml directly in your Ruby code:
33
+
34
+ require 'mixml'
35
+
36
+ tool = Mixml::Tool.new do |t|
37
+ t.save = true
38
+ end
39
+
40
+ tool.work('one.xml', 'two.xml') do
41
+ xpath '//@id' do
42
+ remove
43
+ end
44
+ end
45
+
46
+ Mixml supports building replacement values using
47
+
48
+ * [Ruby string interpolation](http://en.wikibooks.org/wiki/Ruby_Programming/Syntax/Literals#Interpolation)
49
+ * [Erubis templates](http://www.kuwata-lab.com/erubis/)
50
+ * [Nokogiri's XML DSL](http://nokogiri.org/Nokogiri/XML/Builder.html)
51
+ * Plain Ruby
52
+
53
+ You can find more usage examples [here](demo/tool.md).
54
+
55
+ ## Installation
56
+
57
+ Install mixml:
58
+
59
+ $ gem install mixml
60
+
61
+ ## Usage
62
+
63
+ Use the following command to get help:
64
+
65
+ mixml --help
66
+
67
+ ### Pretty print XML
68
+
69
+ mixml pretty *.xml
70
+
71
+ ### Remove nodes
72
+
73
+ mixml remove --xpath '//addresses' *.xml
74
+
75
+ ### Rename nodes
76
+
77
+ mixml rename --xpath '//addresses' --template 'addressbook' *.xml
78
+
79
+ ### Replace nodes
80
+
81
+ mixml replace --xpath '//addresses' --template '<addressbook/>' *.xml
82
+
83
+ ### Append nodes
84
+
85
+ mixml append --xpath '/list' --template '<addressbook/>' *.xml
86
+
87
+ ### Set node value
88
+
89
+ mixml value --xpath '//addresses/@name' --template 'default' *.xml
90
+
91
+ ### Execute a script
92
+
93
+ Example script in `test.mixml`
94
+
95
+ xpath '//addresses[name="default"]' do
96
+ remove
97
+ end
98
+ xpath '//addresses' do
99
+ replace template '<addressbook/>'
100
+ end
101
+
102
+ Use the following command to run the script
103
+
104
+ mixml execute --script test.mixml *.xml
105
+
106
+ Script commands:
107
+
108
+ xpath 'xpath-expression' do
109
+ remove # Remove node
110
+ replace 'xml' # Replace node
111
+ append 'xml'
112
+ value 'text' # Set node value
113
+ rename 'text' # Rename node
114
+ end
115
+
116
+ Instead of using strings for parameters, you can also use a template expression:
117
+
118
+ xpath 'xpath-expression' do
119
+ replace template 'special-{=node.name}' # Prefix nodes with 'special-''
120
+ end
121
+
122
+ This works for all commands that take a string parameter. We use [Erubis](http://www.kuwata-lab.com/erubis) as
123
+ templating engine, and `{` and `}` as delimiters.
124
+
125
+ In addition, you can also use [Nokogiri](http://http://nokogiri.org/)'s
126
+ [builder component](http://nokogiri.org/Nokogiri/XML/Builder.html) to create the XML that replaces an element:
127
+
128
+ xpath '//addresses' do
129
+ replace xml ->(node, xml) {
130
+ xml.addressbook(:name => node['name'])
131
+ }
132
+ end
133
+
134
+ ### Use CSS rules to select nodes
135
+
136
+ You can also use CSS rules instead of XPath expressions to select the nodes to process:
137
+
138
+ css 'addresses:first-child', 'addresses:last-child' do
139
+ remove
140
+ end
141
+
142
+ ### Evaluate an expression
143
+
144
+ You can also pass the script to execute directly to mixml:
145
+
146
+ mixml execute --expression 'xpath("//addresses") { remove }' *.xml
147
+
148
+ ### Write results
149
+
150
+ The standard setting is to leave the input files unchanged and print the resulting files. You can replace the input
151
+ files with the changed XML by using the `inplace` option:
152
+
153
+ mixml remove --inplace --xpath '//addresses' test.xml
154
+
155
+ This will remove all elements named `group` from `test.xml`.
156
+
157
+ ### Pretty print results
158
+
159
+ To pretty print the output, use the `pretty` option:
160
+
161
+ mixml remove --inplace --xpath '//addresses' --pretty test.xml
162
+
163
+ ## Contributing
164
+
165
+ 1. Fork the GitHub repository: [https://github.com/jochenseeber/mixml/fork](https://github.com/jochenseeber/mixml/fork)
166
+ 2. Create your feature branch: `git checkout -b my-new-feature`
167
+ 3. Commit your changes: `git commit -am 'Add some feature'`
168
+ 4. Push to the branch: `git push origin my-new-feature`
169
+ 5. Create a new Pull Request
@@ -0,0 +1,84 @@
1
+ require 'fileutils'
2
+ require 'rspec/expectations'
3
+ require 'rspec/collection_matchers'
4
+ require 'equivalent-xml'
5
+ require 'nokogiri'
6
+
7
+ include RSpec::Matchers
8
+
9
+ # File with test content
10
+ class TestFile
11
+ include RSpec::Matchers
12
+
13
+ # @return [String] File name
14
+ attr_reader :file_name
15
+
16
+ # @param file_name [String] File name
17
+ def initialize(file_name)
18
+ @file_name = file_name
19
+ end
20
+
21
+ # Write text into file
22
+ #
23
+ # @param text [String] Text to write
24
+ # @return [void]
25
+ def <<(text)
26
+ File.open(@file_name, 'w') do |f|
27
+ f.write text
28
+ end
29
+ end
30
+
31
+ # Expect file to match content
32
+ #
33
+ # @param object [String, Nokogiri::XML::Document] Text or XML to match
34
+ # @return [void]
35
+ def matches(object)
36
+ content = File.read(@file_name)
37
+ if object.is_a?(Nokogiri::XML::Document) then
38
+ expect(content).to be_equivalent_to(object).respecting_element_order
39
+ else
40
+ expect(content).to be(object)
41
+ end
42
+ end
43
+ end
44
+
45
+ # Create a new test file
46
+ def file(name)
47
+ FileUtils.mkpath(File.dirname(name))
48
+ TestFile.new(name)
49
+ end
50
+
51
+ # Create a new XML document
52
+ def xml(text)
53
+ Nokogiri::XML(text)
54
+ end
55
+
56
+ # Redirect stdout and return output
57
+ #
58
+ # @yield Execute block with redirected stdout
59
+ # @return [String] Output
60
+ def redirect
61
+ begin
62
+ old_stdout = $stdout
63
+ $stdout = StringIO.new('', 'w')
64
+ yield
65
+ $stdout.string
66
+ ensure
67
+ $stdout = old_stdout
68
+ end
69
+ end
70
+
71
+ # Define a new matcher to compare text files ignoring empty lines as well as leading and trailing spaces.
72
+ RSpec::Matchers.define :match_text do |expected|
73
+ # Remove empty lines and leading and trailing spaces from text
74
+ #
75
+ # @param text [String] Text to clean
76
+ # @return [String] cleaned text
77
+ def clean(text)
78
+ text.gsub(/\n+/, "\n").gsub(/^\s+|\s+$/, '')
79
+ end
80
+
81
+ match do |actual|
82
+ clean(actual) == clean(expected)
83
+ end
84
+ end
@@ -0,0 +1,334 @@
1
+ # Usage examples
2
+
3
+ ## Setup
4
+
5
+ First we need to create a new Tool object and load an XML file. We create a Mixml tool object and use a
6
+ [helper](applique/test.rb) to load the following XML for each example.
7
+
8
+ require 'mixml'
9
+
10
+ Before do
11
+ @tool = Mixml::Tool.new do |t|
12
+ # Pretty print output
13
+ t.pretty = true
14
+
15
+ # Save output after processing
16
+ t.save = true
17
+
18
+ # Don't print documents after processing
19
+ t.print = false
20
+ end
21
+
22
+ # Save test.xml
23
+ file('test.xml') << %{
24
+ <list>
25
+ <philosopher name="Hobbes"/>
26
+ <philosopher name="Rawls"/>
27
+ </list>
28
+ }
29
+
30
+ @tool.load('test.xml')
31
+ end
32
+
33
+ ## Remove nodes
34
+
35
+ Select some nodes with an XPath expression and then remove them
36
+
37
+ @tool.execute do
38
+ xpath '//philosopher' do
39
+ remove
40
+ end
41
+ end
42
+ @tool.flush
43
+
44
+ file('test.xml').matches xml %{
45
+ <list/>
46
+ }
47
+
48
+ ## Replace nodes
49
+
50
+ Select some elements with an XPath expression and then change the element name.
51
+
52
+ @tool.execute do
53
+ xpath '//*[@name = "Hobbes"]' do
54
+ replace '<tiger name="Hobbes"/>'
55
+ end
56
+ end
57
+ @tool.flush
58
+
59
+ file('test.xml').matches xml %{
60
+ <list>
61
+ <tiger name="Hobbes"/>
62
+ <philosopher name="Rawls"/>
63
+ </list>
64
+ }
65
+
66
+ ## Replace nodes with string interpolation
67
+
68
+ Ruby [string interpolation](http://en.wikibooks.org/wiki/Ruby_Programming/Syntax/Literals#Interpolation) is performed on
69
+ string parameters, so you can also select some elements with an XPath expression and then change each element using a
70
+ Ruby expression.
71
+
72
+ @tool.execute do
73
+ xpath '//*[@name = "Hobbes"]' do
74
+ replace '<tiger-and-#{node.name} name="Hobbes"/>'
75
+ end
76
+ end
77
+ @tool.flush
78
+
79
+ file('test.xml').matches xml %{
80
+ <list>
81
+ <tiger-and-philosopher name="Hobbes"/>
82
+ <philosopher name="Rawls"/>
83
+ </list>
84
+ }
85
+
86
+ This works for all commands that take a string parameter.
87
+
88
+ ## Replace nodes with a template
89
+
90
+ If you prefer, you can also use template expressions instead of string parameters. Mixml uses
91
+ [Erubis](http://www.kuwata-lab.com/erubis) as templating engine, and `{` and `}` as delimiters. With this, you can
92
+ select some elements with an XPath expression and then replace each element.
93
+
94
+ @tool.execute do
95
+ xpath '//*[@name = "Hobbes"]' do
96
+ replace template '<tiger-and-{=node.name} name="{=node["name"]}"/>'
97
+ end
98
+ end
99
+ @tool.flush
100
+
101
+ file('test.xml').matches xml %{
102
+ <list>
103
+ <tiger-and-philosopher name="Hobbes"/>
104
+ <philosopher name="Rawls"/>
105
+ </list>
106
+ }
107
+
108
+ This works for all commands that take a string parameter.
109
+
110
+ ## Replace nodes with XML
111
+
112
+ If you prefer, you can also use an XML builder to create values using the simple DSL provided by
113
+ [Nokogiri](http://nokogiri.org/Nokogiri/XML/Builder.html). Using this, you can select some elements with an XPath
114
+ expression and then replace each element.
115
+
116
+ @tool.execute do
117
+ xpath '//*[@name = "Hobbes"]' do
118
+ replace xml ->(node, xml) {
119
+ xml.send(:"tiger-and-philosopher", :name => node['name'])
120
+ }
121
+ end
122
+ end
123
+ @tool.flush
124
+
125
+ file('test.xml').matches xml %{
126
+ <list>
127
+ <tiger-and-philosopher name="Hobbes"/>
128
+ <philosopher name="Rawls"/>
129
+ </list>
130
+ }
131
+
132
+ This works for all commands that take XML text as parameter (e.g. replace and append).
133
+
134
+ ## Replace nodes with Ruby
135
+
136
+ If you prefer, you can also use plain Ruby code to create values. with this, you can select some elements with an XPath
137
+ expression and then replace each element.
138
+
139
+ @tool.execute do
140
+ node xpath '//*[@name = "Hobbes"]' do |node|
141
+ node.name = "tiger-and-#{node.name}"
142
+ end
143
+ end
144
+ @tool.flush
145
+
146
+ file('test.xml').matches xml %{
147
+ <list>
148
+ <tiger-and-philosopher name="Hobbes"/>
149
+ <philosopher name="Rawls"/>
150
+ </list>
151
+ }
152
+
153
+ Instead of processing each node individually, you can also process the selected node sets.
154
+
155
+ @tool.execute do
156
+ nodes xpath('//*[@name = "Hobbes"]') do |nodeset|
157
+ nodeset.each do |node|
158
+ node.name = "tiger-and-#{node.name}"
159
+ end
160
+ end
161
+ end
162
+ @tool.flush
163
+
164
+ file('test.xml').matches xml %{
165
+ <list>
166
+ <tiger-and-philosopher name="Hobbes"/>
167
+ <philosopher name="Rawls"/>
168
+ </list>
169
+ }
170
+
171
+ This works for all commands that take XML text as parameter (e.g. replace and append).
172
+
173
+ ## Append nodes
174
+
175
+ Select some elements with an XPath expression and then append children to them.
176
+
177
+ @tool.execute do
178
+ xpath '/list' do
179
+ append '<tiger name="Hobbes"/>'
180
+ end
181
+ end
182
+ @tool.flush
183
+
184
+ file('test.xml').matches xml %{
185
+ <list>
186
+ <philosopher name="Hobbes"/>
187
+ <philosopher name="Rawls"/>
188
+ <tiger name="Hobbes"/>
189
+ </list>
190
+ }
191
+
192
+ ## Replace attribute values
193
+
194
+ Select some attributes with an XPath expression and change their value.
195
+
196
+ @tool.execute do
197
+ xpath '//philosopher[1]/@name' do
198
+ value 'Thomas Hobbes'
199
+ end
200
+ end
201
+ @tool.flush
202
+
203
+ file('test.xml').matches xml %{
204
+ <list>
205
+ <philosopher name="Thomas Hobbes"/>
206
+ <philosopher name="Rawls"/>
207
+ </list>
208
+ }
209
+
210
+ ## Rename nodes
211
+
212
+ Select some nodes with an XPath expression and change their name.
213
+
214
+ @tool.execute do
215
+ xpath '//philosopher[@name = "Hobbes"]' do
216
+ rename 'tiger-and-#{node.name}'
217
+ end
218
+ end
219
+ @tool.flush
220
+
221
+ file('test.xml').matches xml %{
222
+ <list>
223
+ <tiger-and-philosopher name="Hobbes"/>
224
+ <philosopher name="Rawls"/>
225
+ </list>
226
+ }
227
+
228
+ ## Evaluate a command string
229
+
230
+ Evaluate a command string with mixml commands
231
+
232
+ @tool.execute("xpath('//philosopher') { remove }")
233
+ @tool.flush
234
+
235
+ file('test.xml').matches xml %{
236
+ <list/>
237
+ }
238
+
239
+ ## Select nodes using CSS rules
240
+
241
+ You can also use CSS rules to select the nodes to process
242
+
243
+ @tool.execute("css('philosopher:first-child') { remove }")
244
+ @tool.flush
245
+
246
+ file('test.xml').matches xml %{
247
+ <list>
248
+ <philosopher name="Rawls"/>
249
+ </list>
250
+ }
251
+
252
+ ## Do everything in one step
253
+
254
+ Load files, modify them and optionally save them again in one step using the `work` method.
255
+
256
+ tool = Mixml::Tool.new do |t|
257
+ t.pretty = true
258
+ end
259
+
260
+ @tool.work('test.xml') do
261
+ xpath '//philosopher' do
262
+ remove
263
+ end
264
+ end
265
+
266
+ expect(@tool.documents).to have(0).items
267
+
268
+ file('test.xml').matches xml %{
269
+ <list/>
270
+ }
271
+
272
+ ## Print modified documents without saving
273
+
274
+ Print files whithout saving them.
275
+
276
+ @tool.save = false
277
+ @tool.print = true
278
+
279
+ text = redirect do
280
+ @tool.execute do
281
+ xpath '//philosopher' do
282
+ remove
283
+ end
284
+ end
285
+
286
+ @tool.flush
287
+ end
288
+
289
+ # Check output
290
+ expect(text).to match_text(%{
291
+ <?xml version="1.0"?>
292
+ <list/>
293
+ })
294
+
295
+ # Check if file still is unmodified
296
+ file('test.xml').matches xml %{
297
+ <list>
298
+ <philosopher name="Hobbes"/>
299
+ <philosopher name="Rawls"/>
300
+ </list>
301
+ }
302
+
303
+ ## Print headers when processing multiple documents
304
+
305
+ text = redirect do
306
+ file('more.xml') << %{
307
+ <list>
308
+ <philosopher name="Kant"/>
309
+ <philosopher name="Platon"/>
310
+ </list>
311
+ }
312
+
313
+ @tool.load('more.xml')
314
+ @tool.print_all
315
+ end
316
+
317
+ expect(text).to match_text(%{
318
+ --------
319
+ test.xml
320
+ --------
321
+ <?xml version="1.0"?>
322
+ <list>
323
+ <philosopher name="Hobbes"/>
324
+ <philosopher name="Rawls"/>
325
+ </list>
326
+ --------
327
+ more.xml
328
+ --------
329
+ <?xml version="1.0"?>
330
+ <list>
331
+ <philosopher name="Kant"/>
332
+ <philosopher name="Platon"/>
333
+ </list>
334
+ })