knife-essentials 0.9.6 → 0.9.7
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,77 @@ | |
| 1 | 
            +
            require 'chef_fs/knife'
         | 
| 2 | 
            +
            require 'chef_fs/file_system'
         | 
| 3 | 
            +
            require 'chef_fs/file_system/not_found_error'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            class Chef
         | 
| 6 | 
            +
              class Knife
         | 
| 7 | 
            +
                remove_const(:Edit) if const_defined?(:Edit) && Edit.name == 'Chef::Knife::Edit' # override Chef's version
         | 
| 8 | 
            +
                class Edit < ::ChefFS::Knife
         | 
| 9 | 
            +
                  ChefFS = ::ChefFS
         | 
| 10 | 
            +
                  banner "knife edit [PATTERN1 ... PATTERNn]"
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  common_options
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  option :local,
         | 
| 15 | 
            +
                    :long => '--local',
         | 
| 16 | 
            +
                    :boolean => true,
         | 
| 17 | 
            +
                    :description => "Show local files instead of remote"
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def run
         | 
| 20 | 
            +
                    # Get the matches (recursively)
         | 
| 21 | 
            +
                    error = false
         | 
| 22 | 
            +
                    pattern_args.each do |pattern|
         | 
| 23 | 
            +
                      ChefFS::FileSystem.list(config[:local] ? local_fs : chef_fs, pattern) do |result|
         | 
| 24 | 
            +
                        if result.dir?
         | 
| 25 | 
            +
                          ui.error "#{format_path(result)}: is a directory" if pattern.exact_path
         | 
| 26 | 
            +
                          error = true
         | 
| 27 | 
            +
                        else
         | 
| 28 | 
            +
                          begin
         | 
| 29 | 
            +
                            new_value = edit_text(result.read, File.extname(result.name))
         | 
| 30 | 
            +
                            if new_value
         | 
| 31 | 
            +
                              result.write(new_value)
         | 
| 32 | 
            +
                              output "Updated #{format_path(result)}"
         | 
| 33 | 
            +
                            else
         | 
| 34 | 
            +
                              output "#{format_path(result)} unchanged!"
         | 
| 35 | 
            +
                            end
         | 
| 36 | 
            +
                          rescue ChefFS::FileSystem::OperationNotAllowedError => e
         | 
| 37 | 
            +
                            ui.error "#{format_path(e.entry)}: #{e.reason}."
         | 
| 38 | 
            +
                            error = true
         | 
| 39 | 
            +
                          rescue ChefFS::FileSystem::NotFoundError => e
         | 
| 40 | 
            +
                            ui.error "#{format_path(e.entry)}: No such file or directory"
         | 
| 41 | 
            +
                            error = true
         | 
| 42 | 
            +
                          end
         | 
| 43 | 
            +
                        end
         | 
| 44 | 
            +
                      end
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
                    if error
         | 
| 47 | 
            +
                      exit 1
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def edit_text(text, extension)
         | 
| 52 | 
            +
                    if (!config[:disable_editing])
         | 
| 53 | 
            +
                      file = Tempfile.new([ 'knife-edit-', extension ])
         | 
| 54 | 
            +
                      begin
         | 
| 55 | 
            +
                        # Write the text to a temporary file
         | 
| 56 | 
            +
                        file.open
         | 
| 57 | 
            +
                        file.write(text)
         | 
| 58 | 
            +
                        file.close
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                        # Let the user edit the temporary file
         | 
| 61 | 
            +
                        if !system("#{config[:editor]} #{file.path}")
         | 
| 62 | 
            +
                          raise "Please set EDITOR environment variable"
         | 
| 63 | 
            +
                        end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                        file.open
         | 
| 66 | 
            +
                        result_text = file.read
         | 
| 67 | 
            +
                        return result_text if result_text != text
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                      ensure
         | 
| 70 | 
            +
                        file.close!
         | 
| 71 | 
            +
                      end
         | 
| 72 | 
            +
                    end
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
              end
         | 
| 76 | 
            +
            end
         | 
| 77 | 
            +
             | 
| @@ -0,0 +1,266 @@ | |
| 1 | 
            +
            require 'chef_fs/knife'
         | 
| 2 | 
            +
            require 'chef_fs/file_system'
         | 
| 3 | 
            +
            require 'chef_fs/file_system/not_found_error'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            class Chef
         | 
| 6 | 
            +
              class Knife
         | 
