diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js
index 3e3eaa5b60e17a..359e2b46bdb054 100644
--- a/lib/internal/bootstrap/node.js
+++ b/lib/internal/bootstrap/node.js
@@ -105,6 +105,11 @@
       NativeModule.require('internal/inspector_async_hook').setup();
     }
 
+    if (internalBinding('options').getOptions('--help')) {
+      NativeModule.require('internal/print_help').print(process.stdout);
+      return;
+    }
+
     if (isMainThread) {
       mainThreadSetup.setupChildProcessIpcChannel();
     }
diff --git a/lib/internal/print_help.js b/lib/internal/print_help.js
new file mode 100644
index 00000000000000..9d14671973402e
--- /dev/null
+++ b/lib/internal/print_help.js
@@ -0,0 +1,151 @@
+'use strict';
+const { internalBinding } = require('internal/bootstrap/loaders');
+const { getOptions, types } = internalBinding('options');
+
+const typeLookup = [];
+for (const key of Object.keys(types))
+  typeLookup[types[key]] = key;
+
+// Environment variables are parsed ad-hoc throughout the code base,
+// so we gather the documentation here.
+const { hasIntl, hasSmallICU, hasNodeOptions } = process.binding('config');
+const envVars = new Map([
+  ['NODE_DEBUG', { helpText: "','-separated list of core modules that " +
+    'should print debug information' }],
+  ['NODE_DEBUG_NATIVE', { helpText: "','-separated list of C++ core debug " +
+    'categories that should print debug output' }],
+  ['NODE_DISABLE_COLORS', { helpText: 'set to 1 to disable colors in ' +
+    'the REPL' }],
+  ['NODE_EXTRA_CA_CERTS', { helpText: 'path to additional CA certificates ' +
+    'file' }],
+  ['NODE_NO_WARNINGS', { helpText: 'set to 1 to silence process warnings' }],
+  ['NODE_PATH', { helpText: `'${require('path').delimiter}'-separated list ` +
+    'of directories prefixed to the module search path' }],
+  ['NODE_PENDING_DEPRECATION', { helpText: 'set to 1 to emit pending ' +
+    'deprecation warnings' }],
+  ['NODE_PRESERVE_SYMLINKS', { helpText: 'set to 1 to preserve symbolic ' +
+    'links when resolving and caching modules' }],
+  ['NODE_REDIRECT_WARNINGS', { helpText: 'write warnings to path instead ' +
+    'of stderr' }],
+  ['NODE_REPL_HISTORY', { helpText: 'path to the persistent REPL ' +
+    'history file' }],
+  ['OPENSSL_CONF', { helpText: 'load OpenSSL configuration from file' }]
+].concat(hasIntl ? [
+  ['NODE_ICU_DATA', { helpText: 'data path for ICU (Intl object) data' +
+    hasSmallICU ? '' : ' (will extend linked-in data)' }]
+] : []).concat(hasNodeOptions ? [
+  ['NODE_OPTIONS', { helpText: 'set CLI options in the environment via a ' +
+    'space-separated list' }]
+] : []));
+
+
+function indent(text, depth) {
+  return text.replace(/^/gm, ' '.repeat(depth));
+}
+
+function fold(text, width) {
+  return text.replace(new RegExp(`([^\n]{0,${width}})( |$)`, 'g'),
+                      (_, newLine, end) => newLine + (end === ' ' ? '\n' : ''));
+}
+
+function getArgDescription(type) {
+  switch (typeLookup[type]) {
+    case 'kNoOp':
+    case 'kV8Option':
+    case 'kBoolean':
+      break;
+    case 'kHostPort':
+      return '[host:]port';
+    case 'kInteger':
+    case 'kString':
+    case 'kStringList':
+      return '...';
+    case undefined:
+      break;
+    default:
+      require('assert').fail(`unknown option type ${type}`);
+  }
+}
+
+function format({ options, aliases = new Map(), firstColumn, secondColumn }) {
+  let text = '';
+
+  for (const [
+    name, { helpText, type, value }
+  ] of [...options.entries()].sort()) {
+    if (!helpText) continue;
+
+    let displayName = name;
+    const argDescription = getArgDescription(type);
+    if (argDescription)
+      displayName += `=${argDescription}`;
+
+    for (const [ from, to ] of aliases) {
+      // For cases like e.g. `-e, --eval`.
+      if (to[0] === name && to.length === 1) {
+        displayName = `${from}, ${displayName}`;
+      }
+
+      // For cases like `--inspect-brk[=[host:]port]`.
+      const targetInfo = options.get(to[0]);
+      const targetArgDescription =
+        targetInfo ? getArgDescription(targetInfo.type) : '...';
+      if (from === `${name}=`) {
+        displayName += `[=${targetArgDescription}]`;
+      } else if (from === `${name} <arg>`) {
+        displayName += ` [${targetArgDescription}]`;
+      }
+    }
+
+    let displayHelpText = helpText;
+    if (value === true) {
+      // Mark boolean options we currently have enabled.
+      // In particular, it indicates whether --use-openssl-ca
+      // or --use-bundled-ca is the (current) default.
+      displayHelpText += ' (currently set)';
+    }
+
+    text += displayName;
+    if (displayName.length >= firstColumn)
+      text += '\n' + ' '.repeat(firstColumn);
+    else
+      text += ' '.repeat(firstColumn - displayName.length);
+
+    text += indent(fold(displayHelpText, secondColumn),
+                   firstColumn).trimLeft() + '\n';
+  }
+
+  return text;
+}
+
+function print(stream) {
+  const { options, aliases } = getOptions();
+
+  // TODO(addaleax): Allow a bit of expansion depending on `stream.columns`
+  // if it is set.
+  const firstColumn = 28;
+  const secondColumn = 40;
+
+  options.set('-', { helpText: 'script read from stdin (default; ' +
+                               'interactive mode if a tty)' });
+  options.set('--', { helpText: 'indicate the end of node options' });
+  stream.write(
+    'Usage: node [options] [ -e script | script.js | - ] [arguments]\n' +
+    '       node inspect script.js [arguments]\n\n' +
+    'Options:\n');
+  stream.write(indent(format({
+    options, aliases, firstColumn, secondColumn
+  }), 2));
+
+  stream.write('\nEnvironment variables:\n');
+
+  stream.write(format({
+    options: envVars, firstColumn, secondColumn
+  }));
+
+  stream.write('\nDocumentation can be found at https://nodejs.org/\n');
+}
+
+module.exports = {
+  print
+};
diff --git a/node.gyp b/node.gyp
index 5b8ca26bb2010d..d94e6ff62aa0db 100644
--- a/node.gyp
+++ b/node.gyp
@@ -132,6 +132,7 @@
       'lib/internal/modules/esm/translators.js',
       'lib/internal/safe_globals.js',
       'lib/internal/net.js',
+      'lib/internal/print_help.js',
       'lib/internal/priority_queue.js',
       'lib/internal/process/esm_loader.js',
       'lib/internal/process/main_thread_only.js',
