Ruby Command Line and Environment

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them. The ruby command is the front door of every Ruby program: it decides where the program text comes from, which libraries are loaded before it starts, which warnings are shown, which encodings are the default, which parser and which JIT are used, and how a failure is reported. Almost all of that is controlled by command-line options, and a second layer of it by environment variables, which is what lets a deployment tune an application without editing its code. This page walks through the whole surface: the synopsis and the ways of supplying a program, every option of the 4.0 ruby command grouped by purpose, the text-processing one-liners that grew out of Ruby’s Perl heritage (-n, -p, -a, -F, -l, -i, -0), loading code with -I and -r, warnings and debugging switches, the --enable/--disable feature flags, the parser and the --dump options, JIT switches, and the environment variables (RUBYOPT, RUBYLIB, the RUBY_* runtime tuning variables, the garbage-collector family and the RubyGems and Bundler variables).

The command line is the tool that runs everything else in this section. Installing Ruby and the first programs are in Getting Started; the version policy is in Versions, Releases and Compatibility. How require searches $LOAD_PATH is in Code Loading: require and autoload; gems and Bundler (which set RUBYOPT and GEM_HOME for you) are in RubyGems and Bundler. Writing a program that has its own options (OptionParser, ARGV, exit statuses) is in Command-Line Programs; the interactive console and the debugger are in IRB and Debugging. The JIT compilers and the virtual machine behind --dump are in YJIT, ZJIT and the VM, and the garbage collector behind the RUBY_GC_* variables is in Garbage Collection and Memory. For the equivalent in other languages see Python getting started (the python command and PYTHONPATH), Go getting started and Rust getting started.

Every command on this page was run on Ruby 4.0.7 (macOS, arm64) with LANG=en_US.UTF-8; the Ruby files were syntax-checked with ruby -wc, run, and linted with RuboCop using the configuration of the running shelf gem (target Ruby 4.0, double-quoted strings). The output shown is the real output; numbers that depend on the machine (stack depth, memory limits, the number of load-path entries) are marked. Version attributions were checked by running the same commands on Ruby 3.2, 3.3 and 3.4 where an observable difference exists, and the ones taken from the manual page or the NEWS files without a run are said so. The shell examples use `$ ` only where the command and its output need separating; the data file for the text-processing examples is a small CSV of the shelf catalogue:

title,author,year
Picnic,A. Writer,2019
Ruby Basics,B. Coder,2021
Plain Text,A. Writer,2020

The synopsis of the ruby command

The manual page and ruby --help give the synopsis:

ruby [options] [--] [filepath] [arguments]

Everything before the program is an option of the interpreter; everything after it is an argument of the program and ends up in ARGV. Two details cause most of the confusion:

  • Options are only recognised before the program name. ruby -w script.rb -w runs script.rb in warning mode and gives it ARGV of ["-w"]. The -- option terminator ends interpreter options explicitly, which matters when the program name itself could be mistaken for an option.

  • With -e there is no program file, so every non-option word is an argument: ruby -e 'p ARGV' extra.rb prints ["extra.rb"] and never runs extra.rb.

Where the program comes from

Form Program text $0 and $PROGRAM_NAME

ruby script.rb arg...

The file script.rb.

"script.rb"

ruby -e 'code' -e 'more'

The -e strings joined with newlines; the option can be given several times.

"-e"

ruby or ruby - (no file)

Standard input, read to the end before running. ruby - a b passes a and b to the program.

"-"

ruby -x file

The part of a non-Ruby text file that starts at the first #! line containing ruby (see below).

The file name.

ruby -S name

The first file called name found in RUBYPATH and PATH.

The resolved path.

ruby -r lib -e 1

No program of its own; the point is the library loaded before it.

"-e"

Reading the whole program before running it matters for a point the older books get wrong. A Ruby file is not "parsed and executed line by line": the complete file is parsed first (by Prism since 3.4), compiled to YARV bytecode, and only then run. That is why a syntax error anywhere in a file stops the program before its first line runs, and why ruby -c can check a file without executing it. The parser and the bytecode are shown later in this page with --dump.

When the file does not exist, or the code does not compile, the interpreter exits with status 1:

$ ruby nofile.rb
ruby: No such file or directory -- nofile.rb (LoadError)
$ ruby -c bad.rb
ruby: bad.rb:1: syntax errors found (SyntaxError)
> 1 | puts (1
    |        ^ unexpected end-of-input, assuming it is closing the parent top level context
> 2 |
    | ^ expected a matching `)`
$ echo "rc=$?"
rc=1

The exit status is the program’s result for the shell: 0 for success, the number given to exit (ruby -e 'exit 3' ends with status 3), and 1 for an uncaught exception, a syntax error or a missing file. Exit statuses are covered in Command-Line Programs.

Identifying the interpreter

ruby -v prints the version and sets $VERBOSE (see below); ruby --version prints the same line and exits, and --copyright prints the copyright line. The line is built from constants that a program can read too:

$ ruby --version
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin23]
$ ruby --copyright
ruby - Copyright (C) 1993-2026 Yukihiro Matsumoto
$ ruby -e 'p RUBY_VERSION, RUBY_ENGINE, RUBY_PLATFORM, RUBY_RELEASE_DATE, RUBY_PATCHLEVEL'
"4.0.7"
"ruby"
"arm64-darwin23"
"2026-09-15"
0

The tokens at the end of the version line describe the build: +PRISM (the default parser since Ruby 3.4) appears unless the parser was changed, +YJIT appears once YJIT is enabled, and the bracketed part is the platform. RUBY_ENGINE distinguishes CRuby ("ruby") from JRuby and TruffleRuby, and RbConfig::CONFIG (require "rbconfig") exposes the build configuration, for example RbConfig.ruby for the path of the running interpreter, which is what a program should use to start another Ruby of the same version.

A runtime report

The following script prints how the interpreter that runs it was started. It is used below to show the effect of options and environment variables:

# frozen_string_literal: true

# Prints how this interpreter was started.
categories = Warning.categories.to_h { |name| [name, Warning[name]] }

puts "ruby      #{RUBY_DESCRIPTION}"
puts "program   #{$PROGRAM_NAME}"
puts "arguments #{ARGV.inspect}"
puts "verbose   #{$VERBOSE.inspect}, debug #{$DEBUG}"
puts "warnings  #{categories}"
puts "encodings #{Encoding.default_external} / #{Encoding.default_internal.inspect}"
puts "yjit      #{defined?(RubyVM::YJIT) ? RubyVM::YJIT.enabled? : 'not built'}"
puts "load path #{$LOAD_PATH.size} entries"

Run it plainly, then with several options at once (Warning.categories is Ruby 3.4+):

$ ruby runtime_report.rb a b
ruby      ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin23]
program   runtime_report.rb
arguments ["a", "b"]
verbose   false, debug false
warnings  {deprecated: false, experimental: true, performance: false, strict_unused_block: false}
encodings UTF-8 / nil
yjit      false
load path 8 entries

$ ruby -w --yjit -E ISO-8859-1:UTF-8 -Ishelf runtime_report.rb
ruby      ruby 4.0.7 (2026-09-15 revision 229531a6cf) +YJIT +PRISM [arm64-darwin23]
program   runtime_report.rb
arguments []
verbose   true, debug false
warnings  {deprecated: true, experimental: true, performance: false, strict_unused_block: false}
encodings ISO-8859-1 / #<Encoding:UTF-8>
yjit      true
load path 9 entries

The second run shows four independent effects: -w turned $VERBOSE on and enabled the deprecated warning category, --yjit added +YJIT and made RubyVM::YJIT.enabled? true, -E replaced the two default encodings, and -I added a ninth $LOAD_PATH entry. The load-path count depends on the installation.

