ui: add config knob to redirect status messages to stderr (API)
This option can be used to isolate structured output from status messages.
For now, "stdio" (stdout/err pair) and "stderr" are supported. In future
patches, I'll add the "channel" option which will send status messages to
a separate command-server channel with some metadata attached, maybe in
CBOR encoding.
This is a part of the generic templating plan:
https://www.mercurial-scm.org/wiki/GenericTemplatingPlan#Sanity_check_output
.. api::
Status messages may be sent to a dedicated stream depending on
configuration. Don't use ``ui.status()``, etc. as a shorthand for
conditional writes. Use ``ui.write()`` for data output.
--- a/mercurial/configitems.py Sat Nov 03 20:53:31 2018 +0900
+++ b/mercurial/configitems.py Sat Nov 03 19:42:50 2018 +0900
@@ -1181,6 +1181,9 @@
'{ifeq(branch, "default", "", "{branch} ")}'
'- {author|user}: {desc|firstline}')
)
+coreconfigitem('ui', 'message-output',
+ default='stdio',
+)
coreconfigitem('ui', 'nontty',
default=False,
)
--- a/mercurial/help/config.txt Sat Nov 03 20:53:31 2018 +0900
+++ b/mercurial/help/config.txt Sat Nov 03 19:42:50 2018 +0900
@@ -2246,6 +2246,14 @@
Can be overridden per-merge-tool, see the ``[merge-tools]`` section.
+``message-output``
+ Where to write status and error messages. (default: ``stdio``)
+
+ ``stderr``
+ Everything to stderr.
+ ``stdio``
+ Status to stdout, and error to stderr.
+
``origbackuppath``
The path to a directory used to store generated .orig files. If the path is
not a directory, one will be created. If set, files stored in this
--- a/mercurial/ui.py Sat Nov 03 20:53:31 2018 +0900
+++ b/mercurial/ui.py Sat Nov 03 19:42:50 2018 +0900
@@ -234,6 +234,8 @@
self._fout = src._fout
self._ferr = src._ferr
self._fin = src._fin
+ self._fmsgout = src._fmsgout
+ self._fmsgerr = src._fmsgerr
self._finoutredirected = src._finoutredirected
self.pageractive = src.pageractive
self._disablepager = src._disablepager
@@ -259,6 +261,8 @@
self._fout = procutil.stdout
self._ferr = procutil.stderr
self._fin = procutil.stdin
+ self._fmsgout = self.fout # configurable
+ self._fmsgerr = self.ferr # configurable
self._finoutredirected = False
self.pageractive = False
self._disablepager = False
@@ -416,7 +420,7 @@
if self.plain():
for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
- 'logtemplate', 'statuscopies', 'style',
+ 'logtemplate', 'message-output', 'statuscopies', 'style',
'traceback', 'verbose'):
if k in cfg['ui']:
del cfg['ui'][k]
@@ -469,6 +473,7 @@
if section in (None, 'ui'):
# update ui options
+ self._fmsgout, self._fmsgerr = _selectmsgdests(self)
self.debugflag = self.configbool('ui', 'debug')
self.verbose = self.debugflag or self.configbool('ui', 'verbose')
self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
@@ -891,6 +896,7 @@
@fout.setter
def fout(self, f):
self._fout = f
+ self._fmsgout, self._fmsgerr = _selectmsgdests(self)
@property
def ferr(self):
@@ -899,6 +905,7 @@
@ferr.setter
def ferr(self, f):
self._ferr = f
+ self._fmsgout, self._fmsgerr = _selectmsgdests(self)
@property
def fin(self):
@@ -1364,17 +1371,18 @@
If ui is not interactive, the default is returned.
"""
if not self.interactive():
- self.write(msg, ' ', label='ui.prompt')
- self.write(default or '', "\n", label='ui.promptecho')
+ self._write(self._fmsgout, msg, ' ', label='ui.prompt')
+ self._write(self._fmsgout, default or '', "\n",
+ label='ui.promptecho')
return default
- self._writenobuf(self._fout, msg, label='ui.prompt')
+ self._writenobuf(self._fmsgout, msg, label='ui.prompt')
self.flush()
try:
r = self._readline()
if not r:
r = default
if self.configbool('ui', 'promptecho'):
- self.write(r, "\n", label='ui.promptecho')
+ self._write(self._fmsgout, r, "\n", label='ui.promptecho')
return r
except EOFError:
raise error.ResponseExpected()
@@ -1424,13 +1432,15 @@
r = self.prompt(msg, resps[default])
if r.lower() in resps:
return resps.index(r.lower())
- self.write(_("unrecognized response\n"))
+ # TODO: shouldn't it be a warning?
+ self._write(self._fmsgout, _("unrecognized response\n"))
def getpass(self, prompt=None, default=None):
if not self.interactive():
return default
try:
- self.write_err(self.label(prompt or _('password: '), 'ui.prompt'))
+ self._write(self._fmsgerr, prompt or _('password: '),
+ label='ui.prompt')
# disable getpass() only if explicitly specified. it's still valid
# to interact with tty even if fin is not a tty.
with self.timeblockedsection('stdio'):
@@ -1451,7 +1461,7 @@
'''
if not self.quiet:
opts[r'label'] = opts.get(r'label', '') + ' ui.status'
- self.write(*msg, **opts)
+ self._write(self._fmsgout, *msg, **opts)
def warn(self, *msg, **opts):
'''write warning message to output (stderr)
@@ -1459,7 +1469,7 @@
This adds an output label of "ui.warning".
'''
opts[r'label'] = opts.get(r'label', '') + ' ui.warning'
- self.write_err(*msg, **opts)
+ self._write(self._fmsgerr, *msg, **opts)
def error(self, *msg, **opts):
'''write error message to output (stderr)
@@ -1467,7 +1477,7 @@
This adds an output label of "ui.error".
'''
opts[r'label'] = opts.get(r'label', '') + ' ui.error'
- self.write_err(*msg, **opts)
+ self._write(self._fmsgerr, *msg, **opts)
def note(self, *msg, **opts):
'''write note to output (if ui.verbose is True)
@@ -1476,7 +1486,7 @@
'''
if self.verbose:
opts[r'label'] = opts.get(r'label', '') + ' ui.note'
- self.write(*msg, **opts)
+ self._write(self._fmsgout, *msg, **opts)
def debug(self, *msg, **opts):
'''write debug message to output (if ui.debugflag is True)
@@ -1485,7 +1495,7 @@
'''
if self.debugflag:
opts[r'label'] = opts.get(r'label', '') + ' ui.debug'
- self.write(*msg, **opts)
+ self._write(self._fmsgout, *msg, **opts)
def edit(self, text, user, extra=None, editform=None, pending=None,
repopath=None, action=None):
@@ -1939,3 +1949,11 @@
def haveprogbar():
return _progresssingleton is not None
+
+def _selectmsgdests(ui):
+ name = ui.config(b'ui', b'message-output')
+ if name == b'stdio':
+ return ui.fout, ui.ferr
+ if name == b'stderr':
+ return ui.ferr, ui.ferr
+ raise error.Abort(b'invalid ui.message-output destination: %s' % name)
--- a/tests/test-basic.t Sat Nov 03 20:53:31 2018 +0900
+++ b/tests/test-basic.t Sat Nov 03 19:42:50 2018 +0900
@@ -102,3 +102,118 @@
At the end...
$ cd ..
+
+Status message redirection:
+
+ $ hg init empty
+
+ status messages are sent to stdout by default:
+
+ $ hg outgoing -R t empty -Tjson 2>/dev/null
+ comparing with empty
+ searching for changes
+ [
+ {
+ "bookmarks": [],
+ "branch": "default",
+ "date": [0, 0],
+ "desc": "test",
+ "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
+ "parents": ["0000000000000000000000000000000000000000"],
+ "phase": "draft",
+ "rev": 0,
+ "tags": ["tip"],
+ "user": "test"
+ }
+ ]
+
+ which can be configured to send to stderr, so the output wouldn't be
+ interleaved:
+
+ $ cat <<'EOF' >> "$HGRCPATH"
+ > [ui]
+ > message-output = stderr
+ > EOF
+ $ hg outgoing -R t empty -Tjson 2>/dev/null
+ [
+ {
+ "bookmarks": [],
+ "branch": "default",
+ "date": [0, 0],
+ "desc": "test",
+ "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
+ "parents": ["0000000000000000000000000000000000000000"],
+ "phase": "draft",
+ "rev": 0,
+ "tags": ["tip"],
+ "user": "test"
+ }
+ ]
+ $ hg outgoing -R t empty -Tjson >/dev/null
+ comparing with empty
+ searching for changes
+
+ this option should be turned off by HGPLAIN= since it may break scripting use:
+
+ $ HGPLAIN= hg outgoing -R t empty -Tjson 2>/dev/null
+ comparing with empty
+ searching for changes
+ [
+ {
+ "bookmarks": [],
+ "branch": "default",
+ "date": [0, 0],
+ "desc": "test",
+ "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
+ "parents": ["0000000000000000000000000000000000000000"],
+ "phase": "draft",
+ "rev": 0,
+ "tags": ["tip"],
+ "user": "test"
+ }
+ ]
+
+ but still overridden by --config:
+
+ $ HGPLAIN= hg outgoing -R t empty -Tjson --config ui.message-output=stderr \
+ > 2>/dev/null
+ [
+ {
+ "bookmarks": [],
+ "branch": "default",
+ "date": [0, 0],
+ "desc": "test",
+ "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
+ "parents": ["0000000000000000000000000000000000000000"],
+ "phase": "draft",
+ "rev": 0,
+ "tags": ["tip"],
+ "user": "test"
+ }
+ ]
+
+Invalid ui.message-output option:
+
+ $ hg log -R t --config ui.message-output=bad
+ abort: invalid ui.message-output destination: bad
+ [255]
+
+Underlying message streams should be updated when ui.fout/ferr are set:
+
+ $ cat <<'EOF' > capui.py
+ > from mercurial import pycompat, registrar
+ > cmdtable = {}
+ > command = registrar.command(cmdtable)
+ > @command(b'capui', norepo=True)
+ > def capui(ui):
+ > out = ui.fout
+ > ui.fout = pycompat.bytesio()
+ > ui.status(b'status\n')
+ > ui.ferr = pycompat.bytesio()
+ > ui.warn(b'warn\n')
+ > out.write(b'stdout: %s' % ui.fout.getvalue())
+ > out.write(b'stderr: %s' % ui.ferr.getvalue())
+ > EOF
+ $ hg --config extensions.capui=capui.py --config ui.message-output=stdio capui
+ stdout: status
+ stderr: warn