├── .document ├── .github ├── dependabot.yml └── workflows │ ├── push_gem.yml │ └── test.yml ├── .gitignore ├── .rdoc_options ├── BSDL ├── COPYING ├── Gemfile ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── docs └── kernel.rb ├── lib └── pp.rb ├── pp.gemspec └── test └── test_pp.rb /.document: -------------------------------------------------------------------------------- 1 | BSDL 2 | COPYING 3 | README.md 4 | docs/ 5 | lib/ 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/pp' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/pp 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 31 | 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0 34 | with: 35 | bundler-cache: true 36 | ruby-version: ruby 37 | 38 | - name: Publish to RubyGems 39 | uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1 40 | 41 | - name: Create GitHub release 42 | run: | 43 | tag_name="$(git describe --tags --abbrev=0)" 44 | gh release create "${tag_name}" --verify-tag --generate-notes 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.MATZBOT_GITHUB_WORKFLOW_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '12 3 * * *' 8 | 9 | jobs: 10 | ruby-versions: 11 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 12 | with: 13 | engine: cruby 14 | min_version: 2.7 15 | 16 | test: 17 | needs: ruby-versions 18 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 23 | os: [ ubuntu-latest, macos-latest, windows-latest ] 24 | exclude: 25 | - { os: windows-latest, ruby: head } 26 | include: 27 | - { os: windows-latest, ruby: mingw } 28 | - { os: windows-latest, ruby: mswin } 29 | - { os: ubuntu-latest, ruby: 'jruby-head', bundle: 'bundle exec' } 30 | - { os: ubuntu-latest, ruby: 'truffleruby-head' } 31 | runs-on: ${{ matrix.os }} 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Set up Ruby 35 | uses: ruby/setup-ruby@v1 36 | with: 37 | ruby-version: ${{ matrix.ruby }} 38 | bundler-cache: true # 'bundle install' and enable caching 39 | windows-toolchain: none # no extension 40 | - name: Build 41 | run: ${{matrix.bundle}} rake build 42 | - name: Run test 43 | run: ${{matrix.bundle}} rake test 44 | - name: Installation test 45 | run: gem install pkg/*.gem 46 | if: ${{ !startsWith(matrix.ruby, 'jruby') }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /Gemfile.lock 10 | -------------------------------------------------------------------------------- /.rdoc_options: -------------------------------------------------------------------------------- 1 | --- 2 | main_page: README.md 3 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rake" 4 | gem "test-unit" 5 | gem "ruby2_keywords", group: :test 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PP 2 | 3 | A pretty-printer for Ruby objects. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'pp' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle install 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install pp 20 | 21 | ## Usage 22 | 23 | ```ruby 24 | pp(obj) #=> obj 25 | pp obj #=> obj 26 | pp(obj1, obj2, ...) #=> [obj1, obj2, ...] 27 | pp() #=> nil 28 | ``` 29 | 30 | ## Development 31 | 32 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 33 | 34 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 35 | 36 | ## Contributing 37 | 38 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/pp. 39 | 40 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "pp" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /docs/kernel.rb: -------------------------------------------------------------------------------- 1 | # Built-in module 2 | module Kernel 3 | end 4 | -------------------------------------------------------------------------------- /lib/pp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'prettyprint' 4 | 5 | ## 6 | # A pretty-printer for Ruby objects. 7 | # 8 | ## 9 | # == What PP Does 10 | # 11 | # Standard output by #p returns this: 12 | # #, @group_queue=#], []]>, @buffer=[], @newline="\n", @group_stack=[#], @buffer_width=0, @indent=0, @maxwidth=79, @output_width=2, @output=#> 13 | # 14 | # Pretty-printed output returns this: 15 | # #, 19 | # @group_queue= 20 | # #], 23 | # []]>, 24 | # @group_stack= 25 | # [#], 26 | # @indent=0, 27 | # @maxwidth=79, 28 | # @newline="\n", 29 | # @output=#, 30 | # @output_width=2> 31 | # 32 | ## 33 | # == Usage 34 | # 35 | # pp(obj) #=> obj 36 | # pp obj #=> obj 37 | # pp(obj1, obj2, ...) #=> [obj1, obj2, ...] 38 | # pp() #=> nil 39 | # 40 | # Output obj(s) to $> in pretty printed format. 41 | # 42 | # It returns obj(s). 43 | # 44 | ## 45 | # == Output Customization 46 | # 47 | # To define a customized pretty printing function for your classes, 48 | # redefine method #pretty_print(pp) in the class. 49 | # Note that require 'pp' is needed before redefining #pretty_print(pp). 50 | # 51 | # #pretty_print takes the +pp+ argument, which is an instance of the PP class. 52 | # The method uses #text, #breakable, #nest, #group and #pp to print the 53 | # object. 54 | # 55 | ## 56 | # == Pretty-Print JSON 57 | # 58 | # To pretty-print JSON refer to JSON#pretty_generate. 59 | # 60 | ## 61 | # == Author 62 | # Tanaka Akira 63 | 64 | class PP < PrettyPrint 65 | 66 | # The version string 67 | VERSION = "0.6.2" 68 | 69 | # Returns the usable width for +out+. 70 | # As the width of +out+: 71 | # 1. If +out+ is assigned to a tty device, its width is used. 72 | # 2. Otherwise, or it could not get the value, the +COLUMN+ 73 | # environment variable is assumed to be set to the width. 74 | # 3. If +COLUMN+ is not set to a non-zero number, 80 is assumed. 75 | # 76 | # And finally, returns the above width value - 1. 77 | # * This -1 is for Windows command prompt, which moves the cursor to 78 | # the next line if it reaches the last column. 79 | def PP.width_for(out) 80 | begin 81 | require 'io/console' 82 | _, width = out.winsize 83 | rescue LoadError, NoMethodError, SystemCallError 84 | end 85 | (width || ENV['COLUMNS']&.to_i&.nonzero? || 80) - 1 86 | end 87 | 88 | # Outputs +obj+ to +out+ in pretty printed format of 89 | # +width+ columns in width. 90 | # 91 | # If +out+ is omitted, $> is assumed. 92 | # If +width+ is omitted, the width of +out+ is assumed (see 93 | # width_for). 94 | # 95 | # PP.pp returns +out+. 96 | def PP.pp(obj, out=$>, width=width_for(out)) 97 | q = new(out, width) 98 | q.guard_inspect_key {q.pp obj} 99 | q.flush 100 | #$pp = q 101 | out << "\n" 102 | end 103 | 104 | # Outputs +obj+ to +out+ like PP.pp but with no indent and 105 | # newline. 106 | # 107 | # PP.singleline_pp returns +out+. 108 | def PP.singleline_pp(obj, out=$>) 109 | q = SingleLine.new(out) 110 | q.guard_inspect_key {q.pp obj} 111 | q.flush 112 | out 113 | end 114 | 115 | # :stopdoc: 116 | def PP.mcall(obj, mod, meth, *args, &block) 117 | mod.instance_method(meth).bind_call(obj, *args, &block) 118 | end 119 | # :startdoc: 120 | 121 | if defined? ::Ractor 122 | class << self 123 | # Returns the sharing detection flag as a boolean value. 124 | # It is false (nil) by default. 125 | def sharing_detection 126 | Ractor.current[:pp_sharing_detection] 127 | end 128 | # Sets the sharing detection flag to b. 129 | def sharing_detection=(b) 130 | Ractor.current[:pp_sharing_detection] = b 131 | end 132 | end 133 | else 134 | @sharing_detection = false 135 | class << self 136 | # Returns the sharing detection flag as a boolean value. 137 | # It is false by default. 138 | attr_accessor :sharing_detection 139 | end 140 | end 141 | 142 | # Module that defines helper methods for pretty_print. 143 | module PPMethods 144 | 145 | # Yields to a block 146 | # and preserves the previous set of objects being printed. 147 | def guard_inspect_key 148 | if Thread.current[:__recursive_key__] == nil 149 | Thread.current[:__recursive_key__] = {}.compare_by_identity 150 | end 151 | 152 | if Thread.current[:__recursive_key__][:inspect] == nil 153 | Thread.current[:__recursive_key__][:inspect] = {}.compare_by_identity 154 | end 155 | 156 | save = Thread.current[:__recursive_key__][:inspect] 157 | 158 | begin 159 | Thread.current[:__recursive_key__][:inspect] = {}.compare_by_identity 160 | yield 161 | ensure 162 | Thread.current[:__recursive_key__][:inspect] = save 163 | end 164 | end 165 | 166 | # Check whether the object_id +id+ is in the current buffer of objects 167 | # to be pretty printed. Used to break cycles in chains of objects to be 168 | # pretty printed. 169 | def check_inspect_key(id) 170 | Thread.current[:__recursive_key__] && 171 | Thread.current[:__recursive_key__][:inspect] && 172 | Thread.current[:__recursive_key__][:inspect].include?(id) 173 | end 174 | 175 | # Adds the object_id +id+ to the set of objects being pretty printed, so 176 | # as to not repeat objects. 177 | def push_inspect_key(id) 178 | Thread.current[:__recursive_key__][:inspect][id] = true 179 | end 180 | 181 | # Removes an object from the set of objects being pretty printed. 182 | def pop_inspect_key(id) 183 | Thread.current[:__recursive_key__][:inspect].delete id 184 | end 185 | 186 | private def guard_inspect(object) 187 | recursive_state = Thread.current[:__recursive_key__] 188 | 189 | if recursive_state && recursive_state.key?(:inspect) 190 | begin 191 | push_inspect_key(object) 192 | yield 193 | ensure 194 | pop_inspect_key(object) unless PP.sharing_detection 195 | end 196 | else 197 | guard_inspect_key do 198 | push_inspect_key(object) 199 | yield 200 | end 201 | end 202 | end 203 | 204 | # Adds +obj+ to the pretty printing buffer 205 | # using Object#pretty_print or Object#pretty_print_cycle. 206 | # 207 | # Object#pretty_print_cycle is used when +obj+ is already 208 | # printed, a.k.a the object reference chain has a cycle. 209 | def pp(obj) 210 | # If obj is a Delegator then use the object being delegated to for cycle 211 | # detection 212 | obj = obj.__getobj__ if defined?(::Delegator) and ::Delegator === obj 213 | 214 | if check_inspect_key(obj) 215 | group {obj.pretty_print_cycle self} 216 | return 217 | end 218 | 219 | guard_inspect(obj) do 220 | group do 221 | obj.pretty_print self 222 | rescue NoMethodError 223 | text Kernel.instance_method(:inspect).bind_call(obj) 224 | end 225 | end 226 | end 227 | 228 | # A convenience method which is same as follows: 229 | # 230 | # group(1, '#<' + obj.class.name, '>') { ... } 231 | def object_group(obj, &block) # :yield: 232 | group(1, '#<' + obj.class.name, '>', &block) 233 | end 234 | 235 | # A convenience method, like object_group, but also reformats the Object's 236 | # object_id. 237 | def object_address_group(obj, &block) 238 | str = Kernel.instance_method(:to_s).bind_call(obj) 239 | str.chomp!('>') 240 | group(1, str, '>', &block) 241 | end 242 | 243 | # A convenience method which is same as follows: 244 | # 245 | # text ',' 246 | # breakable 247 | def comma_breakable 248 | text ',' 249 | breakable 250 | end 251 | 252 | # Adds a separated list. 253 | # The list is separated by comma with breakable space, by default. 254 | # 255 | # #seplist iterates the +list+ using +iter_method+. 256 | # It yields each object to the block given for #seplist. 257 | # The procedure +separator_proc+ is called between each yields. 258 | # 259 | # If the iteration is zero times, +separator_proc+ is not called at all. 260 | # 261 | # If +separator_proc+ is nil or not given, 262 | # +lambda { comma_breakable }+ is used. 263 | # If +iter_method+ is not given, :each is used. 264 | # 265 | # For example, following 3 code fragments has similar effect. 266 | # 267 | # q.seplist([1,2,3]) {|v| xxx v } 268 | # 269 | # q.seplist([1,2,3], lambda { q.comma_breakable }, :each) {|v| xxx v } 270 | # 271 | # xxx 1 272 | # q.comma_breakable 273 | # xxx 2 274 | # q.comma_breakable 275 | # xxx 3 276 | def seplist(list, sep=nil, iter_method=:each) # :yield: element 277 | sep ||= lambda { comma_breakable } 278 | first = true 279 | kwsplat = EMPTY_KWHASH 280 | list.__send__(iter_method) {|*v| 281 | if first 282 | first = false 283 | else 284 | sep.call 285 | end 286 | kwsplat ? yield(*v, **kwsplat) : yield(*v) 287 | } 288 | end 289 | EMPTY_KWHASH = if RUBY_VERSION >= "3.0" 290 | {}.freeze 291 | end 292 | private_constant :EMPTY_KWHASH 293 | 294 | # A present standard failsafe for pretty printing any given Object 295 | def pp_object(obj) 296 | object_address_group(obj) { 297 | seplist(obj.pretty_print_instance_variables, lambda { text ',' }) {|v| 298 | breakable 299 | v = v.to_s if Symbol === v 300 | text v 301 | text '=' 302 | group(1) { 303 | breakable '' 304 | pp(obj.instance_eval(v)) 305 | } 306 | } 307 | } 308 | end 309 | 310 | # A pretty print for a Hash 311 | def pp_hash(obj) 312 | group(1, '{', '}') { 313 | seplist(obj, nil, :each_pair) {|k, v| 314 | group { 315 | pp_hash_pair k, v 316 | } 317 | } 318 | } 319 | end 320 | 321 | if RUBY_VERSION >= '3.4.' 322 | # A pretty print for a pair of Hash 323 | def pp_hash_pair(k, v) 324 | if Symbol === k 325 | sym_s = k.inspect 326 | if sym_s[1].match?(/["$@!]/) || sym_s[-1].match?(/[%&*+\-\/<=>@\]^`|~]/) 327 | text "#{k.to_s.inspect}:" 328 | else 329 | text "#{k}:" 330 | end 331 | else 332 | pp k 333 | text ' ' 334 | text '=>' 335 | end 336 | group(1) { 337 | breakable 338 | pp v 339 | } 340 | end 341 | else 342 | def pp_hash_pair(k, v) 343 | pp k 344 | text '=>' 345 | group(1) { 346 | breakable '' 347 | pp v 348 | } 349 | end 350 | end 351 | end 352 | 353 | include PPMethods 354 | 355 | class SingleLine < PrettyPrint::SingleLine # :nodoc: 356 | include PPMethods 357 | end 358 | 359 | module ObjectMixin # :nodoc: 360 | # 1. specific pretty_print 361 | # 2. specific inspect 362 | # 3. generic pretty_print 363 | 364 | # A default pretty printing method for general objects. 365 | # It calls #pretty_print_instance_variables to list instance variables. 366 | # 367 | # If +self+ has a customized (redefined) #inspect method, 368 | # the result of self.inspect is used but it obviously has no 369 | # line break hints. 370 | # 371 | # This module provides predefined #pretty_print methods for some of 372 | # the most commonly used built-in classes for convenience. 373 | def pretty_print(q) 374 | umethod_method = Object.instance_method(:method) 375 | begin 376 | inspect_method = umethod_method.bind_call(self, :inspect) 377 | rescue NameError 378 | end 379 | if inspect_method && inspect_method.owner != Kernel 380 | q.text self.inspect 381 | elsif !inspect_method && self.respond_to?(:inspect) 382 | q.text self.inspect 383 | else 384 | q.pp_object(self) 385 | end 386 | end 387 | 388 | # A default pretty printing method for general objects that are 389 | # detected as part of a cycle. 390 | def pretty_print_cycle(q) 391 | q.object_address_group(self) { 392 | q.breakable 393 | q.text '...' 394 | } 395 | end 396 | 397 | # Returns a sorted array of instance variable names. 398 | # 399 | # This method should return an array of names of instance variables as symbols or strings as: 400 | # +[:@a, :@b]+. 401 | def pretty_print_instance_variables 402 | instance_variables.sort 403 | end 404 | 405 | # Is #inspect implementation using #pretty_print. 406 | # If you implement #pretty_print, it can be used as follows. 407 | # 408 | # alias inspect pretty_print_inspect 409 | # 410 | # However, doing this requires that every class that #inspect is called on 411 | # implement #pretty_print, or a RuntimeError will be raised. 412 | def pretty_print_inspect 413 | if Object.instance_method(:method).bind_call(self, :pretty_print).owner == PP::ObjectMixin 414 | raise "pretty_print is not overridden for #{self.class}" 415 | end 416 | PP.singleline_pp(self, ''.dup) 417 | end 418 | end 419 | end 420 | 421 | class Array # :nodoc: 422 | def pretty_print(q) # :nodoc: 423 | q.group(1, '[', ']') { 424 | q.seplist(self) {|v| 425 | q.pp v 426 | } 427 | } 428 | end 429 | 430 | def pretty_print_cycle(q) # :nodoc: 431 | q.text(empty? ? '[]' : '[...]') 432 | end 433 | end 434 | 435 | class Hash # :nodoc: 436 | def pretty_print(q) # :nodoc: 437 | q.pp_hash self 438 | end 439 | 440 | def pretty_print_cycle(q) # :nodoc: 441 | q.text(empty? ? '{}' : '{...}') 442 | end 443 | end 444 | 445 | class Set # :nodoc: 446 | def pretty_print(pp) # :nodoc: 447 | pp.group(1, '#') { 448 | pp.breakable 449 | pp.group(1, '{', '}') { 450 | pp.seplist(self) { |o| 451 | pp.pp o 452 | } 453 | } 454 | } 455 | end 456 | 457 | def pretty_print_cycle(pp) # :nodoc: 458 | pp.text sprintf('#', empty? ? '' : '...') 459 | end 460 | end 461 | 462 | class << ENV # :nodoc: 463 | def pretty_print(q) # :nodoc: 464 | h = {} 465 | ENV.keys.sort.each {|k| 466 | h[k] = ENV[k] 467 | } 468 | q.pp_hash h 469 | end 470 | end 471 | 472 | class Struct # :nodoc: 473 | def pretty_print(q) # :nodoc: 474 | q.group(1, sprintf("#') { 475 | q.seplist(PP.mcall(self, Struct, :members), lambda { q.text "," }) {|member| 476 | q.breakable 477 | q.text member.to_s 478 | q.text '=' 479 | q.group(1) { 480 | q.breakable '' 481 | q.pp self[member] 482 | } 483 | } 484 | } 485 | end 486 | 487 | def pretty_print_cycle(q) # :nodoc: 488 | q.text sprintf("#", PP.mcall(self, Kernel, :class).name) 489 | end 490 | end 491 | 492 | class Data # :nodoc: 493 | def pretty_print(q) # :nodoc: 494 | class_name = PP.mcall(self, Kernel, :class).name 495 | class_name = " #{class_name}" if class_name 496 | q.group(1, "#') { 497 | 498 | members = PP.mcall(self, Kernel, :class).members 499 | values = [] 500 | members.select! do |member| 501 | begin 502 | values << __send__(member) 503 | true 504 | rescue NoMethodError 505 | false 506 | end 507 | end 508 | 509 | q.seplist(members.zip(values), lambda { q.text "," }) {|(member, value)| 510 | q.breakable 511 | q.text member.to_s 512 | q.text '=' 513 | q.group(1) { 514 | q.breakable '' 515 | q.pp value 516 | } 517 | } 518 | } 519 | end 520 | 521 | def pretty_print_cycle(q) # :nodoc: 522 | q.text sprintf("#", PP.mcall(self, Kernel, :class).name) 523 | end 524 | end if defined?(Data.define) 525 | 526 | class Range # :nodoc: 527 | def pretty_print(q) # :nodoc: 528 | begin_nil = self.begin == nil 529 | end_nil = self.end == nil 530 | q.pp self.begin if !begin_nil || end_nil 531 | q.breakable '' 532 | q.text(self.exclude_end? ? '...' : '..') 533 | q.breakable '' 534 | q.pp self.end if !end_nil || begin_nil 535 | end 536 | end 537 | 538 | class String # :nodoc: 539 | def pretty_print(q) # :nodoc: 540 | lines = self.lines 541 | if lines.size > 1 542 | q.group(0, '', '') do 543 | q.seplist(lines, lambda { q.text ' +'; q.breakable }) do |v| 544 | q.pp v 545 | end 546 | end 547 | else 548 | q.text inspect 549 | end 550 | end 551 | end 552 | 553 | class File < IO # :nodoc: 554 | class Stat # :nodoc: 555 | def pretty_print(q) # :nodoc: 556 | require 'etc' 557 | q.object_group(self) { 558 | q.breakable 559 | q.text sprintf("dev=0x%x", self.dev); q.comma_breakable 560 | q.text "ino="; q.pp self.ino; q.comma_breakable 561 | q.group { 562 | m = self.mode 563 | q.text sprintf("mode=0%o", m) 564 | q.breakable 565 | q.text sprintf("(%s %c%c%c%c%c%c%c%c%c)", 566 | self.ftype, 567 | (m & 0400 == 0 ? ?- : ?r), 568 | (m & 0200 == 0 ? ?- : ?w), 569 | (m & 0100 == 0 ? (m & 04000 == 0 ? ?- : ?S) : 570 | (m & 04000 == 0 ? ?x : ?s)), 571 | (m & 0040 == 0 ? ?- : ?r), 572 | (m & 0020 == 0 ? ?- : ?w), 573 | (m & 0010 == 0 ? (m & 02000 == 0 ? ?- : ?S) : 574 | (m & 02000 == 0 ? ?x : ?s)), 575 | (m & 0004 == 0 ? ?- : ?r), 576 | (m & 0002 == 0 ? ?- : ?w), 577 | (m & 0001 == 0 ? (m & 01000 == 0 ? ?- : ?T) : 578 | (m & 01000 == 0 ? ?x : ?t))) 579 | } 580 | q.comma_breakable 581 | q.text "nlink="; q.pp self.nlink; q.comma_breakable 582 | q.group { 583 | q.text "uid="; q.pp self.uid 584 | begin 585 | pw = Etc.getpwuid(self.uid) 586 | rescue ArgumentError 587 | end 588 | if pw 589 | q.breakable; q.text "(#{pw.name})" 590 | end 591 | } 592 | q.comma_breakable 593 | q.group { 594 | q.text "gid="; q.pp self.gid 595 | begin 596 | gr = Etc.getgrgid(self.gid) 597 | rescue ArgumentError 598 | end 599 | if gr 600 | q.breakable; q.text "(#{gr.name})" 601 | end 602 | } 603 | q.comma_breakable 604 | q.group { 605 | q.text sprintf("rdev=0x%x", self.rdev) 606 | if self.rdev_major && self.rdev_minor 607 | q.breakable 608 | q.text sprintf('(%d, %d)', self.rdev_major, self.rdev_minor) 609 | end 610 | } 611 | q.comma_breakable 612 | q.text "size="; q.pp self.size; q.comma_breakable 613 | q.text "blksize="; q.pp self.blksize; q.comma_breakable 614 | q.text "blocks="; q.pp self.blocks; q.comma_breakable 615 | q.group { 616 | t = self.atime 617 | q.text "atime="; q.pp t 618 | q.breakable; q.text "(#{t.tv_sec})" 619 | } 620 | q.comma_breakable 621 | q.group { 622 | t = self.mtime 623 | q.text "mtime="; q.pp t 624 | q.breakable; q.text "(#{t.tv_sec})" 625 | } 626 | q.comma_breakable 627 | q.group { 628 | t = self.ctime 629 | q.text "ctime="; q.pp t 630 | q.breakable; q.text "(#{t.tv_sec})" 631 | } 632 | } 633 | end 634 | end 635 | end 636 | 637 | class MatchData # :nodoc: 638 | def pretty_print(q) # :nodoc: 639 | nc = [] 640 | self.regexp.named_captures.each {|name, indexes| 641 | indexes.each {|i| nc[i] = name } 642 | } 643 | q.object_group(self) { 644 | q.breakable 645 | q.seplist(0...self.size, lambda { q.breakable }) {|i| 646 | if i == 0 647 | q.pp self[i] 648 | else 649 | if nc[i] 650 | q.text nc[i] 651 | else 652 | q.pp i 653 | end 654 | q.text ':' 655 | q.pp self[i] 656 | end 657 | } 658 | } 659 | end 660 | end 661 | 662 | if defined?(RubyVM::AbstractSyntaxTree) 663 | class RubyVM::AbstractSyntaxTree::Node # :nodoc: 664 | def pretty_print_children(q, names = []) 665 | children.zip(names) do |c, n| 666 | if n 667 | q.breakable 668 | q.text "#{n}:" 669 | end 670 | q.group(2) do 671 | q.breakable 672 | q.pp c 673 | end 674 | end 675 | end 676 | 677 | def pretty_print(q) 678 | q.group(1, "(#{type}@#{first_lineno}:#{first_column}-#{last_lineno}:#{last_column}", ")") { 679 | case type 680 | when :SCOPE 681 | pretty_print_children(q, %w"tbl args body") 682 | when :ARGS 683 | pretty_print_children(q, %w[pre_num pre_init opt first_post post_num post_init rest kw kwrest block]) 684 | when :DEFN 685 | pretty_print_children(q, %w[mid body]) 686 | when :ARYPTN 687 | pretty_print_children(q, %w[const pre rest post]) 688 | when :HSHPTN 689 | pretty_print_children(q, %w[const kw kwrest]) 690 | else 691 | pretty_print_children(q) 692 | end 693 | } 694 | end 695 | end 696 | end 697 | 698 | class Object < BasicObject # :nodoc: 699 | include PP::ObjectMixin 700 | end 701 | 702 | [Numeric, Symbol, FalseClass, TrueClass, NilClass, Module].each {|c| 703 | c.class_eval { 704 | def pretty_print_cycle(q) 705 | q.text inspect 706 | end 707 | } 708 | } 709 | 710 | [Numeric, FalseClass, TrueClass, Module].each {|c| 711 | c.class_eval { 712 | def pretty_print(q) 713 | q.text inspect 714 | end 715 | } 716 | } 717 | 718 | module Kernel 719 | # Returns a pretty printed object as a string. 720 | # 721 | # See the PP module for more information. 722 | def pretty_inspect 723 | PP.pp(self, ''.dup) 724 | end 725 | 726 | # prints arguments in pretty form. 727 | # 728 | # +#pp+ returns argument(s). 729 | def pp(*objs) 730 | objs.each {|obj| 731 | PP.pp(obj) 732 | } 733 | objs.size <= 1 ? objs.first : objs 734 | end 735 | module_function :pp 736 | end 737 | -------------------------------------------------------------------------------- /pp.gemspec: -------------------------------------------------------------------------------- 1 | name = File.basename(__FILE__, ".gemspec") 2 | version = ["lib", Array.new(name.count("-")+1).join("/")].find do |dir| 3 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| 4 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 5 | end rescue nil 6 | end 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = name 10 | spec.version = version 11 | spec.authors = ["Tanaka Akira"] 12 | spec.email = ["akr@fsij.org"] 13 | 14 | spec.summary = %q{Provides a PrettyPrinter for Ruby objects} 15 | spec.description = %q{Provides a PrettyPrinter for Ruby objects} 16 | spec.homepage = "https://github.com/ruby/pp" 17 | spec.licenses = ["Ruby", "BSD-2-Clause"] 18 | 19 | spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0") 20 | 21 | spec.metadata["homepage_uri"] = spec.homepage 22 | spec.metadata["source_code_uri"] = spec.homepage 23 | 24 | spec.files = %w[ 25 | BSDL 26 | COPYING 27 | lib/pp.rb 28 | pp.gemspec 29 | ] 30 | spec.bindir = "exe" 31 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 32 | spec.require_paths = ["lib"] 33 | 34 | spec.add_dependency "prettyprint" 35 | end 36 | -------------------------------------------------------------------------------- /test/test_pp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pp' 4 | require 'delegate' 5 | require 'test/unit' 6 | require 'ruby2_keywords' 7 | 8 | module PPTestModule 9 | 10 | class PPTest < Test::Unit::TestCase 11 | def test_list0123_12 12 | assert_equal("[0, 1, 2, 3]\n", PP.pp([0,1,2,3], ''.dup, 12)) 13 | end 14 | 15 | def test_list0123_11 16 | assert_equal("[0,\n 1,\n 2,\n 3]\n", PP.pp([0,1,2,3], ''.dup, 11)) 17 | end 18 | 19 | OverriddenStruct = Struct.new("OverriddenStruct", :members, :class) 20 | def test_struct_override_members # [ruby-core:7865] 21 | a = OverriddenStruct.new(1,2) 22 | assert_equal("#\n", PP.pp(a, ''.dup)) 23 | end 24 | 25 | def test_redefined_method 26 | o = "".dup 27 | def o.method 28 | end 29 | assert_equal(%(""\n), PP.pp(o, "".dup)) 30 | end 31 | 32 | def test_range 33 | assert_equal("0..1\n", PP.pp(0..1, "".dup)) 34 | assert_equal("0...1\n", PP.pp(0...1, "".dup)) 35 | assert_equal("0...\n", PP.pp(0..., "".dup)) 36 | assert_equal("...1\n", PP.pp(...1, "".dup)) 37 | assert_equal("..false\n", PP.pp(..false, "".dup)) 38 | assert_equal("false..\n", PP.pp(false.., "".dup)) 39 | assert_equal("false..false\n", PP.pp(false..false, "".dup)) 40 | assert_equal("nil..nil\n", PP.pp(nil..nil, "".dup)) 41 | end 42 | end 43 | 44 | class HasInspect 45 | def initialize(a) 46 | @a = a 47 | end 48 | 49 | def inspect 50 | return "" 51 | end 52 | end 53 | 54 | class HasPrettyPrint 55 | def initialize(a) 56 | @a = a 57 | end 58 | 59 | def pretty_print(q) 60 | q.text "" 63 | end 64 | end 65 | 66 | class HasBoth 67 | def initialize(a) 68 | @a = a 69 | end 70 | 71 | def inspect 72 | return "" 73 | end 74 | 75 | def pretty_print(q) 76 | q.text "" 79 | end 80 | end 81 | 82 | class PrettyPrintInspect < HasPrettyPrint 83 | alias inspect pretty_print_inspect 84 | end 85 | 86 | class PrettyPrintInspectWithoutPrettyPrint 87 | alias inspect pretty_print_inspect 88 | end 89 | 90 | class PPInspectTest < Test::Unit::TestCase 91 | def test_hasinspect 92 | a = HasInspect.new(1) 93 | assert_equal("\n", PP.pp(a, ''.dup)) 94 | end 95 | 96 | def test_hasprettyprint 97 | a = HasPrettyPrint.new(1) 98 | assert_equal("\n", PP.pp(a, ''.dup)) 99 | end 100 | 101 | def test_hasboth 102 | a = HasBoth.new(1) 103 | assert_equal("\n", PP.pp(a, ''.dup)) 104 | end 105 | 106 | def test_pretty_print_inspect 107 | a = PrettyPrintInspect.new(1) 108 | assert_equal("", a.inspect) 109 | a = PrettyPrintInspectWithoutPrettyPrint.new 110 | assert_raise(RuntimeError) { a.inspect } 111 | end 112 | 113 | def test_proc 114 | a = proc {1} 115 | assert_equal("#{a.inspect}\n", PP.pp(a, ''.dup)) 116 | end 117 | 118 | def test_to_s_with_iv 119 | a = Object.new 120 | def a.to_s() "aaa" end 121 | a.instance_eval { @a = nil } 122 | result = PP.pp(a, ''.dup) 123 | assert_equal("#{a.inspect}\n", result) 124 | end 125 | 126 | def test_to_s_without_iv 127 | a = Object.new 128 | def a.to_s() "aaa" end 129 | result = PP.pp(a, ''.dup) 130 | assert_equal("#{a.inspect}\n", result) 131 | end 132 | 133 | def test_basic_object 134 | a = BasicObject.new 135 | assert_match(/\A#\n\z/, PP.pp(a, ''.dup)) 136 | end 137 | end 138 | 139 | class PPCycleTest < Test::Unit::TestCase 140 | def test_array 141 | a = [] 142 | a << a 143 | assert_equal("[[...]]\n", PP.pp(a, ''.dup)) 144 | assert_equal("#{a.inspect}\n", PP.pp(a, ''.dup)) 145 | end 146 | 147 | def test_hash 148 | a = {} 149 | a[0] = a 150 | assert_equal("#{a.inspect}\n", PP.pp(a, ''.dup)) 151 | end 152 | 153 | S = Struct.new("S", :a, :b) 154 | def test_struct 155 | a = S.new(1,2) 156 | a.b = a 157 | assert_equal("#>\n", PP.pp(a, ''.dup)) 158 | assert_equal("#{a.inspect}\n", PP.pp(a, ''.dup)) unless RUBY_ENGINE == "truffleruby" 159 | end 160 | 161 | if defined?(Data.define) 162 | D = Data.define(:aaa, :bbb) 163 | def test_data 164 | a = D.new("aaa", "bbb") 165 | assert_equal("#\n", PP.pp(a, ''.dup, 20)) 166 | assert_equal("#{a.inspect}\n", PP.pp(a, ''.dup)) 167 | 168 | b = Data.define(:a).new(42) 169 | assert_equal("#{b.inspect}\n", PP.pp(b, ''.dup)) 170 | end 171 | 172 | D2 = Data.define(:aaa, :bbb) do 173 | private :aaa 174 | end 175 | def test_data_private_member 176 | a = D2.new("aaa", "bbb") 177 | assert_equal("#\n", PP.pp(a, ''.dup, 20)) 178 | end 179 | 180 | D3 = Data.define(:aaa, :bbb) do 181 | remove_method :aaa 182 | end 183 | def test_data_removed_member 184 | a = D3.new("aaa", "bbb") 185 | assert_equal("#\n", PP.pp(a, ''.dup, 20)) 186 | end 187 | end 188 | 189 | def test_object 190 | a = Object.new 191 | a.instance_eval {@a = a} 192 | assert_equal(a.inspect + "\n", PP.pp(a, ''.dup)) 193 | end 194 | 195 | def test_anonymous 196 | a = Class.new.new 197 | assert_equal(a.inspect + "\n", PP.pp(a, ''.dup)) 198 | end 199 | 200 | def test_withinspect 201 | omit if RUBY_ENGINE == "jruby" or RUBY_ENGINE == "truffleruby" 202 | a = [] 203 | a << HasInspect.new(a) 204 | assert_equal("[]\n", PP.pp(a, ''.dup)) 205 | assert_equal("#{a.inspect}\n", PP.pp(a, ''.dup)) 206 | end 207 | 208 | def test_share_nil 209 | begin 210 | PP.sharing_detection = true 211 | a = [nil, nil] 212 | assert_equal("[nil, nil]\n", PP.pp(a, ''.dup)) 213 | ensure 214 | PP.sharing_detection = false 215 | end 216 | end 217 | end 218 | 219 | class PPSingleLineTest < Test::Unit::TestCase 220 | def test_hash 221 | assert_equal({1 => 1}.inspect, PP.singleline_pp({1 => 1}, ''.dup)) # [ruby-core:02699] 222 | assert_equal("[1#{', 1'*99}]", PP.singleline_pp([1]*100, ''.dup)) 223 | end 224 | 225 | def test_hash_symbol_colon_key 226 | omit if RUBY_VERSION < "3.4." 227 | no_quote = "{a: 1, a!: 1, a?: 1}" 228 | unicode_quote = "{\u{3042}: 1}" 229 | quote0 = '{"": 1}' 230 | quote1 = '{"0": 1, "!": 1, "%": 1, "&": 1, "*": 1, "+": 1, "-": 1, "/": 1, "<": 1, ">": 1, "^": 1, "`": 1, "|": 1, "~": 1}' 231 | quote2 = '{"@a": 1, "$a": 1, "+@": 1, "a=": 1, "[]": 1}' 232 | quote3 = '{"a\"b": 1, "@@a": 1, "<=>": 1, "===": 1, "[]=": 1}' 233 | assert_equal(no_quote, PP.singleline_pp(eval(no_quote), ''.dup)) 234 | assert_equal({ "\u3042": 1 }.inspect, PP.singleline_pp(eval(unicode_quote), ''.dup)) 235 | assert_equal(quote0, PP.singleline_pp(eval(quote0), ''.dup)) 236 | assert_equal(quote1, PP.singleline_pp(eval(quote1), ''.dup)) 237 | assert_equal(quote2, PP.singleline_pp(eval(quote2), ''.dup)) 238 | assert_equal(quote3, PP.singleline_pp(eval(quote3), ''.dup)) 239 | end 240 | 241 | def test_hash_in_array 242 | omit if RUBY_ENGINE == "jruby" 243 | assert_equal("[{}]", PP.singleline_pp([->(*a){a.last.clear}.ruby2_keywords.call(a: 1)], ''.dup)) 244 | assert_equal("[{}]", PP.singleline_pp([Hash.ruby2_keywords_hash({})], ''.dup)) 245 | end 246 | 247 | def test_direct_pp 248 | buffer = String.new 249 | 250 | a = [] 251 | a << a 252 | 253 | # Isolate the test from any existing Thread.current[:__recursive_key__][:inspect]. 254 | Thread.new do 255 | q = PP::SingleLine.new(buffer) 256 | q.pp(a) 257 | q.flush 258 | end.join 259 | 260 | assert_equal("[[...]]", buffer) 261 | end 262 | end 263 | 264 | class PPDelegateTest < Test::Unit::TestCase 265 | class A < DelegateClass(Array); end 266 | 267 | def test_delegate 268 | assert_equal("[]\n", A.new([]).pretty_inspect, "[ruby-core:25804]") 269 | end 270 | 271 | def test_delegate_cycle 272 | a = HasPrettyPrint.new nil 273 | 274 | a.instance_eval {@a = a} 275 | cycle_pretty_inspect = a.pretty_inspect 276 | 277 | a.instance_eval {@a = SimpleDelegator.new(a)} 278 | delegator_cycle_pretty_inspect = a.pretty_inspect 279 | 280 | assert_equal(cycle_pretty_inspect, delegator_cycle_pretty_inspect) 281 | end 282 | end 283 | 284 | class PPFileStatTest < Test::Unit::TestCase 285 | def test_nothing_raised 286 | assert_nothing_raised do 287 | File.stat(__FILE__).pretty_inspect 288 | end 289 | end 290 | end 291 | 292 | if defined?(RubyVM) 293 | class PPAbstractSyntaxTree < Test::Unit::TestCase 294 | AST = RubyVM::AbstractSyntaxTree 295 | def test_lasgn_literal 296 | ast = AST.parse("_=1") 297 | integer = RUBY_VERSION >= "3.4." ? "INTEGER" : "LIT" 298 | expected = "(SCOPE@1:0-1:3 tbl: [:_] args: nil body: (LASGN@1:0-1:3 :_ (#{integer}@1:2-1:3 1)))" 299 | assert_equal(expected, PP.singleline_pp(ast, ''.dup), ast) 300 | end 301 | end 302 | end 303 | 304 | class PPInheritedTest < Test::Unit::TestCase 305 | class PPSymbolHash < PP 306 | def pp_hash_pair(k, v) 307 | case k 308 | when Symbol 309 | text k.inspect.delete_prefix(":").tr('"', "'") 310 | text ":" 311 | group(1) { 312 | breakable 313 | pp v 314 | } 315 | else 316 | super 317 | end 318 | end 319 | end 320 | 321 | def test_hash_override 322 | obj = {k: 1, "": :null, "0": :zero, 100 => :ten} 323 | sep = RUBY_VERSION >= "3.4." ? " => " : "=>" 324 | assert_equal <<~EXPECT, PPSymbolHash.pp(obj, "".dup) 325 | {k: 1, '': :null, '0': :zero, 100#{sep}:ten} 326 | EXPECT 327 | end 328 | end 329 | 330 | end 331 | --------------------------------------------------------------------------------