Options by purpose

The 4.0 ruby --help lists the options below. The tables group them by what they control; the sections after them show the ones that need an example. An option with an argument usually takes it attached (-Ilib, -rjson) or separated by a space (-I lib, -r json); the exceptions are marked.

Choosing and checking the program

Option Meaning

-e 'code'

Execute the given code; may be repeated, the pieces are joined by newlines. Extra non-option words become ARGV.

-c

Check the syntax and exit without running (prints Syntax OK). Equivalent to --dump=syntax. Combine with -w to see parse-time warnings.

-Cdirpath

Change to dirpath before running the program. The path of the program file is read after the change.

-x[dirpath]

Run the Ruby program embedded in a text file: skip everything before the first line that starts with #! and contains ruby, stop at __END__ or end of file. An attached directory is entered first.

-S

Look for the program name in RUBYPATH and PATH instead of the current directory.

-s

Parse switches that follow the program name as global variables (see below). Not usable with -e.

-h, --help

Print a short or the long help text. The pager used for the long text is named by RUBY_PAGER (default PAGER).

-v

Print the version and set $VERBOSE to true; with no program it exits after printing.

--version, --copyright

Print the version or the copyright line and exit.

Text-processing loops

Option Meaning

-n

Wrap the program in while gets ... end: the program runs once per input line, with the line in $_.

-p

Like -n, and print $_ after each pass of the loop.

-a

With -n or -p, split each line into the array $F (using $; or the -F separator).

-Fpattern

The field separator used by -a; a string or a regular expression source. The pattern must be attached to the option.

-l

With -n or -p, call chop! on each line and set the output record separator $\ to the input record separator $/.

-0[octal]

Set the input record separator $/: -0 alone is NUL, -00 is paragraph mode ($/ is the empty string), -0777 is slurp mode ($/ is nil: the whole file is one record), any other octal number is that character.

-i[extension]

In-place editing for files read through ARGF: the output of the program replaces each file. With an extension the original is kept as a backup.

Load path, libraries and the working directory

Option Meaning

-Idirpath

Add a directory at the front of $LOAD_PATH; relative directories are expanded against the current directory. Repeatable.

-rlibrary

require the library before the program starts. Repeatable. Runs after RubyGems has loaded (unless --disable=gems).

-Cdirpath

See above; relative -I directories are expanded after the change of directory.

Warnings and diagnostics

Option Meaning

-w

Turn warnings on: $VERBOSE becomes true. See the note on levels below.

-W[level], -W:category

Set the warning level (0 silent, 1 normal, 2 or no number verbose) or switch one warning category on or off (-W:deprecated, -W:no-experimental).

-d, --debug

Set $DEBUG and $VERBOSE to true; the interpreter prints every exception as it is raised, including internal ones.

--verbose

Set $VERBOSE to true; with no program, it does not read standard input.

--backtrace-limit=num

Show at most num backtrace lines (default no limit); see Thread::Backtrace.limit.

--crash-report=template

Path template for the crash report file (same as the RUBY_CRASH_REPORT variable). Ruby 3.3 and later.

Encodings

Option Meaning

-Eexternal[:internal], --encoding=...

Set Encoding.default_external and optionally Encoding.default_internal.

--external-encoding=enc, --internal-encoding=enc

Set only one of the two defaults.

-U

Set Encoding.default_internal to UTF-8.

-Kc

Legacy: set the source and default external encoding by a Japanese-encoding letter (e EUC-JP, s Windows-31J, u UTF-8, n none). Still accepted in 4.0; do not use it in new code.

Features, parser and VM

Option Meaning

--enable=feature,..., --disable=feature,...

Turn a named interpreter feature on or off; --enable-feature and --disable-feature are the same with a hyphen. --enable-all and --disable-all select every feature.

--parser=prism, --parser=parse.y

Choose the parser (Ruby 3.3 and later; Prism is the default from 3.4).

--dump=items

Print internal representations: insns, parsetree, yydebug, plus the modifier items -optimize, +error-tolerant and +comment; and the aliases version, copyright, usage, help, syntax.

-y, --yydebug

Print the parser’s trace (with parse.y only; Prism ignores it). Not stable between versions.

--yjit, --jit, --yjit-*

Enable the YJIT compiler and tune it (--jit is the same as --yjit on this build).

--zjit, --zjit-*

Enable the experimental ZJIT compiler when the build includes it.

-T (taint checking) and $SAFE were removed with the taint system: on Ruby 3.2, 3.3, 3.4 and 4.0, ruby -T -e 1 fails with invalid option -T. --rjit was removed in 4.0 (invalid option --rjit), and MJIT before it. The manual page installed with 4.0.7 still mentions -T, and also describes -w as "level 1" like the options guide does; the observed behaviour of both is described in the sections below, and the observed behaviour is what this page documents.

Running code

One expression: -e

$ ruby -e 'puts "Hello, shelf"'
Hello, shelf
$ ruby -e 'puts "a"' -e 'puts "b"'
a
b
$ ruby -rjson -e 'puts JSON.generate(title: "Picnic", year: 2019)'
{"title":"Picnic","year":2019}

Shell quoting is the usual trap: wrap the code in single quotes so that $_, #{...} and ! reach Ruby untouched. A program that must contain a single quote is better put in a file, or written with %q and %Q literals.

Standard input

A program can be piped in. With no file, ruby reads standard input to the end and then runs it; - makes that explicit and lets arguments follow:

$ echo 'puts 6 * 7' | ruby
42
$ echo 'puts ARGV.inspect' | ruby - x y
["x", "y"]

--verbose and -v without a program do not read standard input: they print (or set) and exit, so echo 'p 1' | ruby -v shows only the version. -w and -W do read it.

Syntax checking: -c and -wc

ruby -c file.rb parses without running. Adding -w includes the parse-time warnings, which is why this page’s examples are all checked with ruby -wc:

$ cat w.rb
# frozen_string_literal: true

def title(book) = book.to_s
x = 1
puts title("a")
$ ruby w.rb
a
$ ruby -w w.rb
w.rb:4: warning: assigned but unused variable - x
a
$ ruby -wc w.rb
w.rb:4: warning: assigned but unused variable - x
Syntax OK
$ ruby -W0 w.rb
a

The warning appears at parse time, so -wc shows it without executing anything. ruby -wc is a cheap CI step and an editor on-save check; static analysers that go much further are in Style, Linting and Static Analysis.

Another directory: -C

-C changes directory before the program (and relative -I paths) is resolved, without changing the caller’s shell:

$ ruby -C cdir where.rb
cdir
$ ruby cdir/where.rb
w
$ ruby -C nosuchdir -e 1
ruby: Can't chdir to nosuchdir (fatal)

where where.rb prints Dir.pwd.split("/").last. In the first run the program’s working directory is cdir; in the second it is the directory the shell was in (called w here).

Programs embedded in text: -x

-x skips leading text up to a #! line that mentions ruby. It is for programs shipped inside a mail, a shell script or a document:

$ cat emb.txt
Leading text.
#!ruby
puts "from embedded: #{ARGV.inspect}"
__END__
trailing
$ ruby -x emb.txt a b
from embedded: ["a", "b"]

Scripts on the PATH: shebang, -S and env

A script is made executable with a shebang line and the executable bit. The portable form uses env so that the first ruby on the PATH is chosen, which is what version managers (mise, rbenv, asdf, chruby) rely on:

#!/usr/bin/env ruby
# frozen_string_literal: true

