plist4r 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +120 -9
- data/VERSION +1 -1
- data/lib/plist4r.rb +54 -0
- data/lib/plist4r/backend.rb +52 -0
- data/lib/plist4r/backend/example.rb +69 -0
- data/lib/plist4r/backend/haml.rb +74 -0
- data/lib/plist4r/backend/libxml4r.rb +80 -0
- data/lib/plist4r/backend/plutil.rb +19 -0
- data/lib/plist4r/backend/ruby_cocoa.rb +191 -0
- data/lib/plist4r/config.rb +18 -0
- data/lib/plist4r/mixin.rb +7 -0
- data/lib/plist4r/mixin/class_attributes.rb +128 -0
- data/lib/plist4r/mixin/data_methods.rb +63 -0
- data/lib/plist4r/mixin/mixlib_config.rb +178 -0
- data/lib/plist4r/mixin/ordered_hash.rb +168 -0
- data/lib/plist4r/mixin/popen4.rb +193 -0
- data/lib/plist4r/mixin/ruby_stdlib.rb +24 -0
- data/lib/plist4r/plist.rb +256 -0
- data/lib/plist4r/plist_cache.rb +66 -0
- data/lib/plist4r/plist_type.rb +59 -0
- data/lib/plist4r/plist_type/info.rb +0 -0
- data/lib/plist4r/plist_type/launchd.rb +572 -0
- data/lib/plist4r/plist_type/plist.rb +0 -0
- data/plist4r.gemspec +86 -0
- data/spec/examples.rb +399 -0
- metadata +25 -2
data/README.rdoc
CHANGED
@@ -1,17 +1,128 @@
|
|
1
1
|
= plist4r
|
2
2
|
|
3
|
-
|
3
|
+
Welcome to Plist4r, a ruby library for reading and writing plist files.
|
4
4
|
|
5
|
-
|
5
|
+
Current project status: in alpha
|
6
|
+
|
7
|
+
== Installation
|
8
|
+
|
9
|
+
gem install plist4r
|
10
|
+
|
11
|
+
== Quick Start
|
12
|
+
|
13
|
+
Plist4r::Config.default_dir "/Library/LaunchDaemons"
|
14
|
+
filename = "com.github.myservice.plist"
|
15
|
+
p = Plist4r.open(filename)
|
16
|
+
|
17
|
+
p.plist_type
|
18
|
+
# => :launchd
|
19
|
+
|
20
|
+
p.file_format
|
21
|
+
# => :xml
|
22
|
+
|
23
|
+
p.<< do
|
24
|
+
ProgramArguments ["/usr/local/bin/myservice"]
|
25
|
+
end
|
26
|
+
|
27
|
+
p.edit do
|
28
|
+
WatchPaths ["/var/db/myservice"]
|
29
|
+
end
|
30
|
+
|
31
|
+
p.save
|
32
|
+
|
33
|
+
== Plist 'Types'
|
34
|
+
|
35
|
+
A Plist type can be one of `%w[info launchd]`, and is the data type for the whole plist file. A plist data type can provide convenience methods to set Type-specific plist structures. For example "Sockets" in a launchd plist.
|
36
|
+
|
37
|
+
Plist types are also useful to disallow keys which arent recognized or supported by that format. Flicking `:unsupported_keys` the Plist4r config will enable this:
|
38
|
+
|
39
|
+
::Plist4r::Config[:unsupported_keys] = false
|
40
|
+
|
41
|
+
Or individually, per plist object with
|
42
|
+
|
43
|
+
plist.unsupported_keys false
|
44
|
+
|
45
|
+
Default is true, which allows editing of any plist keys. We think thats a good choice, since unsupported keys can already be present in existing plist files, which are loadable by Plist4r.
|
46
|
+
|
47
|
+
== Plist4r Backends
|
48
|
+
|
49
|
+
There are now a number of ruby libraries which can read / write plist files. The aim of plist4r is to utilize the individual best features from all of those libraries, as a series of "backends". And hide those behind a "frontend" that is easy to work with.
|
50
|
+
|
51
|
+
Backends often only need to be a single ruby file, which implements the Plist4r API methods and calls out to other (existing) ruby code. No single backend has to provide the whole API. Instead, Plist4r simply iterates over all of the backends it knows about, and then calls the first backend that can responds to the API method.
|
52
|
+
|
53
|
+
There are just 6 supported API methods
|
54
|
+
|
55
|
+
ApiMethods = %w[from_string to_xml to_binary to_next_step open save]
|
56
|
+
|
57
|
+
And (as above) the 3 supported Plist file formats are
|
58
|
+
|
59
|
+
FileFormats = %w[binary xml next_step]
|
60
|
+
|
61
|
+
We believe thats allright for most uses, and decided to include `next_step` for completeness. `NextStep` is also known by other names such as `OpenStep` and (more updated version) `GNU Step`. For example the apple `defaults` command on Mac OS-X will still return `NextStep` formatted plist data.
|
62
|
+
|
63
|
+
== More Examples
|
64
|
+
|
65
|
+
module ::Plist4r::Backend::MyPlistReaderWriter
|
66
|
+
# implement some plist4r api calls here
|
67
|
+
end
|
68
|
+
|
69
|
+
# append my backend to the end of the list
|
70
|
+
Plist4r::Config[:backends] << :my_plist_reader_writer
|
71
|
+
|
72
|
+
# or to the front of the list (executes first)
|
73
|
+
Plist4r::Config[:backends].insert 0 :my_plist_reader_writer
|
74
|
+
|
75
|
+
# The default directory to load / save files from
|
76
|
+
Plist4r::Config.default_path "/Library/Cars"
|
77
|
+
|
78
|
+
car = Plist4r.new("car.plist")
|
79
|
+
|
80
|
+
car.load
|
81
|
+
|
82
|
+
car.file_format :binary
|
83
|
+
# car.plist_type :car # not implemented *yet*
|
84
|
+
|
85
|
+
car.save
|
86
|
+
|
87
|
+
car.<< do
|
88
|
+
road_legal true
|
89
|
+
brake_light_color "red"
|
90
|
+
end
|
91
|
+
|
92
|
+
car.save_as("car2.plist", :binary => true)
|
93
|
+
|
94
|
+
car.<< do
|
95
|
+
eyes "blue"
|
96
|
+
end
|
97
|
+
# => Exception, invalid plist key name "Eyes"
|
98
|
+
|
99
|
+
car.<< do
|
100
|
+
tyres "Pirelli"
|
101
|
+
end
|
102
|
+
|
103
|
+
car.to_xml
|
104
|
+
# => xml string
|
105
|
+
|
106
|
+
== Remaining Work
|
107
|
+
|
108
|
+
Plist4r is currently alpha - quality software. Yet to be completed...
|
109
|
+
|
110
|
+
* Regression Tests (rspec)
|
111
|
+
* Test harness for the backends
|
112
|
+
* Testing of the individual backends
|
113
|
+
* Tests for Plist Types
|
114
|
+
* RDoc Documentation
|
115
|
+
* Script for embedding / inlining Plist4r into Homebrew
|
116
|
+
* A Plist Type for Info.plist
|
117
|
+
* Command line interface (hopefully mixlib-cli)
|
118
|
+
|
119
|
+
== Notes on Patches/Pull Requests
|
6
120
|
|
7
|
-
* Fork the project.
|
121
|
+
* Fork the project, and create a topic branch as per {these instructions}[http://wiki.opscode.com/display/opscode/Working+with+Git].
|
8
122
|
* Make your feature addition or bug fix.
|
9
|
-
*
|
10
|
-
|
11
|
-
* Commit, do not mess with rakefile, version, or history.
|
12
|
-
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
13
|
-
* Send me a pull request. Bonus points for topic branches.
|
123
|
+
* Include documentation for it.
|
124
|
+
* Include a regression test for it. So I don't break it in a future version unintentionally.
|
14
125
|
|
15
126
|
== Copyright
|
16
127
|
|
17
|
-
Copyright (c) 2010
|
128
|
+
Copyright (c) 2010 Dreamcat4. See LICENSE for details.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.1.0
|
data/lib/plist4r.rb
CHANGED
@@ -0,0 +1,54 @@
|
|
1
|
+
|
2
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
3
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
4
|
+
|
5
|
+
require 'plist4r/plist'
|
6
|
+
|
7
|
+
module Plist4r
|
8
|
+
class << self
|
9
|
+
def new *args, &blk
|
10
|
+
puts args.inspect
|
11
|
+
return Plist.new *args, &blk
|
12
|
+
end
|
13
|
+
|
14
|
+
def open filename, *args, &blk
|
15
|
+
puts args.inspect
|
16
|
+
return Plist.new filename, *args, &blk
|
17
|
+
end
|
18
|
+
|
19
|
+
def string_detect_format string
|
20
|
+
s.strip!
|
21
|
+
case s[0,1]
|
22
|
+
when "{","("
|
23
|
+
:next_step
|
24
|
+
when "b"
|
25
|
+
if s =~ /^bplist/
|
26
|
+
:binary
|
27
|
+
else
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
when "<"
|
31
|
+
if s =~ /^\<\?xml/ && s =~ /\<\!DOCTYPE plist/
|
32
|
+
:xml
|
33
|
+
else
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
else
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def file_detect_format filename
|
42
|
+
string_detect_format File.read(filename)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class String
|
48
|
+
def to_plist
|
49
|
+
return ::Plist4r.new(:from_string => self)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
|
2
|
+
require 'plist4r/config'
|
3
|
+
require 'plist4r/mixin/ordered_hash'
|
4
|
+
|
5
|
+
class Plist4r::Backend
|
6
|
+
ApiMethods = %w[from_string to_xml to_binary to_next_step open save]
|
7
|
+
|
8
|
+
def initialize plist, *args, &blk
|
9
|
+
@plist = plist
|
10
|
+
@backends = Config[:backends].collect do |b|
|
11
|
+
case b
|
12
|
+
when Module
|
13
|
+
b
|
14
|
+
when Symbol, String
|
15
|
+
eval "::Plist4r::Backend::#{b.to_s.camelcase}"
|
16
|
+
else
|
17
|
+
raise "Backend #{b.inspect} is of unsupported type: #{b.class}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def call method_sym, *args, &blk
|
23
|
+
raise "Unsupported api call #{method_sym.inspect}" unless ApiMethods.include? method_sym.to_s
|
24
|
+
exceptions = []
|
25
|
+
@backends.each do |backend|
|
26
|
+
if backend.respond_to? method_sym
|
27
|
+
begin
|
28
|
+
return backend.send(method_sym, @plist, *args, &blk)
|
29
|
+
rescue LoadError
|
30
|
+
exceptions << $!
|
31
|
+
rescue
|
32
|
+
exceptions << $!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
if Config[:raise_any_failure] && exceptions.first
|
36
|
+
raise exceptions.first
|
37
|
+
end
|
38
|
+
end
|
39
|
+
if exceptions.empty?
|
40
|
+
raise "Plist4r: No backend found to handle method #{method_sym.inspect}. Could not execute method #{method_sym.inspect} on plist #{@plist.inspect}"
|
41
|
+
else
|
42
|
+
$stderr.puts "Couldn't execute method #{method_sym.inspect} on plist #{@plist.inspect}."
|
43
|
+
exceptions.each do |e|
|
44
|
+
$stderr.puts e.inspect
|
45
|
+
end
|
46
|
+
raise exceptions.first
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
|
2
|
+
require 'plist4r/backend'
|
3
|
+
|
4
|
+
module Plist4r::Backend::Example
|
5
|
+
class << self
|
6
|
+
def from_string plist, string
|
7
|
+
plist_format = Plist4r.string_detect_format string
|
8
|
+
unless [:supported_fmt1,:supported_fmt2].include? plist_format
|
9
|
+
raise "#{self} - cant convert string of format #{plist_format}"
|
10
|
+
end
|
11
|
+
hash = ::ActiveSupport::OrderedHash.new
|
12
|
+
# import / convert plist data into ruby ordered hash
|
13
|
+
plist.import_hash hash
|
14
|
+
plist.file_format = plist_format
|
15
|
+
return plist
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_xml plist
|
19
|
+
hash = plist.to_hash
|
20
|
+
xml_string = "Convert the plists's nested ruby hash into xml here"
|
21
|
+
return xml_string
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_binary
|
25
|
+
hash = plist.to_hash
|
26
|
+
binary_string = "Convert the plists's nested ruby hash into binary format here"
|
27
|
+
return binary_string
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_next_step
|
31
|
+
hash = plist.to_hash
|
32
|
+
next_step_string = "Convert the plists's nested ruby hash into next_step format here"
|
33
|
+
return next_step_string
|
34
|
+
end
|
35
|
+
|
36
|
+
def open plist
|
37
|
+
filename = plist.filename_path
|
38
|
+
file_format = Plist4r.file_detect_format filename
|
39
|
+
unless [:supported_fmt1,:supported_fmt2].include? file_format
|
40
|
+
raise "#{self} - cant load file of format #{file_format}"
|
41
|
+
end
|
42
|
+
plist_file_as_string = File.read(filename)
|
43
|
+
hash = ::ActiveSupport::OrderedHash.new
|
44
|
+
# import / convert plist data into ruby ordered hash
|
45
|
+
plist.import_hash hash
|
46
|
+
plist.file_format = file_format
|
47
|
+
return plist
|
48
|
+
end
|
49
|
+
|
50
|
+
def save plist
|
51
|
+
filename = plist.filename_path
|
52
|
+
file_format = plist.file_format || Config[:default_format]
|
53
|
+
unless [:xml,:binary].include? file_format
|
54
|
+
raise "#{self} - cant save file of format #{file_format}"
|
55
|
+
end
|
56
|
+
hash = plist.to_hash
|
57
|
+
output_string = String.new
|
58
|
+
# convert plist's @hash representation into an output_string,
|
59
|
+
# and formatted to your supported plist file_format(s)
|
60
|
+
File.open(filename,'w') do |out|
|
61
|
+
out << output_string
|
62
|
+
end
|
63
|
+
return true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
|
2
|
+
require 'plist4r/backend'
|
3
|
+
|
4
|
+
module Plist4r::Backend::HamlXmlWriter
|
5
|
+
class << self
|
6
|
+
def to_xml_haml
|
7
|
+
@to_xml_haml ||= <<-'EOC'
|
8
|
+
!!! XML UTF-8
|
9
|
+
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
|
10
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd" >
|
11
|
+
%plist{ :version => '1.0' }
|
12
|
+
%dict
|
13
|
+
- p = Proc.new do |data, block|
|
14
|
+
- data.each_pair do |k,v|
|
15
|
+
- raise "Invalid input. Hash: #{data.inspect} can only contain keys of type Symbol or String. Found key #{k.inspect}, of type: \"#{k.class}\"" unless [Symbol,String].include? k.class
|
16
|
+
- case v
|
17
|
+
- when TrueClass, FalseClass
|
18
|
+
%key #{k}
|
19
|
+
<#{v}/>
|
20
|
+
- when String
|
21
|
+
%key #{k}
|
22
|
+
%string #{v}
|
23
|
+
- when Fixnum
|
24
|
+
%key #{k}
|
25
|
+
%integer #{v}
|
26
|
+
- when Array
|
27
|
+
%key #{k}
|
28
|
+
%array
|
29
|
+
- v.compact.each do |e|
|
30
|
+
- case e
|
31
|
+
- when TrueClass, FalseClass
|
32
|
+
<#{v}/>
|
33
|
+
- when String
|
34
|
+
%string #{e}
|
35
|
+
- when Fixnum
|
36
|
+
%integer #{v}
|
37
|
+
- when Hash
|
38
|
+
%dict
|
39
|
+
- tab_up ; block.call(e, block) ; tab_down
|
40
|
+
- else
|
41
|
+
- raise "Invalid input. Array: #{v.inspect} can only contain elements of type String (<string>) or Hash (<dict>). Found element: #{e.inspect} of type: \"#{e.class}\""
|
42
|
+
- when Hash
|
43
|
+
%key #{k}
|
44
|
+
%dict
|
45
|
+
- tab_up ; block.call(v, block) ; tab_down
|
46
|
+
- else
|
47
|
+
- raise "Invalid input. Hash: #{data.inspect} can only contain values of type true, false, String (<string>) Fixnum (<integer>) Array (<array>) or Hash (<dict>). Found value: #{v.inspect} of type: \"#{v.class}\""
|
48
|
+
- p.call( @hash, p)
|
49
|
+
EOC
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_xml plist
|
53
|
+
require 'haml'
|
54
|
+
# engine = Haml::Engine.new File.read("launchd_plist.haml")
|
55
|
+
engine = Haml::Engine.new to_xml_haml
|
56
|
+
rendered_xml_output = engine.render self
|
57
|
+
File.open(@filename,'w') do |o|
|
58
|
+
o << rendered_xml_output
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def save plist
|
63
|
+
file_format = plist.file_format || Config[:default_format]
|
64
|
+
raise "#{self} - cant save file format #{file_format}" unless file_format == :xml
|
65
|
+
|
66
|
+
hash = plist.to_hash
|
67
|
+
filename = plist.filename_path
|
68
|
+
File.open(filename,'w') do |out|
|
69
|
+
out << to_xml plist
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
@@ -0,0 +1,80 @@
|
|
1
|
+
|
2
|
+
require 'plist4r/backend'
|
3
|
+
|
4
|
+
module Plist4r::Backend::Libxml4rXmlReader
|
5
|
+
class << self
|
6
|
+
def tree_hash n
|
7
|
+
hash = ::ActiveSupport::OrderedHash.new
|
8
|
+
n_xml_keys = n.nodes["key"]
|
9
|
+
n_xml_keys.each do |n|
|
10
|
+
k = n.inner_xml
|
11
|
+
vnode = n.next
|
12
|
+
case vnode.name
|
13
|
+
when "true", "false"
|
14
|
+
hash[k] = eval(vnode.name)
|
15
|
+
when "string"
|
16
|
+
hash[k] = vnode.inner_xml
|
17
|
+
when "integer"
|
18
|
+
hash[k] = vnode.inner_xml.to_i
|
19
|
+
when "array"
|
20
|
+
hash[k] = tree_array(vnode)
|
21
|
+
when "dict"
|
22
|
+
hash[k] = tree_hash(vnode)
|
23
|
+
else
|
24
|
+
raise "Unsupported / not recognized plist key: #{vnode.name}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
return hash
|
28
|
+
end
|
29
|
+
|
30
|
+
def tree_array n
|
31
|
+
array = []
|
32
|
+
n.children.each do |node|
|
33
|
+
case node.name
|
34
|
+
when "true", "false"
|
35
|
+
array << eval(node.name)
|
36
|
+
when "string"
|
37
|
+
array << node.inner_xml
|
38
|
+
when "integer"
|
39
|
+
array << node.inner_xml.to_i
|
40
|
+
when "array"
|
41
|
+
array << tree_array(node)
|
42
|
+
when "dict"
|
43
|
+
array << tree_hash(node)
|
44
|
+
else
|
45
|
+
raise "Unsupported / not recognized plist key: #{vnode.name}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
return array
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse_plist_xml string
|
52
|
+
require 'libxml4r'
|
53
|
+
::LibXML::XML.default_keep_blanks = false
|
54
|
+
doc = string.to_xmldoc
|
55
|
+
doc.strip!
|
56
|
+
root = doc.node["/plist/dict"]
|
57
|
+
ordered_hash = tree_hash root
|
58
|
+
end
|
59
|
+
|
60
|
+
def from_string plist, string
|
61
|
+
plist_format = Plist4r.string_detect_format string
|
62
|
+
raise "#{self} - cant convert string of format #{plist_format}" unless plist_format == :xml
|
63
|
+
|
64
|
+
hash = parse_plist_xml string
|
65
|
+
plist.import_hash hash
|
66
|
+
plist.file_format = file_format
|
67
|
+
return plist
|
68
|
+
end
|
69
|
+
|
70
|
+
def open plist
|
71
|
+
filename = plist.filename_path
|
72
|
+
file_format = Plist4r.file_detect_format filename
|
73
|
+
raise "#{self} - cant load file of format #{file_format}" unless file_format == :xml
|
74
|
+
|
75
|
+
return from_string plist, File.read(filename)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|