diff --git a/src/env.h b/src/env.h
index 556a09754fc465..fa802e4016dfdb 100644
--- a/src/env.h
+++ b/src/env.h
@@ -118,6 +118,7 @@ struct PackageConfig {
 // for the sake of convenience.  Strings should be ASCII-only.
 #define PER_ISOLATE_STRING_PROPERTIES(V)                                      \
   V(address_string, "address")                                                \
+  V(aliases_string, "aliases")                                                \
   V(args_string, "args")                                                      \
   V(async, "async")                                                           \
   V(async_ids_stack_string, "async_ids_stack")                                \
@@ -156,6 +157,7 @@ struct PackageConfig {
   V(entries_string, "entries")                                                \
   V(entry_type_string, "entryType")                                           \
   V(env_pairs_string, "envPairs")                                             \
+  V(env_var_settings_string, "envVarSettings")                                \
   V(errno_string, "errno")                                                    \
   V(error_string, "error")                                                    \
   V(exit_code_string, "exitCode")                                             \
@@ -176,6 +178,7 @@ struct PackageConfig {
   V(get_shared_array_buffer_id_string, "_getSharedArrayBufferId")             \
   V(gid_string, "gid")                                                        \
   V(handle_string, "handle")                                                  \
+  V(help_text_string, "helpText")                                             \
   V(homedir_string, "homedir")                                                \
   V(host_string, "host")                                                      \
   V(hostmaster_string, "hostmaster")                                          \
@@ -233,6 +236,7 @@ struct PackageConfig {
   V(onunpipe_string, "onunpipe")                                              \
   V(onwrite_string, "onwrite")                                                \
   V(openssl_error_stack, "opensslErrorStack")                                 \
+  V(options_string, "options")                                                \
   V(output_string, "output")                                                  \
   V(order_string, "order")                                                    \
   V(parse_error_string, "Parse Error")                                        \
diff --git a/src/node.cc b/src/node.cc
index 565c965cf9fae6..c64dee8983b9d7 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -126,6 +126,8 @@ typedef int mode_t;
 
 namespace node {
 
+using options_parser::kAllowedInEnvironment;
+using options_parser::kDisallowedInEnvironment;
 using v8::Array;
 using v8::ArrayBuffer;
 using v8::Boolean;
@@ -183,6 +185,7 @@ bool linux_at_secure = false;
 // process-relative uptime base, initialized at start-up
 double prog_start_time;
 
+Mutex per_process_opts_mutex;
 std::shared_ptr<PerProcessOptions> per_process_opts {
     new PerProcessOptions() };
 
@@ -2345,157 +2348,6 @@ void LoadEnvironment(Environment* env) {
   }
 }
 
-static void PrintHelp() {
-  // XXX: If you add an option here, please also add it to doc/node.1 and
-  // doc/api/cli.md
-  printf("Usage: node [options] [ -e script | script.js | - ] [arguments]\n"
-         "       node inspect script.js [arguments]\n"
-         "\n"
-         "Options:\n"
-         "  -                          script read from stdin (default; \n"
-         "                             interactive mode if a tty)\n"
-         "  --                         indicate the end of node options\n"
-         "  --abort-on-uncaught-exception\n"
-         "                             aborting instead of exiting causes a\n"
-         "                             core file to be generated for analysis\n"
-#if HAVE_OPENSSL && NODE_FIPS_MODE
-         "  --enable-fips              enable FIPS crypto at startup\n"
-#endif  // NODE_FIPS_MODE && NODE_FIPS_MODE
-         "  --experimental-modules     experimental ES Module support\n"
-         "                             and caching modules\n"
-         "  --experimental-repl-await  experimental await keyword support\n"
-         "                             in REPL\n"
-         "  --experimental-vm-modules  experimental ES Module support\n"
-         "                             in vm module\n"
-         "  --experimental-worker      experimental threaded Worker support\n"
-#if HAVE_OPENSSL && NODE_FIPS_MODE
-         "  --force-fips               force FIPS crypto (cannot be disabled)\n"
-#endif  // HAVE_OPENSSL && NODE_FIPS_MODE
-#if defined(NODE_HAVE_I18N_SUPPORT)
-         "  --icu-data-dir=dir         set ICU data load path to dir\n"
-         "                             (overrides NODE_ICU_DATA)\n"
-#if !defined(NODE_HAVE_SMALL_ICU)
-         "                             note: linked-in ICU data is present\n"
-#endif
-#endif  // defined(NODE_HAVE_I18N_SUPPORT)
-#if HAVE_INSPECTOR
-         "  --inspect-brk[=[host:]port]\n"
-         "                             activate inspector on host:port\n"
-         "                             and break at start of user script\n"
-         "  --inspect-port=[host:]port\n"
-         "                             set host:port for inspector\n"
-         "  --inspect[=[host:]port]    activate inspector on host:port\n"
-         "                             (default: 127.0.0.1:9229)\n"
-#endif  // HAVE_INSPECTOR
-         "  --loader=file              (with --experimental-modules) use the \n"
-         "                             specified file as a custom loader\n"
-         "                             for ECMAScript Modules \n"
-         "  --napi-modules             load N-API modules (no-op - option\n"
-         "                             kept for compatibility)\n"
-         "  --no-deprecation           silence deprecation warnings\n"
-         "  --no-force-async-hooks-checks\n"
-         "                             disable checks for async_hooks\n"
-         "  --no-warnings              silence all process warnings\n"
-#if HAVE_OPENSSL
-         "  --openssl-config=file      load OpenSSL configuration from the\n"
-         "                             specified file (overrides\n"
-         "                             OPENSSL_CONF)\n"
-#endif  // HAVE_OPENSSL
-         "  --pending-deprecation      emit pending deprecation warnings\n"
-         "  --preserve-symlinks        preserve symbolic links when resolving\n"
-         "  --preserve-symlinks-main   preserve symbolic links when resolving\n"
-         "                             the main module\n"
-         "  --prof                     generate V8 profiler output\n"
-         "  --prof-process             process V8 profiler output generated\n"
-         "                             using --prof\n"
-         "  --redirect-warnings=file\n"
-         "                             write warnings to file instead of\n"
-         "                             stderr\n"
-         "  --throw-deprecation        throw an exception on deprecations\n"
-         "  --title=title              the process title to use on start up\n"
-#if HAVE_OPENSSL
-         "  --tls-cipher-list=val      use an alternative default TLS cipher "
-         "list\n"
-#endif  // HAVE_OPENSSL
-         "  --trace-deprecation        show stack traces on deprecations\n"
-         "  --trace-event-categories   comma separated list of trace event\n"
-         "                             categories to record\n"
-         "  --trace-event-file-pattern Template string specifying the\n"
-         "                             filepath for the trace-events data, it\n"
-         "                             supports ${rotation} and ${pid}\n"
-         "                             log-rotation id. %%2$u is the pid.\n"
-         "  --trace-events-enabled     track trace events\n"
-         "  --trace-sync-io            show stack trace when use of sync IO\n"
-         "                             is detected after the first tick\n"
-         "  --trace-warnings           show stack traces on process warnings\n"
-         "  --track-heap-objects       track heap object allocations for heap "
-         "snapshots\n"
-#if HAVE_OPENSSL
-         "  --use-bundled-ca           use bundled CA store"
-#if !defined(NODE_OPENSSL_CERT_STORE)
-         " (default)"
-#endif
-         "\n"
-         "  --use-openssl-ca           use OpenSSL's default CA store"
-#if defined(NODE_OPENSSL_CERT_STORE)
-         " (default)"
-#endif
-#endif  // HAVE_OPENSSL
-         "\n"
-         "  --v8-options               print v8 command line options\n"
-         "  --v8-pool-size=num         set v8's thread pool size\n"
-         "  --zero-fill-buffers        automatically zero-fill all newly "
-         "allocated\n"
-         "                             Buffer and SlowBuffer instances\n"
-         "  -c, --check                syntax check script without executing\n"
-         "  -e, --eval script          evaluate script\n"
-         "  -h, --help                 print node command line options\n"
-         "  -i, --interactive          always enter the REPL even if stdin\n"
-         "                             does not appear to be a terminal\n"
-         "  -p, --print                evaluate script and print result\n"
-         "  -r, --require              module to preload (option can be "
-         "repeated)\n"
-         "  -v, --version              print Node.js version\n"
-         "\n"
-         "Environment variables:\n"
-         "NODE_DEBUG                   ','-separated list of core modules\n"
-         "                             that should print debug information\n"
-         "NODE_DEBUG_NATIVE            ','-separated list of C++ core debug\n"
-         "                             categories that should print debug\n"
-         "                             output\n"
-         "NODE_DISABLE_COLORS          set to 1 to disable colors in the REPL\n"
-         "NODE_EXTRA_CA_CERTS          path to additional CA certificates\n"
-         "                             file\n"
-#if defined(NODE_HAVE_I18N_SUPPORT)
-         "NODE_ICU_DATA                data path for ICU (Intl object) data\n"
-#if !defined(NODE_HAVE_SMALL_ICU)
-         "                             (will extend linked-in data)\n"
-#endif
-#endif  // defined(NODE_HAVE_I18N_SUPPORT)
-         "NODE_NO_WARNINGS             set to 1 to silence process warnings\n"
-#if !defined(NODE_WITHOUT_NODE_OPTIONS)
-         "NODE_OPTIONS                 set CLI options in the environment\n"
-         "                             via a space-separated list\n"
-#endif  // !defined(NODE_WITHOUT_NODE_OPTIONS)
-#ifdef _WIN32
-         "NODE_PATH                    ';'-separated list of directories\n"
-#else
-         "NODE_PATH                    ':'-separated list of directories\n"
-#endif
-         "                             prefixed to the module search path\n"
-         "NODE_PENDING_DEPRECATION     set to 1 to emit pending deprecation\n"
-         "                             warnings\n"
-         "NODE_PRESERVE_SYMLINKS       set to 1 to preserve symbolic links\n"
-         "                             when resolving and caching modules\n"
-         "NODE_REDIRECT_WARNINGS       write warnings to path instead of\n"
-         "                             stderr\n"
-         "NODE_REPL_HISTORY            path to the persistent REPL history\n"
-         "                             file\n"
-         "OPENSSL_CONF                 load OpenSSL configuration from file\n"
-         "\n"
-         "Documentation can be found at https://nodejs.org/\n");
-}
-
 
 static void StartInspector(Environment* env, const char* path,
                            std::shared_ptr<DebugOptions> debug_options) {
@@ -2747,13 +2599,20 @@ void ProcessArgv(std::vector<std::string>* args,
   // Parse a few arguments which are specific to Node.
   std::vector<std::string> v8_args;
   std::string error;
-  PerProcessOptionsParser::instance.Parse(
-      args,
-      exec_args,
-      &v8_args,
-      per_process_opts.get(),
-      is_env ? kAllowedInEnvironment : kDisallowedInEnvironment,
-      &error);
+
+  {
+    // TODO(addaleax): The mutex here should ideally be held during the
+    // entire function, but that doesn't play well with the exit() calls below.
+    Mutex::ScopedLock lock(per_process_opts_mutex);
+    options_parser::PerProcessOptionsParser::instance.Parse(
+        args,
+        exec_args,
+        &v8_args,
+        per_process_opts.get(),
+        is_env ? kAllowedInEnvironment : kDisallowedInEnvironment,
+        &error);
+  }
+
   if (!error.empty()) {
     fprintf(stderr, "%s: %s\n", args->at(0).c_str(), error.c_str());
     exit(9);
@@ -2764,11 +2623,6 @@ void ProcessArgv(std::vector<std::string>* args,
     exit(0);
   }
 
-  if (per_process_opts->print_help) {
-    PrintHelp();
-    exit(0);
-  }
-
   if (per_process_opts->print_v8_help) {
     V8::SetFlagsFromString("--help", 6);
     exit(0);
diff --git a/src/node_config.cc b/src/node_config.cc
index c6e6211da288e4..080d8550665ad3 100644
--- a/src/node_config.cc
+++ b/src/node_config.cc
@@ -73,6 +73,10 @@ static void Initialize(Local<Object> target,
   READONLY_BOOLEAN_PROPERTY("hasTracing");
 #endif
 
+#if !defined(NODE_WITHOUT_NODE_OPTIONS)
+  READONLY_BOOLEAN_PROPERTY("hasNodeOptions");
+#endif
+
   // TODO(addaleax): This seems to be an unused, private API. Remove it?
   READONLY_STRING_PROPERTY(target, "icuDataDir",
       per_process_opts->icu_data_dir);
diff --git a/src/node_internals.h b/src/node_internals.h
index eb9e79d9e8b522..2fa95024669eb1 100644
--- a/src/node_internals.h
+++ b/src/node_internals.h
@@ -119,6 +119,7 @@ struct sockaddr;
     V(js_stream)                                                              \
     V(messaging)                                                              \
     V(module_wrap)                                                            \
+    V(options)                                                                \
     V(os)                                                                     \
     V(performance)                                                            \
     V(pipe_wrap)                                                              \
@@ -176,6 +177,7 @@ extern Mutex environ_mutex;
 // Tells whether it is safe to call v8::Isolate::GetCurrent().
 extern bool v8_initialized;
 
+extern Mutex per_process_opts_mutex;
 extern std::shared_ptr<PerProcessOptions> per_process_opts;
 
 extern const char* const environment_flags[];
diff --git a/src/node_options-inl.h b/src/node_options-inl.h
index e610cd50d11436..ba18e7c19b03c7 100644
--- a/src/node_options-inl.h
+++ b/src/node_options-inl.h
@@ -21,73 +21,88 @@ EnvironmentOptions* PerIsolateOptions::get_per_env_options() {
   return per_env.get();
 }
 
+namespace options_parser {
+
 template <typename Options>
 void OptionsParser<Options>::AddOption(const std::string& name,
+                                       const std::string& help_text,
                                        bool Options::* field,
                                        OptionEnvvarSettings env_setting) {
-  options_.emplace(name, OptionInfo {
-    kBoolean,
-    std::make_shared<SimpleOptionField<bool>>(field),
-    env_setting
-  });
+  options_.emplace(name,
+                   OptionInfo{kBoolean,
+                              std::make_shared<SimpleOptionField<bool>>(field),
+                              env_setting,
+                              help_text});
 }
 
 template <typename Options>
 void OptionsParser<Options>::AddOption(const std::string& name,
+                                       const std::string& help_text,
                                        int64_t Options::* field,
                                        OptionEnvvarSettings env_setting) {
-  options_.emplace(name, OptionInfo {
-    kInteger,
-    std::make_shared<SimpleOptionField<int64_t>>(field),
-    env_setting
-  });
+  options_.emplace(
+      name,
+      OptionInfo{kInteger,
+                 std::make_shared<SimpleOptionField<int64_t>>(field),
+                 env_setting,
+                 help_text});
 }
 
 template <typename Options>
 void OptionsParser<Options>::AddOption(const std::string& name,
+                                       const std::string& help_text,
                                        std::string Options::* field,
                                        OptionEnvvarSettings env_setting) {
-  options_.emplace(name, OptionInfo {
-    kString,
-    std::make_shared<SimpleOptionField<std::string>>(field),
-    env_setting
-  });
+  options_.emplace(
+      name,
+      OptionInfo{kString,
+                 std::make_shared<SimpleOptionField<std::string>>(field),
+                 env_setting,
+                 help_text});
 }
 
 template <typename Options>
 void OptionsParser<Options>::AddOption(
     const std::string& name,
+    const std::string& help_text,
     std::vector<std::string> Options::* field,
     OptionEnvvarSettings env_setting) {
   options_.emplace(name, OptionInfo {
     kStringList,
     std::make_shared<SimpleOptionField<std::vector<std::string>>>(field),
-    env_setting
+    env_setting,
+    help_text
   });
 }
 
 template <typename Options>
 void OptionsParser<Options>::AddOption(const std::string& name,
+                                       const std::string& help_text,
                                        HostPort Options::* field,
                                        OptionEnvvarSettings env_setting) {
-  options_.emplace(name, OptionInfo {
-    kHostPort,
-    std::make_shared<SimpleOptionField<HostPort>>(field),
-    env_setting
-  });
+  options_.emplace(
+      name,
+      OptionInfo{kHostPort,
+                 std::make_shared<SimpleOptionField<HostPort>>(field),
+                 env_setting,
+                 help_text});
 }
 
 template <typename Options>
-void OptionsParser<Options>::AddOption(const std::string& name, NoOp no_op_tag,
+void OptionsParser<Options>::AddOption(const std::string& name,
+                                       const std::string& help_text,
+                                       NoOp no_op_tag,
                                        OptionEnvvarSettings env_setting) {
-  options_.emplace(name, OptionInfo { kNoOp, nullptr, env_setting });
+  options_.emplace(name, OptionInfo{kNoOp, nullptr, env_setting, help_text});
 }
 
 template <typename Options>
 void OptionsParser<Options>::AddOption(const std::string& name,
+                                       const std::string& help_text,
                                        V8Option v8_option_tag,
                                        OptionEnvvarSettings env_setting) {
-  options_.emplace(name, OptionInfo { kV8Option, nullptr, env_setting });
+  options_.emplace(name,
+                   OptionInfo{kV8Option, nullptr, env_setting, help_text});
 }
 
 template <typename Options>
@@ -161,11 +176,10 @@ template <typename ChildOptions>
 auto OptionsParser<Options>::Convert(
     typename OptionsParser<ChildOptions>::OptionInfo original,
     ChildOptions* (Options::* get_child)()) {
-  return OptionInfo {
-    original.type,
-    Convert(original.field, get_child),
-    original.env_setting
-  };
+  return OptionInfo{original.type,
+                    Convert(original.field, get_child),
+                    original.env_setting,
+                    original.help_text};
 }
 
 template <typename Options>
@@ -385,24 +399,21 @@ void OptionsParser<Options>::Parse(
 
     switch (info.type) {
       case kBoolean:
-        *std::static_pointer_cast<OptionField<bool>>(info.field)
-            ->Lookup(options) = true;
+        *Lookup<bool>(info.field, options) = true;
         break;
       case kInteger:
-        *std::static_pointer_cast<OptionField<int64_t>>(info.field)
-            ->Lookup(options) = std::atoll(value.c_str());
+        *Lookup<int64_t>(info.field, options) = std::atoll(value.c_str());
         break;
       case kString:
-        *std::static_pointer_cast<OptionField<std::string>>(info.field)
-            ->Lookup(options) = value;
+        *Lookup<std::string>(info.field, options) = value;
         break;
       case kStringList:
-        std::static_pointer_cast<OptionField<std::vector<std::string>>>(
-            info.field)->Lookup(options)->emplace_back(std::move(value));
+        Lookup<std::vector<std::string>>(info.field, options)
+            ->emplace_back(std::move(value));
         break;
       case kHostPort:
-        std::static_pointer_cast<OptionField<HostPort>>(info.field)
-            ->Lookup(options)->Update(SplitHostPort(value, error));
+        Lookup<HostPort>(info.field, options)
+            ->Update(SplitHostPort(value, error));
         break;
       case kNoOp:
         break;
@@ -415,6 +426,7 @@ void OptionsParser<Options>::Parse(
   }
 }
 
+}  // namespace options_parser
 }  // namespace node
 
 #endif  // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
diff --git a/src/node_options.cc b/src/node_options.cc
index b10de9ef28ab9c..27e518d0f1f2d8 100644
--- a/src/node_options.cc
+++ b/src/node_options.cc
@@ -1,30 +1,55 @@
-#include "node_options-inl.h"
 #include <errno.h>
+#include "node_internals.h"
+#include "node_options-inl.h"
+
+using v8::Boolean;
+using v8::Context;
+using v8::FunctionCallbackInfo;
+using v8::Integer;
+using v8::Isolate;
+using v8::Local;
+using v8::Map;
+using v8::Number;
+using v8::Object;
+using v8::String;
+using v8::Undefined;
+using v8::Value;
 
 namespace node {
+namespace options_parser {
+
+// XXX: If you add an option here, please also add it to doc/node.1 and
+// doc/api/cli.md
+// TODO(addaleax): Make that unnecessary.
 
 DebugOptionsParser::DebugOptionsParser() {
-  AddOption("--inspect-port", &DebugOptions::host_port,
+  AddOption("--inspect-port",
+            "set host:port for inspector",
+            &DebugOptions::host_port,
             kAllowedInEnvironment);
   AddAlias("--debug-port", "--inspect-port");
 
-  AddOption("--inspect", &DebugOptions::inspector_enabled,
+  AddOption("--inspect",
+            "activate inspector on host:port (default: 127.0.0.1:9229)",
+            &DebugOptions::inspector_enabled,
             kAllowedInEnvironment);
   AddAlias("--inspect=", { "--inspect-port", "--inspect" });
 
-  AddOption("--debug", &DebugOptions::deprecated_debug);
+  AddOption("--debug", "", &DebugOptions::deprecated_debug);
   AddAlias("--debug=", { "--inspect-port", "--debug" });
 
-  AddOption("--inspect-brk", &DebugOptions::break_first_line,
+  AddOption("--inspect-brk",
+            "activate inspector on host:port and break at start of user script",
+            &DebugOptions::break_first_line,
             kAllowedInEnvironment);
   Implies("--inspect-brk", "--inspect");
   AddAlias("--inspect-brk=", { "--inspect-port", "--inspect-brk" });
 
-  AddOption("--inspect-brk-node", &DebugOptions::break_node_first_line);
+  AddOption("--inspect-brk-node", "", &DebugOptions::break_node_first_line);
   Implies("--inspect-brk-node", "--inspect");
   AddAlias("--inspect-brk-node=", { "--inspect-port", "--inspect-brk-node" });
 
-  AddOption("--debug-brk", &DebugOptions::break_first_line);
+  AddOption("--debug-brk", "", &DebugOptions::break_first_line);
   Implies("--debug-brk", "--debug");
   AddAlias("--debug-brk=", { "--inspect-port", "--debug-brk" });
 }
@@ -32,46 +57,80 @@ DebugOptionsParser::DebugOptionsParser() {
 DebugOptionsParser DebugOptionsParser::instance;
 
 EnvironmentOptionsParser::EnvironmentOptionsParser() {
-  AddOption("--experimental-modules", &EnvironmentOptions::experimental_modules,
+  AddOption("--experimental-modules",
+            "experimental ES Module support and caching modules",
+            &EnvironmentOptions::experimental_modules,
             kAllowedInEnvironment);
   AddOption("--experimental-repl-await",
+            "experimental await keyword support in REPL",
             &EnvironmentOptions::experimental_repl_await,
             kAllowedInEnvironment);
   AddOption("--experimental-vm-modules",
+            "experimental ES Module support in vm module",
             &EnvironmentOptions::experimental_vm_modules,
             kAllowedInEnvironment);
-  AddOption("--experimental-worker", &EnvironmentOptions::experimental_worker,
+  AddOption("--experimental-worker",
+            "experimental threaded Worker support",
+            &EnvironmentOptions::experimental_worker,
             kAllowedInEnvironment);
-  AddOption("--expose-internals", &EnvironmentOptions::expose_internals);
+  AddOption("--expose-internals", "", &EnvironmentOptions::expose_internals);
   // TODO(addaleax): Remove this when adding -/_ canonicalization to the parser.
   AddAlias("--expose_internals", "--expose-internals");
-  AddOption("--loader", &EnvironmentOptions::userland_loader,
+  AddOption("--loader",
+            "(with --experimental-modules) use the specified file as a "
+            "custom loader",
+            &EnvironmentOptions::userland_loader,
             kAllowedInEnvironment);
-  AddOption("--no-deprecation", &EnvironmentOptions::no_deprecation,
+  AddOption("--no-deprecation",
+            "silence deprecation warnings",
+            &EnvironmentOptions::no_deprecation,
             kAllowedInEnvironment);
   AddOption("--no-force-async-hooks-checks",
+            "disable checks for async_hooks",
             &EnvironmentOptions::no_force_async_hooks_checks,
             kAllowedInEnvironment);
-  AddOption("--no-warnings", &EnvironmentOptions::no_warnings,
+  AddOption("--no-warnings",
+            "silence all process warnings",
+            &EnvironmentOptions::no_warnings,
             kAllowedInEnvironment);
-  AddOption("--pending-deprecation", &EnvironmentOptions::pending_deprecation,
+  AddOption("--pending-deprecation",
+            "emit pending deprecation warnings",
+            &EnvironmentOptions::pending_deprecation,
             kAllowedInEnvironment);
-  AddOption("--preserve-symlinks", &EnvironmentOptions::preserve_symlinks);
+  AddOption("--preserve-symlinks",
+            "preserve symbolic links when resolving",
+            &EnvironmentOptions::preserve_symlinks);
   AddOption("--preserve-symlinks-main",
+            "preserve symbolic links when resolving the main module",
             &EnvironmentOptions::preserve_symlinks_main);
-  AddOption("--prof-process", &EnvironmentOptions::prof_process);
-  AddOption("--redirect-warnings", &EnvironmentOptions::redirect_warnings,
+  AddOption("--prof-process",
+            "process V8 profiler output generated using --prof",
+            &EnvironmentOptions::prof_process);
+  AddOption("--redirect-warnings",
+            "write warnings to file instead of stderr",
+            &EnvironmentOptions::redirect_warnings,
             kAllowedInEnvironment);
-  AddOption("--throw-deprecation", &EnvironmentOptions::throw_deprecation,
+  AddOption("--throw-deprecation",
+            "throw an exception on deprecations",
+            &EnvironmentOptions::throw_deprecation,
             kAllowedInEnvironment);
-  AddOption("--trace-deprecation", &EnvironmentOptions::trace_deprecation,
+  AddOption("--trace-deprecation",
+            "show stack traces on deprecations",
+            &EnvironmentOptions::trace_deprecation,
             kAllowedInEnvironment);
-  AddOption("--trace-sync-io", &EnvironmentOptions::trace_sync_io,
+  AddOption("--trace-sync-io",
+            "show stack trace when use of sync IO is detected after the "
+            "first tick",
+            &EnvironmentOptions::trace_sync_io,
             kAllowedInEnvironment);
-  AddOption("--trace-warnings", &EnvironmentOptions::trace_warnings,
+  AddOption("--trace-warnings",
+            "show stack traces on process warnings",
+            &EnvironmentOptions::trace_warnings,
             kAllowedInEnvironment);
 
-  AddOption("--check", &EnvironmentOptions::syntax_check_only);
+  AddOption("--check",
+            "syntax check script without executing",
+            &EnvironmentOptions::syntax_check_only);
   AddAlias("-c", "--check");
   // This option is only so that we can tell --eval with an empty string from
   // no eval at all. Having it not start with a dash makes it inaccessible
@@ -79,21 +138,28 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
   // TODO(addaleax): When moving --help over to something generated from the
   // programmatic descriptions, this will need some special care.
   // (See also [ssl_openssl_cert_store] below.)
-  AddOption("[has_eval_string]", &EnvironmentOptions::has_eval_string);
-  AddOption("--eval", &EnvironmentOptions::eval_string);
+  AddOption("[has_eval_string]", "", &EnvironmentOptions::has_eval_string);
+  AddOption("--eval", "evaluate script", &EnvironmentOptions::eval_string);
   Implies("--eval", "[has_eval_string]");
-  AddOption("--print", &EnvironmentOptions::print_eval);
+  AddOption("--print",
+            "evaluate script and print result",
+            &EnvironmentOptions::print_eval);
   AddAlias("-e", "--eval");
   AddAlias("--print <arg>", "-pe");
   AddAlias("-pe", { "--print", "--eval" });
   AddAlias("-p", "--print");
-  AddOption("--require", &EnvironmentOptions::preload_modules,
+  AddOption("--require",
+            "module to preload (option can be repeated)",
+            &EnvironmentOptions::preload_modules,
             kAllowedInEnvironment);
   AddAlias("-r", "--require");
-  AddOption("--interactive", &EnvironmentOptions::force_repl);
+  AddOption("--interactive",
+            "always enter the REPL even if stdin does not appear "
+            "to be a terminal",
+            &EnvironmentOptions::force_repl);
   AddAlias("-i", "--interactive");
 
-  AddOption("--napi-modules", NoOp {}, kAllowedInEnvironment);
+  AddOption("--napi-modules", "", NoOp{}, kAllowedInEnvironment);
 
   Insert(&DebugOptionsParser::instance,
          &EnvironmentOptions::get_debug_options);
@@ -102,16 +168,21 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
 EnvironmentOptionsParser EnvironmentOptionsParser::instance;
 
 PerIsolateOptionsParser::PerIsolateOptionsParser() {
-  AddOption("--track-heap-objects", &PerIsolateOptions::track_heap_objects,
+  AddOption("--track-heap-objects",
+            "track heap object allocations for heap snapshots",
+            &PerIsolateOptions::track_heap_objects,
             kAllowedInEnvironment);
 
   // Explicitly add some V8 flags to mark them as allowed in NODE_OPTIONS.
-  AddOption("--abort_on_uncaught_exception", V8Option {},
+  AddOption("--abort_on_uncaught_exception",
+            "aborting instead of exiting causes a core file to be generated "
+            "for analysis",
+            V8Option{},
             kAllowedInEnvironment);
-  AddOption("--max_old_space_size", V8Option {}, kAllowedInEnvironment);
-  AddOption("--perf_basic_prof", V8Option {}, kAllowedInEnvironment);
-  AddOption("--perf_prof", V8Option {}, kAllowedInEnvironment);
-  AddOption("--stack_trace_limit", V8Option {}, kAllowedInEnvironment);
+  AddOption("--max_old_space_size", "", V8Option{}, kAllowedInEnvironment);
+  AddOption("--perf_basic_prof", "", V8Option{}, kAllowedInEnvironment);
+  AddOption("--perf_prof", "", V8Option{}, kAllowedInEnvironment);
+  AddOption("--stack_trace_limit", "", V8Option{}, kAllowedInEnvironment);
 
   Insert(&EnvironmentOptionsParser::instance,
          &PerIsolateOptions::get_per_env_options);
@@ -120,52 +191,97 @@ PerIsolateOptionsParser::PerIsolateOptionsParser() {
 PerIsolateOptionsParser PerIsolateOptionsParser::instance;
 
 PerProcessOptionsParser::PerProcessOptionsParser() {
-  AddOption("--title", &PerProcessOptions::title, kAllowedInEnvironment);
+  AddOption("--title",
+            "the process title to use on startup",
+            &PerProcessOptions::title,
+            kAllowedInEnvironment);
   AddOption("--trace-event-categories",
+            "comma separated list of trace event categories to record",
             &PerProcessOptions::trace_event_categories,
             kAllowedInEnvironment);
   AddOption("--trace-event-file-pattern",
+            "Template string specifying the filepath for the trace-events "
+            "data, it supports ${rotation} and ${pid} log-rotation id. %2$u "
+            "is the pid.",
             &PerProcessOptions::trace_event_file_pattern,
             kAllowedInEnvironment);
   AddAlias("--trace-events-enabled", {
     "--trace-event-categories", "v8,node,node.async_hooks" });
-  AddOption("--v8-pool-size", &PerProcessOptions::v8_thread_pool_size,
+  AddOption("--v8-pool-size",
+            "set V8's thread pool size",
+            &PerProcessOptions::v8_thread_pool_size,
             kAllowedInEnvironment);
-  AddOption("--zero-fill-buffers", &PerProcessOptions::zero_fill_all_buffers,
+  AddOption("--zero-fill-buffers",
+            "automatically zero-fill all newly allocated Buffer and "
+            "SlowBuffer instances",
+            &PerProcessOptions::zero_fill_all_buffers,
             kAllowedInEnvironment);
 
-  AddOption("--security-reverts", &PerProcessOptions::security_reverts);
-  AddOption("--help", &PerProcessOptions::print_help);
+  AddOption("--security-reverts", "", &PerProcessOptions::security_reverts);
+  AddOption("--help",
+            "print node command line options",
+            &PerProcessOptions::print_help);
   AddAlias("-h", "--help");
-  AddOption("--version", &PerProcessOptions::print_version);
+  AddOption(
+      "--version", "print Node.js version", &PerProcessOptions::print_version);
   AddAlias("-v", "--version");
-  AddOption("--v8-options", &PerProcessOptions::print_v8_help);
+  AddOption("--v8-options",
+            "print V8 command line options",
+            &PerProcessOptions::print_v8_help);
 
 #ifdef NODE_HAVE_I18N_SUPPORT
-  AddOption("--icu-data-dir", &PerProcessOptions::icu_data_dir,
+  AddOption("--icu-data-dir",
+            "set ICU data load path to dir (overrides NODE_ICU_DATA)"
+#ifndef NODE_HAVE_SMALL_ICU
+            " (note: linked-in ICU data is present)\n"
+#endif
+            ,
+            &PerProcessOptions::icu_data_dir,
             kAllowedInEnvironment);
 #endif
 
 #if HAVE_OPENSSL
-  AddOption("--openssl-config", &PerProcessOptions::openssl_config,
+  AddOption("--openssl-config",
+            "load OpenSSL configuration from the specified file "
+            "(overrides OPENSSL_CONF)",
+            &PerProcessOptions::openssl_config,
             kAllowedInEnvironment);
-  AddOption("--tls-cipher-list", &PerProcessOptions::tls_cipher_list,
+  AddOption("--tls-cipher-list",
+            "use an alternative default TLS cipher list",
+            &PerProcessOptions::tls_cipher_list,
             kAllowedInEnvironment);
-  AddOption("--use-openssl-ca", &PerProcessOptions::use_openssl_ca,
+  AddOption("--use-openssl-ca",
+            "use OpenSSL's default CA store"
+#if defined(NODE_OPENSSL_CERT_STORE)
+            " (default)"
+#endif
+            ,
+            &PerProcessOptions::use_openssl_ca,
             kAllowedInEnvironment);
-  AddOption("--use-bundled-ca", &PerProcessOptions::use_bundled_ca,
+  AddOption("--use-bundled-ca",
+            "use bundled CA store"
+#if !defined(NODE_OPENSSL_CERT_STORE)
+            " (default)"
+#endif
+            ,
+            &PerProcessOptions::use_bundled_ca,
             kAllowedInEnvironment);
   // Similar to [has_eval_string] above, except that the separation between
   // this and use_openssl_ca only exists for option validation after parsing.
   // This is not ideal.
   AddOption("[ssl_openssl_cert_store]",
+            "",
             &PerProcessOptions::ssl_openssl_cert_store);
   Implies("--use-openssl-ca", "[ssl_openssl_cert_store]");
   ImpliesNot("--use-bundled-ca", "[ssl_openssl_cert_store]");
 #if NODE_FIPS_MODE
-  AddOption("--enable-fips", &PerProcessOptions::enable_fips_crypto,
+  AddOption("--enable-fips",
+            "enable FIPS crypto at startup",
+            &PerProcessOptions::enable_fips_crypto,
             kAllowedInEnvironment);
-  AddOption("--force-fips", &PerProcessOptions::force_fips_crypto,
+  AddOption("--force-fips",
+            "force FIPS crypto (cannot be disabled)",
+            &PerProcessOptions::force_fips_crypto,
             kAllowedInEnvironment);
 #endif
 #endif
@@ -216,4 +332,153 @@ HostPort SplitHostPort(const std::string& arg, std::string* error) {
   return HostPort { RemoveBrackets(arg.substr(0, colon)),
                     ParseAndValidatePort(arg.substr(colon + 1), error) };
 }
+
+// Usage: Either:
+// - getOptions() to get all options + metadata or
+// - getOptions(string) to get the value of a particular option
+void GetOptions(const FunctionCallbackInfo<Value>& args) {
+  Mutex::ScopedLock lock(per_process_opts_mutex);
+  Environment* env = Environment::GetCurrent(args);
+  Isolate* isolate = env->isolate();
+  Local<Context> context = env->context();
+
+  // Temporarily act as if the current Environment's/IsolateData's options were
+  // the default options, i.e. like they are the ones we'd access for global
+  // options parsing, so that all options are available from the main parser.
+  auto original_per_isolate = per_process_opts->per_isolate;
+  per_process_opts->per_isolate = env->isolate_data()->options();
+  auto original_per_env = per_process_opts->per_isolate->per_env;
+  per_process_opts->per_isolate->per_env = env->options();
+  OnScopeLeave on_scope_leave([&]() {
+    per_process_opts->per_isolate->per_env = original_per_env;
+    per_process_opts->per_isolate = original_per_isolate;
+  });
+
+  const auto& parser = PerProcessOptionsParser::instance;
+
+  std::string filter;
+  if (args[0]->IsString()) filter = *node::Utf8Value(isolate, args[0]);
+
+  Local<Map> options = Map::New(isolate);
+  for (const auto& item : parser.options_) {
+    if (!filter.empty() && item.first != filter) continue;
+
+    Local<Value> value;
+    const auto& option_info = item.second;
+    auto field = option_info.field;
+    PerProcessOptions* opts = per_process_opts.get();
+    switch (option_info.type) {
+      case kNoOp:
+      case kV8Option:
+        value = Undefined(isolate);
+        break;
+      case kBoolean:
+        value = Boolean::New(isolate, *parser.Lookup<bool>(field, opts));
+        break;
+      case kInteger:
+        value = Number::New(isolate, *parser.Lookup<int64_t>(field, opts));
+        break;
+      case kString:
+        if (!ToV8Value(context, *parser.Lookup<std::string>(field, opts))
+                 .ToLocal(&value)) {
+          return;
+        }
+        break;
+      case kStringList:
+        if (!ToV8Value(context,
+                       *parser.Lookup<std::vector<std::string>>(field, opts))
+                 .ToLocal(&value)) {
+          return;
+        }
+        break;
+      case kHostPort: {
+        const HostPort& host_port = *parser.Lookup<HostPort>(field, opts);
+        Local<Object> obj = Object::New(isolate);
+        Local<Value> host;
+        if (!ToV8Value(context, host_port.host_name).ToLocal(&host) ||
+            obj->Set(context, env->host_string(), host).IsNothing() ||
+            obj->Set(context,
+                     env->port_string(),
+                     Integer::New(isolate, host_port.port))
+                .IsNothing()) {
+          return;
+        }
+        value = obj;
+        break;
+      }
+      default:
+        UNREACHABLE();
+    }
+    CHECK(!value.IsEmpty());
+
+    if (!filter.empty()) {
+      args.GetReturnValue().Set(value);
+      return;
+    }
+
+    Local<Value> name = ToV8Value(context, item.first).ToLocalChecked();
+    Local<Object> info = Object::New(isolate);
+    Local<Value> help_text;
+    if (!ToV8Value(context, option_info.help_text).ToLocal(&help_text) ||
+        !info->Set(context, env->help_text_string(), help_text)
+             .FromMaybe(false) ||
+        !info->Set(context,
+                   env->env_var_settings_string(),
+                   Integer::New(isolate,
+                                static_cast<int>(option_info.env_setting)))
+             .FromMaybe(false) ||
+        !info->Set(context,
+                   env->type_string(),
+                   Integer::New(isolate, static_cast<int>(option_info.type)))
+             .FromMaybe(false) ||
+        info->Set(context, env->value_string(), value).IsNothing() ||
+        options->Set(context, name, info).IsEmpty()) {
+      return;
+    }
+  }
+
+  if (!filter.empty()) return;
+
+  Local<Value> aliases;
+  if (!ToV8Value(context, parser.aliases_).ToLocal(&aliases)) return;
+
+  Local<Object> ret = Object::New(isolate);
+  if (ret->Set(context, env->options_string(), options).IsNothing() ||
+      ret->Set(context, env->aliases_string(), aliases).IsNothing()) {
+    return;
+  }
+
+  args.GetReturnValue().Set(ret);
+}
+
+void Initialize(Local<Object> target,
+                Local<Value> unused,
+                Local<Context> context) {
+  Environment* env = Environment::GetCurrent(context);
+  Isolate* isolate = env->isolate();
+  env->SetMethodNoSideEffect(target, "getOptions", GetOptions);
+
+  Local<Object> env_settings = Object::New(isolate);
+  NODE_DEFINE_CONSTANT(env_settings, kAllowedInEnvironment);
+  NODE_DEFINE_CONSTANT(env_settings, kDisallowedInEnvironment);
+  target
+      ->Set(
+          context, FIXED_ONE_BYTE_STRING(isolate, "envSettings"), env_settings)
+      .FromJust();
+
+  Local<Object> types = Object::New(isolate);
+  NODE_DEFINE_CONSTANT(types, kNoOp);
+  NODE_DEFINE_CONSTANT(types, kV8Option);
+  NODE_DEFINE_CONSTANT(types, kBoolean);
+  NODE_DEFINE_CONSTANT(types, kInteger);
+  NODE_DEFINE_CONSTANT(types, kString);
+  NODE_DEFINE_CONSTANT(types, kHostPort);
+  NODE_DEFINE_CONSTANT(types, kStringList);
+  target->Set(context, FIXED_ONE_BYTE_STRING(isolate, "types"), types)
+      .FromJust();
+}
+
+}  // namespace options_parser
 }  // namespace node
+
+NODE_MODULE_CONTEXT_AWARE_INTERNAL(options, node::options_parser::Initialize)
diff --git a/src/node_options.h b/src/node_options.h
index 957e2b729d9ff4..7891d550ae3dad 100644
--- a/src/node_options.h
+++ b/src/node_options.h
@@ -141,7 +141,10 @@ class PerProcessOptions {
 
 // The actual options parser, as opposed to the structs containing them:
 
+namespace options_parser {
+
 HostPort SplitHostPort(const std::string& arg, std::string* error);
+void GetOptions(const v8::FunctionCallbackInfo<v8::Value>& args);
 
 enum OptionEnvvarSettings {
   kAllowedInEnvironment,
@@ -176,24 +179,31 @@ class OptionsParser {
   // specified whether the option should be allowed from environment variable
   // sources (i.e. NODE_OPTIONS).
   void AddOption(const std::string& name,
+                 const std::string& help_text,
                  bool Options::* field,
                  OptionEnvvarSettings env_setting = kDisallowedInEnvironment);
   void AddOption(const std::string& name,
+                 const std::string& help_text,
                  int64_t Options::* field,
                  OptionEnvvarSettings env_setting = kDisallowedInEnvironment);
   void AddOption(const std::string& name,
+                 const std::string& help_text,
                  std::string Options::* field,
                  OptionEnvvarSettings env_setting = kDisallowedInEnvironment);
   void AddOption(const std::string& name,
+                 const std::string& help_text,
                  std::vector<std::string> Options::* field,
                  OptionEnvvarSettings env_setting = kDisallowedInEnvironment);
   void AddOption(const std::string& name,
+                 const std::string& help_text,
                  HostPort Options::* field,
                  OptionEnvvarSettings env_setting = kDisallowedInEnvironment);
   void AddOption(const std::string& name,
+                 const std::string& help_text,
                  NoOp no_op_tag,
                  OptionEnvvarSettings env_setting = kDisallowedInEnvironment);
   void AddOption(const std::string& name,
+                 const std::string& help_text,
                  V8Option v8_option_tag,
                  OptionEnvvarSettings env_setting = kDisallowedInEnvironment);
 
@@ -281,6 +291,12 @@ class OptionsParser {
     T Options::* field_;
   };
 
+  template <typename T>
+  inline T* Lookup(std::shared_ptr<BaseOptionField> field,
+                   Options* options) const {
+    return std::static_pointer_cast<OptionField<T>>(field)->Lookup(options);
+  }
+
   // An option consists of:
   // - A type.
   // - A way to store/access the property value.
@@ -289,6 +305,7 @@ class OptionsParser {
     OptionType type;
     std::shared_ptr<BaseOptionField> field;
     OptionEnvvarSettings env_setting;
+    std::string help_text;
   };
 
   // An implied option is composed of the information on where to store a
@@ -319,6 +336,8 @@ class OptionsParser {
 
   template <typename OtherOptions>
   friend class OptionsParser;
+
+  friend void GetOptions(const v8::FunctionCallbackInfo<v8::Value>& args);
 };
 
 class DebugOptionsParser : public OptionsParser<DebugOptions> {
@@ -349,6 +368,7 @@ class PerProcessOptionsParser : public OptionsParser<PerProcessOptions> {
   static PerProcessOptionsParser instance;
 };
 
+}  // namespace options_parser
 }  // namespace node
 
 #endif  // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
diff --git a/src/util-inl.h b/src/util-inl.h
index c6cd263aa2714a..bbee32615ff729 100644
--- a/src/util-inl.h
+++ b/src/util-inl.h
@@ -24,8 +24,9 @@
 
 #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
 
-#include "util.h"
+#include <limits.h>  // INT_MAX
 #include <cstring>
+#include "util.h"
 
 #if defined(_MSC_VER)
 #include <intrin.h>
@@ -397,6 +398,62 @@ inline char* Calloc(size_t n) { return Calloc<char>(n); }
 inline char* UncheckedMalloc(size_t n) { return UncheckedMalloc<char>(n); }
 inline char* UncheckedCalloc(size_t n) { return UncheckedCalloc<char>(n); }
 
+// This is a helper in the .cc file so including util-inl.h doesn't include more
+// headers than we really need to.
+void ThrowErrStringTooLong(v8::Isolate* isolate);
+
+v8::MaybeLocal<v8::Value> ToV8Value(v8::Local<v8::Context> context,
+                                    const std::string& str) {
+  v8::Isolate* isolate = context->GetIsolate();
+  if (UNLIKELY(str.size() >= static_cast<size_t>(v8::String::kMaxLength))) {
+    // V8 only has a TODO comment about adding an exception when the maximum
+    // string size is exceeded.
+    ThrowErrStringTooLong(isolate);
+    return v8::MaybeLocal<v8::Value>();
+  }
+
+  return v8::String::NewFromUtf8(
+             isolate, str.data(), v8::NewStringType::kNormal, str.size())
+      .FromMaybe(v8::Local<v8::String>());
+}
+
+template <typename T>
+v8::MaybeLocal<v8::Value> ToV8Value(v8::Local<v8::Context> context,
+                                    const std::vector<T>& vec) {
+  v8::Isolate* isolate = context->GetIsolate();
+  v8::EscapableHandleScope handle_scope(isolate);
+
+  v8::Local<v8::Array> arr = v8::Array::New(isolate, vec.size());
+  for (size_t i = 0; i < vec.size(); ++i) {
+    v8::Local<v8::Value> val;
+    if (!ToV8Value(context, vec[i]).ToLocal(&val) ||
+        arr->Set(context, i, val).IsNothing()) {
+      return v8::MaybeLocal<v8::Value>();
+    }
+  }
+
+  return handle_scope.Escape(arr);
+}
+
+template <typename T, typename U>
+v8::MaybeLocal<v8::Value> ToV8Value(v8::Local<v8::Context> context,
+                                    const std::unordered_map<T, U>& map) {
+  v8::Isolate* isolate = context->GetIsolate();
+  v8::EscapableHandleScope handle_scope(isolate);
+
+  v8::Local<v8::Map> ret = v8::Map::New(isolate);
+  for (const auto& item : map) {
+    v8::Local<v8::Value> first, second;
+    if (!ToV8Value(context, item.first).ToLocal(&first) ||
+        !ToV8Value(context, item.second).ToLocal(&second) ||
+        ret->Set(context, first, second).IsEmpty()) {
+      return v8::MaybeLocal<v8::Value>();
+    }
+  }
+
+  return handle_scope.Escape(ret);
+}
+
 }  // namespace node
 
 #endif  // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
diff --git a/src/util.cc b/src/util.cc
index 66be18eae2d150..66e2c9199bb23a 100644
--- a/src/util.cc
+++ b/src/util.cc
@@ -19,12 +19,13 @@
 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
 // USE OR OTHER DEALINGS IN THE SOFTWARE.
 
-#include "string_bytes.h"
+#include <stdio.h>
+#include <sstream>
 #include "node_buffer.h"
+#include "node_errors.h"
 #include "node_internals.h"
+#include "string_bytes.h"
 #include "uv.h"
-#include <stdio.h>
-#include <sstream>
 
 namespace node {
 
@@ -132,4 +133,8 @@ std::set<std::string> ParseCommaSeparatedSet(const std::string& in) {
   return out;
 }
 
+void ThrowErrStringTooLong(v8::Isolate* isolate) {
+  isolate->ThrowException(ERR_STRING_TOO_LONG(isolate));
+}
+
 }  // namespace node
diff --git a/src/util.h b/src/util.h
index a9fce79ebeaec1..057346a2d04fca 100644
--- a/src/util.h
+++ b/src/util.h
@@ -34,9 +34,10 @@
 #include <stdlib.h>
 #include <string.h>
 
-#include <string>
 #include <functional>  // std::function
 #include <set>
+#include <string>
+#include <unordered_map>
 
 namespace node {
 
@@ -479,6 +480,15 @@ using DeleteFnPtr = typename FunctionDeleter<T, function>::Pointer;
 
 std::set<std::string> ParseCommaSeparatedSet(const std::string& in);
 
+inline v8::MaybeLocal<v8::Value> ToV8Value(v8::Local<v8::Context> context,
+                                           const std::string& str);
+template <typename T>
+inline v8::MaybeLocal<v8::Value> ToV8Value(v8::Local<v8::Context> context,
+                                           const std::vector<T>& vec);
+template <typename T, typename U>
+inline v8::MaybeLocal<v8::Value> ToV8Value(v8::Local<v8::Context> context,
+                                           const std::unordered_map<T, U>& map);
+
 }  // namespace node
 
 #endif  // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js
index 4a46ac905b8161..81563355d246ff 100644
--- a/test/parallel/test-bootstrap-modules.js
+++ b/test/parallel/test-bootstrap-modules.js
@@ -11,4 +11,4 @@ const list = process.moduleLoadList.slice();
 
 const assert = require('assert');
 
-assert(list.length <= 73, list);
+assert(list.length <= 74, list);
diff --git a/test/parallel/test-cli-node-print-help.js b/test/parallel/test-cli-node-print-help.js
index ad6da699e41c61..e715fdbcb8f576 100644
--- a/test/parallel/test-cli-node-print-help.js
+++ b/test/parallel/test-cli-node-print-help.js
@@ -27,12 +27,12 @@ function validateNodePrintHelp() {
 
   const cliHelpOptions = [
     { compileConstant: HAVE_OPENSSL,
-      flags: [ '--openssl-config=file', '--tls-cipher-list=val',
+      flags: [ '--openssl-config=...', '--tls-cipher-list=...',
                '--use-bundled-ca', '--use-openssl-ca' ] },
     { compileConstant: NODE_FIPS_MODE,
       flags: [ '--enable-fips', '--force-fips' ] },
     { compileConstant: NODE_HAVE_I18N_SUPPORT,
-      flags: [ '--icu-data-dir=dir', 'NODE_ICU_DATA' ] },
+      flags: [ '--icu-data-dir=...', 'NODE_ICU_DATA' ] },
     { compileConstant: HAVE_INSPECTOR,
       flags: [ '--inspect-brk[=[host:]port]', '--inspect-port=[host:]port',
                '--inspect[=[host:]port]' ] },