# A script found through PATH when started as: ruby -S shelf-tool
puts "#{File.basename($PROGRAM_NAME)} started with #{ARGV.inspect}"
$ chmod +x shelf-tool
$ ./shelf-tool x
shelf-tool started with ["x"]
$ ruby shelf-tool y
shelf-tool started with ["y"]
$ PATH="$PWD/bin:$PATH" ruby -S shelf-tool a b
shelf-tool started with ["a", "b"]
$ RUBYPATH="$PWD/bin" ruby -S shelf-tool
shelf-tool started with []

-S with no match fails with No such file or directory — shelf-tool (LoadError). Directories in RUBYPATH are searched before those in PATH.

Options can also be written on the shebang line itself: ruby scans the #! line of a script for options when the line contains the word ruby, so a file that starts with #!/usr/bin/env ruby -w run through ruby file.rb or ./file.rb has warnings on (verified on macOS). Operating systems differ in how a shebang with several words is split before it reaches env; the portable spelling for passing options is #!/usr/bin/env -S ruby -w with GNU and BSD env -S. Prefer putting options a program needs into the program (Warning[:deprecated] = true) or into RUBYOPT, rather than into the shebang.

Switches for the program: -s

With -s, switches written after the program name and before any argument are removed from ARGV and turned into global variables: -term=Picnic sets $term to "Picnic", a bare -all sets $all to true, and dashes inside the name become underscores (-foo-bar=1 sets $foo_bar). This is a feature for tiny scripts; anything larger should use OptionParser (see Command-Line Programs) because -s has no validation, no help and no --long options.

# frozen_string_literal: true

puts "term=#{$term.inspect} all=#{$all.inspect}" # rubocop:disable Style/GlobalVars

The rubocop:disable comment is deliberate: reading a user-defined global variable is exactly what the example is about, and the default Style/GlobalVars cop forbids it.

$ ruby -s switches.rb -term=Picnic -all
term="Picnic" all=true
$ ruby -s switches.rb
term=nil all=nil
$ ruby -s switches.rb -- -term=x
term=nil all=nil
$ ruby switches.rb -term=x
term=nil all=nil

Three of these four runs leave the variables unset: -- ends switch parsing, and without -s the word is just an argument. A switch placed after the first ordinary argument is not parsed either (ruby -s s2.rb file -a leaves $a unset).

Standard-library tools: -run

ruby -run -e name loads the standard un library and runs one of its small file commands, a cross-platform replacement for cp, mv, rm, mkdir, touch, chmod, install, ln, rmdir, wait_writable, mkmf, colorize, httpd and help. It is -r plus un, which is why the odd spelling works: -r followed by un.

$ ruby -run -e mkdir -- -p a/b/c
$ ruby -run -e touch -- a/b/c/x
$ ruby -run -e rm -- -r a
$ ruby -run -e help touch
Update the access and modification times of each FILE to the current time.

  ruby -run -e touch -- [OPTION] FILE

  -v          verbose

The -- separates the ruby options from those of the un command. httpd needs the webrick gem, which has not been part of Ruby since 3.0, so without that gem the command stops with a message that webrick is not found.

One-liners for text processing

The loop options come from Perl and sed. They are worth knowing because a one-liner is often the quickest report on a log or a CSV file, and because the same ideas ($_, $., $F, ARGF) show up in scripts. The options expand like this:

  • -n is while gets; ...; end, reading from ARGF (the files named after the program, or standard input). The current line is in $_, its number in $., and a bare regular expression or print with no argument uses $_.

  • -p is -n that prints $_ at the end of every pass.

  • -a adds $F = $_.split, using $; as the separator; -F sets it.

  • -l chops the newline off $_ and makes print and puts end lines with $\, so the output keeps its line structure.

  • BEGIN { } and END { } blocks run once, before the first and after the last line.

Filtering with -n

$ ruby -ne 'print if /Writer/' books.csv
Picnic,A. Writer,2019
Plain Text,A. Writer,2020
$ ruby -ne 'puts "#{$.}: #{$_}" if $. > 1' books.csv
2: Picnic,A. Writer,2019
3: Ruby Basics,B. Coder,2021
4: Plain Text,A. Writer,2020
$ ruby -ne 'BEGIN { n = 0 }; n += 1; END { puts n }' books.csv
4
$ ruby -ne 'print if 2..3' books.csv
Picnic,A. Writer,2019
Ruby Basics,B. Coder,2021

The last command uses a flip-flop: a range literal used as a condition is compared with the line number $., so 2..3 is true for lines 2 and 3. The flip-flop is explained with the conditional forms in Control Expressions. $_ keeps the newline unless -l is given, which is why the first command prints no blank lines but p $_ shows "...\n".

Fields with -a, -F and -l

$ ruby -F, -lane 'puts $F[0] unless $. == 1' books.csv
Picnic
Ruby Basics
Plain Text
$ ruby -F, -lane 'BEGIN { total = 0 }; total += $F[2].to_i; END { puts total }' books.csv
6060
$ ruby -F, -ane 'next if $. == 1; puts $F.values_at(0, 2).join(" - ")' books.csv
Picnic - 2019
Ruby Basics - 2021
Plain Text - 2020
$ ruby -F'\t' -ane 'p $F' <<< $'a\tb c'
["a", "b c\n"]

The cluster -lane is four options in one word (-l -a -n -e), with -e last because it takes the program as its argument. Note the total: the header’s "year".to_i is 0, so it does not disturb the sum, which is also why -F, is only a sketch of CSV reading. Fields with quoted commas need the csv library (see Serialization: JSON, YAML, CSV and Marshal). In the last command -l was not given, so the final field still carries its newline.

Editing with -p and -i

-p is for filters that change each line. A next inside a -p program skips the print for that line, which makes next a delete:

$ ruby -lpe '$_ = $_.upcase if $. == 1' books.csv
TITLE,AUTHOR,YEAR
Picnic,A. Writer,2019
Ruby Basics,B. Coder,2021
Plain Text,A. Writer,2020
$ ruby -pe 'next if $. == 1; $_.tr!(",", "\t")' books.csv
Picnic	A. Writer	2019
Ruby Basics	B. Coder	2021
Plain Text	A. Writer	2020

The second command drops the header line (no print) and converts the commas of the others to tabs.

-i makes the output replace the input file. With an extension the original is kept; without, it is gone:

$ cp books.csv inplace.csv
$ ruby -i.bak -pe '$_.gsub!("A. Writer", "Ann Writer")' inplace.csv
$ cat inplace.csv
title,author,year
Picnic,Ann Writer,2019
Ruby Basics,B. Coder,2021
Plain Text,Ann Writer,2020
$ head -2 inplace.csv.bak
title,author,year
Picnic,A. Writer,2019

Always give -i an extension while experimenting (ruby -i -pe ... has no way back), and try the command without -i first. -i sets ARGF.inplace_mode, which is nil without the option, "" for a bare -i and the extension otherwise.

Record separators: -0

The input record separator $/ decides what gets (and therefore -n) calls a line:

$ ruby -0777 -ne 'p $_.bytesize, $/' books.csv
92
nil
$ ruby -00 -ne 'p $_' <<'EOF'
one
two

three
EOF
"one\ntwo\n\n"
"three\n"
$ printf 'a;b;' | ruby -0073 -ne 'p $_'
"a;"
"b;"

-0777 reads each file whole (slurp mode); -00 splits on blank lines (paragraph mode); -0073 uses the semicolon, character code 073 in octal. A trap for the careful: String#lines and String#each_line also default to $/. After -0777, $_.lines is a one-element array and $_.lines("\n") is the way to get the four lines; in paragraph mode the same happens with $/ set to the empty string.

