├── .bundle └── config ├── .clang-format ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── build.yml │ ├── daily.yml │ ├── fedora.yml │ ├── ubuntu.yml │ └── windows.yml ├── .gitignore ├── .mdlrc ├── .rubocop.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── GUIDE.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── Ruby_Grant_2014_Report.md ├── bin ├── mdl ├── minitest ├── rake ├── rubocop └── setup.sh ├── byebug.gemspec ├── code_of_conduct.md ├── docker ├── Dockerfile └── manager.rb ├── exe └── byebug ├── ext └── byebug │ ├── breakpoint.c │ ├── byebug.c │ ├── byebug.h │ ├── context.c │ ├── extconf.rb │ ├── locker.c │ └── threads.c ├── gemfiles └── lint │ ├── Gemfile │ └── Gemfile.lock ├── lib ├── byebug.rb └── byebug │ ├── attacher.rb │ ├── breakpoint.rb │ ├── command.rb │ ├── command_list.rb │ ├── commands.rb │ ├── commands │ ├── break.rb │ ├── catch.rb │ ├── condition.rb │ ├── continue.rb │ ├── debug.rb │ ├── delete.rb │ ├── disable.rb │ ├── disable │ │ ├── breakpoints.rb │ │ └── display.rb │ ├── display.rb │ ├── down.rb │ ├── edit.rb │ ├── enable.rb │ ├── enable │ │ ├── breakpoints.rb │ │ └── display.rb │ ├── finish.rb │ ├── frame.rb │ ├── help.rb │ ├── history.rb │ ├── info.rb │ ├── info │ │ ├── breakpoints.rb │ │ ├── display.rb │ │ ├── file.rb │ │ ├── line.rb │ │ └── program.rb │ ├── interrupt.rb │ ├── irb.rb │ ├── kill.rb │ ├── list.rb │ ├── method.rb │ ├── next.rb │ ├── pry.rb │ ├── quit.rb │ ├── restart.rb │ ├── save.rb │ ├── set.rb │ ├── show.rb │ ├── skip.rb │ ├── source.rb │ ├── step.rb │ ├── thread.rb │ ├── thread │ │ ├── current.rb │ │ ├── list.rb │ │ ├── resume.rb │ │ ├── stop.rb │ │ └── switch.rb │ ├── tracevar.rb │ ├── undisplay.rb │ ├── untracevar.rb │ ├── up.rb │ ├── var.rb │ ├── var │ │ ├── all.rb │ │ ├── args.rb │ │ ├── const.rb │ │ ├── global.rb │ │ ├── instance.rb │ │ └── local.rb │ └── where.rb │ ├── context.rb │ ├── core.rb │ ├── errors.rb │ ├── frame.rb │ ├── helpers │ ├── bin.rb │ ├── eval.rb │ ├── file.rb │ ├── frame.rb │ ├── parse.rb │ ├── path.rb │ ├── reflection.rb │ ├── string.rb │ ├── thread.rb │ ├── toggle.rb │ └── var.rb │ ├── history.rb │ ├── interface.rb │ ├── interfaces │ ├── local_interface.rb │ ├── remote_interface.rb │ ├── script_interface.rb │ └── test_interface.rb │ ├── option_setter.rb │ ├── printers │ ├── base.rb │ ├── plain.rb │ └── texts │ │ ├── base.yml │ │ └── plain.yml │ ├── processors │ ├── command_processor.rb │ ├── control_processor.rb │ ├── post_mortem_processor.rb │ └── script_processor.rb │ ├── remote.rb │ ├── remote │ ├── client.rb │ └── server.rb │ ├── runner.rb │ ├── setting.rb │ ├── settings │ ├── autoirb.rb │ ├── autolist.rb │ ├── autopry.rb │ ├── autosave.rb │ ├── basename.rb │ ├── callstyle.rb │ ├── fullpath.rb │ ├── histfile.rb │ ├── histsize.rb │ ├── linetrace.rb │ ├── listsize.rb │ ├── post_mortem.rb │ ├── savefile.rb │ ├── stack_on_error.rb │ └── width.rb │ ├── source_file_formatter.rb │ ├── subcommands.rb │ └── version.rb ├── tasks ├── compile.rake ├── docker.rake ├── docs.rake ├── lint.rake ├── linter.rb ├── release.rake └── test.rake └── test ├── changelog_test.rb ├── commands ├── break_test.rb ├── catch_test.rb ├── condition_test.rb ├── continue_test.rb ├── debug_test.rb ├── delete_test.rb ├── disable_test.rb ├── display_test.rb ├── down_test.rb ├── edit_test.rb ├── enable_test.rb ├── eval_test.rb ├── finish_test.rb ├── frame_test.rb ├── help_test.rb ├── history_test.rb ├── info_test.rb ├── interrupt_test.rb ├── irb_test.rb ├── kill_test.rb ├── list_test.rb ├── method_test.rb ├── next_test.rb ├── pry_test.rb ├── quit_test.rb ├── restart_test.rb ├── save_test.rb ├── set_test.rb ├── show_test.rb ├── skip_test.rb ├── source_test.rb ├── step_test.rb ├── thread_test.rb ├── tracevar_test.rb ├── undisplay_test.rb ├── untracevar_test.rb ├── up_test.rb ├── var_test.rb └── where_test.rb ├── debugger_alias_test.rb ├── helpers └── bin_helper_test.rb ├── interface_test.rb ├── interfaces ├── local_interface_test.rb └── script_interface_test.rb ├── minitest_runner.rb ├── minitest_runner_test.rb ├── post_mortem_test.rb ├── printers └── plain_test.rb ├── processors ├── command_processor_test.rb └── script_processor_test.rb ├── rc_test.rb ├── remote_debugging_test.rb ├── remote_shortcut_test.rb ├── runner_against_program_with_byebug_call_test.rb ├── runner_against_valid_program_test.rb ├── runner_without_target_program_test.rb ├── support ├── assertions.rb ├── matchers.rb ├── remote_debugging_tests.rb ├── temporary.rb ├── test_case.rb └── utils.rb └── test_helper.rb /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_PATH: ".bundle" 3 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | AlwaysBreakAfterDefinitionReturnType: All 2 | BreakBeforeBinaryOperators: NonAssignment 3 | BreakBeforeBraces: Custom 4 | BraceWrapping: 5 | AfterControlStatement: true 6 | AfterEnum: true 7 | AfterFunction: true 8 | AfterStruct: true 9 | AfterUnion: true 10 | BeforeElse: true 11 | ColumnLimit: 0 12 | ContinuationIndentWidth: 2 13 | IndentCaseLabels: true 14 | MaxEmptyLinesToKeep: 2 15 | PointerAlignment: Right 16 | SpaceAfterCStyleCast: false 17 | SpacesBeforeTrailingComments: 1 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | github: deivid-rodriguez 4 | liberapay: byebug 5 | tidelift: "rubygems/byebug" 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Problem description 2 | 3 | Replace this text with a description of the problem you're having. Make 4 | sure that you're running the latest stable release of both byebug and ruby and 5 | that the problem you're reporting hasn't been reported (and potentially fixed) 6 | already. 7 | 8 | ## Expected behavior 9 | 10 | Describe here how you expected byebug to behave in this particular situation. 11 | 12 | ## Actual behavior 13 | 14 | Describe here what actually happened. 15 | 16 | ## Steps to reproduce the problem 17 | 18 | This is extremely important! Providing us with a reliable way to reproduce 19 | a problem will expedite its solution. 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: bundler 5 | directory: / 6 | schedule: 7 | interval: daily 8 | versioning-strategy: increase-if-necessary 9 | 10 | - package-ecosystem: bundler 11 | directory: /gemfiles/lint 12 | schedule: 13 | interval: daily 14 | versioning-strategy: increase-if-necessary 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: build 4 | 5 | on: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-24.04 11 | 12 | strategy: 13 | fail-fast: false 14 | 15 | matrix: 16 | version: [3.1.6, 3.2.7, 3.3.7] 17 | line_editor: [libedit, readline] 18 | compiler: [clang, gcc] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: 3.3.7 26 | bundler-cache: true 27 | 28 | - name: Build and push docker image 29 | run: bin/rake docker:build_and_push[${{ matrix.version }},${{ matrix.line_editor }},${{ matrix.compiler }}] 30 | env: 31 | DOCKER_PASS: ${{ secrets.DOCKER_PASS }} 32 | -------------------------------------------------------------------------------- /.github/workflows/daily.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: daily 4 | 5 | on: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | build_head_images: 11 | container: deividrodriguez/byebug:3.1.6-readline-gcc 12 | 13 | runs-on: ubuntu-24.04 14 | 15 | strategy: 16 | fail-fast: false 17 | 18 | matrix: 19 | line_editor: [libedit, readline] 20 | compiler: [clang, gcc] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Install Docker client 26 | run: | 27 | apk add curl 28 | curl -L -o /tmp/docker.tgz https://download.docker.com/linux/static/stable/x86_64/docker-19.03.5.tgz 29 | tar -xz --strip-components 1 -C /usr/bin -f /tmp/docker.tgz 30 | 31 | - name: Setup environment 32 | run: bin/setup.sh 33 | 34 | - name: Build and push docker image 35 | run: bin/rake docker:build_and_push_head[${{ matrix.line_editor }},${{ matrix.compiler }}] 36 | env: 37 | DOCKER_PASS: ${{ secrets.DOCKER_PASS }} 38 | continue-on-error: true 39 | 40 | test_head_images: 41 | container: deividrodriguez/byebug:head-${{ matrix.line_editor }}-${{ matrix.compiler }} 42 | 43 | needs: build_head_images 44 | 45 | runs-on: ubuntu-24.04 46 | 47 | strategy: 48 | fail-fast: false 49 | 50 | matrix: 51 | line_editor: [libedit, readline] 52 | compiler: [clang, gcc] 53 | 54 | steps: 55 | - name: Run CI checks 56 | run: | 57 | bin/setup.sh 58 | bin/rake 59 | 60 | timeout-minutes: 15 61 | -------------------------------------------------------------------------------- /.github/workflows/fedora.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: fedora 4 | 5 | on: 6 | pull_request: 7 | 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | install_and_run: 14 | container: fedora:42 15 | 16 | runs-on: ubuntu-24.04 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Install OS dependencies 22 | run: dnf install -y ruby ruby-devel make gcc redhat-rpm-config 23 | 24 | - name: Install development dependencies 25 | run: bundle install 26 | 27 | - name: Install byebug 28 | run: bin/rake install 29 | 30 | - name: Run byebug 31 | run: .bundle/ruby/3.4.0/bin/byebug -h 32 | env: 33 | GEM_HOME: .bundle/ruby/3.4.0 34 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: ubuntu 4 | 5 | on: 6 | pull_request: 7 | 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | test: 14 | container: deividrodriguez/byebug:${{ matrix.version }}-${{ matrix.line_editor }}-${{ matrix.compiler }} 15 | 16 | runs-on: ubuntu-24.04 17 | 18 | strategy: 19 | fail-fast: false 20 | 21 | matrix: 22 | version: [3.1.6, 3.2.7, 3.3.7] 23 | line_editor: [libedit, readline] 24 | compiler: [clang, gcc] 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - name: Run CI checks 30 | run: | 31 | bin/setup.sh 32 | bin/rake 33 | 34 | timeout-minutes: 15 35 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: windows 4 | 5 | on: 6 | pull_request: 7 | 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | test: 14 | runs-on: windows-2022 15 | 16 | strategy: 17 | fail-fast: false 18 | 19 | matrix: 20 | version: [3.1.6, 3.2.7, 3.3.7] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{ matrix.version }} 28 | 29 | - name: Patch rb-readline 30 | run: | 31 | git clone -q --depth=5 --no-tags --branch=byebug https://github.com/deivid-rodriguez/rb-readline.git C:\rb-readline 32 | $n_dir = $(ruby -e "print RbConfig::CONFIG['sitelibdir']") 33 | Copy-Item -Path C:\rb-readline\lib\* -Destination $n_dir -Recurse 34 | 35 | - name: Setup dependencies 36 | shell: bash 37 | run: bin/setup.sh 38 | 39 | - name: Run tests 40 | run: ruby bin/rake 41 | 42 | timeout-minutes: 15 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # Only generated files & Byebug specific files to be kept here. 3 | # 4 | tmp 5 | pkg 6 | doc 7 | .yardoc 8 | 9 | .gdb_history 10 | .bundle/* 11 | !.bundle/config 12 | .byebugrc 13 | .byebug_history 14 | .envrc 15 | 16 | lib/byebug/byebug.so 17 | lib/byebug/byebug.bundle 18 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | rules "~MD002,~MD013,~MD024,~MD055,~MD056,~MD057" 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | 7 | group :development do 8 | gem "chandler", "0.9.0" 9 | gem "minitest", "~> 5.25" 10 | gem "pry", "0.15.2" 11 | gem "rake", "~> 13.0" 12 | gem "rake-compiler", "~> 1.0" 13 | gem "yard", "0.9.37" 14 | gem "faraday-retry", "~> 2.2" 15 | end 16 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | byebug (12.0.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | addressable (2.8.7) 10 | public_suffix (>= 2.0.2, < 7.0) 11 | chandler (0.9.0) 12 | netrc 13 | octokit (>= 2.2.0) 14 | coderay (1.1.3) 15 | faraday (2.12.2) 16 | faraday-net_http (>= 2.0, < 3.5) 17 | json 18 | logger 19 | faraday-net_http (3.4.0) 20 | net-http (>= 0.5.0) 21 | faraday-retry (2.3.1) 22 | faraday (~> 2.0) 23 | json (2.10.2) 24 | logger (1.7.0) 25 | method_source (1.0.0) 26 | minitest (5.25.5) 27 | net-http (0.6.0) 28 | uri 29 | netrc (0.11.0) 30 | octokit (9.2.0) 31 | faraday (>= 1, < 3) 32 | sawyer (~> 0.9) 33 | pry (0.15.2) 34 | coderay (~> 1.1) 35 | method_source (~> 1.0) 36 | public_suffix (6.0.1) 37 | rake (13.2.1) 38 | rake-compiler (1.2.9) 39 | rake 40 | sawyer (0.9.2) 41 | addressable (>= 2.3.5) 42 | faraday (>= 0.17.3, < 3) 43 | uri (1.0.3) 44 | yard (0.9.37) 45 | 46 | PLATFORMS 47 | ruby 48 | x86_64-linux 49 | 50 | DEPENDENCIES 51 | bundler (~> 2.0) 52 | byebug! 53 | chandler (= 0.9.0) 54 | faraday-retry (~> 2.2) 55 | minitest (~> 5.25) 56 | pry (= 0.15.2) 57 | rake (~> 13.0) 58 | rake-compiler (~> 1.0) 59 | yard (= 0.9.37) 60 | 61 | BUNDLED WITH 62 | 2.6.6 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 David Rodríguez 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | import "tasks/compile.rake" 4 | import "tasks/test.rake" 5 | import "tasks/release.rake" 6 | import "tasks/docs.rake" 7 | import "tasks/lint.rake" 8 | import "tasks/docker.rake" 9 | 10 | if Gem.win_platform? 11 | task default: %i[compile test] 12 | else 13 | task default: %i[compile test lint] 14 | end 15 | -------------------------------------------------------------------------------- /bin/mdl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | ENV["BUNDLE_GEMFILE"] = File.expand_path("../gemfiles/lint/Gemfile", __dir__) 5 | 6 | require "rubygems" 7 | require "bundler/setup" 8 | 9 | load Gem.bin_path("mdl", "mdl") 10 | -------------------------------------------------------------------------------- /bin/minitest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 5 | 6 | require "rubygems" 7 | require "bundler/setup" 8 | 9 | require_relative "../test/minitest_runner" 10 | 11 | exit Byebug::MinitestRunner.new.run 12 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 5 | 6 | require "rubygems" 7 | require "bundler/setup" 8 | 9 | load Gem.bin_path("rake", "rake") 10 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | ENV["BUNDLE_GEMFILE"] = File.expand_path("../gemfiles/lint/Gemfile", __dir__) 5 | 6 | require "rubygems" 7 | require "bundler/setup" 8 | 9 | load Gem.bin_path("rubocop", "rubocop") 10 | -------------------------------------------------------------------------------- /bin/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | set +x 6 | 7 | gem update --system 3.6.6 8 | 9 | bundle install 10 | 11 | set -x 12 | -------------------------------------------------------------------------------- /byebug.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/byebug/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "byebug" 7 | s.version = Byebug::VERSION 8 | s.authors = ["David Rodriguez", "Kent Sibilev", "Mark Moseley"] 9 | s.email = "deivid.rodriguez@riseup.net" 10 | s.license = "BSD-2-Clause" 11 | s.homepage = "https://github.com/deivid-rodriguez/byebug" 12 | s.summary = "Ruby fast debugger - base + CLI" 13 | s.description = "Byebug is a Ruby debugger. It's implemented using the 14 | TracePoint C API for execution control and the Debug Inspector C API for 15 | call stack navigation. The core component provides support that front-ends 16 | can build on. It provides breakpoint handling and bindings for stack frames 17 | among other things and it comes with an easy to use command line interface." 18 | 19 | s.required_ruby_version = ">= 3.1.0" 20 | 21 | s.files = Dir["lib/**/*.rb", "lib/**/*.yml", "ext/**/*.[ch]", "LICENSE"] 22 | s.bindir = "exe" 23 | s.executables = ["byebug"] 24 | s.extra_rdoc_files = %w[CHANGELOG.md CONTRIBUTING.md README.md GUIDE.md] 25 | s.extensions = ["ext/byebug/extconf.rb"] 26 | s.require_path = "lib" 27 | 28 | s.add_development_dependency "bundler", "~> 2.0" 29 | end 30 | -------------------------------------------------------------------------------- /exe/byebug: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "byebug/runner" 5 | 6 | Byebug::Runner.new.run 7 | -------------------------------------------------------------------------------- /ext/byebug/extconf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "mkmf" 4 | 5 | makefile_config = RbConfig::MAKEFILE_CONFIG 6 | 7 | makefile_config["CC"] = ENV["CC"] if ENV["CC"] 8 | 9 | makefile_config["CFLAGS"] << " -gdwarf-2 -g3 -O0" if ENV["debug"] 10 | 11 | dir_config("ruby") 12 | with_cflags(makefile_config["CFLAGS"]) { create_makefile("byebug/byebug") } 13 | -------------------------------------------------------------------------------- /ext/byebug/locker.c: -------------------------------------------------------------------------------- 1 | #include "byebug.h" 2 | 3 | /** 4 | * A simple linked list containing locked threads, FIFO style. 5 | */ 6 | 7 | typedef struct locked_thread_t 8 | { 9 | VALUE thread; 10 | struct locked_thread_t *next; 11 | } locked_thread_t; 12 | 13 | static locked_thread_t *locked_head = NULL; 14 | static locked_thread_t *locked_tail = NULL; 15 | 16 | static int 17 | is_in_locked(VALUE thread) 18 | { 19 | locked_thread_t *node; 20 | 21 | if (!locked_head) 22 | return 0; 23 | 24 | for (node = locked_head; node != locked_tail; node = node->next) 25 | if (node->thread == thread) 26 | return 1; 27 | 28 | return 0; 29 | } 30 | 31 | extern void 32 | byebug_add_to_locked(VALUE thread) 33 | { 34 | locked_thread_t *node; 35 | 36 | if (is_in_locked(thread)) 37 | return; 38 | 39 | node = ALLOC(locked_thread_t); 40 | node->thread = thread; 41 | node->next = NULL; 42 | 43 | if (locked_tail) 44 | locked_tail->next = node; 45 | 46 | locked_tail = node; 47 | 48 | if (!locked_head) 49 | locked_head = node; 50 | } 51 | 52 | extern VALUE 53 | byebug_pop_from_locked() 54 | { 55 | VALUE thread; 56 | locked_thread_t *node; 57 | 58 | if (!locked_head) 59 | return Qnil; 60 | 61 | node = locked_head; 62 | locked_head = locked_head->next; 63 | 64 | if (locked_tail == node) 65 | locked_tail = NULL; 66 | 67 | thread = node->thread; 68 | xfree(node); 69 | 70 | return thread; 71 | } 72 | 73 | extern void 74 | byebug_remove_from_locked(VALUE thread) 75 | { 76 | locked_thread_t *node; 77 | locked_thread_t *next_node; 78 | 79 | if (NIL_P(thread) || !locked_head || !is_in_locked(thread)) 80 | return; 81 | 82 | if (locked_head->thread == thread) 83 | { 84 | byebug_pop_from_locked(); 85 | return; 86 | } 87 | 88 | for (node = locked_head; node != locked_tail; node = node->next) 89 | if (node->next && node->next->thread == thread) 90 | { 91 | next_node = node->next; 92 | node->next = next_node->next; 93 | xfree(next_node); 94 | return; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /gemfiles/lint/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | group :development do 6 | gem "mdl", "0.13.0" 7 | gem "rubocop", "~> 1.74" 8 | gem "rubocop-packaging", github: "utkarsh2102/rubocop-packaging" 9 | gem "rubocop-performance", "~> 1.24" 10 | gem "yard", "0.9.37" 11 | end 12 | -------------------------------------------------------------------------------- /gemfiles/lint/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/utkarsh2102/rubocop-packaging.git 3 | revision: 277bcfead602e59c3a89dca346c7ab58eeeb18ad 4 | specs: 5 | rubocop-packaging (0.6.0) 6 | lint_roller (~> 1.1.0) 7 | rubocop (>= 1.72.1, < 2.0) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | ast (2.4.3) 13 | chef-utils (18.6.2) 14 | concurrent-ruby 15 | concurrent-ruby (1.3.5) 16 | json (2.10.2) 17 | kramdown (2.5.1) 18 | rexml (>= 3.3.9) 19 | kramdown-parser-gfm (1.1.0) 20 | kramdown (~> 2.0) 21 | language_server-protocol (3.17.0.4) 22 | lint_roller (1.1.0) 23 | mdl (0.13.0) 24 | kramdown (~> 2.3) 25 | kramdown-parser-gfm (~> 1.1) 26 | mixlib-cli (~> 2.1, >= 2.1.1) 27 | mixlib-config (>= 2.2.1, < 4) 28 | mixlib-shellout 29 | mixlib-cli (2.1.8) 30 | mixlib-config (3.0.27) 31 | tomlrb 32 | mixlib-shellout (3.3.8) 33 | chef-utils 34 | parallel (1.26.3) 35 | parser (3.3.7.4) 36 | ast (~> 2.4.1) 37 | racc 38 | prism (1.4.0) 39 | racc (1.8.1) 40 | rainbow (3.1.1) 41 | regexp_parser (2.10.0) 42 | rexml (3.4.1) 43 | rubocop (1.75.2) 44 | json (~> 2.3) 45 | language_server-protocol (~> 3.17.0.2) 46 | lint_roller (~> 1.1.0) 47 | parallel (~> 1.10) 48 | parser (>= 3.3.0.2) 49 | rainbow (>= 2.2.2, < 4.0) 50 | regexp_parser (>= 2.9.3, < 3.0) 51 | rubocop-ast (>= 1.44.0, < 2.0) 52 | ruby-progressbar (~> 1.7) 53 | unicode-display_width (>= 2.4.0, < 4.0) 54 | rubocop-ast (1.44.0) 55 | parser (>= 3.3.7.2) 56 | prism (~> 1.4) 57 | rubocop-performance (1.25.0) 58 | lint_roller (~> 1.1) 59 | rubocop (>= 1.75.0, < 2.0) 60 | rubocop-ast (>= 1.38.0, < 2.0) 61 | ruby-progressbar (1.13.0) 62 | tomlrb (2.0.3) 63 | unicode-display_width (3.1.4) 64 | unicode-emoji (~> 4.0, >= 4.0.4) 65 | unicode-emoji (4.0.4) 66 | yard (0.9.37) 67 | 68 | PLATFORMS 69 | ruby 70 | x86_64-linux 71 | 72 | DEPENDENCIES 73 | mdl (= 0.13.0) 74 | rubocop (~> 1.74) 75 | rubocop-packaging! 76 | rubocop-performance (~> 1.24) 77 | yard (= 0.9.37) 78 | 79 | BUNDLED WITH 80 | 2.6.6 81 | -------------------------------------------------------------------------------- /lib/byebug.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "byebug/attacher" 4 | -------------------------------------------------------------------------------- /lib/byebug/attacher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # 4 | # Main Container for all of Byebug's code 5 | # 6 | module Byebug 7 | # 8 | # Starts byebug, and stops at the first line of user's code. 9 | # 10 | def self.attach 11 | unless started? 12 | self.mode = :attached 13 | 14 | start 15 | run_init_script 16 | end 17 | 18 | current_context.step_out(3, true) 19 | end 20 | 21 | def self.spawn(host = "localhost", port = nil) 22 | require_relative "core" 23 | 24 | self.wait_connection = true 25 | start_server(host, port || PORT) 26 | end 27 | end 28 | 29 | # 30 | # Adds a `byebug` method to the Kernel module. 31 | # 32 | # Dropping a `byebug` call anywhere in your code, you get a debug prompt. 33 | # 34 | module Kernel 35 | def byebug 36 | require_relative "core" 37 | 38 | Byebug.attach unless Byebug.mode == :off 39 | end 40 | 41 | def remote_byebug(host = "localhost", port = nil) 42 | Byebug.spawn(host, port) 43 | 44 | Byebug.attach 45 | end 46 | 47 | alias debugger byebug 48 | end 49 | -------------------------------------------------------------------------------- /lib/byebug/breakpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | # 5 | # Implements breakpoints 6 | # 7 | class Breakpoint 8 | # 9 | # First breakpoint, in order of creation 10 | # 11 | def self.first 12 | Byebug.breakpoints.first 13 | end 14 | 15 | # 16 | # Last breakpoint, in order of creation 17 | # 18 | def self.last 19 | Byebug.breakpoints.last 20 | end 21 | 22 | # 23 | # Adds a new breakpoint 24 | # 25 | # @param [String] file 26 | # @param [Fixnum] line 27 | # @param [String] expr 28 | # 29 | def self.add(file, line, expr = nil) 30 | breakpoint = Breakpoint.new(file, line, expr) 31 | Byebug.breakpoints << breakpoint 32 | breakpoint 33 | end 34 | 35 | # 36 | # Removes a breakpoint 37 | # 38 | # @param id [integer] breakpoint number 39 | # 40 | def self.remove(id) 41 | Byebug.breakpoints.reject! { |b| b.id == id } 42 | end 43 | 44 | # 45 | # Returns an array of line numbers in file named +filename+ where 46 | # breakpoints could be set. The list will contain an entry for each 47 | # distinct line event call so it is possible (and possibly useful) for a 48 | # line number appear more than once. 49 | # 50 | # @param filename [String] File name to inspect for possible breakpoints 51 | # 52 | def self.potential_lines(filename) 53 | name = "#{Time.new.to_i}_#{rand(2**31)}" 54 | iseq = RubyVM::InstructionSequence.compile(File.read(filename), name) 55 | 56 | potential_lines_with_trace_points(iseq, {}) 57 | end 58 | 59 | def self.potential_lines_with_trace_points(iseq, lines) 60 | iseq.trace_points.each { |(line, _)| lines[line] = true } 61 | iseq.each_child do |child| 62 | potential_lines_with_trace_points(child, lines) 63 | end 64 | 65 | lines.keys.sort 66 | end 67 | 68 | private_class_method :potential_lines_with_trace_points 69 | 70 | # 71 | # Returns true if a breakpoint could be set in line number +lineno+ in file 72 | # name +filename. 73 | # 74 | def self.potential_line?(filename, lineno) 75 | potential_lines(filename).member?(lineno) 76 | end 77 | 78 | # 79 | # True if there's no breakpoints 80 | # 81 | def self.none? 82 | Byebug.breakpoints.empty? 83 | end 84 | 85 | # 86 | # Prints all information associated to the breakpoint 87 | # 88 | def inspect 89 | meths = %w[id pos source expr hit_condition hit_count hit_value enabled?] 90 | values = meths.map { |field| "#{field}: #{send(field)}" }.join(", ") 91 | "#" 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/byebug/command_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "errors" 4 | 5 | module Byebug 6 | # 7 | # Holds an array of subcommands for a command 8 | # 9 | class CommandList 10 | include Enumerable 11 | 12 | def initialize(commands) 13 | @commands = commands.sort_by(&:to_s) 14 | end 15 | 16 | def match(input) 17 | find { |cmd| cmd.match(input) } 18 | end 19 | 20 | def each 21 | @commands.each { |cmd| yield(cmd) } 22 | end 23 | 24 | def to_s 25 | "\n" + map { |cmd| cmd.columnize(width) }.join + "\n" 26 | end 27 | 28 | private 29 | 30 | def width 31 | @width ||= map(&:to_s).max_by(&:size).size 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/byebug/commands.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "commands/break" 4 | require_relative "commands/catch" 5 | require_relative "commands/condition" 6 | require_relative "commands/continue" 7 | require_relative "commands/debug" 8 | require_relative "commands/delete" 9 | require_relative "commands/disable" 10 | require_relative "commands/display" 11 | require_relative "commands/down" 12 | require_relative "commands/edit" 13 | require_relative "commands/enable" 14 | require_relative "commands/finish" 15 | require_relative "commands/frame" 16 | require_relative "commands/help" 17 | require_relative "commands/history" 18 | require_relative "commands/info" 19 | require_relative "commands/interrupt" 20 | require_relative "commands/irb" 21 | require_relative "commands/kill" 22 | require_relative "commands/list" 23 | require_relative "commands/method" 24 | require_relative "commands/next" 25 | require_relative "commands/pry" 26 | require_relative "commands/quit" 27 | require_relative "commands/restart" 28 | require_relative "commands/save" 29 | require_relative "commands/set" 30 | require_relative "commands/show" 31 | require_relative "commands/skip" 32 | require_relative "commands/source" 33 | require_relative "commands/step" 34 | require_relative "commands/thread" 35 | require_relative "commands/tracevar" 36 | require_relative "commands/undisplay" 37 | require_relative "commands/untracevar" 38 | require_relative "commands/up" 39 | require_relative "commands/var" 40 | require_relative "commands/where" 41 | -------------------------------------------------------------------------------- /lib/byebug/commands/catch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/eval" 5 | 6 | module Byebug 7 | # 8 | # Implements exception catching. 9 | # 10 | # Enables the user to catch unhandled assertion when they happen. 11 | # 12 | class CatchCommand < Command 13 | include Helpers::EvalHelper 14 | 15 | self.allow_in_post_mortem = true 16 | 17 | def self.regexp 18 | /^\s* cat(?:ch)? (?:\s+(\S+))? (?:\s+(off))? \s*$/x 19 | end 20 | 21 | def self.description 22 | <<-DESCRIPTION 23 | cat[ch][ (off|[ off])] 24 | 25 | #{short_description} 26 | 27 | catch -- lists catchpoints 28 | catch off -- deletes all catchpoints 29 | catch -- enables handling 30 | catch off -- disables handling 31 | DESCRIPTION 32 | end 33 | 34 | def self.short_description 35 | "Handles exception catchpoints" 36 | end 37 | 38 | def execute 39 | return info unless @match[1] 40 | 41 | return @match[1] == "off" ? clear : add(@match[1]) unless @match[2] 42 | 43 | return errmsg pr("catch.errors.off", off: cmd) unless @match[2] == "off" 44 | 45 | remove(@match[1]) 46 | end 47 | 48 | private 49 | 50 | def remove(exception) 51 | return errmsg pr("catch.errors.not_found", exception: exception) unless Byebug.catchpoints.member?(exception) 52 | 53 | puts pr("catch.removed", exception: exception) 54 | Byebug.catchpoints.delete(exception) 55 | end 56 | 57 | def add(exception) 58 | errmsg pr("catch.errors.not_class", class: exception) if warning_eval(exception.is_a?(Class).to_s) 59 | 60 | puts pr("catch.added", exception: exception) 61 | Byebug.add_catchpoint(exception) 62 | end 63 | 64 | def clear 65 | Byebug.catchpoints.clear if confirm(pr("catch.confirmations.delete_all")) 66 | end 67 | 68 | def info 69 | if Byebug.catchpoints && !Byebug.catchpoints.empty? 70 | Byebug.catchpoints.each_key do |exception| 71 | puts("#{exception}: #{exception.is_a?(Class)}") 72 | end 73 | else 74 | puts "No exceptions set to be caught." 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/byebug/commands/condition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/parse" 5 | 6 | module Byebug 7 | # 8 | # Implements conditions on breakpoints. 9 | # 10 | # Adds the ability to stop on breakpoints only under certain conditions. 11 | # 12 | class ConditionCommand < Command 13 | include Helpers::ParseHelper 14 | 15 | self.allow_in_post_mortem = true 16 | 17 | def self.regexp 18 | /^\s* cond(?:ition)? (?:\s+(\d+)(?:\s+(.*))?)? \s*$/x 19 | end 20 | 21 | def self.description 22 | <<-DESCRIPTION 23 | cond[ition] [ expr] 24 | 25 | #{short_description} 26 | 27 | Specify breakpoint number to break only if is true. is 28 | an integer and is an expression to be evaluated whenever 29 | breakpoint is reached. If no expression is specified, the condition 30 | is removed. 31 | DESCRIPTION 32 | end 33 | 34 | def self.short_description 35 | "Sets conditions on breakpoints" 36 | end 37 | 38 | def execute 39 | return puts(help) unless @match[1] 40 | 41 | breakpoints = Byebug.breakpoints.sort_by(&:id) 42 | return errmsg(pr("condition.errors.no_breakpoints")) if breakpoints.empty? 43 | 44 | pos, err = get_int(@match[1], "Condition", 1) 45 | return errmsg(err) if err 46 | 47 | breakpoint = breakpoints.find { |b| b.id == pos } 48 | return errmsg(pr("break.errors.no_breakpoint")) unless breakpoint 49 | 50 | return errmsg(pr("break.errors.not_changed", expr: @match[2])) unless syntax_valid?(@match[2]) 51 | 52 | breakpoint.expr = @match[2] 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/byebug/commands/continue.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/parse" 5 | 6 | module Byebug 7 | # 8 | # Implements the continue command. 9 | # 10 | # Allows the user to continue execution until the next stopping point, a 11 | # specific line number or until program termination. 12 | # 13 | class ContinueCommand < Command 14 | include Helpers::ParseHelper 15 | 16 | def self.regexp 17 | /^\s* c(?:ont(?:inue)?)? (?:(!|\s+unconditionally|\s+\S+))? \s*$/x 18 | end 19 | 20 | def self.description 21 | <<-DESCRIPTION 22 | c[ont[inue]][ ] 23 | 24 | #{short_description} 25 | 26 | Normally the program stops at the next breakpoint. However, if the 27 | parameter "unconditionally" is given or the command is suffixed with 28 | "!", the program will run until the end regardless of any enabled 29 | breakpoints. 30 | DESCRIPTION 31 | end 32 | 33 | def self.short_description 34 | "Runs until program ends, hits a breakpoint or reaches a line" 35 | end 36 | 37 | def execute 38 | if until_line? 39 | num, err = get_int(modifier, "Continue", 0, nil) 40 | return errmsg(err) unless num 41 | 42 | filename = File.expand_path(frame.file) 43 | return errmsg(pr("continue.errors.unstopped_line", line: num)) unless Breakpoint.potential_line?(filename, num) 44 | 45 | Breakpoint.add(filename, num) 46 | end 47 | 48 | processor.proceed! 49 | 50 | Byebug.mode = :off if unconditionally? 51 | Byebug.stop if unconditionally? || Byebug.stoppable? 52 | end 53 | 54 | private 55 | 56 | def until_line? 57 | @match[1] && !["!", "unconditionally"].include?(modifier) 58 | end 59 | 60 | def unconditionally? 61 | @match[1] && ["!", "unconditionally"].include?(modifier) 62 | end 63 | 64 | def modifier 65 | @match[1].lstrip 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/byebug/commands/debug.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/eval" 5 | 6 | module Byebug 7 | # 8 | # Spawns a subdebugger and evaluates the given expression 9 | # 10 | class DebugCommand < Command 11 | include Helpers::EvalHelper 12 | 13 | def self.regexp 14 | /^\s* debug (?:\s+(\S+))? \s*$/x 15 | end 16 | 17 | def self.description 18 | <<-DESCRIPTION 19 | debug 20 | 21 | #{short_description} 22 | 23 | Allows, for example, setting breakpoints on expressions evaluated from 24 | the debugger's prompt. 25 | DESCRIPTION 26 | end 27 | 28 | def self.short_description 29 | "Spawns a subdebugger" 30 | end 31 | 32 | def execute 33 | return puts(help) unless @match[1] 34 | 35 | puts safe_inspect(separate_thread_eval(@match[1])) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/byebug/commands/delete.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/parse" 5 | 6 | module Byebug 7 | # 8 | # Implements breakpoint deletion. 9 | # 10 | class DeleteCommand < Command 11 | include Helpers::ParseHelper 12 | 13 | self.allow_in_control = true 14 | self.allow_in_post_mortem = true 15 | 16 | def self.regexp 17 | /^\s* del(?:ete)? (?:\s+(.*))?$/x 18 | end 19 | 20 | def self.description 21 | <<-DESCRIPTION 22 | del[ete][ nnn...] 23 | 24 | #{short_description} 25 | 26 | Without and argument, deletes all breakpoints. With integer arguments, 27 | it deletes specific breakpoints. 28 | DESCRIPTION 29 | end 30 | 31 | def self.short_description 32 | "Deletes breakpoints" 33 | end 34 | 35 | def execute 36 | unless @match[1] 37 | Byebug.breakpoints.clear if confirm(pr("break.confirmations.delete_all")) 38 | 39 | return 40 | end 41 | 42 | @match[1].split(/ +/).each do |number| 43 | pos, err = get_int(number, "Delete", 1) 44 | 45 | return errmsg(err) unless pos 46 | 47 | if Breakpoint.remove(pos) 48 | puts(pr("break.messages.breakpoint_deleted", pos: pos)) 49 | else 50 | errmsg(pr("break.errors.no_breakpoint_delete", pos: pos)) 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/byebug/commands/disable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../subcommands" 4 | 5 | require_relative "../commands/disable/breakpoints" 6 | require_relative "../commands/disable/display" 7 | 8 | module Byebug 9 | # 10 | # Disabling custom display expressions or breakpoints. 11 | # 12 | class DisableCommand < Command 13 | include Subcommands 14 | 15 | self.allow_in_post_mortem = true 16 | 17 | def self.regexp 18 | /^\s* dis(?:able)? (?:\s+ (.+))? \s*$/x 19 | end 20 | 21 | def self.description 22 | <<-DESCRIPTION 23 | dis[able][[ breakpoints| display)][ n1[ n2[ ...[ nn]]]]] 24 | 25 | #{short_description} 26 | DESCRIPTION 27 | end 28 | 29 | def self.short_description 30 | "Disables breakpoints or displays" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/byebug/commands/disable/breakpoints.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/toggle" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +disable+ command to define the +breakpoints+ subcommand 8 | # 9 | class DisableCommand < Command 10 | # 11 | # Disables all or specific breakpoints 12 | # 13 | class BreakpointsCommand < Command 14 | include Helpers::ToggleHelper 15 | 16 | self.allow_in_post_mortem = true 17 | 18 | def self.regexp 19 | /^\s* b(?:reakpoints)? (?:\s+ (.+))? \s*$/x 20 | end 21 | 22 | def self.description 23 | <<-DESCRIPTION 24 | dis[able] b[reakpoints][ .. ] 25 | 26 | #{short_description} 27 | 28 | Give breakpoint numbers (separated by spaces) as arguments or no 29 | argument at all if you want to disable every breakpoint. 30 | DESCRIPTION 31 | end 32 | 33 | def self.short_description 34 | "Disable all or specific breakpoints." 35 | end 36 | 37 | def execute 38 | enable_disable_breakpoints("disable", @match[1]) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/byebug/commands/disable/display.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/toggle" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +disable+ command to define the +display+ subcommand 8 | # 9 | class DisableCommand < Command 10 | # 11 | # Enables all or specific displays 12 | # 13 | class DisplayCommand < Command 14 | include Helpers::ToggleHelper 15 | 16 | self.allow_in_post_mortem = true 17 | 18 | def self.regexp 19 | /^\s* d(?:isplay)? (?:\s+ (.+))? \s*$/x 20 | end 21 | 22 | def self.description 23 | <<-DESCRIPTION 24 | dis[able] d[isplay][ .. ] 25 | 26 | #{short_description} 27 | 28 | Arguments are the code numbers of the expressions to disable. Do "info 29 | display" to see the current list of code numbers. If no arguments are 30 | specified, all displays are disabled. 31 | DESCRIPTION 32 | end 33 | 34 | def self.short_description 35 | "Disables expressions to be displayed when program stops." 36 | end 37 | 38 | def execute 39 | enable_disable_display("disable", @match[1]) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/byebug/commands/display.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/eval" 5 | 6 | module Byebug 7 | # 8 | # Custom expressions to be displayed every time the debugger stops. 9 | # 10 | class DisplayCommand < Command 11 | include Helpers::EvalHelper 12 | 13 | self.allow_in_post_mortem = false 14 | self.always_run = 2 15 | 16 | def self.regexp 17 | /^\s* disp(?:lay)? (?:\s+ (.+))? \s*$/x 18 | end 19 | 20 | def self.description 21 | <<-DESCRIPTION 22 | disp[lay][ ] 23 | 24 | #{short_description} 25 | 26 | If specified, adds into display expression 27 | list. Otherwise, it lists all expressions. 28 | DESCRIPTION 29 | end 30 | 31 | def self.short_description 32 | "Evaluates expressions every time the debugger stops" 33 | end 34 | 35 | def execute 36 | return print_display_expressions unless @match && @match[1] 37 | 38 | Byebug.displays.push [true, @match[1]] 39 | display_expression(@match[1]) 40 | end 41 | 42 | private 43 | 44 | def display_expression(exp) 45 | print pr("display.result", n: Byebug.displays.size, 46 | exp: exp, 47 | result: eval_expr(exp)) 48 | end 49 | 50 | def print_display_expressions 51 | result = prc("display.result", Byebug.displays) do |item, index| 52 | active, exp = item 53 | 54 | { n: index + 1, exp: exp, result: eval_expr(exp) } if active 55 | end 56 | 57 | print result 58 | end 59 | 60 | def eval_expr(expression) 61 | error_eval(expression).inspect 62 | rescue StandardError 63 | "(undefined)" 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/byebug/commands/down.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pathname" 4 | require_relative "../command" 5 | require_relative "../helpers/frame" 6 | require_relative "../helpers/parse" 7 | 8 | module Byebug 9 | # 10 | # Move the current frame down in the backtrace. 11 | # 12 | class DownCommand < Command 13 | include Helpers::FrameHelper 14 | include Helpers::ParseHelper 15 | 16 | self.allow_in_post_mortem = true 17 | 18 | def self.regexp 19 | /^\s* down (?:\s+(\S+))? \s*$/x 20 | end 21 | 22 | def self.description 23 | <<-DESCRIPTION 24 | down[ count] 25 | 26 | #{short_description} 27 | 28 | Use the "bt" command to find out where you want to go. 29 | DESCRIPTION 30 | end 31 | 32 | def self.short_description 33 | "Moves to a lower frame in the stack trace" 34 | end 35 | 36 | def execute 37 | pos, err = parse_steps(@match[1], "Down") 38 | return errmsg(err) unless pos 39 | 40 | jump_frames(-pos) 41 | 42 | ListCommand.new(processor).execute if Setting[:autolist] 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/byebug/commands/edit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | 5 | module Byebug 6 | # 7 | # Edit a file from byebug's prompt. 8 | # 9 | class EditCommand < Command 10 | self.allow_in_control = true 11 | self.allow_in_post_mortem = true 12 | 13 | def self.regexp 14 | /^\s* ed(?:it)? (?:\s+(\S+))? \s*$/x 15 | end 16 | 17 | def self.description 18 | <<-DESCRIPTION 19 | edit[ file:lineno] 20 | 21 | #{short_description} 22 | 23 | With no argumnt, edits file containing most re line listed. Editing 24 | targets can also be specified to start editing at a specific line in a 25 | specific file 26 | DESCRIPTION 27 | end 28 | 29 | def self.short_description 30 | "Edits source files" 31 | end 32 | 33 | def execute 34 | file, line = location(@match[1]) 35 | return edit_error("not_exist", file) unless File.exist?(file) 36 | return edit_error("not_readable", file) unless File.readable?(file) 37 | 38 | cmd = line ? "#{editor} +#{line} #{file}" : "#{editor} #{file}" 39 | 40 | Kernel.system(cmd) 41 | end 42 | 43 | private 44 | 45 | def location(matched) 46 | if matched.nil? 47 | file = frame.file 48 | return errmsg(pr("edit.errors.state")) unless file 49 | 50 | line = frame.line 51 | elsif (@pos_match = /([^:]+)[:]([0-9]+)/.match(matched)) 52 | file, line = @pos_match.captures 53 | else 54 | file = matched 55 | line = nil 56 | end 57 | 58 | [File.expand_path(file), line] 59 | end 60 | 61 | def editor 62 | ENV["EDITOR"] || "vim" 63 | end 64 | 65 | def edit_error(type, file) 66 | errmsg(pr("edit.errors.#{type}", file: file)) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/byebug/commands/enable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../subcommands" 4 | 5 | require_relative "../commands/enable/breakpoints" 6 | require_relative "../commands/enable/display" 7 | 8 | module Byebug 9 | # 10 | # Enabling custom display expressions or breakpoints. 11 | # 12 | class EnableCommand < Command 13 | include Subcommands 14 | 15 | self.allow_in_post_mortem = true 16 | 17 | def self.regexp 18 | /^\s* en(?:able)? (?:\s+ (.+))? \s*$/x 19 | end 20 | 21 | def self.description 22 | <<-DESCRIPTION 23 | en[able][[ b[reakpoints]| d[isplay])][ n1[ n2[ ...[ nn]]]]] 24 | 25 | #{short_description} 26 | DESCRIPTION 27 | end 28 | 29 | def self.short_description 30 | "Enables breakpoints or displays" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/byebug/commands/enable/breakpoints.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/toggle" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +enable+ command to define the +breakpoints+ subcommand 8 | # 9 | class EnableCommand < Command 10 | # 11 | # Enables all or specific breakpoints 12 | # 13 | class BreakpointsCommand < Command 14 | include Helpers::ToggleHelper 15 | 16 | self.allow_in_post_mortem = true 17 | 18 | def self.regexp 19 | /^\s* b(?:reakpoints)? (?:\s+ (.+))? \s*$/x 20 | end 21 | 22 | def self.description 23 | <<-DESCRIPTION 24 | en[able] b[reakpoints][ ] 25 | 26 | #{short_description} 27 | 28 | Give breakpoint numbers (separated by spaces) as arguments or no 29 | argument at all if you want to enable every breakpoint. 30 | DESCRIPTION 31 | end 32 | 33 | def self.short_description 34 | "Enable all or specific breakpoints." 35 | end 36 | 37 | def execute 38 | enable_disable_breakpoints("enable", @match[1]) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/byebug/commands/enable/display.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/toggle" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +enable+ command to define the +display+ subcommand 8 | # 9 | class EnableCommand < Command 10 | # 11 | # Enables all or specific displays 12 | # 13 | class DisplayCommand < Command 14 | include Helpers::ToggleHelper 15 | 16 | self.allow_in_post_mortem = true 17 | 18 | def self.regexp 19 | /^\s* d(?:isplay)? (?:\s+ (.+))? \s*$/x 20 | end 21 | 22 | def self.description 23 | <<-DESCRIPTION 24 | en[able] d[isplay][ .. ] 25 | 26 | #{short_description} 27 | 28 | Arguments are the code numbers of the expressions to enable. Do "info 29 | display" to see the current list of code numbers. If no arguments are 30 | specified, all displays are enabled. 31 | DESCRIPTION 32 | end 33 | 34 | def self.short_description 35 | "Enables expressions to be displayed when program stops." 36 | end 37 | 38 | def execute 39 | enable_disable_display("enable", @match[1]) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/byebug/commands/finish.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/parse" 5 | 6 | module Byebug 7 | # 8 | # Implements the finish functionality. 9 | # 10 | # Allows the user to continue execution until certain frames are finished. 11 | # 12 | class FinishCommand < Command 13 | include Helpers::ParseHelper 14 | 15 | self.allow_in_post_mortem = false 16 | 17 | def self.regexp 18 | /^\s* fin(?:ish)? (?:\s+(\S+))? \s*$/x 19 | end 20 | 21 | def self.description 22 | <<-DESCRIPTION 23 | fin[ish][ n_frames] 24 | 25 | #{short_description} 26 | 27 | If no number is given, we run until the current frame returns. If a 28 | number of frames `n_frames` is given, then we run until `n_frames` 29 | return from the current position. 30 | DESCRIPTION 31 | end 32 | 33 | def self.short_description 34 | "Runs the program until frame returns" 35 | end 36 | 37 | def execute 38 | if @match[1] 39 | n_frames, err = get_int(@match[1], "finish", 0, max_frames - 1) 40 | return errmsg(err) unless n_frames 41 | else 42 | n_frames = 1 43 | end 44 | 45 | force = n_frames.zero? || false 46 | context.step_out(context.frame.pos + n_frames, force) 47 | context.frame = 0 48 | processor.proceed! 49 | end 50 | 51 | private 52 | 53 | def max_frames 54 | context.stack_size - context.frame.pos 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/byebug/commands/frame.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pathname" 4 | require_relative "../command" 5 | require_relative "../helpers/frame" 6 | require_relative "../helpers/parse" 7 | 8 | module Byebug 9 | # 10 | # Move to specific frames in the backtrace. 11 | # 12 | class FrameCommand < Command 13 | include Helpers::FrameHelper 14 | include Helpers::ParseHelper 15 | 16 | self.allow_in_post_mortem = true 17 | 18 | def self.regexp 19 | /^\s* f(?:rame)? (?:\s+(\S+))? \s*$/x 20 | end 21 | 22 | def self.description 23 | <<-DESCRIPTION 24 | f[rame][ frame-number] 25 | 26 | #{short_description} 27 | 28 | If a frame number has been specified, to moves to that frame. Otherwise 29 | it moves to the newest frame. 30 | 31 | A negative number indicates position from the other end, so "frame -1" 32 | moves to the oldest frame, and "frame 0" moves to the newest frame. 33 | 34 | Without an argument, the command prints the current stack frame. Since 35 | the current position is redisplayed, it may trigger a resyncronization 36 | if there is a front end also watching over things. 37 | 38 | Use the "bt" command to find out where you want to go. 39 | DESCRIPTION 40 | end 41 | 42 | def self.short_description 43 | "Moves to a frame in the call stack" 44 | end 45 | 46 | def execute 47 | return print(pr("frame.line", context.frame.to_hash)) unless @match[1] 48 | 49 | pos, err = get_int(@match[1], "Frame") 50 | return errmsg(err) unless pos 51 | 52 | switch_to_frame(pos) 53 | 54 | ListCommand.new(processor).execute if Setting[:autolist] 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/byebug/commands/help.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../errors" 5 | 6 | module Byebug 7 | # 8 | # Ask for help from byebug's prompt. 9 | # 10 | class HelpCommand < Command 11 | self.allow_in_control = true 12 | self.allow_in_post_mortem = true 13 | 14 | def self.regexp 15 | /^\s* h(?:elp)? (?:\s+(\S+))? (?:\s+(\S+))? \s*$/x 16 | end 17 | 18 | def self.description 19 | <<-DESCRIPTION 20 | h[elp][ [ ]] 21 | 22 | #{short_description} 23 | 24 | help -- prints a summary of all commands 25 | help -- prints help on command 26 | help -- prints help on 's subcommand 27 | DESCRIPTION 28 | end 29 | 30 | def self.short_description 31 | "Helps you using byebug" 32 | end 33 | 34 | def execute 35 | return help_for_all unless @match[1] 36 | 37 | return help_for(@match[1], command) unless @match[2] 38 | 39 | help_for(@match[2], subcommand) 40 | end 41 | 42 | private 43 | 44 | def help_for_all 45 | puts(processor.command_list.to_s) # rubocop:disable Lint/RedundantStringCoercion 46 | end 47 | 48 | def help_for(input, cmd) 49 | raise CommandNotFound.new(input, command) unless cmd 50 | 51 | puts(cmd.help) 52 | end 53 | 54 | def command 55 | @command ||= processor.command_list.match(@match[1]) 56 | end 57 | 58 | def subcommand 59 | return unless command 60 | 61 | @subcommand ||= command.subcommand_list.match(@match[2]) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/byebug/commands/history.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/parse" 5 | 6 | module Byebug 7 | # 8 | # Show history of byebug commands. 9 | # 10 | class HistoryCommand < Command 11 | include Helpers::ParseHelper 12 | 13 | self.allow_in_post_mortem = true 14 | 15 | def self.regexp 16 | /^\s* hist(?:ory)? (?:\s+(?.+))? \s*$/x 17 | end 18 | 19 | def self.description 20 | <<-DESCRIPTION 21 | hist[ory][ num_cmds] 22 | 23 | #{short_description} 24 | DESCRIPTION 25 | end 26 | 27 | def self.short_description 28 | "Shows byebug's history of commands" 29 | end 30 | 31 | def execute 32 | history = processor.interface.history 33 | 34 | size, = get_int(@match[:num_cmds], "history", 1) if @match[:num_cmds] 35 | 36 | puts history.to_s(size) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/byebug/commands/info.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../subcommands" 4 | 5 | require_relative "../commands/info/breakpoints" 6 | require_relative "../commands/info/display" 7 | require_relative "../commands/info/file" 8 | require_relative "../commands/info/line" 9 | require_relative "../commands/info/program" 10 | 11 | module Byebug 12 | # 13 | # Shows info about different aspects of the debugger. 14 | # 15 | class InfoCommand < Command 16 | include Subcommands 17 | 18 | self.allow_in_control = true 19 | self.allow_in_post_mortem = true 20 | 21 | def self.regexp 22 | /^\s* i(?:nfo)? (?:\s+ (.+))? \s*$/x 23 | end 24 | 25 | def self.description 26 | <<-DESCRIPTION 27 | info[ subcommand] 28 | 29 | #{short_description} 30 | DESCRIPTION 31 | end 32 | 33 | def self.short_description 34 | "Shows short description and information about the program being debugged" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/byebug/commands/info/breakpoints.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | # 5 | # Reopens the +info+ command to define the +breakpoints+ subcommand 6 | # 7 | class InfoCommand < Command 8 | # 9 | # Information about current breakpoints 10 | # 11 | class BreakpointsCommand < Command 12 | self.allow_in_post_mortem = true 13 | 14 | def self.regexp 15 | /^\s* b(?:reakpoints)? (?:\s+ (.+))? \s*$/x 16 | end 17 | 18 | def self.description 19 | <<-DESCRIPTION 20 | inf[o] b[reakpoints] 21 | 22 | #{short_description} 23 | DESCRIPTION 24 | end 25 | 26 | def self.short_description 27 | "Status of user settable breakpoints" 28 | end 29 | 30 | def execute 31 | return puts("No breakpoints.") if Byebug.breakpoints.empty? 32 | 33 | breakpoints = Byebug.breakpoints.sort_by(&:id) 34 | 35 | if @match[1] 36 | indices = @match[1].split(/ +/).map(&:to_i) 37 | breakpoints = breakpoints.select { |b| indices.member?(b.id) } 38 | return errmsg("No breakpoints found among list given") if breakpoints.empty? 39 | end 40 | 41 | puts "Num Enb What" 42 | breakpoints.each { |b| info_breakpoint(b) } 43 | end 44 | 45 | private 46 | 47 | def info_breakpoint(brkpt) 48 | interp = format( 49 | "%-3d %-3s at %s:%s%s", 50 | id: brkpt.id, 51 | status: brkpt.enabled? ? "y" : "n", 52 | file: brkpt.source, 53 | line: brkpt.pos, 54 | expression: brkpt.expr.nil? ? "" : " if #{brkpt.expr}" 55 | ) 56 | puts interp 57 | hits = brkpt.hit_count 58 | return unless hits.positive? 59 | 60 | s = hits > 1 ? "s" : "" 61 | puts " breakpoint already hit #{hits} time#{s}" 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/byebug/commands/info/display.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | # 5 | # Reopens the +info+ command to define the +display+ subcommand 6 | # 7 | class InfoCommand < Command 8 | # 9 | # Information about display expressions 10 | # 11 | class DisplayCommand < Command 12 | self.allow_in_post_mortem = true 13 | 14 | def self.regexp 15 | /^\s* d(?:isplay)? \s*$/x 16 | end 17 | 18 | def self.description 19 | <<-DESCRIPTION 20 | inf[o] d[display] 21 | 22 | #{short_description} 23 | DESCRIPTION 24 | end 25 | 26 | def self.short_description 27 | "List of expressions to display when program stops" 28 | end 29 | 30 | def execute 31 | return puts("There are no auto-display expressions now.") unless Byebug.displays.find { |d| d[0] } 32 | 33 | puts "Auto-display expressions now in effect:" 34 | puts "Num Enb Expression" 35 | 36 | Byebug.displays.each_with_index do |d, i| 37 | interp = format( 38 | "%3d: %s %s", 39 | number: i + 1, 40 | status: d[0] ? "y" : "n", 41 | expression: d[1] 42 | ) 43 | 44 | puts(interp) 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/byebug/commands/info/file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/file" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +info+ command to define the +file+ subcommand 8 | # 9 | class InfoCommand < Command 10 | # 11 | # Information about a particular source file 12 | # 13 | class FileCommand < Command 14 | include Helpers::FileHelper 15 | include Helpers::StringHelper 16 | 17 | self.allow_in_post_mortem = true 18 | 19 | def self.regexp 20 | /^\s* f(?:ile)? (?:\s+ (.+))? \s*$/x 21 | end 22 | 23 | def self.description 24 | <<-DESCRIPTION 25 | inf[o] f[ile] 26 | 27 | #{short_description} 28 | 29 | It informs about file name, number of lines, possible breakpoints in 30 | the file, last modification time and sha1 digest. 31 | DESCRIPTION 32 | end 33 | 34 | def self.short_description 35 | "Information about a particular source file." 36 | end 37 | 38 | def execute 39 | file = @match[1] || frame.file 40 | return errmsg(pr("info.errors.undefined_file", file: file)) unless File.exist?(file) 41 | 42 | puts prettify <<-RUBY 43 | File #{info_file_basic(file)} 44 | 45 | Breakpoint line numbers: #{info_file_breakpoints(file)} 46 | 47 | Modification time: #{info_file_mtime(file)} 48 | 49 | Sha1 Signature: #{info_file_sha1(file)} 50 | RUBY 51 | end 52 | 53 | private 54 | 55 | def info_file_basic(file) 56 | path = File.expand_path(file) 57 | return unless File.exist?(path) 58 | 59 | s = n_lines(path) == 1 ? "" : "s" 60 | "#{path} (#{n_lines(path)} line#{s})" 61 | end 62 | 63 | def info_file_breakpoints(file) 64 | breakpoints = Breakpoint.potential_lines(file) 65 | return unless breakpoints 66 | 67 | breakpoints.to_a.sort.join(" ") 68 | end 69 | 70 | def info_file_mtime(file) 71 | File.stat(file).mtime 72 | end 73 | 74 | def info_file_sha1(file) 75 | require "digest/sha1" 76 | Digest::SHA1.hexdigest(file) 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/byebug/commands/info/line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | # 5 | # Reopens the +info+ command to define the +line+ subcommand 6 | # 7 | class InfoCommand < Command 8 | # 9 | # Information about current location 10 | # 11 | class LineCommand < Command 12 | self.allow_in_post_mortem = true 13 | 14 | def self.regexp 15 | /^\s* l(?:ine)? \s*$/x 16 | end 17 | 18 | def self.description 19 | <<-DESCRIPTION 20 | inf[o] l[ine] 21 | 22 | #{short_description} 23 | DESCRIPTION 24 | end 25 | 26 | def self.short_description 27 | "Line number and file name of current position in source file." 28 | end 29 | 30 | def execute 31 | puts "Line #{frame.line} of \"#{frame.file}\"" 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/byebug/commands/info/program.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | # 5 | # Reopens the +info+ command to define the +args+ subcommand 6 | # 7 | class InfoCommand < Command 8 | # 9 | # Information about arguments of the current method/block 10 | # 11 | class ProgramCommand < Command 12 | self.allow_in_post_mortem = true 13 | 14 | def self.regexp 15 | /^\s* p(?:rogram)? \s*$/x 16 | end 17 | 18 | def self.description 19 | <<-DESCRIPTION 20 | inf[o] p[rogram] 21 | 22 | #{short_description} 23 | DESCRIPTION 24 | end 25 | 26 | def self.short_description 27 | "Information about the current status of the debugged program." 28 | end 29 | 30 | def execute 31 | puts "Program stopped. " 32 | format_stop_reason context.stop_reason 33 | end 34 | 35 | private 36 | 37 | def format_stop_reason(stop_reason) 38 | case stop_reason 39 | when :step 40 | puts "It stopped after stepping, next'ing or initial start." 41 | when :breakpoint 42 | puts "It stopped at a breakpoint." 43 | when :catchpoint 44 | puts "It stopped at a catchpoint." 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/byebug/commands/interrupt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | 5 | module Byebug 6 | # 7 | # Interrupting execution of current thread. 8 | # 9 | class InterruptCommand < Command 10 | self.allow_in_control = true 11 | 12 | def self.regexp 13 | /^\s*int(?:errupt)?\s*$/ 14 | end 15 | 16 | def self.description 17 | <<-DESCRIPTION 18 | int[errupt] 19 | 20 | #{short_description} 21 | DESCRIPTION 22 | end 23 | 24 | def self.short_description 25 | "Interrupts the program" 26 | end 27 | 28 | def execute 29 | Byebug.start 30 | 31 | Byebug.thread_context(Thread.main).interrupt 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/byebug/commands/irb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require "irb" 5 | require "English" 6 | 7 | module Byebug 8 | # 9 | # Enter IRB from byebug's prompt 10 | # 11 | class IrbCommand < Command 12 | self.allow_in_post_mortem = true 13 | 14 | def self.regexp 15 | /^\s* irb \s*$/x 16 | end 17 | 18 | def self.description 19 | <<-DESCRIPTION 20 | irb 21 | 22 | #{short_description} 23 | DESCRIPTION 24 | end 25 | 26 | def self.short_description 27 | "Starts an IRB session" 28 | end 29 | 30 | def execute 31 | return errmsg(pr("base.errors.only_local")) unless processor.interface.instance_of?(LocalInterface) 32 | 33 | # @todo IRB tries to parse $ARGV so we must clear it (see #197). Add a 34 | # test case for it so we can remove this comment. 35 | with_clean_argv { IRB.start } 36 | end 37 | 38 | private 39 | 40 | def with_clean_argv 41 | saved_argv = $ARGV.dup 42 | $ARGV.clear 43 | begin 44 | yield 45 | ensure 46 | $ARGV.concat(saved_argv) 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/byebug/commands/kill.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | 5 | module Byebug 6 | # 7 | # Send custom signals to the debugged program. 8 | # 9 | class KillCommand < Command 10 | self.allow_in_control = true 11 | 12 | def self.regexp 13 | /^\s* kill \s* (?:\s+(\S+))? \s*$/x 14 | end 15 | 16 | def self.description 17 | <<-DESCRIPTION 18 | kill[ signal] 19 | 20 | #{short_description} 21 | 22 | Equivalent of Process.kill(Process.pid) 23 | DESCRIPTION 24 | end 25 | 26 | def self.short_description 27 | "Sends a signal to the current process" 28 | end 29 | 30 | def execute 31 | if @match[1] 32 | signame = @match[1] 33 | 34 | return errmsg("signal name #{signame} is not a signal I know about\n") unless Signal.list.member?(signame) 35 | else 36 | return unless confirm("Really kill? (y/n) ") 37 | 38 | signame = "KILL" 39 | end 40 | 41 | processor.interface.close if signame == "KILL" 42 | Process.kill(signame, Process.pid) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/byebug/commands/method.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/eval" 5 | 6 | module Byebug 7 | # 8 | # Show methods of specific classes/modules/objects. 9 | # 10 | class MethodCommand < Command 11 | include Helpers::EvalHelper 12 | 13 | self.allow_in_post_mortem = true 14 | 15 | def self.regexp 16 | /^\s* m(?:ethod)? \s+ (i(:?nstance)?\s+)?/x 17 | end 18 | 19 | def self.description 20 | <<-DESCRIPTION 21 | m[ethod] (i[nstance][ ]|) 22 | 23 | #{short_description} 24 | 25 | When invoked with "instance", shows instance methods of the object 26 | specified as argument or of self no object was specified. 27 | 28 | When invoked only with a class or module, shows class methods of the 29 | class or module specified as argument. 30 | DESCRIPTION 31 | end 32 | 33 | def self.short_description 34 | "Shows methods of an object, class or module" 35 | end 36 | 37 | def execute 38 | obj = warning_eval(@match.post_match) 39 | 40 | result = 41 | if @match[1] 42 | prc("method.methods", obj.methods.sort) { |item, _| { name: item } } 43 | elsif !obj.is_a?(Module) 44 | pr("variable.errors.not_module", object: @match.post_match) 45 | else 46 | prc("method.methods", obj.instance_methods(false).sort) do |item, _| 47 | { name: item } 48 | end 49 | end 50 | puts result 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/byebug/commands/next.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/parse" 5 | 6 | module Byebug 7 | # 8 | # Implements the next functionality. 9 | # 10 | # Allows the user the continue execution until the next instruction in the 11 | # current frame. 12 | # 13 | class NextCommand < Command 14 | include Helpers::ParseHelper 15 | 16 | def self.regexp 17 | /^\s* n(?:ext)? (?:\s+(\S+))? \s*$/x 18 | end 19 | 20 | def self.description 21 | <<-DESCRIPTION 22 | n[ext][ nnn] 23 | 24 | #{short_description} 25 | DESCRIPTION 26 | end 27 | 28 | def self.short_description 29 | "Runs one or more lines of code" 30 | end 31 | 32 | def execute 33 | steps, err = parse_steps(@match[1], "Next") 34 | return errmsg(err) unless steps 35 | 36 | context.step_over(steps, context.frame.pos) 37 | processor.proceed! 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/byebug/commands/pry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/eval" 5 | 6 | module Byebug 7 | # 8 | # Enter Pry from byebug's prompt 9 | # 10 | class PryCommand < Command 11 | self.allow_in_post_mortem = true 12 | 13 | def self.regexp 14 | /^\s* pry \s*$/x 15 | end 16 | 17 | def self.description 18 | <<-DESCRIPTION 19 | pry 20 | 21 | #{short_description} 22 | DESCRIPTION 23 | end 24 | 25 | def self.short_description 26 | "Starts a Pry session" 27 | end 28 | 29 | def execute 30 | return errmsg(pr("base.errors.only_local")) unless processor.interface.instance_of?(LocalInterface) 31 | 32 | begin 33 | require "pry" 34 | rescue LoadError 35 | return errmsg(pr("pry.errors.not_installed")) 36 | end 37 | 38 | Pry.start(context.frame._binding) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/byebug/commands/quit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | 5 | module Byebug 6 | # 7 | # Exit from byebug. 8 | # 9 | class QuitCommand < Command 10 | self.allow_in_control = true 11 | self.allow_in_post_mortem = true 12 | 13 | def self.regexp 14 | /^\s* q(?:uit)? \s* (?:(!|\s+unconditionally))? \s*$/x 15 | end 16 | 17 | def self.description 18 | <<-DESCRIPTION 19 | q[uit][!| unconditionally] 20 | 21 | #{short_description} 22 | 23 | Normally we prompt before exiting. However, if the parameter 24 | "unconditionally" is given or command is suffixed with "!", we exit 25 | without asking further questions. 26 | DESCRIPTION 27 | end 28 | 29 | def self.short_description 30 | "Exits byebug" 31 | end 32 | 33 | def execute 34 | return unless @match[1] || confirm(pr("quit.confirmations.really")) 35 | 36 | processor.interface.autosave 37 | processor.interface.close 38 | 39 | Process.exit! 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/byebug/commands/restart.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/bin" 5 | require_relative "../helpers/path" 6 | require "shellwords" 7 | require "English" 8 | require "rbconfig" 9 | 10 | module Byebug 11 | # 12 | # Restart debugged program from within byebug. 13 | # 14 | class RestartCommand < Command 15 | include Helpers::BinHelper 16 | include Helpers::PathHelper 17 | 18 | self.allow_in_control = true 19 | self.allow_in_post_mortem = true 20 | 21 | def self.regexp 22 | /^\s* restart (?:\s+(?.+))? \s*$/x 23 | end 24 | 25 | def self.description 26 | <<-DESCRIPTION 27 | restart [args] 28 | 29 | #{short_description} 30 | 31 | This is a re-exec - all byebug state is lost. If command arguments are 32 | passed those are used. 33 | DESCRIPTION 34 | end 35 | 36 | def self.short_description 37 | "Restarts the debugged program" 38 | end 39 | 40 | def execute 41 | cmd = [$PROGRAM_NAME] 42 | 43 | cmd = prepend_byebug_bin(cmd) 44 | cmd = prepend_ruby_bin(cmd) 45 | 46 | cmd += (@match[:args] ? @match[:args].shellsplit : $ARGV) 47 | 48 | puts pr("restart.success", cmd: cmd.shelljoin) 49 | Kernel.exec(*cmd) 50 | end 51 | 52 | private 53 | 54 | def prepend_byebug_bin(cmd) 55 | cmd.unshift(bin_file) if Byebug.mode == :standalone 56 | cmd 57 | end 58 | 59 | def prepend_ruby_bin(cmd) 60 | cmd.unshift(RbConfig.ruby) if which("ruby") != which(cmd.first) 61 | cmd 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/byebug/commands/save.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | 5 | module Byebug 6 | # 7 | # Save current settings to use them in another debug session. 8 | # 9 | class SaveCommand < Command 10 | self.allow_in_control = true 11 | self.allow_in_post_mortem = true 12 | 13 | def self.regexp 14 | /^\s* sa(?:ve)? (?:\s+(\S+))? \s*$/x 15 | end 16 | 17 | def self.description 18 | <<-DESCRIPTION 19 | save[ FILE] 20 | 21 | #{short_description} 22 | 23 | Byebug state is saved as a script file. This includes breakpoints, 24 | catchpoints, display expressions and some settings. If no filename is 25 | given, byebug will fabricate one. 26 | 27 | Use the "source" command in another debug session to restore the saved 28 | file. 29 | DESCRIPTION 30 | end 31 | 32 | def self.short_description 33 | "Saves current byebug session to a file" 34 | end 35 | 36 | def execute 37 | file = File.open(@match[1] || Setting[:savefile], "w") 38 | 39 | save_breakpoints(file) 40 | save_catchpoints(file) 41 | save_displays(file) 42 | save_settings(file) 43 | 44 | print pr("save.messages.done", path: file.path) 45 | file.close 46 | end 47 | 48 | private 49 | 50 | def save_breakpoints(file) 51 | Byebug.breakpoints.each do |b| 52 | file.puts "break #{b.source}:#{b.pos}#{" if #{b.expr}" if b.expr}" 53 | end 54 | end 55 | 56 | def save_catchpoints(file) 57 | Byebug.catchpoints.each_key do |c| 58 | file.puts "catch #{c}" 59 | end 60 | end 61 | 62 | def save_displays(file) 63 | Byebug.displays.each { |d| file.puts "display #{d[1]}" if d[0] } 64 | end 65 | 66 | def save_settings(file) 67 | %w[autoirb autolist basename].each do |setting| 68 | file.puts "set #{setting} #{Setting[setting.to_sym]}" 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/byebug/commands/set.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/parse" 5 | 6 | module Byebug 7 | # 8 | # Change byebug settings. 9 | # 10 | class SetCommand < Command 11 | include Helpers::ParseHelper 12 | 13 | self.allow_in_control = true 14 | self.allow_in_post_mortem = true 15 | 16 | def self.regexp 17 | /^\s* set (?:\s+(?\w+))? (?:\s+(?\S+))? \s*$/x 18 | end 19 | 20 | def self.description 21 | <<-DESCRIPTION 22 | set 23 | 24 | #{short_description} 25 | 26 | Boolean values take "on", "off", "true", "false", "1" or "0". If you 27 | don't specify a value, the boolean setting will be enabled. Conversely, 28 | you can use "set no" to disable them. 29 | 30 | You can see these environment settings with the "show" command. 31 | DESCRIPTION 32 | end 33 | 34 | def self.short_description 35 | "Modifies byebug settings" 36 | end 37 | 38 | def self.help 39 | super + Setting.help_all 40 | end 41 | 42 | def execute 43 | key = @match[:setting] 44 | value = @match[:value] 45 | return puts(help) if key.nil? && value.nil? 46 | 47 | setting = Setting.find(key) 48 | return errmsg(pr("set.errors.unknown_setting", key: key)) unless setting 49 | 50 | if !setting.boolean? && value.nil? 51 | err = pr("set.errors.must_specify_value", key: key) 52 | elsif setting.boolean? 53 | value, err = get_onoff(value, /^no/.match?(key) ? false : true) 54 | elsif setting.integer? 55 | value, err = get_int(value, setting.to_sym, 1) 56 | end 57 | return errmsg(err) if value.nil? 58 | 59 | setting.value = value 60 | 61 | puts setting 62 | end 63 | 64 | private 65 | 66 | def get_onoff(arg, default) 67 | return default if arg.nil? 68 | 69 | case arg 70 | when "1", "on", "true" 71 | true 72 | when "0", "off", "false" 73 | false 74 | else 75 | [nil, pr("set.errors.on_off", arg: arg)] 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/byebug/commands/show.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | 5 | module Byebug 6 | # 7 | # Show byebug settings. 8 | # 9 | class ShowCommand < Command 10 | self.allow_in_control = true 11 | self.allow_in_post_mortem = true 12 | 13 | def self.regexp 14 | /^\s* show (?:\s+(?\w+))? \s*$/x 15 | end 16 | 17 | def self.description 18 | <<-DESCRIPTION 19 | show 20 | 21 | #{short_description} 22 | 23 | You can change them with the "set" command. 24 | DESCRIPTION 25 | end 26 | 27 | def self.short_description 28 | "Shows byebug settings" 29 | end 30 | 31 | def self.help 32 | super + Setting.help_all 33 | end 34 | 35 | def execute 36 | key = @match[:setting] 37 | return puts(help) unless key 38 | 39 | setting = Setting.find(key) 40 | return errmsg(pr("show.errors.unknown_setting", key: key)) unless setting 41 | 42 | puts Setting.settings[setting.to_sym] 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/byebug/commands/skip.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/parse" 5 | 6 | module Byebug 7 | # 8 | # Allows the user to continue execution until the next breakpoint, as 9 | # long as it is different from the current one 10 | # 11 | class SkipCommand < Command 12 | include Helpers::ParseHelper 13 | 14 | class << self 15 | attr_writer :file_line, :file_path 16 | attr_reader :previous_autolist 17 | 18 | def file_line 19 | @file_line ||= 0 20 | end 21 | 22 | def file_path 23 | @file_path ||= "" 24 | end 25 | 26 | def setup_autolist(value) 27 | @previous_autolist = ListCommand.always_run 28 | ListCommand.always_run = value 29 | end 30 | 31 | def restore_autolist 32 | ListCommand.always_run = @previous_autolist 33 | @previous_autolist = nil 34 | end 35 | end 36 | 37 | def self.regexp 38 | /^\s* sk(?:ip)? \s*$/x 39 | end 40 | 41 | def self.description 42 | <<-DESCRIPTION 43 | sk[ip] 44 | 45 | #{short_description} 46 | DESCRIPTION 47 | end 48 | 49 | def self.short_description 50 | "Runs until the next breakpoint as long as it is different from the current one" 51 | end 52 | 53 | def initialize_attributes 54 | self.class.always_run = 2 55 | self.class.setup_autolist(0) 56 | self.class.file_path = frame.file 57 | self.class.file_line = frame.line 58 | end 59 | 60 | def keep_execution 61 | [self.class.file_path, self.class.file_line] == [frame.file, frame.line] 62 | end 63 | 64 | def reset_attributes 65 | self.class.always_run = 0 66 | ListCommand.new(processor).execute if self.class.previous_autolist == 1 67 | self.class.restore_autolist 68 | end 69 | 70 | def auto_run 71 | return false unless self.class.always_run == 2 72 | 73 | keep_execution ? processor.proceed! : reset_attributes 74 | true 75 | end 76 | 77 | def execute 78 | return if auto_run 79 | 80 | initialize_attributes 81 | processor.proceed! 82 | Byebug.stop if Byebug.stoppable? 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/byebug/commands/source.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | 5 | module Byebug 6 | # 7 | # Execute a file containing byebug commands. 8 | # 9 | # It can be used to restore a previously saved debugging session. 10 | # 11 | class SourceCommand < Command 12 | self.allow_in_control = true 13 | self.allow_in_post_mortem = true 14 | 15 | def self.regexp 16 | /^\s* so(?:urce)? (?:\s+(\S+))? \s*$/x 17 | end 18 | 19 | def self.description 20 | <<-DESCRIPTION 21 | source 22 | 23 | #{short_description} 24 | DESCRIPTION 25 | end 26 | 27 | def self.short_description 28 | "Restores a previously saved byebug session" 29 | end 30 | 31 | def execute 32 | return puts(help) unless @match[1] 33 | 34 | file = File.expand_path(@match[1]).strip 35 | return errmsg(pr("source.errors.not_found", file: file)) unless File.exist?(file) 36 | 37 | processor.interface.read_file(file) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/byebug/commands/step.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/parse" 5 | 6 | module Byebug 7 | # 8 | # Implements the step functionality. 9 | # 10 | # Allows the user the continue execution until the next instruction, possibly 11 | # in a different frame. Use step to step into method calls or blocks. 12 | # 13 | class StepCommand < Command 14 | include Helpers::ParseHelper 15 | 16 | def self.regexp 17 | /^\s* s(?:tep)? (?:\s+(\S+))? \s*$/x 18 | end 19 | 20 | def self.description 21 | <<-DESCRIPTION 22 | s[tep][ times] 23 | 24 | #{short_description} 25 | DESCRIPTION 26 | end 27 | 28 | def self.short_description 29 | "Steps into blocks or methods one or more times" 30 | end 31 | 32 | def execute 33 | steps, err = parse_steps(@match[1], "Steps") 34 | return errmsg(err) unless steps 35 | 36 | context.step_into(steps, context.frame.pos) 37 | processor.proceed! 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/byebug/commands/thread.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../subcommands" 4 | 5 | require_relative "../commands/thread/current" 6 | require_relative "../commands/thread/list" 7 | require_relative "../commands/thread/resume" 8 | require_relative "../commands/thread/stop" 9 | require_relative "../commands/thread/switch" 10 | 11 | module Byebug 12 | # 13 | # Manipulation of Ruby threads 14 | # 15 | class ThreadCommand < Command 16 | include Subcommands 17 | 18 | def self.regexp 19 | /^\s* th(?:read)? (?:\s+ (.+))? \s*$/x 20 | end 21 | 22 | def self.description 23 | <<-DESCRIPTION 24 | th[read] 25 | 26 | #{short_description} 27 | DESCRIPTION 28 | end 29 | 30 | def self.short_description 31 | "Commands to manipulate threads" 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/byebug/commands/thread/current.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/thread" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +thread+ command to define the +current+ subcommand 8 | # 9 | class ThreadCommand < Command 10 | # 11 | # Information about the current thread 12 | # 13 | class CurrentCommand < Command 14 | include Helpers::ThreadHelper 15 | 16 | def self.regexp 17 | /^\s* c(?:urrent)? \s*$/x 18 | end 19 | 20 | def self.description 21 | <<-DESCRIPTION 22 | th[read] c[urrent] 23 | 24 | #{short_description} 25 | DESCRIPTION 26 | end 27 | 28 | def self.short_description 29 | "Shows current thread information." 30 | end 31 | 32 | def execute 33 | display_context(context) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/byebug/commands/thread/list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/thread" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +thread+ command to define the +list+ subcommand 8 | # 9 | class ThreadCommand < Command 10 | # 11 | # Information about threads 12 | # 13 | class ListCommand < Command 14 | include Helpers::ThreadHelper 15 | 16 | def self.regexp 17 | /^\s* l(?:ist)? \s*$/x 18 | end 19 | 20 | def self.description 21 | <<-DESCRIPTION 22 | th[read] l[ist] 23 | 24 | #{short_description} 25 | DESCRIPTION 26 | end 27 | 28 | def self.short_description 29 | "Lists all threads." 30 | end 31 | 32 | def execute 33 | contexts = Byebug.contexts.sort_by(&:thnum) 34 | 35 | thread_list = prc("thread.context", contexts) do |context, _| 36 | thread_arguments(context) 37 | end 38 | 39 | print(thread_list) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/byebug/commands/thread/resume.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/thread" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +thread+ command to define the +resume+ subcommand 8 | # 9 | class ThreadCommand < Command 10 | # 11 | # Resumes the specified thread 12 | # 13 | class ResumeCommand < Command 14 | include Helpers::ThreadHelper 15 | 16 | def self.regexp 17 | /^\s* r(?:esume)? (?: \s* (\d+))? \s*$/x 18 | end 19 | 20 | def self.description 21 | <<-DESCRIPTION 22 | th[read] r[esume] 23 | 24 | #{short_description} 25 | DESCRIPTION 26 | end 27 | 28 | def self.short_description 29 | "Resumes execution of the specified thread." 30 | end 31 | 32 | def execute 33 | return puts(help) unless @match[1] 34 | 35 | context, err = context_from_thread(@match[1]) 36 | return errmsg(err) if err 37 | 38 | return errmsg(pr("thread.errors.already_running")) unless context.suspended? 39 | 40 | context.resume 41 | display_context(context) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/byebug/commands/thread/stop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/thread" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +thread+ command to define the +stop+ subcommand 8 | # 9 | class ThreadCommand < Command 10 | # 11 | # Stops the specified thread 12 | # 13 | class StopCommand < Command 14 | include Helpers::ThreadHelper 15 | 16 | def self.regexp 17 | /^\s* st(?:op)? (?: \s* (\d+))? \s*$/x 18 | end 19 | 20 | def self.description 21 | <<-DESCRIPTION 22 | th[read] st[op] 23 | 24 | #{short_description} 25 | DESCRIPTION 26 | end 27 | 28 | def self.short_description 29 | "Stops the execution of the specified thread." 30 | end 31 | 32 | def execute 33 | return puts(help) unless @match[1] 34 | 35 | context, err = context_from_thread(@match[1]) 36 | return errmsg(err) if err 37 | 38 | context.suspend 39 | display_context(context) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/byebug/commands/thread/switch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/thread" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +thread+ command to define the +switch+ subcommand 8 | # 9 | class ThreadCommand < Command 10 | # 11 | # Switches to the specified thread 12 | # 13 | class SwitchCommand < Command 14 | include Helpers::ThreadHelper 15 | 16 | def self.regexp 17 | /^\s* sw(?:itch)? (?: \s* (\d+))? \s*$/x 18 | end 19 | 20 | def self.description 21 | <<-DESCRIPTION 22 | th[read] sw[itch] 23 | 24 | #{short_description} 25 | DESCRIPTION 26 | end 27 | 28 | def self.short_description 29 | "Switches execution to the specified thread" 30 | end 31 | 32 | def execute 33 | return puts(help) unless @match[1] 34 | 35 | context, err = context_from_thread(@match[1]) 36 | return errmsg(err) if err 37 | 38 | display_context(context) 39 | 40 | context.switch 41 | 42 | processor.proceed! 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/byebug/commands/tracevar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | 5 | module Byebug 6 | # 7 | # Show (and possibly stop) at every line that changes a global variable. 8 | # 9 | class TracevarCommand < Command 10 | def self.regexp 11 | /^\s* tr(?:acevar)? (?: \s+ (\S+))? # (variable-name)? 12 | (?: \s+ (stop|nostop))? 13 | \s*$/x 14 | end 15 | 16 | def self.description 17 | <<-DESCRIPTION 18 | tr[acevar] [[no]stop] 19 | 20 | #{short_description} 21 | 22 | If "stop" is specified, execution will stop every time the variable 23 | changes its value. If nothing or "nostop" is specified, execution won't 24 | stop, changes will just be logged in byebug's output. 25 | DESCRIPTION 26 | end 27 | 28 | def self.short_description 29 | "Enables tracing of a global variable" 30 | end 31 | 32 | def execute 33 | var = @match[1] 34 | return errmsg(pr("trace.errors.needs_global_variable")) unless var 35 | return errmsg(pr("trace.errors.var_is_not_global", name: var)) unless global_variables.include?(:"#{var}") 36 | 37 | stop = @match[2] && @match[2] !~ /nostop/ 38 | 39 | instance_eval do 40 | trace_var(:"#{var}") { |val| on_change(var, val, stop) } 41 | end 42 | 43 | puts pr("trace.messages.success", var: var) 44 | end 45 | 46 | private 47 | 48 | def on_change(name, value, stop) 49 | puts pr("trace.messages.on_change", name: name, value: value) 50 | 51 | context.step_out(1, false) if stop 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/byebug/commands/undisplay.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | require_relative "../helpers/parse" 5 | 6 | module Byebug 7 | # 8 | # Remove expressions from display list. 9 | # 10 | class UndisplayCommand < Command 11 | include Helpers::ParseHelper 12 | 13 | self.allow_in_post_mortem = true 14 | 15 | def self.regexp 16 | /^\s* undisp(?:lay)? (?:\s+(\S+))? \s*$/x 17 | end 18 | 19 | def self.description 20 | <<-DESCRIPTION 21 | undisp[lay][ nnn] 22 | 23 | #{short_description} 24 | 25 | Arguments are the code numbers of the expressions to stop displaying. No 26 | argument means cancel all automatic-display expressions. Type "info 27 | display" to see the current list of code numbers. 28 | DESCRIPTION 29 | end 30 | 31 | def self.short_description 32 | "Stops displaying all or some expressions when program stops" 33 | end 34 | 35 | def execute 36 | if @match[1] 37 | pos, err = get_int(@match[1], "Undisplay", 1, Byebug.displays.size) 38 | return errmsg(err) unless err.nil? 39 | 40 | last_display = Byebug.displays[pos - 1] 41 | return errmsg(pr("display.errors.undefined", expr: pos)) unless last_display 42 | 43 | last_display[0] = nil 44 | else 45 | return unless confirm(pr("display.confirmations.clear_all")) 46 | 47 | Byebug.displays.each { |d| d[0] = false } 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/byebug/commands/untracevar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../command" 4 | 5 | module Byebug 6 | # 7 | # Stop tracing a global variable. 8 | # 9 | class UntracevarCommand < Command 10 | def self.regexp 11 | /^\s* untr(?:acevar)? (?:\s+ (\S+))? \s*$/x 12 | end 13 | 14 | def self.description 15 | <<-DESCRIPTION 16 | untr[acevar] 17 | 18 | #{short_description} 19 | DESCRIPTION 20 | end 21 | 22 | def self.short_description 23 | "Stops tracing a global variable." 24 | end 25 | 26 | def execute 27 | var = @match[1] 28 | if global_variables.include?(:"#{var}") 29 | untrace_var(:"#{var}") 30 | puts pr("trace.messages.undo", var: var) 31 | else 32 | errmsg pr("trace.errors.var_is_not_global", name: var) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/byebug/commands/up.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pathname" 4 | require_relative "../command" 5 | require_relative "../helpers/frame" 6 | require_relative "../helpers/parse" 7 | 8 | module Byebug 9 | # 10 | # Move the current frame up in the backtrace. 11 | # 12 | class UpCommand < Command 13 | include Helpers::FrameHelper 14 | include Helpers::ParseHelper 15 | 16 | self.allow_in_post_mortem = true 17 | 18 | def self.regexp 19 | /^\s* up (?:\s+(\S+))? \s*$/x 20 | end 21 | 22 | def self.description 23 | <<-DESCRIPTION 24 | up[ count] 25 | 26 | #{short_description} 27 | 28 | Use the "bt" command to find out where you want to go. 29 | DESCRIPTION 30 | end 31 | 32 | def self.short_description 33 | "Moves to a higher frame in the stack trace" 34 | end 35 | 36 | def execute 37 | pos, err = parse_steps(@match[1], "Up") 38 | return errmsg(err) unless pos 39 | 40 | jump_frames(pos) 41 | 42 | ListCommand.new(processor).execute if Setting[:autolist] 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/byebug/commands/var.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../subcommands" 4 | 5 | require_relative "var/all" 6 | require_relative "var/args" 7 | require_relative "var/const" 8 | require_relative "var/instance" 9 | require_relative "var/local" 10 | require_relative "var/global" 11 | 12 | module Byebug 13 | # 14 | # Shows variables and its values 15 | # 16 | class VarCommand < Command 17 | include Subcommands 18 | 19 | self.allow_in_post_mortem = true 20 | 21 | def self.regexp 22 | /^\s* v(?:ar)? (?:\s+ (.+))? \s*$/x 23 | end 24 | 25 | def self.description 26 | <<-DESCRIPTION 27 | [v]ar 28 | 29 | #{short_description} 30 | DESCRIPTION 31 | end 32 | 33 | def self.short_description 34 | "Shows variables and its values" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/byebug/commands/var/all.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/var" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +var+ command to define the +all+ subcommand 8 | # 9 | class VarCommand < Command 10 | # 11 | # Shows global, instance and local variables 12 | # 13 | class AllCommand < Command 14 | include Helpers::VarHelper 15 | 16 | self.allow_in_post_mortem = true 17 | 18 | def self.regexp 19 | /^\s* a(?:ll)? \s*$/x 20 | end 21 | 22 | def self.description 23 | <<-DESCRIPTION 24 | v[ar] a[ll] 25 | 26 | #{short_description} 27 | DESCRIPTION 28 | end 29 | 30 | def self.short_description 31 | "Shows local, global and instance variables of self." 32 | end 33 | 34 | def execute 35 | var_global 36 | var_instance("self") 37 | var_local 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/byebug/commands/var/args.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/var" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +var+ command to define the +args+ subcommand 8 | # 9 | class VarCommand < Command 10 | # 11 | # Information about arguments of the current method/block 12 | # 13 | class ArgsCommand < Command 14 | include Helpers::VarHelper 15 | 16 | self.allow_in_post_mortem = true 17 | 18 | def self.regexp 19 | /^\s* a(?:rgs)? \s*$/x 20 | end 21 | 22 | def self.description 23 | <<-DESCRIPTION 24 | v[ar] a[args] 25 | 26 | #{short_description} 27 | DESCRIPTION 28 | end 29 | 30 | def self.short_description 31 | "Information about arguments of the current scope." 32 | end 33 | 34 | def execute 35 | var_args 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/byebug/commands/var/const.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/eval" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +var+ command to define the +const+ subcommand 8 | # 9 | class VarCommand < Command 10 | # 11 | # Shows constants 12 | # 13 | class ConstCommand < Command 14 | include Helpers::EvalHelper 15 | 16 | self.allow_in_post_mortem = true 17 | 18 | def self.regexp 19 | /^\s* c(?:onst)? (?:\s+ (.+))? \s*$/x 20 | end 21 | 22 | def self.description 23 | <<-DESCRIPTION 24 | v[ar] c[onstant] 25 | 26 | #{short_description} 27 | DESCRIPTION 28 | end 29 | 30 | def self.short_description 31 | "Shows constants of an object." 32 | end 33 | 34 | def execute 35 | obj = warning_eval(str_obj) 36 | return errmsg(pr("variable.errors.not_module", object: str_obj)) unless obj.is_a?(Module) 37 | 38 | constants = warning_eval("#{str_obj}.constants") 39 | puts prv(constants.sort.map { |c| [c, obj.const_get(c)] }, "constant") 40 | end 41 | 42 | private 43 | 44 | def str_obj 45 | @str_obj ||= @match[1] || "self.class" 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/byebug/commands/var/global.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | # 5 | # Reopens the +var+ command to define the +global+ subcommand 6 | # 7 | class VarCommand < Command 8 | # 9 | # Shows global variables 10 | # 11 | class GlobalCommand < Command 12 | include Helpers::VarHelper 13 | 14 | self.allow_in_post_mortem = true 15 | 16 | def self.regexp 17 | /^\s* g(?:lobal)? \s*$/x 18 | end 19 | 20 | def self.description 21 | <<-DESCRIPTION 22 | v[ar] g[lobal] 23 | 24 | #{short_description} 25 | DESCRIPTION 26 | end 27 | 28 | def self.short_description 29 | "Shows global variables." 30 | end 31 | 32 | def execute 33 | var_global 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/byebug/commands/var/instance.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/var" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +var+ command to define the +instance+ subcommand 8 | # 9 | class VarCommand < Command 10 | # 11 | # Shows instance variables 12 | # 13 | class InstanceCommand < Command 14 | include Helpers::VarHelper 15 | 16 | self.allow_in_post_mortem = true 17 | 18 | def self.regexp 19 | /^\s* i(?:nstance)? (?:\s+ (.+))? \s*$/x 20 | end 21 | 22 | def self.description 23 | <<-DESCRIPTION 24 | v[ar] i[nstance][ ] 25 | 26 | #{short_description} 27 | DESCRIPTION 28 | end 29 | 30 | def self.short_description 31 | "Shows instance variables of self or a specific object." 32 | end 33 | 34 | def execute 35 | var_instance(@match[1]) 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/byebug/commands/var/local.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../helpers/var" 4 | 5 | module Byebug 6 | # 7 | # Reopens the +var+ command to define the +local+ subcommand 8 | # 9 | class VarCommand < Command 10 | # 11 | # Shows local variables in current scope 12 | # 13 | class LocalCommand < Command 14 | include Helpers::VarHelper 15 | 16 | self.allow_in_post_mortem = true 17 | 18 | def self.regexp 19 | /^\s* l(?:ocal)? \s*$/x 20 | end 21 | 22 | def self.description 23 | <<-DESCRIPTION 24 | v[ar] l[ocal] 25 | 26 | #{short_description} 27 | DESCRIPTION 28 | end 29 | 30 | def self.short_description 31 | "Shows local variables in current scope." 32 | end 33 | 34 | def execute 35 | var_local 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/byebug/commands/where.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pathname" 4 | require_relative "../command" 5 | require_relative "../helpers/frame" 6 | 7 | module Byebug 8 | # 9 | # Show current backtrace. 10 | # 11 | class WhereCommand < Command 12 | include Helpers::FrameHelper 13 | 14 | self.allow_in_post_mortem = true 15 | 16 | def self.regexp 17 | /^\s* (?:w(?:here)?|bt|backtrace) (?:\s+(\S+))? \s*$/x 18 | end 19 | 20 | def self.description 21 | <<-DESCRIPTION 22 | w[here]|bt|backtrace[ maximum-frame] 23 | 24 | #{short_description} 25 | 26 | Print the entire stack frame. Each frame is numbered; the most recent 27 | frame is 0. A frame number can be referred to in the "frame" command. 28 | "up" and "down" add or subtract respectively to frame numbers shown. 29 | The position of the current frame is marked with -->. C-frames hang 30 | from their most immediate Ruby frame to indicate that they are not 31 | navigable. 32 | 33 | Without an argument, the command prints all the frames. With an argument, 34 | the command prints the nth first frames, where n is the largest between 35 | the argument or the maximum stack frame. 36 | DESCRIPTION 37 | end 38 | 39 | def self.short_description 40 | "Displays the backtrace" 41 | end 42 | 43 | def execute 44 | print_backtrace 45 | end 46 | 47 | private 48 | 49 | def print_backtrace 50 | max_frame = 51 | if @match[1] && @match[1].to_i <= context.stack_size 52 | @match[1].to_i 53 | else 54 | context.stack_size 55 | end 56 | 57 | bt = prc("frame.line", (0...max_frame)) do |_, index| 58 | Frame.new(context, index).to_hash 59 | end 60 | 61 | print(bt) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/byebug/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | # 5 | # Custom exception exception to signal "command not found" errors 6 | # 7 | class CommandNotFound < NoMethodError 8 | def initialize(input, parent = nil) 9 | @input = input 10 | @parent = parent 11 | 12 | super("Unknown command '#{name}'. Try '#{help}'") 13 | end 14 | 15 | private 16 | 17 | def name 18 | build_cmd(@parent, @input) 19 | end 20 | 21 | def help 22 | build_cmd("help", @parent) 23 | end 24 | 25 | def build_cmd(*args) 26 | args.compact.join(" ") 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/byebug/helpers/bin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | module Helpers 5 | # 6 | # Utilities for interaction with executables 7 | # 8 | module BinHelper 9 | # 10 | # Cross-platform way of finding an executable in the $PATH. 11 | # Adapted from: https://gist.github.com/steakknife/88b6c3837a5e90a08296 12 | # 13 | def which(cmd) 14 | return File.expand_path(cmd) if File.exist?(cmd) 15 | 16 | [nil, *search_paths].each do |path| 17 | exe = find_executable(path, cmd) 18 | return exe if exe 19 | end 20 | 21 | nil 22 | end 23 | 24 | def find_executable(path, cmd) 25 | executable_file_extensions.each do |ext| 26 | exe = File.expand_path(cmd + ext, path) 27 | 28 | return exe if real_executable?(exe) 29 | end 30 | 31 | nil 32 | end 33 | 34 | def search_paths 35 | ENV["PATH"].split(File::PATH_SEPARATOR) 36 | end 37 | 38 | def executable_file_extensions 39 | ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""] 40 | end 41 | 42 | def real_executable?(file) 43 | File.executable?(file) && !File.directory?(file) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/byebug/helpers/file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | module Helpers 5 | # 6 | # Utilities for interaction with files 7 | # 8 | module FileHelper 9 | # 10 | # Reads lines of source file +filename+ into an array 11 | # 12 | def get_lines(filename) 13 | File.foreach(filename).reduce([]) { |acc, elem| acc << elem.chomp } 14 | end 15 | 16 | # 17 | # Reads line number +lineno+ from file named +filename+ 18 | # 19 | def get_line(filename, lineno) 20 | File.open(filename) do |f| 21 | f.gets until f.lineno == lineno - 1 22 | f.gets 23 | end 24 | end 25 | 26 | # 27 | # Returns the number of lines in file +filename+ in a portable, 28 | # one-line-at-a-time way. 29 | # 30 | def n_lines(filename) 31 | File.foreach(filename).reduce(0) { |acc, _elem| acc + 1 } 32 | end 33 | 34 | # 35 | # Regularize file name. 36 | # 37 | def normalize(filename) 38 | return filename if virtual_file?(filename) 39 | 40 | return File.basename(filename) if Setting[:basename] 41 | 42 | File.exist?(filename) ? File.realpath(filename) : filename 43 | end 44 | 45 | # 46 | # A short version of a long path 47 | # 48 | def shortpath(fullpath) 49 | components = Pathname(fullpath).each_filename.to_a 50 | return fullpath if components.size <= 2 51 | 52 | File.join("...", components[-3..-1]) 53 | end 54 | 55 | # 56 | # True for special files like -e, false otherwise 57 | # 58 | def virtual_file?(name) 59 | if Gem.ruby_version >= Gem::Version.new("3.3.a") 60 | ["(irb)", "-e", "(byebug)"].include?(name) || name.start_with?("(eval ") 61 | else 62 | ["(irb)", "-e", "(byebug)", "(eval)"].include?(name) 63 | end 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/byebug/helpers/frame.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | module Helpers 5 | # 6 | # Utilities to assist frame navigation 7 | # 8 | module FrameHelper 9 | def switch_to_frame(frame) 10 | new_frame = index_from_start(frame) 11 | return frame_err("c_frame") if Frame.new(context, new_frame).c_frame? 12 | 13 | adjust_frame(new_frame) 14 | end 15 | 16 | def jump_frames(steps) 17 | adjust_frame(navigate_to_frame(steps)) 18 | end 19 | 20 | private 21 | 22 | def adjust_frame(new_frame) 23 | return frame_err("too_low") if new_frame >= context.stack_size 24 | return frame_err("too_high") if new_frame.negative? 25 | 26 | context.frame = new_frame 27 | processor.prev_line = nil 28 | end 29 | 30 | def navigate_to_frame(jump_no) 31 | current_jumps = 0 32 | current_pos = context.frame.pos 33 | 34 | loop do 35 | current_pos += direction(jump_no) 36 | break if out_of_bounds?(current_pos) 37 | 38 | next if Frame.new(context, current_pos).c_frame? 39 | 40 | current_jumps += 1 41 | break if current_jumps == jump_no.abs 42 | end 43 | 44 | current_pos 45 | end 46 | 47 | def out_of_bounds?(pos) 48 | !(0...context.stack_size).cover?(pos) 49 | end 50 | 51 | def frame_err(msg) 52 | errmsg(pr("frame.errors.#{msg}")) 53 | end 54 | 55 | # 56 | # @param step [Integer] A positive or negative integer 57 | # 58 | # @return [Integer] +1 if step is positive / -1 if negative 59 | # 60 | def direction(step) 61 | step / step.abs 62 | end 63 | 64 | # 65 | # Convert a possibly negative index to a positive index from the start 66 | # of the callstack. -1 is the last position in the stack and so on. 67 | # 68 | # @param i [Integer] Integer to be converted in a proper positive index. 69 | # 70 | def index_from_start(index) 71 | index >= 0 ? index : context.stack_size + index 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/byebug/helpers/parse.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | module Helpers 5 | # 6 | # Utilities to assist command parsing 7 | # 8 | module ParseHelper 9 | # 10 | # Parses +str+ of command +cmd+ as an integer between +min+ and +max+. 11 | # 12 | # If either +min+ or +max+ is nil, that value has no bound. 13 | # 14 | # @todo Remove the `cmd` parameter. It has nothing to do with the method's 15 | # purpose. 16 | # 17 | def get_int(str, cmd, min = nil, max = nil) 18 | return nil, pr("parse.errors.int.not_number", cmd: cmd, str: str) unless /\A-?[0-9]+\z/.match?(str) 19 | 20 | int = str.to_i 21 | if min && int < min 22 | err = pr("parse.errors.int.too_low", cmd: cmd, str: str, min: min) 23 | return nil, err 24 | elsif max && int > max 25 | err = pr("parse.errors.int.too_high", cmd: cmd, str: str, max: max) 26 | return nil, err 27 | end 28 | 29 | int 30 | end 31 | 32 | # 33 | # @return true if code is syntactically correct for Ruby, false otherwise 34 | # 35 | def syntax_valid?(code) 36 | return true unless code 37 | 38 | if defined?(RubyVM::InstructionSequence.compile) 39 | without_stderr do 40 | RubyVM::InstructionSequence.compile(code) 41 | true 42 | rescue SyntaxError 43 | false 44 | end 45 | else 46 | require "ripper" unless defined?(Ripper) 47 | without_stderr do 48 | !Ripper.sexp(code).nil? 49 | end 50 | end 51 | end 52 | 53 | # 54 | # @return +str+ as an integer or 1 if +str+ is empty. 55 | # 56 | def parse_steps(str, cmd) 57 | return 1 unless str 58 | 59 | steps, err = get_int(str, cmd, 1) 60 | return nil, err unless steps 61 | 62 | steps 63 | end 64 | 65 | private 66 | 67 | # 68 | # Temporarily disable output to $stderr 69 | # 70 | def without_stderr 71 | old_stderr = $stderr 72 | $stderr = StringIO.new 73 | 74 | yield 75 | ensure 76 | $stderr = old_stderr 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/byebug/helpers/path.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | module Helpers 5 | # 6 | # Utilities for managing gem paths 7 | # 8 | module PathHelper 9 | def bin_file 10 | @bin_file ||= File.join(root_path, "exe", "byebug") 11 | end 12 | 13 | def root_path 14 | @root_path ||= File.expand_path(File.join("..", "..", ".."), __dir__) 15 | end 16 | 17 | def lib_files 18 | @lib_files ||= glob_for("lib") 19 | end 20 | 21 | def test_files 22 | @test_files ||= glob_for("test") 23 | end 24 | 25 | def gem_files 26 | @gem_files ||= [bin_file] + lib_files 27 | end 28 | 29 | def all_files 30 | @all_files ||= gem_files + test_files 31 | end 32 | 33 | private 34 | 35 | def glob_for(dir) 36 | Dir.glob(File.join(root_path, dir, "**", "*.rb")) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/byebug/helpers/reflection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | module Helpers 5 | # 6 | # Reflection utility 7 | # 8 | module ReflectionHelper 9 | # 10 | # List of "command" classes in the including module 11 | # 12 | def commands 13 | constants(false) 14 | .map { |const| const_get(const, false) } 15 | .select { |c| c.is_a?(Class) && c.name =~ /[a-z]Command$/ } 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/byebug/helpers/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | module Helpers 5 | # 6 | # Utilities for interaction with strings 7 | # 8 | module StringHelper 9 | # 10 | # Converts +str+ from an_underscored-or-dasherized_string to 11 | # ACamelizedString. 12 | # 13 | def camelize(str) 14 | str.dup.split(/[_-]/).map(&:capitalize).join("") 15 | end 16 | 17 | # 18 | # Improves indentation and spacing in +str+ for readability in Byebug's 19 | # command prompt. 20 | # 21 | def prettify(str) 22 | "\n" + deindent(str) + "\n" 23 | end 24 | 25 | # 26 | # Removes a number of leading whitespace for each input line. 27 | # 28 | def deindent(str, leading_spaces: 6) 29 | str.gsub(/^ {#{leading_spaces}}/, "") 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/byebug/helpers/thread.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | module Helpers 5 | # 6 | # Utilities for thread subcommands 7 | # 8 | module ThreadHelper 9 | def display_context(ctx) 10 | puts pr("thread.context", thread_arguments(ctx)) 11 | end 12 | 13 | def thread_arguments(ctx) 14 | { 15 | status_flag: status_flag(ctx), 16 | debug_flag: debug_flag(ctx), 17 | id: ctx.thnum, 18 | thread: ctx.thread.inspect, 19 | file_line: location(ctx), 20 | pid: Process.pid, 21 | status: ctx.thread.status, 22 | current: current_thread?(ctx) 23 | } 24 | end 25 | 26 | def current_thread?(ctx) 27 | ctx.thread == Thread.current 28 | end 29 | 30 | def context_from_thread(thnum) 31 | ctx = Byebug.contexts.find { |c| c.thnum.to_s == thnum } 32 | 33 | err = if ctx.nil? 34 | pr("thread.errors.no_thread") 35 | elsif ctx == context 36 | pr("thread.errors.current_thread") 37 | elsif ctx.ignored? 38 | pr("thread.errors.ignored", arg: thnum) 39 | end 40 | 41 | [ctx, err] 42 | end 43 | 44 | private 45 | 46 | # @todo Check whether it is Byebug.current_context or context 47 | def location(ctx) 48 | return context.location if ctx == Byebug.current_context 49 | 50 | backtrace = ctx.thread.backtrace_locations 51 | return "" unless backtrace && backtrace[0] 52 | 53 | "#{backtrace[0].path}:#{backtrace[0].lineno}" 54 | end 55 | 56 | def status_flag(ctx) 57 | return "$" if ctx.suspended? 58 | 59 | current_thread?(ctx) ? "+" : " " 60 | end 61 | 62 | def debug_flag(ctx) 63 | ctx.ignored? ? "!" : " " 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/byebug/helpers/toggle.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "parse" 4 | 5 | module Byebug 6 | module Helpers 7 | # 8 | # Utilities to assist breakpoint/display enabling/disabling. 9 | # 10 | module ToggleHelper 11 | include ParseHelper 12 | 13 | def enable_disable_breakpoints(is_enable, args) 14 | raise pr("toggle.errors.no_breakpoints") if Breakpoint.none? 15 | 16 | select_breakpoints(is_enable, args).each do |b| 17 | enabled = (is_enable == "enable") 18 | raise pr("toggle.errors.expression", expr: b.expr) if enabled && !syntax_valid?(b.expr) 19 | 20 | puts pr("toggle.messages.toggled", bpnum: b.id, 21 | endis: enabled ? "en" : "dis") 22 | b.enabled = enabled 23 | end 24 | end 25 | 26 | def enable_disable_display(is_enable, args) 27 | raise pr("toggle.errors.no_display") if n_displays.zero? 28 | 29 | selected_displays = args ? args.split(/ +/) : [1..n_displays + 1] 30 | 31 | selected_displays.each do |pos| 32 | pos, err = get_int(pos, "#{is_enable} display", 1, n_displays) 33 | raise err unless err.nil? 34 | 35 | Byebug.displays[pos - 1][0] = (is_enable == "enable") 36 | end 37 | end 38 | 39 | private 40 | 41 | def select_breakpoints(is_enable, args) 42 | all_breakpoints = Byebug.breakpoints.sort_by(&:id) 43 | return all_breakpoints if args.nil? 44 | 45 | selected_ids = [] 46 | args.split(/ +/).each do |pos| 47 | last_id = all_breakpoints.last.id 48 | pos, err = get_int(pos, "#{is_enable} breakpoints", 1, last_id) 49 | raise(ArgumentError, err) unless pos 50 | 51 | selected_ids << pos 52 | end 53 | 54 | all_breakpoints.select { |b| selected_ids.include?(b.id) } 55 | end 56 | 57 | def n_displays 58 | Byebug.displays.size 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/byebug/helpers/var.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "eval" 4 | 5 | module Byebug 6 | module Helpers 7 | # 8 | # Utilities for variable subcommands 9 | # 10 | module VarHelper 11 | include EvalHelper 12 | 13 | def var_list(ary, binding = context.frame._binding) 14 | vars = ary.sort.map do |name| 15 | code = name.to_s 16 | 17 | if code == "$SAFE" 18 | code = <<~RUBY 19 | original_stderr = $stderr 20 | 21 | begin 22 | $stderr = StringIO.new 23 | 24 | #{code} 25 | ensure 26 | $stderr = original_stderr 27 | end 28 | RUBY 29 | end 30 | 31 | [name, safe_inspect(silent_eval(code, binding))] 32 | end 33 | 34 | puts prv(vars, "instance") 35 | end 36 | 37 | def var_global 38 | globals = global_variables.reject do |v| 39 | %i[$IGNORECASE $= $KCODE $-K $binding].include?(v) 40 | end 41 | 42 | var_list(globals) 43 | end 44 | 45 | def var_instance(str) 46 | obj = warning_eval(str || "self") 47 | 48 | var_list(obj.instance_variables, obj.instance_eval { binding }) 49 | end 50 | 51 | def var_local 52 | locals = context.frame.locals 53 | cur_self = context.frame._self 54 | locals[:self] = cur_self unless cur_self.to_s == "main" 55 | puts prv(locals.keys.sort.map { |k| [k, locals[k]] }, "instance") 56 | end 57 | 58 | def var_args 59 | args = context.frame.args 60 | return if args == [[:rest]] 61 | 62 | all_locals = context.frame.locals 63 | arg_values = args.map { |arg| arg[1] } 64 | 65 | locals = all_locals.select { |k, _| arg_values.include?(k) } 66 | puts prv(locals.keys.sort.map { |k| [k, locals[k]] }, "instance") 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/byebug/interfaces/local_interface.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | # 5 | # Interface class for standard byebug use. 6 | # 7 | class LocalInterface < Interface 8 | EOF_ALIAS = "continue" 9 | 10 | def initialize 11 | super() 12 | @input = $stdin 13 | @output = $stdout 14 | @error = $stderr 15 | end 16 | 17 | # 18 | # Reads a single line of input using Readline. If Ctrl-D is pressed, it 19 | # returns "continue", meaning that program's execution will go on. 20 | # 21 | # @param prompt Prompt to be displayed. 22 | # 23 | def readline(prompt) 24 | with_repl_like_sigint { without_readline_completion { Readline.readline(prompt) || EOF_ALIAS } } 25 | end 26 | 27 | # 28 | # Yields the block handling Ctrl-C the following way: if pressed while 29 | # waiting for input, the line is reset to only the prompt and we ask for 30 | # input again. 31 | # 32 | # @note Any external 'INT' traps are overriden during this method. 33 | # 34 | def with_repl_like_sigint 35 | orig_handler = trap("INT") { raise Interrupt } 36 | yield 37 | rescue Interrupt 38 | puts("^C") 39 | retry 40 | ensure 41 | trap("INT", orig_handler) 42 | end 43 | 44 | # 45 | # Disable any Readline completion procs. 46 | # 47 | # Other gems, for example, IRB could've installed completion procs that are 48 | # dependent on them being loaded. Disable those while byebug is the REPL 49 | # making use of Readline. 50 | # 51 | def without_readline_completion 52 | orig_completion = Readline.completion_proc 53 | return yield unless orig_completion 54 | 55 | begin 56 | Readline.completion_proc = ->(_) { nil } 57 | yield 58 | ensure 59 | Readline.completion_proc = orig_completion 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/byebug/interfaces/remote_interface.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../history" 4 | 5 | module Byebug 6 | # 7 | # Interface class for remote use of byebug. 8 | # 9 | class RemoteInterface < Interface 10 | def initialize(socket) 11 | super() 12 | @input = socket 13 | @output = socket 14 | @error = socket 15 | end 16 | 17 | def read_command(prompt) 18 | super("PROMPT #{prompt}") 19 | rescue Errno::EPIPE, Errno::ECONNABORTED 20 | "continue" 21 | end 22 | 23 | def confirm(prompt) 24 | super("CONFIRM #{prompt}") 25 | rescue Errno::EPIPE, Errno::ECONNABORTED 26 | false 27 | end 28 | 29 | def print(message) 30 | super(message) 31 | rescue Errno::EPIPE, Errno::ECONNABORTED 32 | nil 33 | end 34 | 35 | def puts(message) 36 | super(message) 37 | rescue Errno::EPIPE, Errno::ECONNABORTED 38 | nil 39 | end 40 | 41 | def close 42 | output.close 43 | end 44 | 45 | def readline(prompt) 46 | puts(prompt) 47 | (input.gets || "continue").chomp 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/byebug/interfaces/script_interface.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | # 5 | # Interface class for command execution from script files. 6 | # 7 | class ScriptInterface < Interface 8 | def initialize(file, verbose = false) 9 | super() 10 | @verbose = verbose 11 | @input = File.open(file) 12 | @output = verbose ? $stdout : StringIO.new 13 | @error = $stderr 14 | end 15 | 16 | def read_command(prompt) 17 | readline(prompt, false) 18 | end 19 | 20 | def close 21 | input.close 22 | end 23 | 24 | def readline(*) 25 | while (result = input.gets) 26 | output.puts "+ #{result}" if @verbose 27 | next if /^\s*#/.match?(result) 28 | 29 | return result.chomp 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/byebug/interfaces/test_interface.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | # 5 | # Custom interface for easier assertions 6 | # 7 | class TestInterface < Interface 8 | attr_accessor :test_block 9 | 10 | def initialize 11 | super() 12 | 13 | clear 14 | end 15 | 16 | def errmsg(message) 17 | error.concat(prepare(message)) 18 | end 19 | 20 | def print(message) 21 | output.concat(prepare(message)) 22 | end 23 | 24 | def puts(message) 25 | output.concat(prepare(message)) 26 | end 27 | 28 | def read_command(prompt) 29 | cmd = super(prompt) 30 | 31 | return cmd unless cmd.nil? && test_block 32 | 33 | test_block.call 34 | self.test_block = nil 35 | end 36 | 37 | def clear 38 | @input = [] 39 | @output = [] 40 | @error = [] 41 | history.clear 42 | end 43 | 44 | def inspect 45 | [ 46 | "Input:", input.join("\n"), 47 | "Output:", output.join("\n"), 48 | "Error:", error.join("\n") 49 | ].join("\n") 50 | end 51 | 52 | def readline(prompt) 53 | puts(prompt) 54 | 55 | cmd = input.shift 56 | cmd.is_a?(Proc) ? cmd.call : cmd 57 | end 58 | 59 | private 60 | 61 | def prepare(message) 62 | return message.map(&:to_s) if message.respond_to?(:map) 63 | 64 | message.to_s.split("\n") 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/byebug/option_setter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Byebug 4 | # 5 | # Handles byebug's command line options 6 | # 7 | class OptionSetter 8 | def initialize(runner, opts) 9 | @runner = runner 10 | @opts = opts 11 | end 12 | 13 | def setup 14 | debug 15 | include_flag 16 | post_mortem 17 | quit 18 | rc 19 | stop 20 | require_flag 21 | remote 22 | trace 23 | version 24 | help 25 | end 26 | 27 | private 28 | 29 | def debug 30 | @opts.on "-d", "--debug", "Set $DEBUG=true" do 31 | $DEBUG = true 32 | end 33 | end 34 | 35 | def include_flag 36 | @opts.on "-I", "--include list", "Add paths to $LOAD_PATH" do |list| 37 | $LOAD_PATH.push(list.split(":")).flatten! 38 | end 39 | end 40 | 41 | def post_mortem 42 | @opts.on "-m", "--[no-]post-mortem", "Use post-mortem mode" do |v| 43 | Setting[:post_mortem] = v 44 | end 45 | end 46 | 47 | def quit 48 | @opts.on "-q", "--[no-]quit", "Quit when script finishes" do |v| 49 | @runner.quit = v 50 | end 51 | end 52 | 53 | def rc 54 | @opts.on "-x", "--[no-]rc", "Run byebug initialization file" do |v| 55 | @runner.init_script = v 56 | end 57 | end 58 | 59 | def stop 60 | @opts.on "-s", "--[no-]stop", "Stop when script is loaded" do |v| 61 | @runner.stop = v 62 | end 63 | end 64 | 65 | def require_flag 66 | @opts.on "-r", "--require file", "Require library before script" do |lib| 67 | require lib 68 | end 69 | end 70 | 71 | def remote 72 | @opts.on "-R", "--remote [host:]port", "Remote debug [host:]port" do |p| 73 | @runner.remote = p 74 | end 75 | end 76 | 77 | def trace 78 | @opts.on "-t", "--[no-]trace", "Turn on line tracing" do |v| 79 | Setting[:linetrace] = v 80 | end 81 | end 82 | 83 | def version 84 | @opts.on "-v", "--version", "Print program version" do 85 | @runner.version = Byebug::VERSION 86 | end 87 | end 88 | 89 | def help 90 | @opts.on "-h", "--help", "Display this message" do 91 | @runner.help = @opts.help 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/byebug/printers/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "yaml" 4 | 5 | module Byebug 6 | module Printers 7 | # 8 | # Base printer 9 | # 10 | class Base 11 | class MissedPath < StandardError; end 12 | class MissedArgument < StandardError; end 13 | 14 | SEPARATOR = "." 15 | 16 | def type 17 | self.class.name.split("::").last.downcase 18 | end 19 | 20 | private 21 | 22 | def locate(path) 23 | result = nil 24 | contents.each_value do |contents| 25 | result = parts(path).reduce(contents) do |r, part| 26 | r&.key?(part) ? r[part] : nil 27 | end 28 | break if result 29 | end 30 | raise MissedPath, "Can't find part path '#{path}'" unless result 31 | 32 | result 33 | end 34 | 35 | def translate(string, args = {}) 36 | # they may contain #{} string interpolation 37 | string.gsub(/\|\w+$/, "").gsub(/([^#]?){([^}]*)}/) do 38 | key = Regexp.last_match[2].to_s 39 | raise MissedArgument, "Missed argument #{key} for '#{string}'" unless args.key?(key.to_sym) 40 | 41 | "#{Regexp.last_match[1]}#{args[key.to_sym]}" 42 | end 43 | end 44 | 45 | def parts(path) 46 | path.split(SEPARATOR) 47 | end 48 | 49 | def contents 50 | @contents ||= contents_files.each_with_object({}) do |filename, hash| 51 | hash[filename] = YAML.load_file(filename) || {} 52 | end 53 | end 54 | 55 | def array_of_args(collection, &_block) 56 | collection_with_index = collection.each.with_index 57 | collection_with_index.each_with_object([]) do |(item, index), array| 58 | args = yield item, index 59 | array << args if args 60 | end 61 | end 62 | 63 | def contents_files 64 | [File.join(__dir__, "texts", "base.yml")] 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/byebug/printers/plain.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "base" 4 | 5 | module Byebug 6 | module Printers 7 | # 8 | # Plain text printer 9 | # 10 | class Plain < Base 11 | def print(path, args = {}) 12 | message = translate(locate(path), args) 13 | tail = parts(path).include?("confirmations") ? " (y/n) " : "\n" 14 | message << tail 15 | end 16 | 17 | def print_collection(path, collection, &block) 18 | lines = array_of_args(collection, &block).map do |args| 19 | print(path, args) 20 | end 21 | 22 | lines.join 23 | end 24 | 25 | def print_variables(variables, *_unused) 26 | print_collection("variable.variable", variables) do |(key, value), _| 27 | value = value.nil? ? "nil" : value.to_s 28 | if "#{key} = #{value}".size > Setting[:width] 29 | key_size = "#{key} = ".size 30 | value = value[0..Setting[:width] - key_size - 4] + "..." 31 | end 32 | 33 | { key: key, value: value } 34 | end 35 | end 36 | 37 | private 38 | 39 | def contents_files 40 | [File.join(__dir__, "texts", "plain.yml")] + super 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/byebug/printers/texts/plain.yml: -------------------------------------------------------------------------------- 1 | break: 2 | created: "Created breakpoint {id} at {file}:{line}" 3 | 4 | display: 5 | result: "{n}: {exp} = {result}" 6 | 7 | eval: 8 | exception: "{text_message}" 9 | result: "{result}" 10 | 11 | frame: 12 | line: "{mark} #{pos} {call} at {file}:{line}" 13 | 14 | method: 15 | methods: "{name}|c" 16 | 17 | restart: 18 | success: "Re exec'ing:\n {cmd}" 19 | 20 | thread: 21 | context: "{status_flag}{debug_flag}{id} {thread} {file_line}" 22 | 23 | trace: 24 | messages: 25 | success: "Tracing global variable \"{var}\"." 26 | on_change: "traced global variable '{name}' has value '{value}'" 27 | undo: "Not tracing global variable \"{var}\" anymore." 28 | errors: 29 | var_is_not_global: "'{name}' is not a global variable." 30 | needs_global_variable: "tracevar needs a global variable name" 31 | 32 | variable: 33 | variable: "{key} = {value}" 34 | -------------------------------------------------------------------------------- /lib/byebug/processors/control_processor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "command_processor" 4 | 5 | module Byebug 6 | # 7 | # Processes commands when there's not program running 8 | # 9 | class ControlProcessor < CommandProcessor 10 | # 11 | # Available commands 12 | # 13 | def commands 14 | super.select(&:allow_in_control) 15 | end 16 | 17 | # 18 | # Prompt shown before reading a command. 19 | # 20 | def prompt 21 | "(byebug:ctrl) " 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/byebug/processors/post_mortem_processor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "command_processor" 4 | 5 | module Byebug 6 | # 7 | # Processes commands in post_mortem mode 8 | # 9 | class PostMortemProcessor < CommandProcessor 10 | def commands 11 | super.select(&:allow_in_post_mortem) 12 | end 13 | 14 | def prompt 15 | "(byebug:post_mortem) " 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/byebug/processors/script_processor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "command_processor" 4 | 5 | module Byebug 6 | # 7 | # Processes commands from a file 8 | # 9 | class ScriptProcessor < CommandProcessor 10 | # 11 | # Available commands 12 | # 13 | def commands 14 | super.select(&:allow_in_control) 15 | end 16 | 17 | def repl 18 | while (input = interface.read_command(prompt)) 19 | safely do 20 | command = command_list.match(input) 21 | raise CommandNotFound.new(input) unless command 22 | 23 | command.new(self, input).execute 24 | end 25 | end 26 | end 27 | 28 | def after_repl 29 | super 30 | 31 | interface.close 32 | end 33 | 34 | # 35 | # Prompt shown before reading a command. 36 | # 37 | def prompt 38 | "(byebug:ctrl) " 39 | end 40 | 41 | private 42 | 43 | def without_exceptions 44 | yield 45 | rescue StandardError 46 | nil 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/byebug/remote.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "socket" 4 | require_relative "processors/control_processor" 5 | require_relative "remote/server" 6 | require_relative "remote/client" 7 | 8 | # 9 | # Remote debugging functionality. 10 | # 11 | module Byebug 12 | # Port number used for remote debugging 13 | PORT = 8989 unless defined?(PORT) 14 | 15 | class << self 16 | # If in remote mode, wait for the remote connection 17 | attr_accessor :wait_connection 18 | 19 | # The actual port that the server is started at 20 | def actual_port 21 | server.actual_port 22 | end 23 | 24 | # The actual port that the control server is started at 25 | def actual_control_port 26 | control.actual_port 27 | end 28 | 29 | # 30 | # Interrupts the current thread 31 | # 32 | def interrupt 33 | current_context.interrupt 34 | end 35 | 36 | # 37 | # Starts the remote server main thread 38 | # 39 | def start_server(host = nil, port = PORT) 40 | start_control(host, port.zero? ? 0 : port + 1) 41 | 42 | server.start(host, port) 43 | end 44 | 45 | # 46 | # Starts the remote server control thread 47 | # 48 | def start_control(host = nil, port = PORT + 1) 49 | control.start(host, port) 50 | end 51 | 52 | # 53 | # Connects to the remote byebug 54 | # 55 | def start_client(host = "localhost", port = PORT) 56 | client.start(host, port) 57 | end 58 | 59 | def parse_host_and_port(host_port_spec) 60 | location = host_port_spec.split(":") 61 | location[1] ? [location[0], location[1].to_i] : ["localhost", location[0]] 62 | end 63 | 64 | private 65 | 66 | def client 67 | @client ||= Remote::Client.new(Context.interface) 68 | end 69 | 70 | def server 71 | @server ||= Remote::Server.new(wait_connection: wait_connection) do |s| 72 | Context.interface = RemoteInterface.new(s) 73 | end 74 | end 75 | 76 | def control 77 | @control ||= Remote::Server.new(wait_connection: false) do |s| 78 | context = Byebug.current_context 79 | interface = RemoteInterface.new(s) 80 | 81 | ControlProcessor.new(context, interface).process_commands 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/byebug/remote/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "socket" 4 | 5 | module Byebug 6 | module Remote 7 | # 8 | # Client for remote debugging 9 | # 10 | class Client 11 | attr_reader :interface, :socket 12 | 13 | def initialize(interface) 14 | @interface = interface 15 | @socket = nil 16 | end 17 | 18 | # 19 | # Connects to the remote byebug 20 | # 21 | def start(host = "localhost", port = PORT) 22 | connect_at(host, port) 23 | 24 | while (line = socket.gets) 25 | case line 26 | when /^PROMPT (.*)$/ 27 | input = interface.read_command(Regexp.last_match[1]) 28 | break unless input 29 | 30 | socket.puts input 31 | when /^CONFIRM (.*)$/ 32 | input = interface.readline(Regexp.last_match[1]) 33 | break unless input 34 | 35 | socket.puts input 36 | else 37 | interface.puts line 38 | end 39 | end 40 | 41 | socket.close 42 | end 43 | 44 | def started? 45 | !socket.nil? 46 | end 47 | 48 | private 49 | 50 | def connect_at(host, port) 51 | interface.puts "Connecting to byebug server at #{host}:#{port}..." 52 | @socket = TCPSocket.new(host, port) 53 | interface.puts "Connected." 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/byebug/remote/server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "socket" 4 | 5 | module Byebug 6 | module Remote 7 | # 8 | # Server for remote debugging 9 | # 10 | class Server 11 | attr_reader :actual_port, :wait_connection 12 | 13 | def initialize(wait_connection:, &block) 14 | @thread = nil 15 | @wait_connection = wait_connection 16 | @main_loop = block 17 | end 18 | 19 | # 20 | # Start the remote debugging server 21 | # 22 | def start(host, port) 23 | return if @thread 24 | 25 | if wait_connection 26 | mutex = Mutex.new 27 | proceed = ConditionVariable.new 28 | end 29 | 30 | server = TCPServer.new(host, port) 31 | @actual_port = server.addr[1] 32 | 33 | yield if block_given? 34 | 35 | @thread = DebugThread.new do 36 | while (session = server.accept) 37 | @main_loop.call(session) 38 | 39 | mutex.synchronize { proceed.signal } if wait_connection 40 | end 41 | end 42 | 43 | mutex.synchronize { proceed.wait(mutex) } if wait_connection 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/byebug/setting.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helpers/string" 4 | 5 | module Byebug 6 | # 7 | # Parent class for all byebug settings. 8 | # 9 | class Setting 10 | attr_accessor :value 11 | 12 | DEFAULT = false 13 | 14 | def initialize 15 | @value = self.class::DEFAULT 16 | end 17 | 18 | def boolean? 19 | [true, false].include?(value) 20 | end 21 | 22 | def integer? 23 | Integer(value) ? true : false 24 | rescue ArgumentError 25 | false 26 | end 27 | 28 | def help 29 | prettify(banner) 30 | end 31 | 32 | def to_sym 33 | name = self.class.name.gsub(/^Byebug::/, "").gsub(/Setting$/, "") 34 | name.gsub(/(.)([A-Z])/, '\1_\2').downcase.to_sym 35 | end 36 | 37 | def to_s 38 | "#{to_sym} is #{value ? 'on' : 'off'}\n" 39 | end 40 | 41 | class << self 42 | def settings 43 | @settings ||= {} 44 | end 45 | 46 | def [](name) 47 | settings[name].value 48 | end 49 | 50 | def []=(name, value) 51 | settings[name].value = value 52 | end 53 | 54 | def find(shortcut) 55 | abbr = /^no/.match?(shortcut) ? shortcut[2..-1] : shortcut 56 | matches = settings.select do |key, value| 57 | key =~ (value.boolean? ? /#{abbr}/ : /#{shortcut}/) 58 | end 59 | matches.size == 1 ? matches.values.first : nil 60 | end 61 | 62 | # 63 | # @todo DRY this up. Very similar code exists in the CommandList class 64 | # 65 | def help_all 66 | output = " List of supported settings:\n\n" 67 | width = settings.keys.max_by(&:size).size 68 | settings.each_value do |sett| 69 | output += format( 70 | " %-#{width}s -- %s\n", 71 | name: sett.to_sym, 72 | description: sett.banner 73 | ) 74 | end 75 | output + "\n" 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/byebug/settings/autoirb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | require_relative "../commands/irb" 5 | 6 | module Byebug 7 | # 8 | # Setting for automatically invoking IRB on every stop. 9 | # 10 | class AutoirbSetting < Setting 11 | DEFAULT = 0 12 | 13 | def initialize 14 | IrbCommand.always_run = DEFAULT 15 | end 16 | 17 | def banner 18 | "Invoke IRB on every stop" 19 | end 20 | 21 | def value=(val) 22 | IrbCommand.always_run = val ? 1 : 0 23 | end 24 | 25 | def value 26 | IrbCommand.always_run == 1 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/byebug/settings/autolist.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | require_relative "../commands/list" 5 | 6 | module Byebug 7 | # 8 | # Setting for automatically listing source code on every stop. 9 | # 10 | class AutolistSetting < Setting 11 | DEFAULT = 1 12 | 13 | def initialize 14 | ListCommand.always_run = DEFAULT 15 | end 16 | 17 | def banner 18 | "Invoke list command on every stop" 19 | end 20 | 21 | def value=(val) 22 | ListCommand.always_run = val ? 1 : 0 23 | end 24 | 25 | def value 26 | ListCommand.always_run == 1 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/byebug/settings/autopry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | require_relative "../commands/pry" 5 | 6 | module Byebug 7 | # 8 | # Setting for automatically invoking Pry on every stop. 9 | # 10 | class AutoprySetting < Setting 11 | DEFAULT = 0 12 | 13 | def initialize 14 | PryCommand.always_run = DEFAULT 15 | end 16 | 17 | def banner 18 | "Invoke Pry on every stop" 19 | end 20 | 21 | def value=(val) 22 | PryCommand.always_run = val ? 1 : 0 23 | end 24 | 25 | def value 26 | PryCommand.always_run == 1 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/byebug/settings/autosave.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | 5 | module Byebug 6 | # 7 | # Setting for automatically saving previously entered commands to history 8 | # when exiting the debugger. 9 | # 10 | class AutosaveSetting < Setting 11 | DEFAULT = true 12 | 13 | def banner 14 | "Automatically save command history record on exit" 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/byebug/settings/basename.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | 5 | module Byebug 6 | # 7 | # Command to display short paths in file names. 8 | # 9 | # For example, when displaying source code information. 10 | # 11 | class BasenameSetting < Setting 12 | def banner 13 | ": information after every stop uses short paths" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/byebug/settings/callstyle.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | 5 | module Byebug 6 | # 7 | # Setting to customize the verbosity level for stack frames. 8 | # 9 | class CallstyleSetting < Setting 10 | DEFAULT = "long" 11 | 12 | def banner 13 | "Set how you want method call parameters to be displayed" 14 | end 15 | 16 | def to_s 17 | "Frame display callstyle is '#{value}'" 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/byebug/settings/fullpath.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | 5 | module Byebug 6 | # 7 | # Setting to display full paths in backtraces. 8 | # 9 | class FullpathSetting < Setting 10 | DEFAULT = true 11 | 12 | def banner 13 | "Display full file names in backtraces" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/byebug/settings/histfile.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | 5 | module Byebug 6 | # 7 | # Setting to customize the file where byebug's history is saved. 8 | # 9 | class HistfileSetting < Setting 10 | DEFAULT = File.expand_path(".byebug_history") 11 | 12 | def banner 13 | "File where cmd history is saved to. Default: ./.byebug_history" 14 | end 15 | 16 | def to_s 17 | "The command history file is #{value}\n" 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/byebug/settings/histsize.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | 5 | module Byebug 6 | # 7 | # Setting to customize the number of byebug commands to be saved in history. 8 | # 9 | class HistsizeSetting < Setting 10 | DEFAULT = 256 11 | 12 | def banner 13 | "Maximum number of commands that can be stored in byebug history" 14 | end 15 | 16 | def to_s 17 | "Maximum size of byebug's command history is #{value}" 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/byebug/settings/linetrace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | 5 | module Byebug 6 | # 7 | # Setting to enable/disable linetracing. 8 | # 9 | class LinetraceSetting < Setting 10 | def banner 11 | "Enable line execution tracing" 12 | end 13 | 14 | def value=(val) 15 | Byebug.tracing = val 16 | end 17 | 18 | def value 19 | Byebug.tracing? 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/byebug/settings/listsize.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | 5 | module Byebug 6 | # 7 | # Setting to customize the number of source code lines to be displayed every 8 | # time the "list" command is invoked. 9 | # 10 | class ListsizeSetting < Setting 11 | DEFAULT = 10 12 | 13 | def banner 14 | "Set number of source lines to list by default" 15 | end 16 | 17 | def to_s 18 | "Number of source lines to list is #{value}\n" 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/byebug/settings/post_mortem.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | 5 | module Byebug 6 | # 7 | # Setting to enable/disable post_mortem mode, i.e., a debugger prompt after 8 | # program termination by unhandled exception. 9 | # 10 | class PostMortemSetting < Setting 11 | def initialize 12 | Byebug.post_mortem = DEFAULT 13 | end 14 | 15 | def banner 16 | "Enable/disable post-mortem mode" 17 | end 18 | 19 | def value=(val) 20 | Byebug.post_mortem = val 21 | end 22 | 23 | def value 24 | Byebug.post_mortem? 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/byebug/settings/savefile.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | 5 | module Byebug 6 | # 7 | # Setting to customize the file where byebug's settings are saved. 8 | # 9 | class SavefileSetting < Setting 10 | DEFAULT = File.expand_path("#{ENV['HOME'] || '.'}/.byebug_save") 11 | 12 | def banner 13 | "File where settings are saved to. Default: ~/.byebug_save" 14 | end 15 | 16 | def to_s 17 | "The settings file is #{value}\n" 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/byebug/settings/stack_on_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | 5 | module Byebug 6 | # 7 | # Setting to enable/disable the display of backtraces when evaluations raise 8 | # errors. 9 | # 10 | class StackOnErrorSetting < Setting 11 | def banner 12 | "Display stack trace when `eval` raises an exception" 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/byebug/settings/width.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../setting" 4 | 5 | module Byebug 6 | # 7 | # Setting to customize the maximum width of byebug's output. 8 | # 9 | class WidthSetting < Setting 10 | DEFAULT = 160 11 | 12 | def banner 13 | "Number of characters per line in byebug's output" 14 | end 15 | 16 | def to_s 17 | "Maximum width of byebug's output is #{value}" 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/byebug/source_file_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "helpers/file" 4 | require_relative "setting" 5 | 6 | module Byebug 7 | # 8 | # Formats specific line ranges in a source file 9 | # 10 | class SourceFileFormatter 11 | include Helpers::FileHelper 12 | 13 | attr_reader :file, :annotator 14 | 15 | def initialize(file, annotator) 16 | @file = file 17 | @annotator = annotator 18 | end 19 | 20 | def lines(min, max) 21 | File.foreach(file).with_index.map do |line, lineno| 22 | next unless (min..max).cover?(lineno + 1) 23 | 24 | format( 25 | "%s %#{max.to_s.size}d: %s", 26 | annotation: annotator.call(lineno + 1), 27 | lineno: lineno + 1, 28 | source: line 29 | ) 30 | end 31 | end 32 | 33 | def lines_around(center) 34 | lines(*range_around(center)) 35 | end 36 | 37 | def range_around(center) 38 | range_from(center - size / 2) 39 | end 40 | 41 | def range_from(min) 42 | first = amend_initial(min) 43 | 44 | [first, first + size - 1] 45 | end 46 | 47 | def amend_initial(line) 48 | amend(line, max_initial_line) 49 | end 50 | 51 | def amend_final(line) 52 | amend(line, max_line) 53 | end 54 | 55 | def max_initial_line 56 | max_line - size + 1 57 | end 58 | 59 | def max_line 60 | @max_line ||= n_lines(file) 61 | end 62 | 63 | def size 64 | [Setting[:listsize], max_line].min 65 | end 66 | 67 | def amend(line, ceiling) 68 | [ceiling, [1, line].max].min 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/byebug/subcommands.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "forwardable" 4 | 5 | require_relative "helpers/reflection" 6 | require_relative "command_list" 7 | 8 | module Byebug 9 | # 10 | # Subcommand additions. 11 | # 12 | module Subcommands 13 | def self.included(command) 14 | command.extend(ClassMethods) 15 | end 16 | 17 | extend Forwardable 18 | def_delegators "self.class", :subcommand_list 19 | 20 | # 21 | # Delegates to subcommands or prints help if no subcommand specified. 22 | # 23 | def execute 24 | subcmd_name = @match[1] 25 | return puts(help) unless subcmd_name 26 | 27 | subcmd = subcommand_list.match(subcmd_name) 28 | raise CommandNotFound.new(subcmd_name, self.class) unless subcmd 29 | 30 | subcmd.new(processor, arguments).execute 31 | end 32 | 33 | # 34 | # Class methods added to subcommands 35 | # 36 | module ClassMethods 37 | include Helpers::ReflectionHelper 38 | 39 | # 40 | # Default help text for a command with subcommands 41 | # 42 | def help 43 | super + subcommand_list.to_s 44 | end 45 | 46 | # 47 | # Command's subcommands. 48 | # 49 | def subcommand_list 50 | @subcommand_list ||= CommandList.new(commands) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/byebug/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # 4 | # Reopen main module to define the library version 5 | # 6 | module Byebug 7 | VERSION = "12.0.0" 8 | end 9 | -------------------------------------------------------------------------------- /tasks/compile.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rake/extensiontask" 4 | 5 | spec = Gem::Specification.load("byebug.gemspec") 6 | Rake::ExtensionTask.new("byebug", spec) { |ext| ext.lib_dir = "lib/byebug" } 7 | -------------------------------------------------------------------------------- /tasks/docker.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :docker do 4 | require_relative "../docker/manager" 5 | 6 | desc "Build all docker images" 7 | task :build_all do 8 | Docker::Manager.build_all 9 | end 10 | 11 | desc "Build the default docker image" 12 | task :build do 13 | Docker::Manager.build_default 14 | end 15 | 16 | desc "Build a ruby trunk image" 17 | task :build_and_push_head, %i[line_editor compiler] do |_t, opts| 18 | manager = Docker::Manager.new( 19 | version: "head", 20 | line_editor: opts[:line_editor], 21 | compiler: opts[:compiler] 22 | ) 23 | 24 | manager.build 25 | manager.login 26 | manager.push 27 | end 28 | 29 | desc "Build and push an image" 30 | task :build_and_push, %i[version line_editor compiler] do |_t, opts| 31 | manager = Docker::Manager.new( 32 | version: opts[:version], 33 | line_editor: opts[:line_editor], 34 | compiler: opts[:compiler] 35 | ) 36 | 37 | manager.build 38 | manager.login 39 | manager.push 40 | end 41 | 42 | desc "Test all docker images" 43 | task :test_all do 44 | Docker::Manager.test_all 45 | end 46 | 47 | desc "Test the default docker image" 48 | task :test do 49 | Docker::Manager.test_default 50 | end 51 | 52 | desc "Push all docker images to dockerhub" 53 | task :push_all do 54 | Docker::Manager.push_all 55 | end 56 | 57 | desc "Push the default docker image to dockerhub" 58 | task :push do 59 | Docker::Manager.push_default 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /tasks/docs.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "yard" 4 | 5 | YARD::Rake::YardocTask.new 6 | -------------------------------------------------------------------------------- /tasks/lint.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :lint do 4 | desc "Install lint tools" 5 | task :install do 6 | Bundler.original_system({ "BUNDLE_GEMFILE" => "gemfiles/lint/Gemfile" }, "bundle", "install", exception: true) 7 | end 8 | 9 | desc "Run all linters" 10 | task all: %i[clang_format executables tabs trailing_whitespace rubocop mdl] 11 | 12 | require_relative "linter" 13 | 14 | desc "Run clang_format on C files" 15 | task :clang_format do 16 | if Gem.win_platform? 17 | puts "Skipping C file linting on Windows since clang-format is not available" 18 | else 19 | puts "Running linter on C files" 20 | 21 | CLangFormatLinter.new.run 22 | end 23 | end 24 | 25 | desc "Check unnecessary execute permissions" 26 | task :executables do 27 | if Gem.win_platform? 28 | puts "Skipping check for exectuables on Windows since it does not support execute permissions separately from read permissions" 29 | else 30 | puts "Checking for unnecessary executables" 31 | 32 | ExecutableLinter.new.run 33 | end 34 | end 35 | 36 | desc "Check for tabs" 37 | task :tabs do 38 | puts "Checking for unnecessary tabs" 39 | 40 | TabLinter.new.run 41 | end 42 | 43 | desc "Check for trailing whitespace" 44 | task :trailing_whitespace do 45 | puts "Checking for unnecessary trailing whitespace" 46 | 47 | TrailingWhitespaceLinter.new.run 48 | end 49 | 50 | desc "Checks ruby code style with RuboCop" 51 | task :rubocop do 52 | puts "Running rubocop" 53 | 54 | Bundler.original_system("bin/rubocop", exception: true) 55 | end 56 | 57 | desc "Checks markdown code style with Markdownlint" 58 | task :mdl do 59 | puts "Running mdl" 60 | 61 | Bundler.original_system("bin/mdl", *Dir.glob("*.md"), exception: true) 62 | end 63 | 64 | desc "Checks shell code style with shellcheck" 65 | task :shellcheck do 66 | puts "Running shellcheck" 67 | 68 | sh("shellcheck", *Dir.glob("bin/*.sh")) 69 | end 70 | end 71 | 72 | desc "Runs lint tasks" 73 | task lint: ["lint:install", "lint:all"] 74 | -------------------------------------------------------------------------------- /tasks/release.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "chandler/tasks" 5 | 6 | # 7 | # Add chandler as a prerequisite for `rake release` 8 | # 9 | task "release:rubygem_push" => "chandler:push" 10 | -------------------------------------------------------------------------------- /tasks/test.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | desc "Runs the test suite" 4 | task :test do 5 | require_relative "../test/minitest_runner" 6 | 7 | raise unless Byebug::MinitestRunner.new.run 8 | end 9 | -------------------------------------------------------------------------------- /test/changelog_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests sanity of changelog file. 8 | # 9 | class ChangelogTest < Minitest::Test 10 | def test_has_definitions_for_all_releases 11 | changelog.scan(/\[([0-9]+\.[0-9]+\.[0-9]+)\] /).flatten.uniq.each do |release_version| 12 | assert_match %r{^\[#{release_version}\]: https://github\.com/deivid-rodriguez/byebug/compare/v[0-9]+\.[0-9]+\.[0-9]+...v#{release_version}}, changelog 13 | end 14 | end 15 | 16 | private 17 | 18 | def changelog 19 | File.read("CHANGELOG.md") 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/commands/catch_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests exception catching 8 | # 9 | class CatchTest < TestCase 10 | def test_catch_adds_catchpoints 11 | enter "catch NoMethodError" 12 | debug_code(minimal_program) 13 | 14 | assert_equal 1, Byebug.catchpoints.size 15 | end 16 | 17 | def test_catch_removes_specific_catchpoint 18 | enter "catch NoMethodError", "catch NoMethodError off" 19 | debug_code(minimal_program) 20 | 21 | assert_empty Byebug.catchpoints 22 | end 23 | 24 | def test_catch_off_removes_all_catchpoints_after_confirmation 25 | enter "catch NoMethodError", "catch off", "y" 26 | debug_code(minimal_program) 27 | 28 | assert_empty Byebug.catchpoints 29 | end 30 | 31 | def test_catch_without_arguments_and_no_exceptions_caught 32 | enter "catch" 33 | debug_code(minimal_program) 34 | 35 | check_output_includes "No exceptions set to be caught." 36 | end 37 | 38 | def test_catch_without_arguments_and_exceptions_caught 39 | enter "catch NoMethodError", "catch" 40 | debug_code(minimal_program) 41 | 42 | check_output_includes "NoMethodError: false" 43 | end 44 | 45 | def test_catch_help 46 | enter "help catch" 47 | debug_code(minimal_program) 48 | 49 | check_output_includes "cat[ch][ (off|[ off])]" 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/commands/debug_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests launching new debuggers from byebug's prompt 8 | # 9 | class SubdebuggersTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: # 14 | 3: # Toy class to test subdebuggers inside evaluation prompt 15 | 4: # 16 | 5: class #{example_class} 17 | 6: def self.a 18 | 7: byebug 19 | 8: end 20 | 9: end 21 | 10: 22 | 11: byebug 23 | 12: 24 | 13: "Bye!" 25 | 14: end 26 | RUBY 27 | end 28 | 29 | def test_subdebugger_stops_at_correct_point_when_invoked_through_byebug_call 30 | enter "debug #{example_class}.a" 31 | 32 | debug_code(program) { assert_equal 8, frame.line } 33 | end 34 | 35 | def test_subdebugger_stops_at_correct_point_when_invoked_from_breakpoint 36 | enter "break #{example_class}.a", "debug #{example_class}.a" 37 | 38 | debug_code(program) { assert_equal 7, frame.line } 39 | end 40 | 41 | def test_subdebugger_goes_back_to_previous_debugger_after_continue 42 | enter "debug #{example_class}.a", "continue" 43 | 44 | debug_code(program) { assert_equal 13, frame.line } 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/commands/delete_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests deleting breakpoints. 8 | # 9 | class DeleteTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: # 14 | 3: # Toy class to test breakpoints 15 | 4: # 16 | 5: class #{example_class} 17 | 6: def add_two(n) 18 | 7: byebug 19 | 8: n += 1 20 | 9: n += 1 21 | 10: n 22 | 11: end 23 | 12: end 24 | 13: 25 | 14: #{example_class}.new.add_two(0) 26 | 15: end 27 | RUBY 28 | end 29 | 30 | def test_deleting_a_breakpoint_removes_it_from_breakpoints_list 31 | enter "break 9", -> { "delete #{Breakpoint.first.id}" } 32 | 33 | debug_code(program) { assert_empty Byebug.breakpoints } 34 | end 35 | 36 | def test_deleting_a_breakpoint_shows_a_success_message 37 | enter "break 9", -> { "delete #{Breakpoint.first.id}" } 38 | debug_code(program) 39 | 40 | check_output_includes(/Deleted breakpoint/) 41 | end 42 | 43 | def test_does_not_stop_at_the_deleted_breakpoint 44 | enter "b 9", "b 10", -> { "delete #{Breakpoint.first.id}" }, "cont" 45 | 46 | debug_code(program) { assert_equal 10, frame.line } 47 | end 48 | 49 | def test_delete_by_itself_deletes_all_breakpoints_if_confirmed 50 | enter "break 9", "break 10", "delete", "y" 51 | 52 | debug_code(program) { assert_empty Byebug.breakpoints } 53 | end 54 | 55 | def test_delete_by_itself_keeps_current_breakpoints_if_not_confirmed 56 | enter "break 9", "break 10", "delete", "n" 57 | 58 | debug_code(program) { assert_equal 2, Byebug.breakpoints.size } 59 | end 60 | 61 | def test_delete_with_an_invalid_breakpoint_id_shows_error 62 | enter "break 9", -> { "delete #{Breakpoint.last.id + 1}" }, "cont" 63 | debug_code(program) 64 | 65 | check_error_includes(/No breakpoint number/) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/commands/display_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests displaying values of expressions on every stop. 8 | # 9 | class DisplayTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: d = 0 14 | 3: 15 | 4: byebug 16 | 5: 17 | 6: d += 3 18 | 7: d + 6 19 | 8: end 20 | RUBY 21 | end 22 | 23 | def test_shows_expressions 24 | enter "display d + 1" 25 | debug_code(program) { clear_displays } 26 | 27 | check_output_includes "1: d + 1 = 1" 28 | end 29 | 30 | def test_shows_undefined_expressions 31 | enter "display e" 32 | debug_code(program) { clear_displays } 33 | 34 | check_output_includes "1: e = (undefined)" 35 | end 36 | 37 | def test_saves_displayed_expressions 38 | enter "display d + 1" 39 | 40 | debug_code(program) do 41 | assert_equal [[true, "d + 1"]], Byebug.displays 42 | clear_displays 43 | end 44 | end 45 | 46 | def test_displays_all_expressions_available 47 | enter "display d", "display d + 1", "display" 48 | 49 | debug_code(program) { clear_displays } 50 | 51 | check_output_includes "1: d = 0", "2: d + 1 = 1" 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/commands/down_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests commands which deal with backtraces. 8 | # 9 | class DownTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: # 14 | 3: # Toy class to test backtraces. 15 | 4: # 16 | 5: class #{example_class} 17 | 6: def initialize(letter) 18 | 7: @letter = encode(letter) 19 | 8: end 20 | 9: 21 | 10: def encode(str) 22 | 11: integerize(str + "x") + 5 23 | 12: end 24 | 13: 25 | 14: def integerize(str) 26 | 15: byebug 27 | 16: str.ord 28 | 17: end 29 | 18: end 30 | 19: 31 | 20: frame = #{example_class}.new("f") 32 | 21: 33 | 22: frame 34 | 23: end 35 | RUBY 36 | end 37 | 38 | def test_down_moves_down_in_the_callstack 39 | enter "up", "down" 40 | 41 | debug_code(program) { assert_equal 16, frame.line } 42 | end 43 | 44 | def test_down_autolists_new_source_location_when_autolist_enabled 45 | with_setting :autolist, true do 46 | enter "up 2", "down" 47 | debug_code(program) 48 | 49 | check_output_includes '=> 11: integerize(str + "x") + 5' 50 | end 51 | end 52 | 53 | def test_down_does_not_autolist_new_source_location_when_autolist_disabled 54 | with_setting :autolist, false do 55 | enter "up 2", "down" 56 | debug_code(program) 57 | 58 | check_output_doesnt_include '=> 11: integerize(str + "x") + 5' 59 | end 60 | end 61 | 62 | def test_down_moves_down_in_the_callstack_a_specific_number_of_frames 63 | enter "up 3", "down 2" 64 | 65 | debug_code(program) { assert_equal 11, frame.line } 66 | end 67 | 68 | def test_down_skips_c_frames 69 | enter "up 3", "down", "frame" 70 | debug_code(program) 71 | 72 | check_output_includes( 73 | /--> #2 .*initialize\(letter#String\)\s* at .*#{example_path}:7/ 74 | ) 75 | end 76 | 77 | def test_down_does_not_move_if_frame_number_to_too_low 78 | enter "down" 79 | 80 | debug_code(program) { assert_equal 16, frame.line } 81 | check_error_includes "Can't navigate beyond the newest frame" 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/commands/edit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests file editing from within Byebug. 8 | # 9 | class EditTest < TestCase 10 | def test_edit_opens_current_file_in_current_line_in_configured_editor 11 | with_env("EDITOR", "edi") do 12 | assert_calls(Kernel, :system, "edi +4 #{example_path}") do 13 | enter "edit" 14 | debug_code(minimal_program) 15 | end 16 | end 17 | end 18 | 19 | def test_edit_calls_vim_if_no_editor_environment_variable_is_set 20 | with_env("EDITOR", nil) do 21 | assert_calls(Kernel, :system, "vim +4 #{example_path}") do 22 | enter "edit" 23 | debug_code(minimal_program) 24 | end 25 | end 26 | end 27 | 28 | def test_edit_opens_configured_editor_at_specific_line_and_file 29 | with_env("EDITOR", "edi") do 30 | assert_calls(Kernel, :system, "edi +3 #{readme_path}") do 31 | enter "edit README.md:3" 32 | debug_code(minimal_program) 33 | end 34 | end 35 | end 36 | 37 | def test_edit_shows_an_error_if_specified_file_does_not_exist 38 | file = File.expand_path("no_such_file") 39 | enter "edit no_such_file:6" 40 | debug_code(minimal_program) 41 | 42 | check_error_includes "File #{file} does not exist." 43 | end 44 | 45 | def test_edit_shows_an_error_if_the_specified_file_is_not_readable 46 | File.stub(:readable?, false) do 47 | enter "edit README.md:6" 48 | debug_code(minimal_program) 49 | 50 | check_error_includes "File #{readme_path} is not readable." 51 | end 52 | end 53 | 54 | def test_edit_accepts_no_line_specification 55 | with_env("EDITOR", "edi") do 56 | assert_calls(Kernel, :system, "edi #{readme_path}") do 57 | enter "edit README.md" 58 | debug_code(minimal_program) 59 | end 60 | end 61 | end 62 | 63 | private 64 | 65 | def readme_path 66 | File.expand_path("README.md") 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/commands/eval_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests expression evaluation. 8 | # 9 | class EvalTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: Foo = "Foo constant" 14 | 3: foo = :foo_variable 15 | 4: byebug 16 | 5: end 17 | RUBY 18 | end 19 | 20 | def test_eval_prints_values 21 | enter "Foo", "foo" 22 | debug_code(program) 23 | check_output_includes('"Foo constant"') 24 | check_output_includes(":foo_variable") 25 | end 26 | end 27 | 28 | # 29 | # Tests expression evalution of the code that uses TracePoint. 30 | # 31 | class EvalTracePointClassTest < TestCase 32 | def program 33 | strip_line_numbers <<-RUBY 34 | 1: result = :tp_class_not_called 35 | 2: autoload :Foo, "./foo" 36 | 3: TracePoint.new(:class) { |tp| result = :tp_class_called }.enable 37 | 4: byebug 38 | 5: result 39 | RUBY 40 | end 41 | 42 | def foo_program 43 | strip_line_numbers <<-RUBY 44 | 1: module Foo 45 | 2: def self.bar 46 | 3: "Foo.bar called" 47 | 4: end 48 | 5: end 49 | RUBY 50 | end 51 | 52 | def test_eval_triggers_class_tracepoint 53 | with_new_file("foo.rb", foo_program) do 54 | enter "Foo.bar", "result" 55 | debug_code(program) 56 | check_output_includes('"Foo.bar called"') 57 | check_output_includes(":tp_class_called") 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/commands/frame_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests commands which show frames 8 | # 9 | class FrameTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: # 14 | 3: # Toy class to test backtraces. 15 | 4: # 16 | 5: class #{example_class} 17 | 6: def initialize(letter) 18 | 7: @letter = encode(letter) 19 | 8: end 20 | 9: 21 | 10: def encode(str) 22 | 11: integerize(str + "x") + 5 23 | 12: end 24 | 13: 25 | 14: def integerize(str) 26 | 15: byebug 27 | 16: str.ord 28 | 17: end 29 | 18: end 30 | 19: 31 | 20: frame = #{example_class}.new("f") 32 | 21: 33 | 22: frame 34 | 23: end 35 | RUBY 36 | end 37 | 38 | def test_frame_moves_to_a_specific_frame 39 | enter "frame 2" 40 | 41 | debug_code(program) { assert_equal 7, frame.line } 42 | end 43 | 44 | def test_frame_autolists_new_source_location_when_autolist_enabled 45 | with_setting :autolist, true do 46 | enter "frame 2" 47 | debug_code(program) 48 | 49 | check_output_includes "=> 7: @letter = encode(letter)" 50 | end 51 | end 52 | 53 | def test_frame_does_not_autolist_new_source_location_when_autolist_disabled 54 | with_setting :autolist, false do 55 | enter "frame 2" 56 | debug_code(program) 57 | 58 | check_output_doesnt_include "=> 7: @letter = encode(letter)" 59 | end 60 | end 61 | 62 | def test_frame_prints_the_callstack_when_called_without_arguments 63 | enter "up", "frame" 64 | debug_code(program) 65 | 66 | check_output_includes( 67 | /--> #1 .*encode\(str#String\)\s* at .*#{example_path}:11/ 68 | ) 69 | end 70 | 71 | def test_frame_0_sets_frame_to_the_first_one 72 | enter "up", "frame 0" 73 | 74 | debug_code(program) { assert_equal 16, frame.line } 75 | end 76 | 77 | def test_frame_minus_one_sets_frame_to_the_last_one 78 | enter "frame -1" 79 | 80 | debug_code(program) { assert_location example_path, 1 } 81 | end 82 | 83 | def test_frame_cannot_navigate_to_c_frames 84 | enter "frame 3" 85 | debug_code(program) 86 | 87 | check_error_includes "Can't navigate to c-frame" 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/commands/history_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | unless ENV["LIBEDIT"] 6 | module Byebug 7 | # 8 | # Tests Byebug's command line history. 9 | # 10 | class HistoryTest < TestCase 11 | def program 12 | strip_line_numbers <<-RUBY 13 | 1: module Byebug 14 | 2: byebug 15 | 3: 16 | 4: a = 2 17 | 5: a + 3 18 | 6: end 19 | RUBY 20 | end 21 | 22 | def test_history_displays_latest_records_from_readline_history 23 | enter "show", "history" 24 | debug_code(program) 25 | 26 | check_output_includes(/\d+ show$/, /\d+ history$/) 27 | end 28 | 29 | def test_history_n_displays_whole_history_if_n_is_bigger_than_history_size 30 | enter "show", "history 3" 31 | debug_code(program) 32 | 33 | check_output_includes(/\d+ show$/, /\d+ history 3$/) 34 | end 35 | 36 | def test_history_n_displays_latest_n_records_from_readline_history 37 | enter "show width", "show autolist", "history 2" 38 | debug_code(program) 39 | 40 | check_output_includes(/\d+ show autolist$/, /\d+ history 2$/) 41 | end 42 | 43 | def test_history_does_not_save_empty_commands 44 | enter "show", "show width", "", "history 3" 45 | debug_code(program) 46 | 47 | check_output_includes( 48 | /\d+ show$/, /\d+ show width$/, /\d+ history 3$/ 49 | ) 50 | end 51 | 52 | def test_history_does_not_save_duplicated_consecutive_commands 53 | enter "show", "show width", "show width", "history 3" 54 | debug_code(program) 55 | 56 | check_output_includes( 57 | /\d+ show$/, /\d+ show width$/, /\d+ history 3$/ 58 | ) 59 | end 60 | 61 | def test_cmds_from_previous_repls_are_remembered_if_autosave_enabled 62 | with_setting :autosave, true do 63 | enter "next", "history 2" 64 | debug_code(program) 65 | 66 | check_output_includes(/\d+ next$/, /\d+ history 2$/) 67 | end 68 | end 69 | 70 | def test_cmds_from_previous_repls_are_not_remembered_if_autosave_disabled 71 | with_setting :autosave, false do 72 | enter "next", "history" 73 | debug_code(program) 74 | 75 | check_output_includes(/\d+ history$/) 76 | check_output_doesnt_include(/\d+ next$/) 77 | end 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/commands/interrupt_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests interrupt command. 8 | # 9 | class InterruptTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: byebug 14 | 3: 15 | 4: ex = 0 16 | 5: 17 | 6: 1.times do 18 | 7: ex += 1 19 | 8: end 20 | 9: end 21 | RUBY 22 | end 23 | 24 | def test_interrupt_stops_at_the_next_statement 25 | enter "interrupt", "continue" 26 | 27 | debug_code(program) { assert_equal 6, frame.line } 28 | end 29 | 30 | def test_interrupt_steps_into_blocks 31 | enter "next", "interrupt", "continue" 32 | 33 | debug_code(program) { assert_equal 7, frame.line } 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/commands/irb_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "minitest/mock" 5 | 6 | module Byebug 7 | # 8 | # Tests entering IRB from within Byebug. 9 | # 10 | class IrbTest < TestCase 11 | def program 12 | strip_line_numbers <<-RUBY 13 | 1: module Byebug 14 | 2: byebug 15 | 3: 16 | 4: a = 2 17 | 5: a + 4 18 | 6: end 19 | RUBY 20 | end 21 | 22 | def test_irb_command_starts_an_irb_session 23 | interface.stub(:instance_of?, true) do 24 | assert_calls(IRB, :start) do 25 | enter "irb" 26 | debug_code(minimal_program) 27 | end 28 | end 29 | end 30 | 31 | def test_autoirb_calls_irb_automatically_after_every_stop 32 | interface.stub(:instance_of?, true) do 33 | assert_calls(IRB, :start) do 34 | enter "set autoirb", "cont 5", "set noautoirb" 35 | debug_code(program) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/commands/kill_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Test signal sending functionality. 8 | # 9 | class KillTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: # 14 | 3: # Toy class to test signals 15 | 4: # 16 | 5: class #{example_class} 17 | 6: def self.kill_me 18 | 7: "dieeee" 19 | 8: end 20 | 9: end 21 | 10: 22 | 11: byebug 23 | 12: 24 | 13: #{example_class}.kill_me 25 | 14: end 26 | RUBY 27 | end 28 | 29 | def test_kill_sends_signal_to_some_pid 30 | assert_calls(Process, :kill, "TERM #{Process.pid}") do 31 | enter "kill TERM" 32 | debug_code(program) 33 | end 34 | end 35 | 36 | def test_kill_closes_interface_when_sending_kill_signal_explicitly 37 | Process.stub(:kill, nil) do 38 | assert_calls(interface, :close) do 39 | enter "kill KILL" 40 | debug_code(program) 41 | end 42 | end 43 | end 44 | 45 | def test_kill_asks_confirmation_when_sending_kill_implicitly 46 | assert_calls(Process, :kill, "KILL #{Process.pid}") do 47 | enter "kill", "y" 48 | debug_code(program) 49 | 50 | check_output_includes "Really kill? (y/n)" 51 | end 52 | end 53 | 54 | def test_kill_does_not_send_an_unknown_signal 55 | refute_calls(Process, :kill, "BLA #{Process.pid}") do 56 | enter "kill BLA" 57 | debug_code(program) 58 | end 59 | end 60 | 61 | def test_kill_shows_an_error_when_the_signal_is_unknown 62 | enter "kill BLA" 63 | debug_code(program) 64 | 65 | check_error_includes "signal name BLA is not a signal I know about" 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/commands/method_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests commands for listing available methods. 8 | # 9 | class MethodTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: # 14 | 3: # Toy class to test the method command. 15 | 4: # 16 | 5: class #{example_class} 17 | 6: def initialize 18 | 7: @a = "b" 19 | 8: @c = "d" 20 | 9: end 21 | 10: 22 | 11: def self.foo 23 | 12: "asdf" 24 | 13: end 25 | 14: 26 | 15: def bla 27 | 16: "asdf" 28 | 17: end 29 | 18: end 30 | 19: 31 | 20: byebug 32 | 21: 33 | 22: a = #{example_class}.new 34 | 23: a.bla 35 | 24: end 36 | RUBY 37 | end 38 | 39 | def test_method_shows_instance_methods_of_a_class 40 | enter "cont 7", "method #{example_class}" 41 | debug_code(program) 42 | 43 | check_output_includes("bla") 44 | check_output_doesnt_include("foo") 45 | end 46 | 47 | def test_m_shows_an_error_if_specified_object_is_not_a_class_or_module 48 | enter "m a" 49 | debug_code(program) 50 | 51 | check_output_includes "Should be Class/Module: a" 52 | end 53 | 54 | def test_method_instance_shows_methods_of_object 55 | enter "cont 23", "method instance a" 56 | debug_code(program) 57 | 58 | check_output_includes("bla") 59 | check_output_doesnt_include("foo") 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/commands/pry_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pry" 4 | require "test_helper" 5 | require "minitest/mock" 6 | 7 | module Byebug 8 | # 9 | # Tests entering Pry from within Byebug. 10 | # 11 | class PryTest < TestCase 12 | def program 13 | strip_line_numbers <<-RUBY 14 | 1: module Byebug 15 | 2: byebug 16 | 3: 17 | 4: a = 2 18 | 5: a + 4 19 | 6: end 20 | RUBY 21 | end 22 | 23 | def test_pry_command_starts_a_pry_session_if_pry_installed 24 | interface.stub(:instance_of?, true) do 25 | assert_calls(Pry, :start) do 26 | enter "pry" 27 | debug_code(minimal_program) 28 | end 29 | end 30 | end 31 | 32 | def test_autopry_calls_pry_automatically_after_every_stop 33 | interface.stub(:instance_of?, true) do 34 | assert_calls(Pry, :start) do 35 | enter "set autopry", "cont 5", "set noautopry" 36 | debug_code(program) 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/commands/quit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "minitest/mock" 5 | 6 | module Byebug 7 | # 8 | # Tests exiting Byebug functionality. 9 | # 10 | class QuitTest < TestCase 11 | def faking_exit! 12 | Process.stub(:exit!, nil) { yield } 13 | end 14 | 15 | def test_quit_finishes_byebug_if_user_confirms 16 | faking_exit! do 17 | enter "quit", "y" 18 | debug_code(minimal_program) 19 | 20 | check_output_includes "Really quit? (y/n)" 21 | end 22 | end 23 | 24 | def test_quit_quits_immediately_if_used_with_bang 25 | faking_exit! do 26 | enter "quit!" 27 | debug_code(minimal_program) 28 | 29 | check_output_doesnt_include "Really quit? (y/n)" 30 | end 31 | end 32 | 33 | def test_quit_quits_immediately_if_used_with_unconditionally 34 | faking_exit! do 35 | enter "quit unconditionally" 36 | debug_code(minimal_program) 37 | 38 | check_output_doesnt_include "Really quit? (y/n)" 39 | end 40 | end 41 | 42 | def test_does_not_quit_if_user_did_not_confirm 43 | enter "quit", "n" 44 | debug_code(minimal_program) 45 | 46 | check_output_includes "Really quit? (y/n)" 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/commands/restart_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "rbconfig" 5 | require "minitest/mock" 6 | 7 | module Byebug 8 | # 9 | # Tests restarting functionality. 10 | # 11 | class RestartTest < TestCase 12 | def test_restart_with_no_args_original_script_with_no_args_standalone_mode 13 | with_mode(:standalone) do 14 | with_command_line(example_path) do 15 | assert_restarts(nil, "#{ruby_bin} #{byebug_bin} #{example_path}") 16 | end 17 | end 18 | end 19 | 20 | def test_restart_with_no_args_original_script_with_no_args_attached_mode 21 | with_mode(:attached) do 22 | with_command_line(example_path) do 23 | assert_restarts(nil, "#{ruby_bin} #{example_path}") 24 | end 25 | end 26 | end 27 | 28 | def test_restart_with_no_args_original_script_through_ruby_attached_mode 29 | with_mode(:attached) do 30 | with_command_line("ruby", example_path) do 31 | assert_restarts(nil, "ruby #{example_path}") 32 | end 33 | end 34 | end 35 | 36 | def test_restart_with_no_args_in_standalone_mode 37 | with_mode(:standalone) do 38 | with_command_line(example_path, "1") do 39 | assert_restarts(nil, "#{ruby_bin} #{byebug_bin} #{example_path} 1") 40 | end 41 | end 42 | end 43 | 44 | def test_restart_with_args_in_standalone_mode 45 | with_mode(:standalone) do 46 | with_command_line(example_path, "1") do 47 | assert_restarts("2", "#{ruby_bin} #{byebug_bin} #{example_path} 2") 48 | end 49 | end 50 | end 51 | 52 | def test_restart_with_no_args_in_attached_mode 53 | with_mode(:attached) do 54 | with_command_line(example_path, "1") do 55 | assert_restarts(nil, "#{ruby_bin} #{example_path} 1") 56 | end 57 | end 58 | end 59 | 60 | def test_restart_with_args_in_attached_mode 61 | with_mode(:attached) do 62 | with_command_line(example_path, "1") do 63 | assert_restarts(2, "#{ruby_bin} #{example_path} 2") 64 | end 65 | end 66 | end 67 | 68 | private 69 | 70 | def assert_restarts(arg, expected_cmd_line) 71 | assert_calls(Kernel, :exec, expected_cmd_line) do 72 | enter ["restart", arg].compact.join(" ") 73 | debug_code(minimal_program) 74 | 75 | check_output_includes "Re exec'ing:" 76 | end 77 | end 78 | 79 | def ruby_bin 80 | RbConfig.ruby 81 | end 82 | 83 | def byebug_bin 84 | Context.bin_file 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/commands/save_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests saving Byebug commands to a file. 8 | # 9 | class SaveTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: byebug 14 | 3: a = 2 15 | 4: a + 3 16 | 5: end 17 | RUBY 18 | end 19 | 20 | def cleanup(file) 21 | File.delete(file) 22 | end 23 | 24 | def file_contents 25 | File.read(Setting[:savefile]) 26 | end 27 | 28 | def test_save_records_regular_breakpoints 29 | enter "break 3", "save" 30 | debug_code(program) 31 | 32 | assert_includes file_contents, "break #{example_path}:3" 33 | cleanup(Setting[:savefile]) 34 | end 35 | 36 | def test_save_records_conditional_breakpoints 37 | enter "break 4 if true", "save" 38 | debug_code(program) 39 | 40 | assert_includes file_contents, "break #{example_path}:4 if true" 41 | cleanup(Setting[:savefile]) 42 | end 43 | 44 | def test_save_records_catchpoints 45 | enter "catch NoMethodError", "save", "catch NoMethodError off" 46 | debug_code(program) 47 | 48 | assert_includes file_contents, "catch NoMethodError" 49 | cleanup(Setting[:savefile]) 50 | end 51 | 52 | def test_save_records_displays 53 | enter "display 2 + 3", "save" 54 | debug_code(program) { clear_displays } 55 | 56 | assert_includes file_contents, "display 2 + 3" 57 | cleanup(Setting[:savefile]) 58 | end 59 | 60 | def test_save_records_current_state_of_settings 61 | enter "save" 62 | debug_code(program) 63 | 64 | assert_includes file_contents, "set basename false" 65 | assert_includes file_contents, "set autolist true" 66 | assert_includes file_contents, "set autoirb false" 67 | cleanup(Setting[:savefile]) 68 | end 69 | 70 | def test_save_shows_a_success_message 71 | enter "save" 72 | debug_code(program) 73 | 74 | check_output_includes "Saved to '#{Setting[:savefile]}'" 75 | cleanup(Setting[:savefile]) 76 | end 77 | 78 | def test_save_without_a_filename_uses_a_default_file 79 | enter "save output.txt" 80 | debug_code(program) 81 | 82 | assert_includes File.read("output.txt"), "set autoirb false" 83 | cleanup("output.txt") 84 | end 85 | 86 | def test_save_without_a_filename_shows_a_message_with_the_file_used 87 | enter "save output.txt" 88 | debug_code(program) 89 | 90 | check_output_includes "Saved to 'output.txt'" 91 | cleanup("output.txt") 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /test/commands/show_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Test settings display functionality. 8 | # 9 | class ShowTest < TestCase 10 | settings = 11 | %i[autolist autosave basename fullpath post_mortem stack_on_error] 12 | 13 | settings.each do |set| 14 | define_method(:"test_show_#{set}_shows_disabled_bool_setting_#{set}") do 15 | with_setting set, false do 16 | enter "show #{set}" 17 | debug_code(minimal_program) 18 | 19 | check_output_includes "#{set} is off" 20 | end 21 | end 22 | 23 | define_method(:"test_show_#{set}_shows_enabled_bool_setting_#{set}") do 24 | with_setting set, true do 25 | enter "show #{set}" 26 | debug_code(minimal_program) 27 | 28 | check_output_includes "#{set} is on" 29 | end 30 | end 31 | end 32 | 33 | def test_show_callstyle 34 | enter "show callstyle" 35 | debug_code(minimal_program) 36 | 37 | check_output_includes "Frame display callstyle is 'long'" 38 | end 39 | 40 | def test_show_listsize 41 | enter "show listsize" 42 | debug_code(minimal_program) 43 | 44 | check_output_includes "Number of source lines to list is 10" 45 | end 46 | 47 | def test_show_width 48 | width = Setting[:width] 49 | enter "show width" 50 | debug_code(minimal_program) 51 | 52 | check_output_includes "Maximum width of byebug's output is #{width}" 53 | end 54 | 55 | def test_show_unknown_setting 56 | enter "show bla" 57 | debug_code(minimal_program) 58 | 59 | check_error_includes "Unknown setting :bla" 60 | end 61 | 62 | def test_show_histfile 63 | filename = Setting[:histfile] 64 | enter "show histfile" 65 | debug_code(minimal_program) 66 | 67 | check_output_includes "The command history file is #{filename}" 68 | end 69 | 70 | def test_show_histsize 71 | max_size = Setting[:histsize] 72 | enter "show histsize" 73 | debug_code(minimal_program) 74 | 75 | check_output_includes \ 76 | "Maximum size of byebug's command history is #{max_size}" 77 | end 78 | 79 | def test_show_savefile 80 | enter "show savefile" 81 | debug_code(minimal_program) 82 | 83 | check_output_includes "The settings file is #{Dir.home}/.byebug_save" 84 | end 85 | 86 | def test_show_without_arguments_displays_help_for_the_show_command 87 | enter "show" 88 | debug_code(minimal_program) 89 | 90 | check_output_includes("Shows byebug settings", 91 | "List of supported settings:") 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /test/commands/skip_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests for continue command 8 | # 9 | class SkipTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: # 14 | 3: # Toy class to test continue_breakpoint command. 15 | 4: # 16 | 5: class #{example_class} 17 | 6: def factor(num) 18 | 7: i = 1 19 | 8: num.times do |new_number| 20 | 9: byebug 21 | 10: i *= new_number 22 | 11: end 23 | 12: end 24 | 13: end 25 | 14: c = 5 26 | 15: 27 | 16: result = #{example_class}.new.factor(c) 28 | 17: sleep 0 29 | 18: "Result is: " + result.to_s 30 | 19: end 31 | RUBY 32 | end 33 | 34 | def test_continues_until_the_end_if_no_line_specified_and_no_breakpoints 35 | enter "break 17", "skip" 36 | 37 | debug_code(program) { assert_location example_path, 17 } 38 | end 39 | 40 | def test_works_in_abbreviated_mode_too 41 | enter "break 17", "sk" 42 | 43 | debug_code(program) { assert_location example_path, 17 } 44 | end 45 | 46 | def test_does_not_list_code_for_intermediate_breakpoints 47 | enter "break 17", "skip" 48 | 49 | debug_code(program) 50 | 51 | check_output_includes "=> 10: i *= new_number" 52 | check_output_doesnt_include "=> 10: i *= new_number", "=> 10: i *= new_number" 53 | check_output_includes "=> 17: sleep 0" 54 | end 55 | 56 | def test_restores_previous_autolisting_after_skip 57 | with_setting :autolist, false do 58 | enter "break 17", "skip", "continue 18" 59 | 60 | debug_code(program) 61 | 62 | check_output_doesnt_include '=> 18: "Result is: " + result.to_s' 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/commands/source_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests reading Byebug commands from a file. 8 | # 9 | class SourceTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: byebug 14 | 3: 15 | 4: a = 2 16 | 5: a + 3 17 | 6: end 18 | RUBY 19 | end 20 | 21 | def setup 22 | File.open("source_example.txt", "w") do |f| 23 | f.puts "break 4" 24 | f.puts "break 5 if true" 25 | end 26 | 27 | super 28 | end 29 | 30 | def teardown 31 | File.delete("source_example.txt") 32 | super 33 | rescue StandardError 34 | retry 35 | end 36 | 37 | def test_source_runs_byebug_commands_from_file 38 | enter "source source_example.txt" 39 | 40 | debug_code(program) do 41 | assert_equal 4, Breakpoint.first.pos 42 | assert_equal 5, Breakpoint.last.pos 43 | assert_equal "true", Breakpoint.last.expr 44 | end 45 | end 46 | 47 | def test_source_shows_an_error_if_file_not_found 48 | enter "source blabla" 49 | debug_code(program) 50 | 51 | check_error_includes(/File ".*blabla" not found/) 52 | end 53 | 54 | def test_source_without_arguments_shows_help 55 | enter "source" 56 | debug_code(program) 57 | 58 | check_output_includes("Restores a previously saved byebug session") 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/commands/tracevar_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests gloabal variable tracing functionality. 8 | # 9 | class TracevarTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: # 14 | 3: # Toy class to test global variable tracing 15 | 4: # 16 | 5: class #{example_class} 17 | 6: def with_verbose(value) 18 | 7: previous = $VERBOSE 19 | 8: $VERBOSE = value 20 | 9: yield 21 | 10: ensure 22 | 11: $VERBOSE = previous 23 | 12: end 24 | 13: end 25 | 14: 26 | 15: #{example_class}.new.with_verbose(true) do 27 | 16: byebug 28 | 17: $VERBOSE = false 29 | 18: $VERBOSE ||= true 30 | 19: $VERBOSE &&= false 31 | 20: end 32 | 21: end 33 | RUBY 34 | end 35 | 36 | def test_tracevar_tracks_global_variables 37 | enter "tracevar $VERBOSE", "cont 19", "untracevar $VERBOSE" 38 | debug_code(program) 39 | 40 | check_output_includes \ 41 | "traced global variable '$VERBOSE' has value 'false'", 42 | "traced global variable '$VERBOSE' has value 'true'" 43 | end 44 | 45 | def test_tracevar_stop_makes_program_stop_when_global_var_changes 46 | enter "tracevar $VERBOSE stop", "cont 19", "untracevar $VERBOSE" 47 | 48 | debug_code(program) { assert_equal 18, frame.line } 49 | end 50 | 51 | def test_tracevar_nostop_does_not_stop_when_global_var_changes 52 | enter "tracevar $VERBOSE nostop", "cont 19", "untracevar $VERBOSE" 53 | 54 | debug_code(program) { assert_equal 19, frame.line } 55 | end 56 | 57 | def test_tracevar_shows_an_error_message_if_no_global_variable_is_specified 58 | enter "tracevar" 59 | debug_code(program) 60 | 61 | check_error_includes("tracevar needs a global variable name") 62 | end 63 | 64 | def test_tracevar_shows_an_error_message_if_there_is_no_such_global_var 65 | enter "tracevar $FOO" 66 | debug_code(program) 67 | 68 | check_error_includes "'$FOO' is not a global variable." 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /test/commands/untracevar_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests gloabal variable untracing functionality. 8 | # 9 | class UntracevarTest < TestCase 10 | def test_untracevar_help 11 | enter "help untracevar" 12 | debug_code(minimal_program) 13 | 14 | check_output_includes "Stops tracing a global variable." 15 | end 16 | 17 | def test_untracevar_not_global 18 | enter "untracevar $foo" 19 | debug_code(minimal_program) 20 | 21 | check_error_includes "'$foo' is not a global variable." 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/commands/up_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests the +up+ command. 8 | # 9 | class UpTest < TestCase 10 | def program 11 | strip_line_numbers <<-RUBY 12 | 1: module Byebug 13 | 2: # 14 | 3: # Toy class to test backtraces. 15 | 4: # 16 | 5: class #{example_class} 17 | 6: def initialize(letter) 18 | 7: @letter = encode(letter) 19 | 8: end 20 | 9: 21 | 10: def encode(str) 22 | 11: integerize(str + "x") + 5 23 | 12: end 24 | 13: 25 | 14: def integerize(str) 26 | 15: byebug 27 | 16: str.ord 28 | 17: end 29 | 18: end 30 | 19: 31 | 20: frame = #{example_class}.new("f") 32 | 21: 33 | 22: frame 34 | 23: end 35 | RUBY 36 | end 37 | 38 | def test_up_moves_up_in_the_callstack 39 | enter "up" 40 | 41 | debug_code(program) { assert_equal 11, frame.line } 42 | end 43 | 44 | def test_up_autolists_new_source_location_when_autolist_enabled 45 | with_setting :autolist, true do 46 | enter "up" 47 | debug_code(program) 48 | 49 | check_output_includes '=> 11: integerize(str + "x") + 5' 50 | end 51 | end 52 | 53 | def test_up_does_not_autolist_new_source_location_when_autolist_disabled 54 | with_setting :autolist, false do 55 | enter "up" 56 | debug_code(program) 57 | 58 | check_output_doesnt_include '=> 11: integerize(str + "x") + 5' 59 | end 60 | end 61 | 62 | def test_up_moves_up_in_the_callstack_a_specific_number_of_frames 63 | enter "up 2" 64 | 65 | debug_code(program) { assert_equal 7, frame.line } 66 | end 67 | 68 | def test_up_does_not_move_if_frame_number_to_too_high 69 | enter "up 100" 70 | 71 | debug_code(program) { assert_equal 16, frame.line } 72 | check_error_includes "Can't navigate beyond the oldest frame" 73 | end 74 | 75 | def test_up_skips_c_frames 76 | enter "up 3", "frame" 77 | debug_code(program) 78 | 79 | check_output_includes(/--> #4 at #{example_path}:20/) 80 | end 81 | 82 | def test_up_plays_well_with_evaluation 83 | enter "str", "up", "str", "up" 84 | debug_code(program) 85 | 86 | check_output_includes '"fx"', '"f"' 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /test/debugger_alias_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests that the old "debugger" kernel method still works. It will be 8 | # eventually removed. 9 | # 10 | class DebuggerAliasTest < Minitest::Test 11 | def test_aliases_debugger_to_byebug 12 | assert_equal Kernel.method(:byebug), Kernel.method(:debugger) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/helpers/bin_helper_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | module Helpers 7 | class BinHelperTest < Minitest::Test 8 | include BinHelper 9 | 10 | def test_which_resolves_ruby 11 | assert_equal true, File.exist?(which("ruby")) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/interface_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # A subclass of Interface to test the base class functionality 8 | # 9 | class SpecificInterface < Interface 10 | attr_accessor :fake_input_queue 11 | 12 | def readline(_prompt) 13 | @fake_input_queue.pop 14 | end 15 | end 16 | 17 | # 18 | # Tests the Interface class 19 | # 20 | class InterfaceTest < Minitest::Test 21 | def setup 22 | @interface = SpecificInterface.new 23 | end 24 | 25 | def teardown 26 | @interface.history.clear 27 | end 28 | 29 | def test_reads_simple_commands 30 | @interface.fake_input_queue = ["a_command"] 31 | 32 | assert_equal "a_command", @interface.read_command("byebug") 33 | end 34 | 35 | def test_reads_multiple_commands_in_same_line_separated_by_semicolon 36 | @interface.fake_input_queue = ["a_command; another"] 37 | 38 | assert_equal "a_command", @interface.read_command("byebug") 39 | assert_equal "another", @interface.read_command("byebug") 40 | end 41 | 42 | def test_understands_ruby_commands_using_semicolon_if_escaped 43 | @interface.fake_input_queue = ['a_command \; another'] 44 | 45 | assert_equal "a_command ; another", @interface.read_command("byebug") 46 | end 47 | 48 | def test_keeps_an_internal_command_buffer 49 | @interface.fake_input_queue = ["a_command"] 50 | @interface.command_queue = ["a_buffered_command"] 51 | 52 | assert_equal "a_buffered_command", @interface.read_command("byebug") 53 | assert_equal "a_command", @interface.read_command("byebug") 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/interfaces/local_interface_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "minitest/mock" 5 | 6 | module Byebug 7 | # 8 | # Tests the local interface 9 | # 10 | class LocalInterfaceTest < TestCase 11 | def test_continues_by_control_d 12 | # `Readline.readline` returns nil for Control-D 13 | Readline.stub(:readline, nil) do 14 | interface = LocalInterface.new 15 | assert_equal "continue", interface.readline("(byebug)") 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/interfaces/script_interface_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests the script interface (batch execution of byebug commands from a file) 8 | # 9 | class ScriptInterfaceTest < TestCase 10 | def test_initialize_wires_up_dependencies 11 | with_new_tempfile("show") do |path| 12 | interface = ScriptInterface.new(path) 13 | 14 | assert_instance_of File, interface.input 15 | assert_instance_of StringIO, interface.output 16 | assert_equal $stderr, interface.error 17 | end 18 | end 19 | 20 | def test_initialize_verbose_writes_to_stdout_and_stderr 21 | with_new_tempfile("show") do |path| 22 | interface = ScriptInterface.new(path, true) 23 | 24 | assert_instance_of File, interface.input 25 | assert_equal $stdout, interface.output 26 | assert_equal $stderr, interface.error 27 | end 28 | end 29 | 30 | def test_readline_reads_input_until_first_non_comment 31 | with_new_tempfile("# Run the show command\nshow\n") do |path| 32 | interface = ScriptInterface.new(path) 33 | 34 | assert_equal "show", interface.readline 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/minitest_runner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.expand_path(File.join("..", "lib"), __dir__)) 4 | $LOAD_PATH.unshift(__dir__) 5 | 6 | require "minitest" 7 | require "English" 8 | require "shellwords" 9 | require "timeout" 10 | 11 | module Byebug 12 | # 13 | # Helper class to aid running minitest 14 | # 15 | class MinitestRunner 16 | def initialize 17 | Minitest.seed = ENV["SEED"] || srand 18 | @test_suites = extract_from_argv { |cmd_arg| test_suite?(cmd_arg) } 19 | end 20 | 21 | def run 22 | test_suites.each { |f| require File.expand_path(f) } 23 | 24 | flags = if ENV["TESTOPTS"] 25 | ENV["TESTOPTS"].split(" ") 26 | else 27 | ["--name=/#{filtered_methods.join('|')}/"] 28 | end 29 | 30 | Minitest.run(flags + $ARGV) 31 | end 32 | 33 | private 34 | 35 | def runnables 36 | Minitest::Runnable.runnables 37 | end 38 | 39 | def test_suite?(str) 40 | all_test_suites.include?(str) 41 | end 42 | 43 | def test_suites 44 | return all_test_suites if @test_suites.empty? 45 | 46 | @test_suites 47 | end 48 | 49 | def test_methods(str) 50 | if /test_.*/.match?(str) 51 | filter_runnables_by_method(str) 52 | else 53 | filter_runnables_by_class(str) 54 | end 55 | end 56 | 57 | def filter_runnables_by_method(str) 58 | filter_runnables do |runnable| 59 | "#{runnable}##{str}" if runnable.runnable_methods.include?(str) 60 | end 61 | end 62 | 63 | def filter_runnables_by_class(str) 64 | filter_runnables do |runnable| 65 | runnable.runnable_methods if runnable.name == "Byebug::#{str}" 66 | end 67 | end 68 | 69 | def filter_runnables 70 | selected = runnables.flat_map do |runnable| 71 | yield(runnable) 72 | end.compact 73 | 74 | return unless selected.any? 75 | 76 | selected 77 | end 78 | 79 | def filtered_methods 80 | @filtered_methods ||= extract_from_argv do |cmd_arg| 81 | test_methods(cmd_arg) 82 | end 83 | end 84 | 85 | def all_test_suites 86 | Dir.glob("test/**/*_test.rb") 87 | end 88 | 89 | def extract_from_argv 90 | matching, non_matching = $ARGV.partition { |arg| yield(arg) } 91 | 92 | $ARGV.replace(non_matching) 93 | 94 | matching 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/minitest_runner_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | class MinitestRunnerTest < TestCase 7 | def test_runs 8 | output = run_minitest_runner("test/debugger_alias_test.rb") 9 | 10 | assert_includes output, "\n.\n" 11 | end 12 | 13 | def test_per_test_class 14 | output = run_minitest_runner("DebuggerAliasTest") 15 | 16 | assert_includes output, "\n.\n" 17 | end 18 | 19 | def test_per_test 20 | output = run_minitest_runner("test_aliases_debugger_to_byebug") 21 | 22 | assert_includes output, "\n.\n" 23 | end 24 | 25 | def test_combinations 26 | output = run_minitest_runner( 27 | "DebuggerAliasTest", 28 | "test_script_processor_clears_history" 29 | ) 30 | 31 | assert_includes output, "\n..\n" 32 | end 33 | 34 | def test_with_verbose_option 35 | output = run_minitest_runner("DebuggerAliasTest", "--verbose") 36 | 37 | assert_includes \ 38 | output, 39 | "Byebug::DebuggerAliasTest#test_aliases_debugger_to_byebug = 0.00 s = ." 40 | 41 | assert_includes \ 42 | output, 43 | "Run options: --name=/DebuggerAliasTest/ --verbose" 44 | end 45 | 46 | def test_with_seed_option 47 | output = run_minitest_runner("DebuggerAliasTest", "--seed=37") 48 | 49 | assert_includes output, "\n.\n" 50 | 51 | assert_includes \ 52 | output, 53 | "Run options: --name=/DebuggerAliasTest/ --seed=37" 54 | end 55 | 56 | private 57 | 58 | def run_minitest_runner(*args) 59 | Bundler.with_original_env do 60 | output, status = Open3.capture2e(shell_out_env, *binstub, *args) 61 | 62 | assert_equal true, status.success?, output 63 | 64 | output 65 | end 66 | end 67 | 68 | def binstub 69 | cmd = "bin/minitest" 70 | return [cmd] unless Gem.win_platform? 71 | 72 | %W[#{RbConfig.ruby} #{cmd}] 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/processors/script_processor_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests script processor in isolation 8 | # 9 | class ScriptProcessorTest < Minitest::Test 10 | include TestUtils 11 | 12 | def test_script_processor_clears_history 13 | previous_history = Readline::HISTORY.to_a 14 | 15 | process_rc_file("set callstyle long") 16 | 17 | assert_equal previous_history, Readline::HISTORY.to_a 18 | end 19 | 20 | def test_script_processor_closes_files 21 | process_rc_file("set callstyle long") 22 | 23 | assert_equal 0, dangling_descriptors.count 24 | end 25 | 26 | private 27 | 28 | def dangling_descriptors 29 | ObjectSpace.each_object(File).select do |f| 30 | f.path == Byebug.init_file && !f.closed? 31 | end 32 | end 33 | 34 | def process_rc_file(content) 35 | with_init_file(content) do 36 | interface = ScriptInterface.new(Byebug.init_file) 37 | 38 | ScriptProcessor.new(nil, interface).process_commands 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/rc_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | require "byebug/runner" 5 | 6 | module Byebug 7 | class RcTest < TestCase 8 | def setup 9 | super 10 | 11 | example_file.write("sleep 0") 12 | example_file.close 13 | end 14 | 15 | def test_run_with_no_rc_option 16 | with_command_line("exe/byebug", "--no-rc", example_path) do 17 | refute_calls(Byebug, :run_init_script) { non_stop_runner.run } 18 | end 19 | end 20 | 21 | def test_rc_file_commands_are_properly_run_by_default 22 | rc_positive_test(nil) 23 | end 24 | 25 | def test_rc_file_commands_are_properly_run_by_explicit_option 26 | rc_positive_test("--rc") 27 | end 28 | 29 | def test_rc_file_commands_are_properly_run_when_home_folder_not_known 30 | with_env("HOME", nil) { rc_positive_test(nil) } 31 | end 32 | 33 | def test_rc_file_with_invalid_commands 34 | with_init_file("seta callstyle long") do 35 | with_command_line("exe/byebug", "--rc", example_path) do 36 | assert_output(nil, /Unknown command 'seta callstyle long'/) do 37 | non_stop_runner.run 38 | end 39 | end 40 | end 41 | end 42 | 43 | private 44 | 45 | def rc_positive_test(flag) 46 | args = [flag, example_path].compact 47 | 48 | with_setting :callstyle, "short" do 49 | with_init_file("set callstyle long") do 50 | with_command_line("exe/byebug", *args) do 51 | non_stop_runner.run 52 | 53 | assert_equal "long", Setting[:callstyle] 54 | end 55 | end 56 | end 57 | end 58 | 59 | def non_stop_runner 60 | @non_stop_runner ||= Byebug::Runner.new(false) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/remote_debugging_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | require_relative "support/remote_debugging_tests" 5 | 6 | module Byebug 7 | # 8 | # Tests remote debugging functionality. 9 | # 10 | class RemoteDebuggingTest < TestCase 11 | include RemoteDebuggingTests 12 | 13 | def program 14 | strip_line_numbers <<-RUBY 15 | 1: require "byebug" 16 | 2: 17 | 3: module Byebug 18 | 4: # 19 | 5: # Toy class to test remote debugging 20 | 6: # 21 | 7: class #{example_class} 22 | 8: def a 23 | 9: 3 24 | 10: end 25 | 11: end 26 | 12: 27 | 13: require "byebug/core" 28 | 14: self.wait_connection = true 29 | 15: self.start_server("127.0.0.1") 30 | 16: 31 | 17: byebug 32 | 18: 33 | 19: #{example_class}.new.a 34 | 20: end 35 | RUBY 36 | end 37 | 38 | def program_with_two_breakpoints 39 | strip_line_numbers <<-RUBY 40 | 1: require "byebug" 41 | 2: require "byebug/core" 42 | 3: 43 | 4: module Byebug 44 | 5: # 45 | 6: # Toy class to test remote debugging 46 | 7: # 47 | 8: class #{example_class} 48 | 9: def a 49 | 10: 3 50 | 11: end 51 | 12: end 52 | 13: 53 | 14: self.wait_connection = true 54 | 15: self.start_server("127.0.0.1") 55 | 16: 56 | 17: byebug 57 | 18: thingy = #{example_class}.new 58 | 19: thingy.a 59 | 20: sleep 3 60 | 21: thingy.a 61 | 22: byebug 62 | 23: thingy.a 63 | 24: end 64 | RUBY 65 | end 66 | 67 | def test_interrupting_client_doesnt_abort_server_after_a_second_breakpoint 68 | write_program(program_with_two_breakpoints) 69 | 70 | status = remote_debug_connect_and_interrupt("cont") 71 | 72 | assert_equal true, status.success? 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/remote_shortcut_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | require_relative "support/remote_debugging_tests" 5 | 6 | module Byebug 7 | # 8 | # Tests the remote debugging shortcut. 9 | # 10 | class RemoteShortcutTest < TestCase 11 | include RemoteDebuggingTests 12 | 13 | def program 14 | strip_line_numbers <<-RUBY 15 | 1: require "byebug" 16 | 2: 17 | 3: module Byebug 18 | 4: # 19 | 5: # Toy class to test remote debugging 20 | 6: # 21 | 7: class #{example_class} 22 | 8: def a 23 | 9: 3 24 | 10: end 25 | 11: end 26 | 12: 27 | 13: remote_byebug("127.0.0.1") 28 | 14: 29 | 15: #{example_class}.new.a 30 | 16: end 31 | RUBY 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/runner_against_program_with_byebug_call_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module Byebug 6 | # 7 | # Tests standalone byebug when debugging a target program 8 | # 9 | class RunnerAgainstProgramWithByebugCallTest < TestCase 10 | def setup 11 | super 12 | 13 | example_file.write("require 'byebug'\nbyebug\nsleep 0") 14 | example_file.close 15 | end 16 | 17 | def test_run_with_a_script_to_debug 18 | stdout = run_program( 19 | [RbConfig.ruby, example_path], 20 | 'puts "Program: #{$PROGRAM_NAME}"' 21 | ) 22 | 23 | assert_match(/Program: #{example_path}/, stdout) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/support/matchers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "support/assertions" 4 | 5 | module Byebug 6 | # 7 | # Some custom matches for byebug's output 8 | # 9 | module TestMatchers 10 | def check_output_includes(*args) 11 | check_stream(:assert_includes_in_order, interface.output, *args) 12 | end 13 | 14 | def check_error_includes(*args) 15 | check_stream(:assert_includes_in_order, interface.error, *args) 16 | end 17 | 18 | def check_output_doesnt_include(*args) 19 | check_stream(:refute_includes_in_order, interface.output, *args) 20 | end 21 | 22 | def check_error_doesnt_include(*args) 23 | check_stream(:refute_includes_in_order, interface.error, *args) 24 | end 25 | 26 | private 27 | 28 | def check_stream(check_method, stream, *args) 29 | send(check_method, Array(args), stream.map(&:strip)) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "support/test_case" 4 | 5 | Byebug::TestCase.before_suite 6 | --------------------------------------------------------------------------------