| 7 | 
            +
                remove_const(:Xargs) if const_defined?(:Xargs) && Xargs.name == 'Chef::Knife::Xargs' # override Chef's version
         | 
| 8 | 
            +
                class Xargs < ::ChefFS::Knife
         | 
| 9 | 
            +
                  ChefFS = ::ChefFS
         | 
| 10 | 
            +
                  banner "knife xargs [COMMAND]"
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  common_options
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  # TODO modify to remote-only / local-only pattern (more like delete)
         | 
| 15 | 
            +
                  option :local,
         | 
| 16 | 
            +
                    :long => '--local',
         | 
| 17 | 
            +
                    :boolean => true,
         | 
| 18 | 
            +
                    :description => "Xargs local files instead of remote"
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  option :patterns,
         | 
| 21 | 
            +
                    :long => '--pattern [PATTERN]',
         | 
| 22 | 
            +
                    :short => '-p [PATTERN]',
         | 
| 23 | 
            +
                    :description => "Pattern on command line (if these are not specified, a list of patterns is expected on standard input).  Multiple patterns may be passed in this way.",
         | 
| 24 | 
            +
                    :arg_arity => [1,-1]
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  option :diff,
         | 
| 27 | 
            +
                    :long => '--[no-]diff',
         | 
| 28 | 
            +
                    :default => true,
         | 
| 29 | 
            +
                    :boolean => true,
         | 
| 30 | 
            +
                    :description => "Whether to show a diff when files change (default: true)"
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  option :dry_run,
         | 
| 33 | 
            +
                    :long => '--dry-run',
         | 
| 34 | 
            +
                    :boolean => true,
         | 
| 35 | 
            +
                    :description => "Prevents changes from actually being uploaded to the server."
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  option :force,
         | 
| 38 | 
            +
                    :long => '--[no-]force',
         | 
| 39 | 
            +
                    :boolean => true,
         | 
| 40 | 
            +
                    :default => false,
         | 
| 41 | 
            +
                    :description => "Force upload of files even if they are not changed (quicker and harmless, but doesn't print out what it changed)"
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  option :replace_first,
         | 
| 44 | 
            +
                    :long => '--replace-first REPLACESTR',
         | 
| 45 | 
            +
                    :short => '-J REPLACESTR',
         | 
| 46 | 
            +
                    :description => "String to replace with filenames.  -J will only replace the FIRST occurrence of the replacement string."
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  option :replace_all,
         | 
| 49 | 
            +
                    :long => '--replace REPLACESTR',
         | 
| 50 | 
            +
                    :short => '-I REPLACESTR',
         | 
| 51 | 
            +
                    :description => "String to replace with filenames.  -I will replace ALL occurrence of the replacement string."
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  option :max_arguments_per_command,
         | 
| 54 | 
            +
                    :long => '--max-args MAXARGS',
         | 
| 55 | 
            +
                    :short => '-n MAXARGS',
         | 
| 56 | 
            +
                    :description => "Maximum number of arguments per command line."
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  option :max_command_line,
         | 
| 59 | 
            +
                    :long => '--max-chars LENGTH',
         | 
| 60 | 
            +
                    :short => '-s LENGTH',
         | 
| 61 | 
            +
                    :description => "Maximum size of command line, in characters"
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  option :verbose_commands,
         | 
| 64 | 
            +
                    :short => '-t',
         | 
| 65 | 
            +
                    :description => "Print command to be run on the command line"
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  option :null_separator,
         | 
| 68 | 
            +
                    :short => '-0',
         | 
| 69 | 
            +
                    :boolean => true,
         | 
| 70 | 
            +
                    :description => "Use the NULL character (\0) as a separator, instead of whitespace"
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  def run
         | 
| 73 | 
            +
                    error = false
         | 
| 74 | 
            +
                    # Get the matches (recursively)
         | 
| 75 | 
            +
                    files = []
         | 
| 76 | 
            +
                    pattern_args_from(get_patterns).each do |pattern|
         | 
| 77 | 
            +
                      ChefFS::FileSystem.list(config[:local] ? local_fs : chef_fs, pattern) do |result|
         | 
| 78 | 
            +
                        if result.dir?
         | 
| 79 | 
            +
                          # TODO option to include directories
         | 
| 80 | 
            +
                          ui.warn "#{format_path(result)}: is a directory.  Will not run #{command} on it."
         | 
| 81 | 
            +
                        else
         | 
| 82 | 
            +
                          files << result
         | 