Other loop patterns

ARGF is the object behind gets and $<; a one-liner without -n can use it directly, which also gives access to several files at once:

$ ruby -e 'puts ARGF.read.lines.size' books.csv books.csv
8
$ ruby -rjson -e 'puts JSON.generate(ARGF.each_line.drop(1).map { |l| t, _, y = l.chomp.split(","); { title: t, year: y.to_i } })' books.csv
[{"title":"Picnic","year":2019},{"title":"Ruby Basics","year":2021},{"title":"Plain Text","year":2020}]

Both lines are short enough to be useful in a shell history and long enough that a script file is the better home once it grows; ARGF, $stdin and the other process-level streams are developed in Command-Line Programs and IO, Files and Directories.

Loading code: -I, -r and RUBYLIB

-I dir adds a directory to the front of $LOAD_PATH, so that require "name" finds dir/name.rb; -r lib requires a library before the program starts. Together they are the quickest way to try the shelf gem without installing it:

$ cd shelf
$ ruby -Ilib -rshelf -e 'puts Shelf::VERSION'
0.1.0
$ ruby -Ilib exe/shelf --search ruby
ruby basics by A. Writer
$ ruby -Ilib exe/shelf -v
0.1.0
$ ruby -Ilib -rshelf -e 'p Shelf::Isbn.new("978-0-306-40615-7")'
#<data Shelf::Isbn digits="9780306406157">
$ ruby -rshelf -e 1
<internal:.../rubygems/core_ext/kernel_require.rb>:139:in 'Kernel#require': cannot load such file -- shelf (LoadError)

The last command fails because the library directory is not on the load path: a gem works with require only once it is installed (or loaded by Bundler, see below), and an uninstalled working copy needs -I. Relative -I directories are expanded to absolute paths when the interpreter starts, so a later Dir.chdir does not break them, and that expansion happens relative to the directory selected with -C.

A script can show where it got its load path:

# frozen_string_literal: true

puts $LOAD_PATH.first(3).map { |dir| File.basename(dir) }.inspect
require "shelf"
puts Shelf::VERSION
puts $LOADED_FEATURES.grep(/shelf/).size
$ ruby -Ilib load_path.rb
["lib", "4.0.0", "arm64-darwin23"]
0.1.0
9
$ ruby load_path.rb
load_path.rb:4:in 'Kernel#require': cannot load such file -- shelf (LoadError)
	from load_path.rb:4:in '<main>'
["4.0.0", "arm64-darwin23", "site_ruby"]
$ RUBYLIB=lib ruby load_path.rb
["lib", "4.0.0", "arm64-darwin23"]
0.1.0
9
$ RUBYLIB=/x ruby -Ilib load_path.rb
["lib", "x", "4.0.0"]
0.1.0
9

(In the second run the error is printed to standard error, which is why the two lines appear in that order.) RUBYLIB is the environment counterpart of -I: its directories are separated by : (; on Windows) and come after -I directories but before the standard library. The $LOADED_FEATURES count is 9 because requiring shelf loads its eight files plus shelf.rb itself.

The search order, then, is: -I directories (in the order given), RUBYLIB, gem paths added by RubyGems at require time, and the Ruby installation’s own directories (site_ruby, vendor_ruby, the standard library). require itself, require_relative, load and autoload are in Code Loading: require and autoload.

Warnings and debugging

Levels: -w, -W and $VERBOSE

Ruby has three warning levels, stored in $VERBOSE (and as a number in $-W). The options set them at startup:

Option $VERBOSE $-W Effect

-W0

nil

0

Silent: even warn prints nothing, and every warning category is off.

none, or -W1

false

1

Normal: the interpreter’s default warnings.

-w, -W2, -W, -v, --verbose

true

2

Verbose: extra warnings (unused variables, ambiguous syntax, redefined methods) and the deprecated category.

