Skip to content

Instantly share code, notes, and snippets.

@pvdb
Last active February 16, 2025 17:42
Show Gist options
  • Save pvdb/da0d384cbf98907f507e019844de8e2d to your computer and use it in GitHub Desktop.
Save pvdb/da0d384cbf98907f507e019844de8e2d to your computer and use it in GitHub Desktop.
wrap a non-interactive command in an interactive REPL _(read-eval-print loop)_
#!/usr/bin/env ruby
# frozen_string_literal: true
REPL_NAME = 'repl'
REPL_VERSION = '1.0.0'
#
# INSTALLATION
#
# ln -s ${PWD}/repl $(brew --prefix)/bin/
# sudo ln -s ${PWD}/repl /usr/local/bin/
#
# DEPENDENCIES
#
# brew install rlwrap # optional, but recommended
#
##
# run `repl` as a subcommand of `git`, `brew`, `rbenv`, etc.
name = File.basename($PROGRAM_NAME) # `git-repl`, `brew-repl`, etc.
match = name.match(/\A(?<cmd>.+)-repl\z/) # <cmd> === `git`, `brew`, etc.
exec('repl', match[:cmd], *ARGV) if match # 'git repl *' --> 'repl git *'
require 'mkmf'
require 'English'
require 'benchmark'
require 'fileutils'
require 'shellwords'
require 'io/console'
##
# script-specific IO::console extension
class IO
def console.log(message, color, width: 78, debug: false, quiet: false)
# suppress output when in quiet mode
return if quiet
# deduct length of '===[]==' (7 characters)
max_msg_length = width < 78 ? 71 : (width - 7)
# truncate message if too long
log_msg = if message.length > max_msg_length
"#{message[0, (max_msg_length - 3)]}..."
else
message
end
# add padding to message to fill overall width
padding = '=' * (max_msg_length - log_msg.length)
puts debug ? "===[#{log_msg.send(color)}]==#{padding}" : message
end
end
##
# script-specific Kernel extension
module Kernel
def which(executable)
MakeMakefile.find_executable0(executable)
end
end
##
# script-specific String extensions
class String
def colorize(color_code)
"\e[#{color_code}m#{self}\e[0m"
end
def strip_ansi
gsub(/\e\[(\d+)(;\d+)*m/, '')
end
# rubocop:disable Style/SingleLineMethods
def bold() colorize('1'); end
def invert() colorize('7'); end
def red() colorize('31'); end
def green() colorize('32'); end
def yellow() colorize('33'); end
def blue() colorize('34'); end
# rubocop:enable Style/SingleLineMethods
def true?
strip == 'true'
end
def comment?
strip.start_with? '#'
end
end
##
# version of rlwrap utility
def rlwrap_version
`rlwrap --version`.chomp
rescue Errno::ENOENT
'rlwrap not found'
end
##
# version of Ruby runtime
def runtime_version
"#{RUBY_ENGINE} #{RUBY_VERSION}"
end
##
# version of repl script
def repl_version
"#{REPL_NAME} #{REPL_VERSION} (#{rlwrap_version}, #{runtime_version})"
end
if ARGV.delete('--version')
IO.console.puts(repl_version)
exit(0)
end
if ARGV.delete('--help')
IO.console.puts(DATA.read)
exit(0)
end
if ARGV.delete('--man')
exec('man', File.join(__dir__, 'repl.1'))
exit(0) # superfluous, really...
end
if ARGV.delete('--html')
exec('open', File.join(__dir__, 'repl.1.html'))
exit(0) # superfluous, really...
end
# list of remaining options (pipeline and logging)
OPTIONS = %w[--stdin --printf --escape --debug --quiet].freeze
##
# is repl running "inside" rlwrap?
def repl_wrapped?
!ENV['__RLWRAP_REPL__'].nil?
end
##
# is repl interactive (not piped)?
def interactive?
$stdin.tty?
end
##
# location of the REPL configuration file
def repl_conf
ENV.fetch('REPL_CONF', File.join(Dir.home, '.repl.conf'))
end
unless repl_wrapped?
# merge additional REPL config into ENV
if File.file?(repl_conf)
File.readlines(repl_conf, chomp: true).each do |line|
next if line.empty?
next if line.comment?
key, value = line.split(/\s*=\s*/, 2).map(&:strip)
# strip surrounding quotes
value.delete_prefix!('"')
value.delete_suffix!('"')
ENV[key] ||= value # ENV takes precedence
end
end
# replace process with `rlwrap`-ed version
# if `rlwrap` is installed and also `repl`
# is running interactively (and not piped)
if interactive? && (rlwrap_exec = which('rlwrap'))
rlwrap_args = []
# suppress all default rlwrap break characters
# specifically the '-' (hyphen/dash) character
# note that whitespace is always word-breaking
rlwrap_args += ['-b', "''"]
# rubocop:disable Metrics/BlockNesting
# rubocop:disable Style/SoleNestedConditional
unless (base_cmd = (ARGV - OPTIONS).map(&:strip).first).nil?
if File.executable?(base_cmd) || !which(base_cmd).nil?
base_cmd = File.basename(base_cmd)
default_history_dir = Dir.home
history_dir = ENV['REPL_HISTORY_DIR'] || default_history_dir
if File.directory? history_dir = File.expand_path(history_dir)
history_file = File.join(history_dir, ".#{base_cmd}_history")
rlwrap_args += ['-H', history_file]
end
default_completion_dir = File.join(Dir.home, '.repl')
completion_dir = ENV['REPL_COMPLETION_DIR'] || default_completion_dir
if File.directory? completion_dir = File.expand_path(completion_dir)
if File.exist? completion_file = File.join(completion_dir, base_cmd)
rlwrap_args += ['-f', completion_file]
end
end
end
end
# rubocop:enable Style/SoleNestedConditional
# rubocop:enable Metrics/BlockNesting
ENV['__RLWRAP_REPL__'] = Process.pid.to_s
exec(rlwrap_exec, *rlwrap_args, $PROGRAM_NAME, *ARGV)
end
end
# process the pipeline options
stdin = ARGV.delete('--stdin')
printf = ARGV.delete('--printf')
escape = ARGV.delete('--escape')
# process the logging options (can be set in ~/.repl.conf or ENV)
debug = ARGV.delete('--debug') || ENV['REPL_DEBUG']&.true?
quiet = ARGV.delete('--quiet') || ENV['REPL_QUIET']&.true?
# command is whatever's left
if (cmd_string = ARGV.join(' ').strip).empty?
IO.console.puts('No command specified... use `--help`')
exit(0)
end
cmd_template = if stdin
pipe_cmd = which(printf ? 'printf' : 'echo')
"#{pipe_cmd} \"%s\" | #{cmd_string}"
elsif ARGV.grep(/%s/).any?
cmd_string # the '%s' is embedded
else
"#{cmd_string} %s"
end
cmd_prompt = if debug
if repl_wrapped?
"rlwrap(repl(\"#{cmd_template.blue}\"))"
else
"repl(\"#{cmd_template.blue}\")"
end
else
"\"#{cmd_template.blue}\""
end
log_options = {
debug: debug, quiet: quiet,
width: cmd_prompt.strip_ansi.length
}.freeze
repl_prompt = ENV.fetch('REPL_PROMPT', '>>')
full_prompt = "#{cmd_prompt} #{repl_prompt}"
# rubocop:disable Metrics/BlockLength
loop do
# prompt user for cmd arguments
IO.console.print(full_prompt, ' ') if interactive? || !quiet
line = begin
$stdin.gets&.strip # nil when ^D
rescue Interrupt
nil # Interrupt is raised for ^C
end
# echo input if read from piped stdin
IO.console.puts(line) unless interactive? || quiet
# terminate `repl` loop on (^C|^D|EOF)
break unless line
unless line.empty? || line.comment?
line = Shellwords.escape(line) if escape
# command = format(cmd_template, line) # expand single '%s' placeholder
command = cmd_template.gsub('%s', line) # expand _all_ '%s' placeholders
begin
# print "expanded" command to be executed
message = "Executing: '#{command}'"
IO.console.log(message, :blue, **log_options)
tms = Benchmark.measure do
system(command, exception: true)
rescue Interrupt
# print message when command is interrupted
message = 'Command was interrupted'
IO.console.log(message, :red, **log_options)
end
# print elapsed real time to execute command
message = format('Command took %.2fs to execute', tms.real)
IO.console.log(message, :green, **log_options)
rescue RuntimeError, Errno::ENOENT
# print exception message when command fails
message = $ERROR_INFO.message
IO.console.log(message, :red, **log_options)
next unless line =~ /(quit|exit)/i
# print message when command fails due to 'quit'/'exit'
message = 'Use ^C or ^D to exit repl'
IO.console.log(message, :yellow, **log_options)
end
end
# empty separator line
IO.console.puts if interactive? || !quiet
end
# rubocop:enable Metrics/BlockLength
exit(0) # cleanly exit repl
__END__
Usage: repl [options] command ...
Options:
--version Display repl version information
--help Display repl usage information
--man Display the repl man page
--html Open HTML version of man page
--stdin Pipe input to command's STDIN
--printf Avoid newline chars in STDIN
--escape Shell escape user's input
--debug Display each command being executed
--quiet Don't echo the prompt in pipelines
Homepage:
http://github.com/pvdb/repl
Bug reports, suggestions, updates:
http://github.com/pvdb/repl/issues
That's all Folks!
.\" generated with Ronn-NG/v0.10.1
.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
.TH "REPL" "1" "January 2025" "PVDB" "Awesome Utilities"
.SH "NAME"
\fBrepl\fR \- sometimes you \fIreally\fR need a repl
.SH "SYNOPSIS"
\fBrepl\fR \fI[options]\fR \fIcommand\fR <\|\.\|\.\|\.>
.SH "DESCRIPTION"
\fBrepl\fR wraps a non\-interactive \fBcommand\fR in an interactive read\-eval\-print\-loop prompt\. Each line you type into the prompt is executed as arguments to \fBcommand\fR\. Anything written to standard output or standard error by the \fBcommand\fR is displayed\.
.P
If you have \fBrlwrap(1)\fR installed you'll automatically get the full benefits of readline: history, reverse searches, etc\.
.P
\fBrepl\fR is meant to wrap programs which accept command line arguments and print to the standard output\. It keeps no state between executed lines and, as such, cannot be used to replace \fBirb\fR or the Python REPL (for example)\.
.SH "EXAMPLES"
Using \fBrepl\fR with \fBredis\-cli\fR:
.IP "" 4
.nf
$ repl redis\-cli
>> set name chris
OK
>> get name
chris
>> info
redis_version:1\.000
uptime_in_seconds:182991
uptime_in_days:2
\&\.\. etc \.\.
.fi
.IP "" 0
.P
Using \fBrepl\fR with Ruby's \fBgem\fR:
.IP "" 4
.nf
$ repl gem
>> \-\-version
1\.3\.5
>> search yajl
*** LOCAL GEMS ***
yajl\-ruby (0\.6\.7)
>> search yajl \-r
*** REMOTE GEMS ***
brianmario\-yajl\-ruby (0\.6\.3)
filipegiusti\-yajl\-ruby (0\.6\.4)
jdg\-yajl\-ruby (0\.5\.12)
oortle\-yajl\-ruby (0\.5\.8)
yajl\-ruby (0\.6\.7)
.fi
.IP "" 0
.P
Using \fBrepl\fR with \fBgit\fR:
.IP "" 4
.nf
$ repl git
>> branch
gh\-pages
* master
>> tag
rm
v0\.1\.0
v0\.1\.1
v0\.1\.2
v0\.1\.3
>> tag \-d rm
Deleted tag 'rm'
>> pwd
git: 'pwd' is not a git\-command\. See 'git \-\-help'\.
Did you mean this?
add
.fi
.IP "" 0
.SH "OPTIONS"
.TP
\fB\-\-version\fR
Display \fBrepl\fR version information\.
.TP
\fB\-\-help\fR
Display \fBrepl\fR usage information\.
.TP
\fB\-\-man\fR
Display the \fBrepl\fR man page
.TP
\fB\-\-html\fR
Open HTML version of man page
.TP
\fB\-\-debug\fR
Display each command being executed
.TP
\fB\-\-quiet\fR
Don't echo the prompt in pipelines
.SH "COMPLETION"
Because \fBrlwrap\fR supports completion, \fBrepl\fR does too\. Any file in \fB~/\.repl\fR matching the name of the command you start \fBrepl\fR with will be used for completion\.
.P
For instance, a file named \fB~/\.repl/redis\-cli\fR containing "get set info" will cause "get", "set", and "info" to be tab completeable at the \fBrepl redis\-cli\fR prompt\.
.P
The directory searched for completion files can be configured using the \fBREPL_COMPLETION_DIR\fR environment variable\.
.SH "COMMAND HISTORY"
Because \fBrlwrap\fR supports command history, \fBrepl\fR does too\. Any file in \fB~/\fR matching the name of the command you start \fBrepl\fR with prefix with a dot and suffixed with "_history" will be used for completion\.
.P
For instance, a file named \fB~/\.redis\-cli_history\fR containing a newline separated list of "get set info" will cause "get", "set", and "info" to be reachable using the up arrow as command history at the \fBrepl redis\-cli\fR prompt\.
.P
The directory searched for history files can be configured using the \fBREPL_HISTORY_DIR\fR environment variable\.
.SH "ENVIRONMENT"
The following environment variables can be used to configure \fBrepl\fR's behaviour, and can be set permanently in the \fB~/\.repl\.conf\fR configuration file:
.SS "REPL_PROMPT"
the prompt to display before each line of input (defaults to \fB>>\fR)
.SS "REPL_DEBUG"
print out the expanded command before executing it
.SS "REPL_QUIET"
suppress superfluous output when inside a pipeline
.SS "REPL_HISTORY_DIR"
directory in which command history files are kept
.SS "REPL_COMPLETION_DIR"
directory in which command completion files are kept
.SH "HOMEPAGE"
\fIhttp://github\.com/pvdb/repl\fR
.P
You will find many more practical examples there!
.SH "BUGS"
\fIhttp://github\.com/pvdb/repl/issues\fR
.SH "AUTHOR"
Peter Vandenberk :: @pvdb \fIhttps://github\.com/pvdb\fR
.SH "CREDITS"
Chris Wanstrath :: @defunkt \fIhttps://github\.com/defunkt\fR
.P
Check out his (awesome, but unmaintained) original version \fIhttps://github\.com/defunkt/repl\fR on which this one is based!
.SH "SEE ALSO"
rlwrap(1), readline(3)
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='content-type' content='text/html;charset=utf-8'>
<meta name='generator' content='Ronn-NG/v0.10.1 (http://github.com/apjanke/ronn-ng/tree/0.10.1)'>
<title>repl(1) - sometimes you &lt;em&gt;really&lt;/em&gt; need a repl</title>
<style type='text/css' media='all'>
/* style: man */
body#manpage {margin:0}
.mp {max-width:100ex;padding:0 9ex 1ex 4ex}
.mp p,.mp pre,.mp ul,.mp ol,.mp dl {margin:0 0 20px 0}
.mp h2 {margin:10px 0 0 0}
.mp > p,.mp > pre,.mp > ul,.mp > ol,.mp > dl {margin-left:8ex}
.mp h3 {margin:0 0 0 4ex}
.mp dt {margin:0;clear:left}
.mp dt.flush {float:left;width:8ex}
.mp dd {margin:0 0 0 9ex}
.mp h1,.mp h2,.mp h3,.mp h4 {clear:left}
.mp pre {margin-bottom:20px}
.mp pre+h2,.mp pre+h3 {margin-top:22px}
.mp h2+pre,.mp h3+pre {margin-top:5px}
.mp img {display:block;margin:auto}
.mp h1.man-title {display:none}
.mp,.mp code,.mp pre,.mp tt,.mp kbd,.mp samp,.mp h3,.mp h4 {font-family:monospace;font-size:14px;line-height:1.42857142857143}
.mp h2 {font-size:16px;line-height:1.25}
.mp h1 {font-size:20px;line-height:2}
.mp {text-align:justify;background:#fff}
.mp,.mp code,.mp pre,.mp pre code,.mp tt,.mp kbd,.mp samp {color:#131211}
.mp h1,.mp h2,.mp h3,.mp h4 {color:#030201}
.mp u {text-decoration:underline}
.mp code,.mp strong,.mp b {font-weight:bold;color:#131211}
.mp em,.mp var {font-style:italic;color:#232221;text-decoration:none}
.mp a,.mp a:link,.mp a:hover,.mp a code,.mp a pre,.mp a tt,.mp a kbd,.mp a samp {color:#0000ff}
.mp b.man-ref {font-weight:normal;color:#434241}
.mp pre {padding:0 4ex}
.mp pre code {font-weight:normal;color:#434241}
.mp h2+pre,h3+pre {padding-left:0}
ol.man-decor,ol.man-decor li {margin:3px 0 10px 0;padding:0;float:left;width:33%;list-style-type:none;text-transform:uppercase;color:#999;letter-spacing:1px}
ol.man-decor {width:100%}
ol.man-decor li.tl {text-align:left}
ol.man-decor li.tc {text-align:center;letter-spacing:4px}
ol.man-decor li.tr {text-align:right;float:right}
</style>
</head>
<!--
The following styles are deprecated and will be removed at some point:
div#man, div#man ol.man, div#man ol.head, div#man ol.man.
The .man-page, .man-decor, .man-head, .man-foot, .man-title, and
.man-navigation should be used instead.
-->
<body id='manpage'>
<div class='mp' id='man'>
<div class='man-navigation' style='display:none'>
<a href="#NAME">NAME</a>
<a href="#SYNOPSIS">SYNOPSIS</a>
<a href="#DESCRIPTION">DESCRIPTION</a>
<a href="#EXAMPLES">EXAMPLES</a>
<a href="#OPTIONS">OPTIONS</a>
<a href="#COMPLETION">COMPLETION</a>
<a href="#COMMAND-HISTORY">COMMAND HISTORY</a>
<a href="#ENVIRONMENT">ENVIRONMENT</a>
<a href="#HOMEPAGE">HOMEPAGE</a>
<a href="#BUGS">BUGS</a>
<a href="#AUTHOR">AUTHOR</a>
<a href="#CREDITS">CREDITS</a>
<a href="#SEE-ALSO">SEE ALSO</a>
</div>
<ol class='man-decor man-head man head'>
<li class='tl'>repl(1)</li>
<li class='tc'>Awesome Utilities</li>
<li class='tr'>repl(1)</li>
</ol>
<h2 id="NAME">NAME</h2>
<p class="man-name">
<code>repl</code> - <span class="man-whatis">sometimes you <em>really</em> need a repl</span>
</p>
<h2 id="SYNOPSIS">SYNOPSIS</h2>
<p><code>repl</code> <var>[options]</var> <var>command</var> &lt;...&gt;</p>
<h2 id="DESCRIPTION">DESCRIPTION</h2>
<p><code>repl</code> wraps a non-interactive <code>command</code> in an interactive
read-eval-print-loop prompt. Each line you type into the prompt is
executed as arguments to <code>command</code>. Anything written to standard
output or standard error by the <code>command</code> is displayed.</p>
<p>If you have <code>rlwrap(1)</code> installed you'll automatically get the full
benefits of readline: history, reverse searches, etc.</p>
<p><code>repl</code> is meant to wrap programs which accept command line arguments
and print to the standard output. It keeps no state between executed
lines and, as such, cannot be used to replace <code>irb</code> or the Python
REPL (for example).</p>
<h2 id="EXAMPLES">EXAMPLES</h2>
<p>Using <code>repl</code> with <code>redis-cli</code>:</p>
<pre><code>$ repl redis-cli
&gt;&gt; set name chris
OK
&gt;&gt; get name
chris
&gt;&gt; info
redis_version:1.000
uptime_in_seconds:182991
uptime_in_days:2
.. etc ..
</code></pre>
<p>Using <code>repl</code> with Ruby's <code>gem</code>:</p>
<pre><code>$ repl gem
&gt;&gt; --version
1.3.5
&gt;&gt; search yajl
*** LOCAL GEMS ***
yajl-ruby (0.6.7)
&gt;&gt; search yajl -r
*** REMOTE GEMS ***
brianmario-yajl-ruby (0.6.3)
filipegiusti-yajl-ruby (0.6.4)
jdg-yajl-ruby (0.5.12)
oortle-yajl-ruby (0.5.8)
yajl-ruby (0.6.7)
</code></pre>
<p>Using <code>repl</code> with <code>git</code>:</p>
<pre><code>$ repl git
&gt;&gt; branch
gh-pages
* master
&gt;&gt; tag
rm
v0.1.0
v0.1.1
v0.1.2
v0.1.3
&gt;&gt; tag -d rm
Deleted tag 'rm'
&gt;&gt; pwd
git: 'pwd' is not a git-command. See 'git --help'.
Did you mean this?
add
</code></pre>
<h2 id="OPTIONS">OPTIONS</h2>
<dl>
<dt><code>--version</code></dt>
<dd>Display <code>repl</code> version information.</dd>
<dt><code>--help</code></dt>
<dd>Display <code>repl</code> usage information.</dd>
<dt><code>--man</code></dt>
<dd>Display the <code>repl</code> man page</dd>
<dt><code>--html</code></dt>
<dd>Open HTML version of man page</dd>
<dt><code>--debug</code></dt>
<dd>Display each command being executed</dd>
<dt><code>--quiet</code></dt>
<dd>Don't echo the prompt in pipelines</dd>
</dl>
<h2 id="COMPLETION">COMPLETION</h2>
<p>Because <code>rlwrap</code> supports completion, <code>repl</code> does too. Any file in
<code>~/.repl</code> matching the name of the command you start <code>repl</code> with will
be used for completion.</p>
<p>For instance, a file named <code>~/.repl/redis-cli</code> containing "get set
info" will cause "get", "set", and "info" to be tab completeable at
the <code>repl redis-cli</code> prompt.</p>
<p>The directory searched for completion files can be configured using
the <code>REPL_COMPLETION_DIR</code> environment variable.</p>
<h2 id="COMMAND-HISTORY">COMMAND HISTORY</h2>
<p>Because <code>rlwrap</code> supports command history, <code>repl</code> does too. Any file in
<code>~/</code> matching the name of the command you start <code>repl</code> with prefix
with a dot and suffixed with "_history" will be used for completion.</p>
<p>For instance, a file named <code>~/.redis-cli_history</code> containing a newline
separated list of "get set info" will cause "get", "set", and "info"
to be reachable using the up arrow as command history at the <code>repl
redis-cli</code> prompt.</p>
<p>The directory searched for history files can be configured using the
<code>REPL_HISTORY_DIR</code> environment variable.</p>
<h2 id="ENVIRONMENT">ENVIRONMENT</h2>
<p>The following environment variables can be used to configure <code>repl</code>'s behaviour, and can be set permanently in the <code>~/.repl.conf</code> configuration file:</p>
<h3 id="REPL_PROMPT">REPL_PROMPT</h3>
<p>the prompt to display before each line of input (defaults to <code>&gt;&gt;</code>)</p>
<h3 id="REPL_DEBUG">REPL_DEBUG</h3>
<p>print out the expanded command before executing it</p>
<h3 id="REPL_QUIET">REPL_QUIET</h3>
<p>suppress superfluous output when inside a pipeline</p>
<h3 id="REPL_HISTORY_DIR">REPL_HISTORY_DIR</h3>
<p>directory in which command history files are kept</p>
<h3 id="REPL_COMPLETION_DIR">REPL_COMPLETION_DIR</h3>
<p>directory in which command completion files are kept</p>
<h2 id="HOMEPAGE">HOMEPAGE</h2>
<p><a href="http://github.com/pvdb/repl" data-bare-link="true">http://github.com/pvdb/repl</a></p>
<p>You will find many more practical examples there!</p>
<h2 id="BUGS">BUGS</h2>
<p><a href="http://github.com/pvdb/repl/issues" data-bare-link="true">http://github.com/pvdb/repl/issues</a></p>
<h2 id="AUTHOR">AUTHOR</h2>
<p>Peter Vandenberk :: <a href="https://github.com/pvdb">@pvdb</a></p>
<h2 id="CREDITS">CREDITS</h2>
<p>Chris Wanstrath :: <a href="https://github.com/defunkt">@defunkt</a></p>
<p>Check out his (awesome, but unmaintained) <a href="https://github.com/defunkt/repl">original version</a> on which this one is based!</p>
<h2 id="SEE-ALSO">SEE ALSO</h2>
<p><span class="man-ref">rlwrap<span class="s">(1)</span></span>, <span class="man-ref">readline<span class="s">(3)</span></span></p>
<ol class='man-decor man-foot man foot'>
<li class='tl'>PVDB</li>
<li class='tc'>January 2025</li>
<li class='tr'>repl(1)</li>
</ol>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment