mixml 0.0.3 → 0.9

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.
@@ -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
+ })