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.
- checksums.yaml +8 -8
- data/.yardopts +11 -0
- data/LICENSE.txt +661 -0
- data/README.md +169 -0
- data/demo/applique/test.rb +84 -0
- data/demo/tool.md +334 -0
- data/lib/mixml.rb +4 -0
- data/lib/mixml/selection.rb +29 -13
- data/lib/mixml/template/xml.rb +8 -1
- data/lib/mixml/tool.rb +47 -17
- data/lib/mixml/version.rb +1 -1
- metadata +29 -10
data/README.md
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
# mixml
|
2
|
+
|
3
|
+
[](http://badge.fury.io/rb/mixml)
|
4
|
+
[](https://travis-ci.org/jochenseeber/mixml)
|
5
|
+
[](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
|
data/demo/tool.md
ADDED
@@ -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
|
+
})
|