| 83 | 
            +
                          ran = false
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                          # If the command would be bigger than max command line, back it off a bit
         | 
| 86 | 
            +
                          # and run a slightly smaller command (with one less arg)
         | 
| 87 | 
            +
                          if config[:max_command_line]
         | 
| 88 | 
            +
                            command, tempfiles = create_command(files)
         | 
| 89 | 
            +
                            begin
         | 
| 90 | 
            +
                              if command.length > config[:max_command_line].to_i
         | 
| 91 | 
            +
                                if files.length > 1
         | 
| 92 | 
            +
                                  command, tempfiles_minus_one = create_command(files[0..-2])
         | 
| 93 | 
            +
                                  begin
         | 
| 94 | 
            +
                                    error = true if xargs_files(command, tempfiles_minus_one)
         | 
| 95 | 
            +
                                    files = [ files[-1] ]
         | 
| 96 | 
            +
                                    ran = true
         | 
| 97 | 
            +
                                  ensure
         | 
| 98 | 
            +
                                    destroy_tempfiles(tempfiles)
         | 
| 99 | 
            +
                                  end
         | 
| 100 | 
            +
                                else
         | 
| 101 | 
            +
                                  error = true if xargs_files(command, tempfiles)
         | 
| 102 | 
            +
                                  files = [ ]
         | 
| 103 | 
            +
                                  ran = true
         | 
| 104 | 
            +
                                end
         | 
| 105 | 
            +
                              end
         | 
| 106 | 
            +
                            ensure
         | 
| 107 | 
            +
                              destroy_tempfiles(tempfiles)
         | 
| 108 | 
            +
                            end
         | 
| 109 | 
            +
                          end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                          # If the command has hit the limit for the # of arguments, run it
         | 
| 112 | 
            +
                          if !ran && config[:max_arguments_per_command] && files.size >= config[:max_arguments_per_command].to_i
         | 
| 113 | 
            +
                            command, tempfiles = create_command(files)
         | 
| 114 | 
            +
                            begin
         | 
| 115 | 
            +
                              error = true if xargs_files(command, tempfiles)
         | 
| 116 | 
            +
                              files = []
         | 
| 117 | 
            +
                              ran = true
         | 
| 118 | 
            +
                            ensure
         | 
| 119 | 
            +
                              destroy_tempfiles(tempfiles)
         | 
| 120 | 
            +
                            end
         | 
| 121 | 
            +
                          end
         | 
| 122 | 
            +
                        end
         | 
| 123 | 
            +
                      end
         | 
| 124 | 
            +
                    end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                    # Any leftovers commands shall be run
         | 
| 127 | 
            +
                    if files.size > 0
         | 
| 128 | 
            +
                      command, tempfiles = create_command(files)
         | 
| 129 | 
            +
                      begin
         | 
| 130 | 
            +
                        error = true if xargs_files(command, tempfiles)
         | 
| 131 | 
            +
                      ensure
         | 
| 132 | 
            +
                        destroy_tempfiles(tempfiles)
         | 
| 133 | 
            +
                      end
         | 
| 134 | 
            +
                    end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                    if error
         | 
| 137 | 
            +
                      exit 1
         | 
| 138 | 
            +
                    end
         | 
| 139 | 
            +
                  end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                  def get_patterns
         | 
| 142 | 
            +
                    if config[:patterns]
         | 
| 143 | 
            +
                      [ config[:patterns] ].flatten
         | 
| 144 | 
            +
                    elsif config[:null_separator]
         | 
| 145 | 
            +
                      stdin.binmode
         | 
| 146 | 
            +
                      stdin.read.split("\000")
         | 
| 147 | 
            +
                    else
         | 
| 148 | 
            +
                      stdin.read.split(/\s+/)
         | 
| 149 | 
            +
                    end
         | 
| 150 | 
            +
                  end
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                  def create_command(files)
         | 
| 153 | 
            +
                    command = name_args.join(' ')
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                    # Create the (empty) tempfiles
         | 
| 156 | 
            +
                    tempfiles = {}
         | 
| 157 | 
            +
                    begin
         | 
| 158 | 
            +
                      # Create the temporary files
         | 
| 159 | 
            +
                      files.each do |file|
         | 
| 160 | 
            +
                        tempfile = Tempfile.new(file.name)
         | 
| 161 | 
            +
                        tempfiles[tempfile] = { :file => file }
         | 
| 162 | 
            +
                      end
         | 
