class RunLoop::PlistBuddy

A class for reading and writing property list values.

Why not use CFPropertyList? Because it is super wonky. Among its other faults, it matches Boolean to a string type with ‘true/false’ values which is problematic for our purposes.

Public Instance Methods

create_plist(path) click to toggle source

Creates an new empty plist at ‘path`.

Is not responsible for creating directories or ensuring write permissions.

@param [String] path Where to create the new plist.

# File lib/run_loop/plist_buddy.rb, line 90
def create_plist(path)
  File.open(path, 'w') do |file|
    file.puts "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
    file.puts "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
    file.puts "<plist version=\"1.0\">"
    file.puts '<dict>'
    file.puts '</dict>'
    file.puts '</plist>'
  end
  path
end
ensure_plist(directory, name) click to toggle source

Ensures a plist exists at path by creating necessary directories and creating an empty plist if none exists.

# File lib/run_loop/plist_buddy.rb, line 104
def ensure_plist(directory, name)
  FileUtils.mkdir_p(directory) if !File.exist?(directory)

  plist = File.join(directory, name)

  create_plist(plist) if !File.exists?(plist)

  plist
end
plist_key_exists?(key, file, opts={}) click to toggle source

Checks if the key exists in plist. @param [String] key the key to inspect (may not be nil or empty) @param [String] file the plist to read @param [Hash] opts options for controlling execution @option opts [Boolean] :verbose (false) controls log level @return [Boolean] true if the key exists in plist file

# File lib/run_loop/plist_buddy.rb, line 41
def plist_key_exists?(key, file, opts={})
  plist_read(key, file, opts) != nil
end
plist_read(key, file, opts={}) click to toggle source

Reads key from file and returns the result. @param [String] key the key to inspect (may not be nil or empty) @param [String] file the plist to read @param [Hash] opts options for controlling execution @option opts [Boolean] :verbose (false) controls log level @return [String] the value of the key @raise [ArgumentError] if nil or empty key

# File lib/run_loop/plist_buddy.rb, line 22
def plist_read(key, file, opts={})
  if key.nil? or key.length == 0
    raise(ArgumentError, "key '#{key}' must not be nil or empty")
  end
  cmd = build_plist_cmd(:print, {:key => key}, file)
  success, output = execute_plist_cmd(cmd, file, opts)
  if !success
    nil
  else
    output
  end
end
plist_set(key, type, value, file, opts={}) click to toggle source

Replaces or creates the value of key in the file.

@param [String] key the key to set (may not be nil or empty) @param [String] type the plist type (used only when adding a value) @param [String] value the new value @param [String] file the plist to read @param [Hash] opts options for controlling execution @option opts [Boolean] :verbose (false) controls log level @return [Boolean] true if the operation was successful @raise [ArgumentError] if nil or empty key

# File lib/run_loop/plist_buddy.rb, line 55
    def plist_set(key, type, value, file, opts={})
      default_opts = {:verbose => false}
      merged = default_opts.merge(opts)

      if key.nil? or key.length == 0
        raise(ArgumentError, "key '#{key}' must not be nil or empty")
      end

      cmd_args = {:key => key,
                  :type => type,
                  :value => value}

      if plist_key_exists?(key, file, merged)
        cmd = build_plist_cmd(:set, cmd_args, file)
      else
        cmd = build_plist_cmd(:add, cmd_args, file)
      end

      success, output = execute_plist_cmd(cmd, file, merged)
      if !success
        raise RuntimeError, %Q[
Encountered an error performing operation on plist:

#{plist_buddy} -c "#{cmd}" #{file}
=> #{output}
]
      end
      success
    end
run_command(cmd, file, opts={}) click to toggle source

Sends an arbitrary command (-c) to PlistBuddy.

This class does not handle setting data, date, dictionary, or array or manipulating elements in existing array or dictionary types. This method is an attempt to bridge this gap.

When setting/adding bool, real, integer, string values, use plist_set.

For reading values, use plist_read.

@param [String] cmd The command passed to PlistBuddy with -c @param [String] file Path the plist file @param [Hash] opts options for controlling execution @option opts [Boolean] :verbose (false) controls log level @raise RuntimeError when running the command fails. @return Boolean, String Success and the output of running the command.

# File lib/run_loop/plist_buddy.rb, line 130
    def run_command(cmd, file, opts={})
      success, output = execute_plist_cmd(cmd, file, opts)
      if !success
        raise RuntimeError, %Q[
Encountered an error performing operation on plist:

#{plist_buddy} -c "#{cmd}" #{file}
=> #{output}
]
      end
      return success, output
    end
unshift_array(key, type, value, path, opts={}) click to toggle source

Add value to the head of an array type.

@param [String] key The plist key @param [String] type any allowed plist type @param [Object] value the value to add @param [String] path the plist path @param [Hash] opts options for controlling execution @option opts [Boolean] :verbose (false) controls log level @raise RuntimeError when running the command fails. @raise RuntimeError if attempt to push value onto non-array container.

# File lib/run_loop/plist_buddy.rb, line 153
    def unshift_array(key, type, value, path, opts={})
      if !plist_key_exists?(key, path)
        run_command("Add :#{key} array", path, opts)
      else
        key_type = plist_read(key, path).split(" ")[0]
        if key_type != "Array"
          raise RuntimeError, %Q[
Could not push #{value} onto array:
  Expected:  key #{key} be of type Array
     Found:  had type #{key_type}

in plist:

  #{path}
]
        end
      end

      run_command("Add :#{key}:0 #{type} #{value}", path, opts)
    end

Private Instance Methods

build_plist_cmd(type, args_hash, file) click to toggle source

Composes a PlistBuddy command that can be executed as a shell command.

@param [Symbol] type should be one of [:print, :set, :add]

@param [Hash] args_hash arguments used to construct plist command @option args_hash [String] :key (required) the plist key @option args_hash [String] :value (required for :set and :add) the new value @option args_hash [String] :type (required for :add) the new type of the value

@param [String] file the plist file to interact with (must exist)

@raise [RuntimeError] if file does not exist @raise [ArgumentError] when invalid type is passed @raise [ArgumentError] when args_hash does not include required key/value pairs

@return [String] a shell-ready PlistBuddy command

# File lib/run_loop/plist_buddy.rb, line 219
def build_plist_cmd(type, args_hash, file)

  unless File.exist?(File.expand_path(file))
    raise(RuntimeError, "plist '#{file}' does not exist - could not read")
  end

  case type
    when :add
      value_type = args_hash[:type]
      unless value_type
        raise(ArgumentError, ':value_type is a required key for :add command')
      end
      allowed_value_types = ['string', 'bool', 'real', 'integer']
      unless allowed_value_types.include?(value_type)
        raise(ArgumentError, "expected '#{value_type}' to be one of '#{allowed_value_types}'")
      end
      value = args_hash[:value]
      if value_type == 'bool'
        value = !!value
      elsif !value
        raise(ArgumentError, ':value is a required key for :add command')
      end
      key = args_hash[:key]
      unless key
        raise(ArgumentError, ':key is a required key for :add command')
      end
      cmd_part = "Add :#{key} #{value_type} #{value}"
    when :print
      key = args_hash[:key]
      unless key
        raise(ArgumentError, ':key is a required key for :print command')
      end
      cmd_part = "Print :#{key}"
    when :set
      value = args_hash[:value]
      if args_hash[:type] == 'bool'
        value = !!value
      elsif !value
        raise(ArgumentError, ':value is a required key for :add command')
      end
      key = args_hash[:key]
      unless key
        raise(ArgumentError, ':key is a required key for :set command')
      end
      cmd_part = "Set :#{key} #{value}"
    else
      cmds = [:add, :print, :set]
      raise(ArgumentError, "expected '#{type}' to be one of '#{cmds}'")
  end

  cmd_part
end
execute_plist_cmd(cmd, file, opts={}) click to toggle source

Executes cmd as a shell command and returns the result.

@param [String] cmd shell command to execute @param [Hash] opts options for controlling execution @option opts [Boolean] :verbose (false) controls log level @return [Boolean,String] ‘true` if command was successful. If :print’ing

the result, the value of the key.  If there is an error, the output of
stderr.
# File lib/run_loop/plist_buddy.rb, line 190
def execute_plist_cmd(cmd, file, opts={})
  default_opts = {:verbose => false }
  merged = default_opts.merge(opts)

  merged[:log_cmd] = merged[:verbose]

  args = [plist_buddy, "-c", cmd, file]

  hash = run_shell_command(args, merged)

  return hash[:exit_status] == 0, hash[:out]
end
plist_buddy() click to toggle source

returns the path to the PlistBuddy executable @return [String] path to PlistBuddy

# File lib/run_loop/plist_buddy.rb, line 178
def plist_buddy
  '/usr/libexec/PlistBuddy'
end