The table is what Ruby 4.0.7 does when asked, using ruby -W1 -e 'p [$VERBOSE, $-W]' and so on (for -w it prints [true, 2], for -W1 [false, 1]); the same results come out of 3.2, 3.3 and 3.4. The options guide and ruby --help call -w a "synonym for -W1`", which does not match: `-w is the verbose level, and -W1 is the default. -d additionally sets $DEBUG.

$ for o in "" -w -W0 -W1 -W2 -W -v -d; do echo "[$o]"; ruby $o -e 'p [$VERBOSE, $-W, $DEBUG]' 2>&1 | tail -1; done
[]
[false, 1, false]
[-w]
[true, 2, false]
[-W0]
[nil, 0, false]
[-W1]
[false, 1, false]
[-W2]
[true, 2, false]
[-W]
[true, 2, false]
[-v]
[true, 2, false]
[-d]
[true, 2, true]

(The [true, 2, false] of -v is the last line of its output: -v prints the version line first.) Code can read and change $VERBOSE at runtime ($VERBOSE = nil silences warnings inside a block of code, restored in an ensure), but a library should not change a global that belongs to its caller. $DEBUG makes the interpreter print each exception as it is raised, including the ones raised and rescued inside RubyGems during startup (a line starting Exception 'LoadError' at and ending with cannot load such file -- rubygems/defaults/operating_system), which is noisy but useful when something rescues too much.

Categories: -W:category

Individual categories can be switched with -W:name and -W:no-name, and from code with Warning[:name] = true. Warning.categories lists them (Ruby 3.4+; on 3.3 the call is a NoMethodError):

Category Covers Default

deprecated

Features slated for removal (for example, in 3.4, modifying a chilled string literal).

off (on with -w)

experimental

Features that may change (Ruby::Box, for example).

on

performance

Code that is slow for an avoidable reason.

off

strict_unused_block

A block passed to a method that never uses it (Ruby 3.4+; Ruby 3.2 and 3.3 report an unknown category).

off

An unknown category is itself a warning (ruby: warning: unknown warning category: 'foo'). -W0 turns every category off, but a later -W:performance turns one back on; the order of options matters.

Kernel#warn takes a category: keyword, so a library can emit a message that follows the user’s choice, and an uplevel: keyword to point at the caller’s line:

# frozen_string_literal: true

$stdout.sync = true # interleave stdout and stderr when both go to a pipe
warn "plain warnings always print (unless -W0)"
warn "shown only when :deprecated is on", category: :deprecated
warn "shown only when :experimental is on", category: :experimental
puts "deprecated=#{Warning[:deprecated]} experimental=#{Warning[:experimental]}"
Warning[:deprecated] = true
warn "now :deprecated is on", category: :deprecated
$ ruby warn_categories.rb 2>&1
plain warnings always print (unless -W0)
shown only when :experimental is on
deprecated=false experimental=true
now :deprecated is on
$ ruby -W:deprecated warn_categories.rb 2>&1
plain warnings always print (unless -W0)
shown only when :deprecated is on
shown only when :experimental is on
deprecated=true experimental=true
now :deprecated is on
$ ruby -W0 warn_categories.rb 2>&1
deprecated=false experimental=false

In the third run -W0 silenced both the plain warn and the categories, but the program’s own puts is, of course, unaffected. Libraries that deprecate an API should call warn ..., category: :deprecated so that users see the notice when they run with -w or -W:deprecated, and do not see it otherwise.

Two categories matter for the migration advice in this section:

  • Chilled string literals (Ruby 3.4): without a frozen_string_literal magic comment, a string literal is "chilled": it can still be modified, but doing so warns when the deprecated category is on. This is the step towards frozen literals that the Ruby team has taken instead of flipping the default (the default is still not frozen in 4.0):

    $ ruby -e 'a = "x"; a << "y"; p a'
    "xy"
    $ ruby -W:deprecated -e 'a = "x"; a << "y"'
    -e:1: warning: literal string will be frozen in the future (run with --debug-frozen-string-literal for more information)

    Ruby 3.3 has no warning here, and ruby --dump=insns on 3.3 shows putstring where 3.4 and 4.0 show putchilledstring.

  • Unused blocks (strict_unused_block, 3.4): ruby -W:strict_unused_block -e 'def m = 1; m { }' prints warning: the block passed to 'Object#m' defined at -e:1 may be ignored.

Debugging aids

--backtrace-limit=n trims long backtraces, which is the setting for noisy frameworks; the omitted part is summarised:

$ cat bt.rb
def a = b
def b = c
def c = raise("boom")
a
$ ruby bt.rb
bt.rb:3:in 'Object#c': boom (RuntimeError)
	from bt.rb:2:in 'Object#b'
	from bt.rb:1:in 'Object#a'
	from bt.rb:4:in '<main>'
$ ruby --backtrace-limit=1 bt.rb
bt.rb:3:in 'Object#c': boom (RuntimeError)
	from bt.rb:2:in 'Object#b'
	 ... 2 levels...
$ ruby --backtrace-limit=0 bt.rb
bt.rb:3:in 'Object#c': boom (RuntimeError)
	 ... 3 levels...

The format of the lines ('Object#c' with a single quote and the class name) is the 3.4 style; older books show a backtick before the method name and no class. The error message formats are in Exceptions and Error Handling.

Three helper gems that ship with Ruby print better diagnostics, and each can be disabled with --disable:

$ cat eh.rb
book = nil
puts book.title
$ ruby eh.rb
eh.rb:2:in '<main>': undefined method 'title' for nil (NoMethodError)

puts book.title
         ^^^^^^
$ ruby --disable=error_highlight eh.rb
eh.rb:2:in '<main>': undefined method 'title' for nil (NoMethodError)
$ ruby -e 'prnt 1'
-e:1:in '<main>': undefined method 'prnt' for main (NoMethodError)
Did you mean?  print
$ ruby --disable=did_you_mean -e 'prnt 1'
-e:1:in '<main>': undefined method 'prnt' for main (NoMethodError)

error_highlight underlines the call that failed (it needs a source file, so -e code gets no marker), did_you_mean suggests names, and syntax_suggest points at the unbalanced end when a file does not parse:

$ cat sx.rb
class Loan
  def due
    [1, 2
  end
end
$ ruby sx.rb
sx.rb: --> sx.rb

Unmatched `[', missing `]' ?

  1  class Loan
  2    def due
> 3      [1, 2
  4    end
  5  end

sx.rb:4: syntax errors found (SyntaxError)
  2 |   def due
  3 |     [1, 2
> 4 |   end
    | ^ expected an expression for the array element
    | ^ unexpected 'end'; expected a `,` separator for the array elements
    |   ^~~ unexpected 'end'; expected a `]` to close the array
    |   ^~~ unexpected 'end', assuming it is closing the parent method definition
  5 | end

$ ruby --disable=syntax_suggest sx.rb
sx.rb: sx.rb:4: syntax errors found (SyntaxError)
  2 |   def due
  3 |     [1, 2
> 4 |   end
    | ^ expected an expression for the array element
    | ^ unexpected 'end'; expected a `,` separator for the array elements
    |   ^~~ unexpected 'end'; expected a `]` to close the array
    |   ^~~ unexpected 'end', assuming it is closing the parent method definition
  5 | end

The interactive debugger and the debug gem are in IRB and Debugging, and TracePoint and tracing in Reflection, Introspection and Tracing.

Interpreter features: --enable and --disable

--enable=a,b and --disable=a,b switch named features of the interpreter; the hyphenated forms --enable-a and --disable-a mean the same. The 4.0.7 ruby --help lists:

Feature What it controls Default

gems

RubyGems. Disabling it removes Gem, Kernel#gem and gem-aware require; the manual marks it "only for debugging".

enabled

error_highlight

The marker under the failing expression in NameError/NoMethodError/TypeError/ArgumentError messages.

enabled

did_you_mean

"Did you mean?" suggestions.

enabled

syntax_suggest

The explanation for unbalanced end and brackets.

enabled

rubyopt

Reading the RUBYOPT environment variable. --disable=rubyopt is a way to run with a clean interpreter.

enabled

frozen-string-literal

Freeze all string literals of files that have no frozen_string_literal magic comment.

disabled

yjit

The in-process JIT compiler (same as --yjit).

disabled

--disable-all and --enable-all apply to every feature at once (with --disable-all, defined?(Gem) and defined?(DidYouMean) are both nil). The 4.0 options guide lists a shorter set and names jit where --help names yjit (--enable=jit also turns YJIT on); the table above is what the --help of the 4.0.7 build prints. Feature names keep their own spelling: frozen-string-literal has hyphens and did_you_mean has underscores.

Frozen string literals

--enable=frozen-string-literal (or --enable-frozen-string-literal) freezes every string literal of the program, the switch that lets a team try the future default:

$ cat f.rb
s = "shelf"
puts s.frozen?
begin
  s << "!"
rescue FrozenError => e
  puts e.message
end
$ ruby f.rb
false
$ ruby --enable-frozen-string-literal f.rb
true
can't modify frozen String: "shelf"
$ ruby --debug-frozen-string-literal -W:deprecated f.rb
f.rb:4: warning: literal string will be frozen in the future
f.rb:1: info: the string was created here
false

--debug-frozen-string-literal records where each literal was created, so that the error message of a FrozenError (or the chilled-string warning above) names the creation site. The magic comment of a file wins over the switch, in both directions: a file with # frozen_string_literal: false stays modifiable under --enable-frozen-string-literal, and one with # frozen_string_literal: true stays frozen under --disable-frozen-string-literal.

# frozen_string_literal: false

title = "Picnic"
puts title.frozen?
$ ruby --enable=frozen-string-literal frozen_probe.rb
false
$ ruby --disable=frozen-string-literal frozen_probe2.rb
true

where frozen_probe2.rb is the same file with true. The strings page (Strings) shows the idioms for building strings without relying on mutating a literal. Frozen literals are still not the default in 4.0, even though some books say that 3.4 made them the default: 3.4 added the chilled-string warning, not the freeze.

Encodings: -E, -U and the locale

The default external encoding comes from the locale (LANG, LC_ALL, LC_CTYPE) and the default internal encoding is nil. -E external:internal overrides them, -U sets the internal one to UTF-8:

$ ruby -e 'p [Encoding.default_external, Encoding.default_internal, __ENCODING__]'
[#<Encoding:UTF-8>, nil, #<Encoding:UTF-8>]
$ ruby -E ISO-8859-1:UTF-8 -e 'p [Encoding.default_external, Encoding.default_internal]'
[#<Encoding:ISO-8859-1>, #<Encoding:UTF-8>]
$ ruby -U -e 'p Encoding.default_internal'
#<Encoding:UTF-8>
$ LC_ALL=C ruby -e 'p Encoding.default_external'
#<Encoding:US-ASCII>
$ LC_ALL=C ruby -E UTF-8 -e 'p Encoding.default_external'
#<Encoding:UTF-8>

__ENCODING__ is the source encoding, which is UTF-8 unless a # encoding: magic comment (or the legacy -K) says otherwise; it does not follow the locale. A container or cron job with LANG unset starts with US-ASCII as the default external encoding, which is a classic source of Encoding::UndefinedConversionError when reading UTF-8 files; setting LANG=C.UTF-8 or passing -E UTF-8 fixes it. More in Encodings and Unicode.

The parser and the VM: --parser and --dump

Since Ruby 3.4 the default parser is Prism, a hand-written recursive-descent parser that produces an abstract syntax tree and helpful syntax-error messages; the older parse.y (a Bison grammar) is still available. The version line shows which is active (+PRISM), and --parser chooses (the option was added in 3.3, where Prism was experimental and printed a warning):

$ ruby --parser=prism -v
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin23]
$ ruby --parser=parse.y -v
ruby 4.0.7 (2026-09-15 revision 229531a6cf) [arm64-darwin23]
$ ruby --parser=foo -e 1
ruby: unknown parser foo (RuntimeError)

For ordinary programs the two parsers behave the same; the choice matters for tool authors and for diagnosing a suspected parser bug. RUBYOPT=--parser=parse.y also works. The Prism module (require "prism") exposes the same parser to Ruby code, which is how RuboCop, Ruby LSP and other tools read source (see Style, Linting and Static Analysis).

--dump

--dump=item prints an internal form and, for the dump items, does not run the program. The items are insns (YARV bytecode), parsetree (the AST), yydebug (the parse.y parse trace); version, copyright, usage, help and syntax behave like --version, --copyright, -h, --help and -c. Modifiers: -optimize (compile without optimisation), and +error-tolerant (affects the yydebug and parsetree items) and +comment (adds comments to the AST of parse.y). Several items are separated by commas.

$ cat sum.rb
total = 1 + 2
$ ruby --dump=insns sum.rb
== disasm: #<ISeq:<main>@sum.rb:1 (1,0)-(1,13)>
local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] total@0
0000 putobject_INT2FIX_1_                                             (   1)[Li]
0001 putobject                              2
0003 opt_plus                               <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
0005 dup
0006 setlocal_WC_0                          total@0
0008 leave
$ ruby --dump=insns,-optimize sum.rb | tail -4
0004 send                                   <calldata!mid:+, argc:1, ARGS_SIMPLE>, nil
0007 dup
0008 setlocal                               total@0, 0
0011 leave
$ ruby --dump=parsetree sum.rb | head -8
@ ProgramNode (location: (1,0)-(1,13))
+-- locals: [:total]
+-- statements:
    @ StatementsNode (location: (1,0)-(1,13))
    +-- body: (length: 1)
        +-- @ LocalVariableWriteNode (location: (1,0)-(1,13))
            +-- name: :total
            +-- depth: 0
$ ruby --dump=insns -e 'puts "Picnic"'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,13)>
0000 putself                                                          (   1)[Li]
0001 putchilledstring                       "Picnic"
0003 opt_send_without_block                 <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
0005 leave

Reading the first listing: 1 + 2 was compiled to the specialised instruction opt_plus, and with -optimize it becomes a generic send of +. The putchilledstring in the last listing is the chilled literal from the previous section. The same bytecode is available to a program as RubyVM::InstructionSequence:

# frozen_string_literal: true

iseq = RubyVM::InstructionSequence.compile("total = 1 + 2")
puts iseq.disasm

It prints the same disassembly as --dump=insns (with <compiled> in place of the file name). The AST dump under Prism is Prism’s own text form; under --parser=parse.y it is the older NODE_* format, which begins with a banner saying it must "not be used for any purpose other than debug and research": none of these formats is a stable interface. The VM, the instruction set and the JIT that consumes the bytecode are in YJIT, ZJIT and the VM.

ruby -y and --yydebug ask the parse.y parser for its trace; under the default Prism parser they print nothing, so add --parser=parse.y (or use --dump=yydebug). Unknown dump items are reported and ignored: ruby: warning: don't know how to dump 'foo', but only [version, copyright, usage, help, yydebug, syntax, parsetree, insns]. (note that prism_parsetree is not an item; parsetree prints the Prism tree on a Prism build).

JIT switches

Ruby 4.0 has two just-in-time compilers; neither is on by default.

Switch Meaning

--yjit, --jit, --enable=yjit, RUBY_YJIT_ENABLE=1

Compile hot Ruby methods to machine code with YJIT. --jit is the same as --yjit on this build.

--yjit-mem-size=MiB

Soft limit of YJIT memory (default 128).

--yjit-exec-mem-size=MiB

Hard limit of the executable memory block.

--yjit-call-threshold=n

Calls before a method is compiled (the YJIT guide says the default is 30, raised to 120 for very large programs).

--yjit-cold-threshold=n

Calls after which a never-compiled method is considered cold (default 200000).

--yjit-stats[=quiet], --yjit-log[=file]

Collect and print statistics; log compilation events. Both cost run time.

--yjit-disable

Start with YJIT disabled but allow RubyVM::YJIT.enable later; the way to pass tuning options and enable lazily.

--yjit-code-gc, --yjit-perf, --yjit-trace-exits[=counter], --yjit-trace-exits-sample-rate=n

Discard code at the memory limit; frame pointers for Linux perf; record where compiled code exits.

--zjit, --zjit-*

The experimental method-based compiler (Ruby 4.0). Available only in a build that includes ZJIT (needs Rust 1.85+ at build time).

YJIT is production-ready (since Ruby 3.2) and on a supported platform is the first thing to try for a CPU-bound web application. Whether it is active is visible in three places, and ruby --yjit -v and ruby -v differ by +YJIT:

$ ruby -e 'p RubyVM::YJIT.enabled?'
false
$ ruby --yjit -e 'p RubyVM::YJIT.enabled?'
true
$ RUBY_YJIT_ENABLE=1 ruby -e 'p RubyVM::YJIT.enabled?'
true
$ ruby --yjit -v
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +YJIT +PRISM [arm64-darwin23]

Enabling YJIT after boot

Compiling during start-up wastes memory on code that runs once. RubyVM::YJIT.enable (Ruby 3.3) turns the compiler on from Ruby code, typically after the application has loaded (or in a forked worker, after fork); the optional mem_size: and call_threshold: keywords were added in Ruby 4.0 (on 3.3 and 3.4 passing them is an ArgumentError):

# frozen_string_literal: true

puts "before: #{RubyVM::YJIT.enabled?}"
RubyVM::YJIT.enable(mem_size: 64, call_threshold: 10)
puts "after:  #{RubyVM::YJIT.enabled?}"
$ ruby yjit_enable.rb
before: false
after:  true
$ ruby --yjit-disable yjit_enable.rb
before: false
after:  true
$ ruby --yjit yjit_enable.rb
before: true
after:  true

The second run is the useful one in practice: any --yjit-* tuning option otherwise starts YJIT at boot, and --yjit-disable says "keep the tuning, delay the start".

ZJIT (experimental)

ZJIT was introduced in Ruby 4.0 as an experimental method-based compiler. The NEWS file for 4.0 says it is faster than the interpreter but not yet as fast as YJIT, and advises against production use; the stated goal is to make it faster than YJIT and production-ready in 4.1. ZJIT needs a Rust toolchain when Ruby is built; in a build without it (like the one used for this page) the option only warns:

$ ruby --zjit -e 'p 1'
ruby: warning: Ruby was built without ZJIT support. You may need to install rustc to build Ruby with ZJIT.
1

When it is available it has options of its own (--zjit-mem-size, --zjit-call-threshold, --zjit-num-profiles, --zjit-stats, --zjit-disable, --zjit-perf, --zjit-log-compiled-iseqs, --zjit-trace-exits) and RubyVM::ZJIT.enable. These are described in the ZJIT guide and were not run here. Treat ZJIT as something to experiment with, not to deploy.

Environment variables

An environment variable configures a process without touching its command line, which is what makes it the right tool for deployment (container images, systemd units, CI) and for wrappers like Bundler. The set below is the one described by the ruby(1) manual page and the options guides of 4.0; a variable that depends on a Ruby build or platform is marked.

RUBYOPT

RUBYOPT holds options that are added to the command line of every Ruby process that sees the variable, including the ones started by a Ruby program, bundle exec, gem, rake and tests. That reach is both its use and its danger: a stray RUBYOPT=-w in a shell profile floods the output of every tool with warnings from gems. Only options that do not choose the program or change how it is read are accepted. Observed on 4.0.7:

$ RUBYOPT="-w" ruby w.rb
w.rb:4: warning: assigned but unused variable - x
a
$ RUBYOPT="-W:deprecated -rset" ruby -e 'p Warning[:deprecated]'
true
$ RUBYOPT="--enable-frozen-string-literal" ruby -e 'p "a".frozen?'
true
$ RUBYOPT="--yjit" ruby -e 'p RubyVM::YJIT.enabled?'
true
$ RUBYOPT="-Ishelf/lib -rshelf" ruby -e 'p Shelf::VERSION'
"0.1.0"
$ RUBYOPT="-e 'p 1'" ruby -e 1
ruby: invalid switch in RUBYOPT: -e (RuntimeError)
$ RUBYOPT="-n" ruby -e 1
ruby: invalid switch in RUBYOPT: -n (RuntimeError)

The rules, as observed:

  • Accepted: -d, -E, -I, -r, -w, -W (with levels and :category), --backtrace-limit, --enable/--disable and their hyphen forms, --parser and --jit/--yjit. The manual page also lists -K, -U, -v and --debug, which were not run here, and does not list --backtrace-limit, --parser or the JIT switches, which work.

  • Rejected with invalid switch in RUBYOPT: the ones that select or reshape the program: -e, -c, -C, -x, -S, -s, -a, -l, -n, -p, -i, -0, -F, -h, --version and --dump.

  • The dash of the first option may be omitted (RUBYOPT=w works), and several options are separated by spaces.

  • Command-line options are processed after RUBYOPT, so the command line wins: RUBYOPT=-w ruby -W0 w.rb prints no warning while RUBYOPT=-W0 ruby -w w.rb does.

  • --disable=rubyopt on the command line ignores the variable altogether.

bundle exec is the best-known user of the variable: it sets RUBYOPT to -r/path/to/bundler/setup (plus BUNDLE_GEMFILE) in the child process so that the Gemfile’s gem set is the only one that require sees:

$ bundle exec ruby -e 'puts ENV["RUBYOPT"], ENV["BUNDLE_GEMFILE"]'
-r/.../lib/ruby/4.0.0/bundler/setup
/.../shelf/Gemfile

(The paths are abbreviated.) That is also the explanation for a common surprise: a Ruby program started from inside a bundle exec shell (a subprocess, a test runner, system("ruby ...")) inherits Bundler’s environment and cannot load gems that are not in the bundle. Bundler.with_unbundled_env removes it. RubyGems and Bundler are in RubyGems and Bundler.

RUBYLIB, RUBYPATH and friends

Variable Meaning

RUBYLIB

Directories added to $LOAD_PATH, separated by : (; on Windows). After -I directories, before the standard library.

RUBYPATH

Directories searched for the program when -S is used, before PATH.

RUBYSHELL

The shell command used by system and backticks on Windows (mswin, mingw and OS/2 builds); defaults to COMSPEC.

PATH

Used by Kernel#system and friends to find commands, and by -S.

RUBY_PAGER

The pager used for ruby --help (default PAGER). Ruby 3.0, per the manual page.

RUBY_FREE_AT_EXIT

If set, free all allocated memory at exit (useful with leak checkers such as Valgrind). Ruby 3.3, per the manual page.

RUBY_CRASH_REPORT

Path template of the crash report file written if the interpreter itself crashes; the same as --crash-report. The template can contain %e (executable basename), %E, %f (program basename), %F, %p (process id), %t (time) and %%; if it starts with a pipe character the rest is a command that receives the report. Not run here (it needs a crash).

Runtime variables

Variable Meaning

RUBY_YJIT_ENABLE

1 enables YJIT, like --yjit. For deployment scripts where adding a command-line option is awkward (it also works on 3.2).

RUBY_BOX

1 enables Ruby::Box, the experimental isolation feature of Ruby 4.0. Without it Ruby::Box.enabled? is false; with it the first use prints an "experimental" warning that -W:no-experimental silences.

RUBY_MN_THREADS

1 enables the M:N thread scheduler on the main Ractor (many Ruby threads on a pool of native threads). Ruby 3.3, per the manual page.

RUBY_MAX_CPU

Maximum native threads of the M:N scheduler (default 8). Ruby 3.3, per the manual page.

RUBY_THREAD_TIMESLICE

Thread time slice in milliseconds (default 100). Ruby 3.4, per the manual page.

RUBY_THREAD_VM_STACK_SIZE, RUBY_THREAD_MACHINE_STACK_SIZE

VM and machine stack size, in bytes, of new threads (default 1048576 each on 64-bit).

RUBY_FIBER_VM_STACK_SIZE, RUBY_FIBER_MACHINE_STACK_SIZE

The same for fibers (defaults 131072 and 524288 on 64-bit).

RUBY_SHARED_FIBER_POOL_FREE_STACKS

1 (default) frees the stacks of pooled fibers; 0 keeps them.

RUBY_TCP_NO_FAST_FALLBACK

1 disables the Happy Eyeballs v2 fast fallback in TCPSocket.new and Socket.tcp. Ruby 3.4.

RUBY_IO_BUFFER_DEFAULT_SIZE

The default size of an IO::Buffer in bytes.

Three of them can be observed directly:

$ ruby -rsocket -e 'p Socket.tcp_fast_fallback'
true
$ RUBY_TCP_NO_FAST_FALLBACK=1 ruby -rsocket -e 'p Socket.tcp_fast_fallback'
false
$ ruby -e 'p IO::Buffer::DEFAULT_SIZE'
65536
$ RUBY_IO_BUFFER_DEFAULT_SIZE=8192 ruby -e 'p IO::Buffer::DEFAULT_SIZE'
8192
$ ruby -e 'p Ruby::Box.enabled?'
false
$ RUBY_BOX=1 ruby -W:no-experimental -e 'p Ruby::Box.enabled?'
true

Ruby::Box is experimental in 4.0 and may change; it is mentioned here only as a switch and described in Code Loading: require and autoload. RUBY_MN_THREADS is the entry point to the scheduler of Threads and the GVL; Ractors and fibers are in Ractors and Fibers and the Fiber Scheduler.

Stack sizes

The stack-size variables decide how deep a thread’s recursion can go before SystemStackError. A smaller stack lets a program keep more threads or fibers alive, a larger stack allows deeper recursion; too small a machine stack combined with C extensions risks a segmentation fault instead of an exception, so change them only with measurements. The numbers below are for the 64-bit macOS build used here and depend on the platform and the size of each frame:

# frozen_string_literal: true

# How deep can a thread recurse before SystemStackError?
depth = 0
recurse = -> { depth += 1 and recurse.call }

Thread.new do
  recurse.call
rescue SystemStackError => e
  puts "#{e.class}: #{e.message} after #{depth} calls"
end.join
$ ruby stack_depth.rb
SystemStackError: stack level too deep after 13104 calls
$ RUBY_THREAD_VM_STACK_SIZE=65536 ruby stack_depth.rb
SystemStackError: stack level too deep after 816 calls
$ RUBY_THREAD_VM_STACK_SIZE=4194304 ruby stack_depth.rb
SystemStackError: stack level too deep after 52426 calls

Garbage-collector variables

The garbage collector reads a family of RUBY_GC_* variables at start-up. The page Garbage Collection and Memory explains the heap, generations and the tools (GC.stat, GC.config); the table is the reference of the variables in the 4.0.7 manual page:

Variable Meaning Default

RUBY_GC_HEAP_n_INIT_SLOTS (n is 0 to 4)

Initial slots of one slot-size heap (see the keys of GC.stat_heap). Ruby 3.3 and later.

none

RUBY_GC_HEAP_FREE_SLOTS

Slots kept free after a collection.

4096

RUBY_GC_HEAP_GROWTH_FACTOR

Multiplier for heap growth (minimum 1.0, no growth).

1.8

RUBY_GC_HEAP_GROWTH_MAX_SLOTS

Upper bound of slots added at once (0 means no limit).

0

RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR

A full collection runs when old objects exceed this factor times the count after the last full collection.

2.0

RUBY_GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO

Ratio of old objects used to bound remembered write-barrier-unprotected objects. Ruby 3.3 and later.

0.1

RUBY_GC_HEAP_FREE_SLOTS_MIN_RATIO, RUBY_GC_HEAP_FREE_SLOTS_MAX_RATIO, RUBY_GC_HEAP_FREE_SLOTS_GOAL_RATIO

Free-slot ratios that trigger adding pages, freeing pages, and the target after growth.

0.2, 0.4, 0.65

RUBY_GC_MALLOC_LIMIT, RUBY_GC_MALLOC_LIMIT_MAX, RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR

Young-generation malloc bytes before a collection starts, its cap, and its growth factor.

16 MB, 32 MB, 1.4

RUBY_GC_OLDMALLOC_LIMIT, RUBY_GC_OLDMALLOC_LIMIT_MAX, RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR

The same for the old generation (a full collection).

16 MB, 128 MB, 1.2

The malloc limits are observable:

# frozen_string_literal: true

stat = GC.stat
puts "malloc limit #{stat[:malloc_increase_bytes_limit]} bytes"
puts "old malloc limit #{stat[:oldmalloc_increase_bytes_limit]} bytes"
$ ruby gc_limit.rb
malloc limit 16777216 bytes
old malloc limit 16777216 bytes
$ RUBY_GC_MALLOC_LIMIT=1000000 RUBY_GC_OLDMALLOC_LIMIT=2000000 ruby gc_limit.rb
malloc limit 2271519 bytes
old malloc limit 2400000 bytes

The reported limits are not exactly the requested values (the first is adjusted at start-up; the second is the requested 2000000 multiplied by its growth factor 1.2), but they move with the variables. A Ruby 3.2 to 3.3 migration note, verified by running both: on 3.2 RUBY_GC_HEAP_INIT_SLOTS enlarges the initial heap, on 3.3 and later it is ignored and the per-heap RUBY_GC_HEAP_0_INIT_SLOTS (to _4_) takes its place. Tuning the collector is a measured activity: change one variable at a time and compare GC.stat and the run time, see Performance, Benchmarking and Profiling.

RubyGems, Bundler and the locale

RubyGems reads its own variables (they are documented by gem help environment and the RubyGems guides); the ones a developer meets constantly:

Variable Meaning

GEM_HOME

The directory where gems are installed (Gem.dir). Version managers and CI caches set it.

GEM_PATH

Directories searched for gems, in addition to GEM_HOME; the user directory (~/.gem/ruby/4.0.0) is included by default.

BUNDLE_GEMFILE

The Gemfile to use instead of searching upwards from the current directory. Also set by bundle exec.

BUNDLE_*

Every Bundler setting has a BUNDLE_ variable form (BUNDLE_PATH, BUNDLE_WITHOUT, BUNDLE_DEPLOYMENT, and so on); see the Bundler settings manual. Not run here.

LANG, LC_ALL, LC_CTYPE

The locale; it determines the default external encoding (see above).

$ ruby -e 'p Gem.dir'
"/.../ruby/4.0.7/lib/ruby/gems/4.0.0"
$ GEM_HOME=$PWD/gh ruby -e 'p Gem.dir'
"/.../w/gh"
$ GEM_PATH=$PWD/gp ruby -e 'p Gem.path.first(2)'
["/.../w/gp", "/.../ruby/4.0.7/lib/ruby/gems/4.0.0"]

(Paths abbreviated.) Setting GEM_HOME to a project directory is the manual way to isolate gems per project; Bundler’s bundle config set path does the same with a setting instead of an environment variable. The detail is in RubyGems and Bundler and in Creating and Publishing Gems, and Rake. A secret (an API key, a token) belongs in an environment variable read with ENV.fetch at run time, never in a file in the repository; see Security.

Where the books are outdated

Older Ruby books and tutorials describe the interpreter in terms that no longer hold in 4.0:

Older statement Ruby 4.0

The interpreter parses and runs a script "line by line".

The whole file is parsed (by Prism since 3.4), compiled to YARV bytecode and then run; ruby -c and --dump=insns show both stages.

ruby -T and $SAFE for taint checking.

Removed. -T is invalid option -T on 3.2 through 4.0; $SAFE is an ordinary, always-nil global variable.

-w is "level 1" and $VERBOSE is nil, false or true with no categories.

-w is the verbose level ($-W is 2); warnings have categories (-W:deprecated, -W:no-experimental); 3.4 adds Warning.categories and the strict_unused_block category.

"Strings are frozen by default since 3.4."

Not true. 3.4 introduced chilled strings, which warn only with -W:deprecated; the magic comment or --enable-frozen-string-literal still freezes literals in 4.0.

ruby -rubygems to load RubyGems; MJIT and RJIT as the JIT options.

RubyGems is always loaded (switch it off with --disable=gems); MJIT was removed in 3.3 and RJIT in 4.0; YJIT is the supported JIT (production-ready since 3.2) and ZJIT is experimental in 4.0.

The default encoding is the locale’s and -K selects the Japanese ones.

UTF-8 source files are the default; the default external encoding is still the locale’s, and -E/-U are the modern switches. -K is a legacy option.

Backtraces quote the method name with a backtick and a straight quote, and messages say NilClass (the message form of Ruby 3.2 and earlier).

3.4 and 4.0 print 'Class#method' and undefined method 'x' for nil.

The standard library is "everything installed with Ruby".

Some libraries are bundled gems and need a Gemfile entry under Bundler (bundle exec limits require to the bundle).

A checklist for the command line

  • Put the program’s own options after its name, and interpreter options before it (-- if the name could be mistaken for an option).

  • Use ruby -wc file.rb as a cheap syntax and warning check; use -W:deprecated before an upgrade to see what the next version will reject.

  • Prefer -Ilib -rname over editing $LOAD_PATH in the program, and bundle exec over a hand-built RUBYLIB.

  • Treat RUBYOPT as global: set it on one command (RUBYOPT=-w rake test), not in a shell profile, and unset it when a tool misbehaves (--disable=rubyopt runs without it).

  • Give -i an extension while you learn it, and remember that String#lines follows $/, so -0777 changes it.

  • Enable YJIT with --yjit, RUBY_YJIT_ENABLE=1 or RubyVM::YJIT.enable after boot; leave ZJIT, Ruby::Box and the RUBY_GC_* tuning to measured experiments.

  • Set LANG=C.UTF-8 (or -E UTF-8) in containers so the default external encoding is not US-ASCII.