| 163 | 
            +
                    rescue
         | 
| 164 | 
            +
                      destroy_tempfiles(files)
         | 
| 165 | 
            +
                      raise
         | 
| 166 | 
            +
                    end
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                    # Create the command
         | 
| 169 | 
            +
                    paths = tempfiles.keys.map { |tempfile| tempfile.path }.join(' ')
         | 
| 170 | 
            +
                    if config[:replace_all]
         | 
| 171 | 
            +
                      final_command = command.gsub(config[:replace_all], paths)
         | 
| 172 | 
            +
                    elsif config[:replace_first]
         | 
| 173 | 
            +
                      final_command = command.sub(config[:replace_first], paths)
         | 
| 174 | 
            +
                    else
         | 
| 175 | 
            +
                      final_command = "#{command} #{paths}"
         | 
| 176 | 
            +
                    end
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                    [final_command, tempfiles]
         | 
| 179 | 
            +
                  end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                  def destroy_tempfiles(tempfiles)
         | 
| 182 | 
            +
                    # Unlink the files now that we're done with them
         | 
| 183 | 
            +
                    tempfiles.keys.each { |tempfile| tempfile.close! }
         | 
| 184 | 
            +
                  end
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                  def xargs_files(command, tempfiles)
         | 
| 187 | 
            +
                    error = false
         | 
| 188 | 
            +
                    # Create the temporary files
         | 
| 189 | 
            +
                    tempfiles.each_pair do |tempfile, file|
         | 
| 190 | 
            +
                      begin
         | 
| 191 | 
            +
                        value = file[:file].read
         | 
| 192 | 
            +
                        file[:value] = value
         | 
| 193 | 
            +
                        tempfile.open
         | 
| 194 | 
            +
                        tempfile.write(value)
         | 
| 195 | 
            +
                        tempfile.close
         | 
| 196 | 
            +
                      rescue ChefFS::FileSystem::OperationNotAllowedError => e
         | 
| 197 | 
            +
                        ui.error "#{format_path(e.entry)}: #{e.reason}."
         | 
| 198 | 
            +
                        error = true
         | 
| 199 | 
            +
                        tempfile.close!
         | 
| 200 | 
            +
                        tempfiles.delete(tempfile)
         | 
| 201 | 
            +
                        next
         | 
| 202 | 
            +
                      rescue ChefFS::FileSystem::NotFoundError => e
         | 
| 203 | 
            +
                        ui.error "#{format_path(e.entry)}: No such file or directory"
         | 
| 204 | 
            +
                        error = true
         | 
| 205 | 
            +
                        tempfile.close!
         | 
| 206 | 
            +
                        tempfiles.delete(tempfile)
         | 
| 207 | 
            +
                        next
         | 
| 208 | 
            +
                      end
         | 
| 209 | 
            +
                    end
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                    return error if error && tempfiles.size == 0
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                    # Run the command
         | 
| 214 | 
            +
                    if config[:verbose_commands] || Chef::Config[:verbosity] && Chef::Config[:verbosity] >= 1
         | 
| 215 | 
            +
                      output sub_filenames(command, tempfiles)
         | 
| 216 | 
            +
                    end
         | 
| 217 | 
            +
                    command_output = `#{command}`
         | 
| 218 | 
            +
                    command_output = sub_filenames(command_output, tempfiles)
         | 
| 219 | 
            +
                    output command_output
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                    # Check if the output is different
         | 
| 222 | 
            +
                    tempfiles.each_pair do |tempfile, file|
         | 
| 223 | 
            +
                      # Read the new output
         | 
| 224 | 
            +
                      new_value = IO.binread(tempfile.path)
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                      # Upload the output if different
         | 
| 227 | 
            +
                      if config[:force] || new_value != file[:value]
         | 
| 228 | 
            +
                        if config[:dry_run]
         | 
| 229 | 
            +
                          output "Would update #{format_path(file[:file])}"
         | 
| 230 | 
            +
                        else
         | 
| 231 | 
            +
                          file[:file].write(new_value)
         | 
| 232 | 
            +
                          output "Updated #{format_path(file[:file])}"
         | 
| 233 | 
            +
                        end
         | 
| 234 | 
            +
                      end
         | 
| 235 | 
            +
             | 
| 236 | 
            +
                      # Print a diff of what was uploaded
         | 
| 237 | 
            +
                      if config[:diff] && new_value != file[:value]
         | 
| 238 | 
            +
                        old_file = Tempfile.open(file[:file].name)
         | 
