# HG changeset patch # User Yuya Nishihara # Date 2018-11-03 10:42:50 # Node ID 840cd57cde32a0ab15593a2e6e1a9968f1035ae2 # Parent 7bffbbe03e903f96d7f1783ea9643f5ad9b309cb 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. diff --git a/mercurial/configitems.py b/mercurial/configitems.py --- a/mercurial/configitems.py +++ b/mercurial/configitems.py @@ -1181,6 +1181,9 @@ coreconfigitem('ui', 'mergemarkertemplat '{ifeq(branch, "default", "", "{branch} ")}' '- {author|user}: {desc|firstline}') ) +coreconfigitem('ui', 'message-output', + default='stdio', +) coreconfigitem('ui', 'nontty', default=False, ) diff --git a/mercurial/help/config.txt b/mercurial/help/config.txt --- a/mercurial/help/config.txt +++ b/mercurial/help/config.txt @@ -2246,6 +2246,14 @@ User interface controls. 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 diff --git a/mercurial/ui.py b/mercurial/ui.py --- a/mercurial/ui.py +++ b/mercurial/ui.py @@ -234,6 +234,8 @@ class ui(object): 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 @@ class ui(object): 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 @@ class ui(object): 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 @@ class ui(object): 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 @@ class ui(object): @fout.setter def fout(self, f): self._fout = f + self._fmsgout, self._fmsgerr = _selectmsgdests(self) @property def ferr(self): @@ -899,6 +905,7 @@ class ui(object): @ferr.setter def ferr(self, f): self._ferr = f + self._fmsgout, self._fmsgerr = _selectmsgdests(self) @property def fin(self): @@ -1364,17 +1371,18 @@ class ui(object): 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 @@ class ui(object): 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 @@ class ui(object): ''' 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 @@ class ui(object): 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 @@ class ui(object): 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 @@ class ui(object): ''' 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 @@ class ui(object): ''' 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 getprogbar(ui): 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) diff --git a/tests/test-basic.t b/tests/test-basic.t --- a/tests/test-basic.t +++ b/tests/test-basic.t @@ -102,3 +102,118 @@ Repository root: 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