| 239 | 
            +
                        begin
         | 
| 240 | 
            +
                          old_file.write(file[:value])
         | 
| 241 | 
            +
                          old_file.close
         | 
| 242 | 
            +
             | 
| 243 | 
            +
                          diff = `diff -u #{old_file.path} #{tempfile.path}`
         | 
| 244 | 
            +
                          diff.gsub!(old_file.path, "#{format_path(file[:file])} (old)")
         | 
| 245 | 
            +
                          diff.gsub!(tempfile.path, "#{format_path(file[:file])} (new)")
         | 
| 246 | 
            +
                          output diff
         | 
| 247 | 
            +
                        ensure
         | 
| 248 | 
            +
                          old_file.close!
         | 
| 249 | 
            +
                        end
         | 
| 250 | 
            +
                      end
         | 
| 251 | 
            +
                    end
         | 
| 252 | 
            +
             | 
| 253 | 
            +
                    error
         | 
| 254 | 
            +
                  end
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                  def sub_filenames(str, tempfiles)
         | 
| 257 | 
            +
                    tempfiles.each_pair do |tempfile, file|
         | 
| 258 | 
            +
                      str.gsub!(tempfile.path, format_path(file[:file]))
         | 
| 259 | 
            +
                    end
         | 
| 260 | 
            +
                    str
         | 
| 261 | 
            +
                  end
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                end
         | 
| 264 | 
            +
              end
         | 
| 265 | 
            +
            end
         | 
| 266 | 
            +
             | 
| @@ -47,7 +47,7 @@ module ChefFS | |
| 47 47 | 
             
                  end
         | 
| 48 48 |  | 
| 49 49 | 
             
                  def create_child(name, file_contents)
         | 
| 50 | 
            -
                    raise OperationNotAllowedError.new(:create_child, self)
         | 
| 50 | 
            +
                    raise OperationNotAllowedError.new(:create_child, self), "ACLs can only be updated, and can only be created when the corresponding object is created."
         | 
| 51 51 | 
             
                  end
         | 
| 52 52 |  | 
| 53 53 | 
             
                  def data_handler
         | 
    
        data/lib/chef_fs/version.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: knife-essentials
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.9. | 
| 4 | 
            +
              version: 0.9.7
         | 
| 5 5 | 
             
              prerelease: 
         | 
| 6 6 | 
             
            platform: ruby
         | 
| 7 7 | 
             
            authors:
         | 
| @@ -9,7 +9,7 @@ authors: | |
| 9 9 | 
             
            autorequire: 
         | 
| 10 10 | 
             
            bindir: bin
         | 
| 11 11 | 
             
            cert_chain: []
         | 
| 12 | 
            -
            date: 2013- | 
| 12 | 
            +
            date: 2013-04-09 00:00:00.000000000 Z
         | 
| 13 13 | 
             
            dependencies:
         | 
| 14 14 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 15 15 | 
             
              name: chef-zero
         | 
| @@ -58,10 +58,12 @@ files: | |
| 58 58 | 
             
            - lib/chef/knife/deps_essentials.rb
         | 
| 59 59 | 
             
            - lib/chef/knife/diff_essentials.rb
         | 
| 60 60 | 
             
            - lib/chef/knife/download_essentials.rb
         | 
| 61 | 
            +
            - lib/chef/knife/edit_essentials.rb
         | 
| 61 62 | 
             
            - lib/chef/knife/list_essentials.rb
         | 
| 62 63 | 
             
            - lib/chef/knife/raw_essentials.rb
         | 
| 63 64 | 
             
            - lib/chef/knife/show_essentials.rb
         | 
| 64 65 | 
             
            - lib/chef/knife/upload_essentials.rb
         | 
| 66 | 
            +
            - lib/chef/knife/xargs_essentials.rb
         | 
| 65 67 | 
             
            - lib/chef_fs/command_line.rb
         | 
| 66 68 | 
             
            - lib/chef_fs/data_handler/acl_data_handler.rb
         | 
| 67 69 | 
             
            - lib/chef_fs/data_handler/client_data_handler.rb
         | 
| @@ -158,3 +160,4 @@ signing_key: | |
| 158 160 | 
             
            specification_version: 3
         | 
| 159 161 | 
             
            summary: Universal knife verbs that work with your Chef repository
         | 
| 160 162 | 
             
            test_files: []
         | 
| 163 | 
            +
            has_rdoc: true
         |