Merge pull request #607 from scopatz/xonfig

Xonfig
This commit is contained in:
adam j hartz 2016-01-13 20:57:27 -05:00
commit 57e7c1f438
20 changed files with 1526 additions and 300 deletions

1
.gitignore vendored
View file

@ -16,6 +16,7 @@ build/
dist/
xonsh.egg-info/
docs/_build/
docs/envvarsbody
# temporary files from vim and emacs
*~

View file

@ -6,6 +6,10 @@ Current Developments
====================
**Added:**
* New configuration utility 'xonfig' which reports current system
setup information and creates config files through an interactive
wizard.
* Toolkit for creating wizards now available
* timeit and which aliases will now complete their arguments.
* $COMPLETIONS_MENU_ROWS environment variable controls the size of the
tab-completion menu in prompt-toolkit.
@ -14,6 +18,10 @@ Current Developments
**Changed:**
* The xonfig wizard will run on interactive startup if no configuration
file is found.
* BaseShell now has a singleline() method for prompting a single input.
* Environment variable docs are now auto-generated.
* Prompt-toolkit shell will now dynamically allocate space for the
tab-completion menu.
* Looking up nonexistent environment variables now generates an error

View file

@ -56,3 +56,5 @@ For those of you who want the gritty details.
main
pyghooks
jupyter_kernel
wizard
xonfig

11
docs/api/wizard.rst Normal file
View file

@ -0,0 +1,11 @@
.. _xonsh_wizard:
******************************************
Wizard Making Tools (``xonsh.wizard``)
******************************************
.. automodule:: xonsh.wizard
:members:
:undoc-members:
:inherited-members:

11
docs/api/xonfig.rst Normal file
View file

@ -0,0 +1,11 @@
.. _xonsh_xonfig:
***********************************************
Xonsh Configuration Utility (``xonsh.xonfig``)
***********************************************
.. automodule:: xonsh.xonfig
:members:
:undoc-members:
:inherited-members:

View file

@ -10,6 +10,7 @@
import sys, os
from xonsh import __version__ as XONSH_VERSION
from xonsh.environ import DEFAULT_DOCS, Env
# -- General configuration -----------------------------------------------------
@ -230,3 +231,43 @@ autosummary_generate = []
# Prevent numpy from making silly tables
numpydoc_show_class_members = False
#
# Auto-generate some docs
#
def make_envvars():
env = Env()
vars = sorted(DEFAULT_DOCS.keys())
s = ('.. list-table::\n'
' :header-rows: 0\n\n')
table = []
ncol = 3
row = ' {0} - :ref:`${1} <{2}>`'
for i, var in enumerate(vars):
star = '*' if i%ncol == 0 else ' '
table.append(row.format(star, var, var.lower()))
table.extend([' -']*((ncol - len(vars)%ncol)%ncol))
s += '\n'.join(table) + '\n\n'
s += ('Listing\n'
'-------\n\n')
sec = ('.. _{low}:\n\n'
'{title}\n'
'{under}\n'
'{docstr}\n\n'
'**configurable:** {configurable}\n\n'
'**default:** {default}\n\n'
'-------\n\n')
for var in vars:
title = '$' + var
under = '.' * len(title)
vd = env.get_docs(var)
s += sec.format(low=var.lower(), title=title, under=under,
docstr=vd.docstr, configurable=vd.configurable,
default=vd.default)
s = s[:-9]
fname = os.path.join(os.path.dirname(__file__), 'envvarsbody')
with open(fname, 'w') as f:
f.write(s)
make_envvars()

View file

@ -4,241 +4,4 @@ The following table displays information about the environment variables that
effect XONSH performance in some way. It also lists their default values, if
applicable.
.. Please keep the following in alphabetic order - scopatz
.. list-table::
:widths: 1 1 3
:header-rows: 1
* - variable
- default
- description
* - ANSICON
- No default set
- This is used on Windows to set the title, if available.
* - AUTO_CD
- ``False``
- Flag to enable changing to a directory by entering the dirname or full path only (without the `cd` command)
* - AUTO_PUSHD
- ``False``
- Flag for automatically pushing directories onto the directory stack.
* - AUTO_SUGGEST
- ``True``
- Enable automatic command suggestions based on history (like in fish shell).
Pressing the right arrow key inserts the currently displayed suggestion.
(Only usable with SHELL_TYPE=prompt_toolkit)
* - BASH_COMPLETIONS
- Normally this is ``('/etc/bash_completion', '/usr/share/bash-completion/completions/git')``
but on Mac is ``('/usr/local/etc/bash_completion', '/opt/local/etc/profile.d/bash_completion.sh')``
and on Arch Linux is ``('/usr/share/bash-completion/bash_completion',
'/usr/share/bash-completion/completions/git')``.
- This is a list (or tuple) of strings that specifies where the BASH completion
files may be found. The default values are platform dependent, but sane.
To specify an alternate list, do so in the run control file.
* - CASE_SENSITIVE_COMPLETIONS
- ``True`` on Linux, otherwise ``False``
- Sets whether completions should be case sensitive or case insensitive.
* - CDPATH
- ``[]``
- A list of paths to be used as roots for a ``cd``, breaking compatibility with
bash, xonsh always prefer an existing relative path.
* - COMPLETIONS_DISPLAY
- ``'multi'``
- Configure if and how Python completions are displayed by the prompt_toolkit shell.
This option does not affect bash completions, auto-suggestions etc.
Changing it at runtime will take immediate effect, so you can quickly
disable and enable completions during shell sessions.
- If COMPLETIONS_DISPLAY is ``'none'`` or ``'false'``, do not display those completions.
- If COMPLETIONS_DISPLAY is ``'single'``, display completions in a single column while typing.
- If COMPLETIONS_DISPLAY is ``'multi'`` or ``'true'``, display completions in multiple columns while typing.
These option values are not case- or type-sensitive, so e.g.
writing ``$COMPLETIONS_DISPLAY = None`` and ``$COMPLETIONS_DISPLAY = 'none'`` is equivalent.
(Only usable with SHELL_TYPE=prompt_toolkit)
* - COMPLETIONS_MENU_ROWS
- ``5``
- Number of rows to reserve for tab-completions menu if
``$COMPLETIONS_DISPLAY`` is ``'single'`` or ``'multi'``. This only
effects the prompt-toolkit shell.
* - DIRSTACK_SIZE
- ``20``
- Maximum size of the directory stack.
* - EXPAND_ENV_VARS
- ``True``
- Toggles whether environment variables are expanded inside of strings in subprocess mode.
* - FORCE_POSIX_PATHS
- ``False``
- Forces forward slashes (``/``) on Windows systems when using auto completion if
set to anything truthy.
* - FORMATTER_DICT
- xonsh.environ.FORMATTER_DICT
- Dictionary containing variables to be used when formatting PROMPT and TITLE
see `Customizing the Prompt <tutorial.html#customizing-the-prompt>`_.
* - HISTCONTROL
- ``set([])``
- A set of strings (comma-separated list in string form) of options that
determine what commands are saved to the history list. By default all
commands are saved. The option ``ignoredups`` will not save the command
if it matches the previous command. The option ``ignoreerr`` will cause
any commands that fail (i.e. return non-zero exit status) to not be
added to the history list.
* - IGNOREEOF
- ``False``
- Prevents Ctrl-D from exiting the shell.
* - INDENT
- ``' '``
- Indentation string for multiline input
* - MOUSE_SUPPORT
- ``False``
- Enable mouse support in the prompt_toolkit shell.
This allows clicking for positioning the cursor or selecting a completion. In some terminals
however, this disables the ability to scroll back through the history of the terminal.
(Only usable with SHELL_TYPE=prompt_toolkit)
* - MULTILINE_PROMPT
- ``'.'``
- Prompt text for 2nd+ lines of input, may be str or function which returns
a str.
* - OLDPWD
- No default
- Used to represent a previous present working directory.
* - PATH
- ``()``
- List of strings representing where to look for executables.
* - PATHEXT
- ``()``
- List of strings for filtering valid exeutables by.
* - PROMPT
- xonsh.environ.DEFAULT_PROMPT
- The prompt text. May contain keyword arguments which are auto-formatted,
see `Customizing the Prompt <tutorial.html#customizing-the-prompt>`_.
* - PROMPT_TOOLKIT_COLORS
- ``{}``
- This is a mapping of from color names to HTML color codes. Whenever
prompt-toolkit would color a word a particular color (in the prompt, or
in syntax highlighting), it will use the value specified here to
represent that color, instead of its default. If a color is not
specified here, prompt-toolkit uses the colors from
``xonsh.tools._PT_COLORS``.
* - PROMPT_TOOLKIT_STYLES
- ``None``
- This is a mapping of user-specified styles for prompt-toolkit. See the
prompt-toolkit documentation for more details. If None, this is skipped.
* - PUSHD_MINUS
- ``False``
- Flag for directory pushing functionality. False is the normal behaviour.
* - PUSHD_SILENT
- ``False``
- Whether or not to supress directory stack manipulation output.
* - SHELL_TYPE
- ``'prompt_toolkit'`` if on Windows, otherwise ``'readline'``
- Which shell is used. Currently two base shell types are supported:
``'readline'`` that is backed by Python's readline module, and
``'prompt_toolkit'`` that uses external library of the same name.
To use the prompt_toolkit shell you need to have
`prompt_toolkit <https://github.com/jonathanslenders/python-prompt-toolkit>`_
library installed. To specify which shell should be used, do so in the run
control file. Additionally, you may also set this value to ``'random'``
to get a random choice of shell type on startup.
* - SUGGEST_COMMANDS
- ``True``
- When a user types an invalid command, xonsh will try to offer suggestions of
similar valid commands if this is ``True``.
* - SUGGEST_MAX_NUM
- ``5``
- xonsh will show at most this many suggestions in response to an invalid command.
If negative, there is no limit to how many suggestions are shown.
* - SUGGEST_THRESHOLD
- ``3``
- An error threshold. If the Levenshtein distance between the entered command and
a valid command is less than this value, the valid command will be offered as a
suggestion.
* - TEEPTY_PIPE_DELAY
- ``0.01``
- The number of [seconds] to delay a spawned process if it has information
being piped in via stdin. This value must be a float. If a value less than
or equal to zero is passed in, no delay is used. This can be used to fix
situations where a spawned process, such as piping into ``grep``, exits
too quickly for the piping operation itself. TeePTY (and thus this variable)
are currently only used when ``$XONSH_STORE_STDOUT`` is ``True``.
* - TERM
- No default
- TERM is sometimes set by the terminal emulator. This is used (when valid)
to determine whether or not to set the title. Users shouldn't need to
set this themselves.
* - TITLE
- xonsh.environ.DEFAULT_TITLE
- The title text for the window in which xonsh is running. Formatted in the same
manner as PROMPT,
see `Customizing the Prompt <tutorial.html#customizing-the-prompt>`_.
* - VI_MODE
- ``False``
- Flag to enable ``vi_mode`` in the ``prompt_toolkit`` shell.
* - XDG_CONFIG_HOME
- ``~/.config``
- Open desktop standard configuration home dir. This is the same default as
used in the standard.
* - XDG_DATA_HOME
- ``~/.local/share``
- Open desktop standard data home dir. This is the same default as used
in the standard.
* - XONSHCONFIG
- ``$XONSH_CONFIG_DIR/config.json``
- The location of the static xonsh configuration file, if it exists. This is
in JSON format.
* - XONSHRC
- ``('/etc/xonshrc', '~/.xonshrc')`` (Linux and OSX)
``('%ALLUSERSPROFILE%\xonsh\xonshrc', '~/.xonshrc')`` (Windows)
- A tuple of the locations of run control files, if they exist. User defined
run control file will supercede values set in system-wide control file if there
is a naming collision.
* - XONSH_CONFIG_DIR
- ``$XDG_CONFIG_HOME/xonsh``
- This is location where xonsh configuration information is stored.
* - XONSH_DATA_DIR
- ``$XDG_DATA_HOME/xonsh``
- This is the location where xonsh data files are stored, such as history.
* - XONSH_ENCODING
- ``sys.getdefaultencoding()``
- This is the that xonsh should use for subrpocess operations.
* - XONSH_ENCODING_ERRORS
- ``'surrogateescape'``
- The flag for how to handle encoding errors should they happen.
Any string flag that has been previously registered with Python
is allowed. See the `Python codecs documentation <https://docs.python.org/3/library/codecs.html#error-handlers>`_
for more information and available options.
* - XONSH_HISTORY_FILE
- ``'~/.xonsh_history'``
- Location of history file (deprecated).
* - XONSH_HISTORY_SIZE
- ``(8128, 'commands')`` or ``'8128 commands'``
- Value and units tuple that sets the size of history after garbage collection.
Canonical units are ``'commands'`` for the number of past commands executed,
``'files'`` for the number of history files to keep, ``'s'`` for the number of
seconds in the past that are allowed, and ``'b'`` for the number of bytes that
are allowed for history to consume. Common abbreviations, such as ``6 months``
or ``1 GB`` are also allowed.
* - XONSH_INTERACTIVE
-
- ``True`` if xonsh is running interactively, and ``False`` otherwise.
* - XONSH_LOGIN
- ``True`` if xonsh is running as a login shell, and ``False`` otherwise.
- Whether or not xonsh is a login shell.
* - XONSH_SHOW_TRACEBACK
- ``False`` but not set
- Controls if a traceback is shown exceptions occur in the shell. Set ``True``
to always show or ``False`` to always hide. If undefined then traceback is
hidden but a notice is shown on how to enable the traceback.
* - XONSH_STORE_STDOUT
- ``False``
- Whether or not to store the stdout and stderr streams in the history files.
.. include:: envvarsbody

53
tests/test_wizard.py Normal file
View file

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""Tests the xonsh lexer."""
from __future__ import unicode_literals, print_function
import os
import nose
from nose.tools import assert_equal, assert_true, assert_false
from xonsh.wizard import (Node, Wizard, Pass, PrettyFormatter,
Message, Question, StateVisitor)
TREE0 = Wizard(children=[Pass(), Message(message='yo')])
TREE1 = Question('wakka?', {'jawaka': Pass()})
def test_pretty_format_tree0():
exp = ('Wizard(children=[\n'
' Pass(),\n'
" Message('yo')\n"
'])')
obs = PrettyFormatter(TREE0).visit()
yield assert_equal, exp, obs
yield assert_equal, exp, str(TREE0)
yield assert_equal, exp.replace('\n', ''), repr(TREE0)
def test_pretty_format_tree1():
exp = ('Question(\n'
" question='wakka?',\n"
' responses={\n'
" 'jawaka': Pass()\n"
' }\n'
')')
obs = PrettyFormatter(TREE1).visit()
yield assert_equal, exp, obs
yield assert_equal, exp, str(TREE1)
yield assert_equal, exp.replace('\n', ''), repr(TREE1)
def test_state_visitor_store():
exp = {'rick': [{}, {}, {'and': 'morty'}]}
sv = StateVisitor()
sv.store('/rick/2/and', 'morty')
obs = sv.state
yield assert_equal, exp, obs
exp['rick'][1]['mr'] = 'meeseeks'
sv.store('/rick/-2/mr', 'meeseeks')
yield assert_equal, exp, obs
if __name__ == '__main__':
nose.runmodule()

View file

@ -162,6 +162,12 @@ def bang_bang(args, stdin=None):
return bang_n(['-1'])
def xonfig(args, stdin=None):
"""Runs the xonsh configuration utility."""
from xonsh.xonfig import main # lazy import
return main(args)
DEFAULT_ALIASES = {
'cd': cd,
'pushd': pushd,
@ -182,6 +188,7 @@ DEFAULT_ALIASES = {
'!!': bang_bang,
'!n': bang_n,
'timeit': timeit_alias,
'xonfig': xonfig,
'scp-resume': ['rsync', '--partial', '-h', '--progress', '--rsh=ssh'],
'ipynb': ['ipython', 'notebook', '--no-browser'],
}

View file

@ -30,6 +30,7 @@ class _TeeOut(object):
def write(self, data):
"""Writes data to the original stdout and the buffer."""
data = data.replace('\001', '').replace('\002', '')
self.stdout.write(data)
self.buffer.write(data)
@ -62,6 +63,7 @@ class _TeeErr(object):
def write(self, data):
"""Writes data to the original stderr and the buffer."""
data = data.replace('\001', '').replace('\002', '')
self.stderr.write(data)
self.buffer.write(data)
@ -117,6 +119,11 @@ class BaseShell(object):
self.need_more_lines = False
self.default('')
def singleline(self, **kwargs):
"""Reads a single line of input from the shell."""
msg = '{0} has not implemented singleline().'
raise RuntimeError(msg.format(self.__class__.__name__))
def precmd(self, line):
"""Called just before execution of line."""
return line if self.need_more_lines else line.lstrip()

View file

@ -664,13 +664,13 @@ def ensure_list_of_strs(x):
return rtn
def load_builtins(execer=None):
def load_builtins(execer=None, config=None):
"""Loads the xonsh builtins into the Python builtins. Sets the
BUILTINS_LOADED variable to True.
"""
global BUILTINS_LOADED, ENV
# private built-ins
builtins.__xonsh_env__ = ENV = Env(default_env())
builtins.__xonsh_env__ = ENV = Env(default_env(config=config))
builtins.__xonsh_ctx__ = {}
builtins.__xonsh_help__ = helper
builtins.__xonsh_superhelp__ = superhelper

View file

@ -9,7 +9,9 @@ import locale
import builtins
import subprocess
from warnings import warn
from pprint import pformat
from functools import wraps
from contextlib import contextmanager
from collections import MutableMapping, MutableSequence, MutableSet, namedtuple
from xonsh import __version__ as XONSH_VERSION
@ -19,7 +21,8 @@ from xonsh.tools import (
env_path_to_str, is_bool, to_bool, bool_to_str, is_history_tuple, to_history_tuple,
history_tuple_to_str, is_float, string_types, is_string, DEFAULT_ENCODING,
is_completions_display_value, to_completions_display_value, is_string_set,
csv_to_set, set_to_csv, get_sep, is_int
csv_to_set, set_to_csv, get_sep, is_int, is_bool_seq, csv_to_bool_seq,
bool_seq_to_csv
)
from xonsh.dirstack import _get_cwd
from xonsh.foreign_shells import DEFAULT_SHELLS, load_foreign_envs
@ -53,6 +56,7 @@ represent environment variable validation, conversion, detyping.
DEFAULT_ENSURERS = {
'AUTO_CD': (is_bool, to_bool, bool_to_str),
'AUTO_PUSHD': (is_bool, to_bool, bool_to_str),
'AUTO_SUGGEST': (is_bool, to_bool, bool_to_str),
'BASH_COMPLETIONS': (is_env_path, str_to_env_path, env_path_to_str),
'CASE_SENSITIVE_COMPLETIONS': (is_bool, to_bool, bool_to_str),
@ -67,7 +71,9 @@ DEFAULT_ENSURERS = {
'LC_MESSAGES': (always_false, locale_convert('LC_MESSAGES'), ensure_string),
'LC_MONETARY': (always_false, locale_convert('LC_MONETARY'), ensure_string),
'LC_NUMERIC': (always_false, locale_convert('LC_NUMERIC'), ensure_string),
'LC_TIME': (always_false, locale_convert('LC_TIME'), ensure_string),
'LC_TIME': (always_false, locale_convert('LC_TIME'), ensure_string),
'LOADED_CONFIG': (is_bool, to_bool, bool_to_str),
'LOADED_RC_FILES': (is_bool_seq, csv_to_bool_seq, bool_seq_to_csv),
'MOUSE_SUPPORT': (is_bool, to_bool, bool_to_str),
re.compile('\w*PATH$'): (is_env_path, str_to_env_path, env_path_to_str),
'PATHEXT': (is_env_path, str_to_env_path, env_path_to_str),
@ -157,6 +163,8 @@ DEFAULT_VALUES = {
'LC_TIME': locale.setlocale(locale.LC_TIME),
'LC_MONETARY': locale.setlocale(locale.LC_MONETARY),
'LC_NUMERIC': locale.setlocale(locale.LC_NUMERIC),
'LOADED_CONFIG': False,
'LOADED_RC_FILES': (),
'MOUSE_SUPPORT': False,
'MULTILINE_PROMPT': '.',
'PATH': (),
@ -199,6 +207,240 @@ class DefaultNotGivenType(object):
DefaultNotGiven = DefaultNotGivenType()
VarDocs = namedtuple('VarDocs', ['docstr', 'configurable', 'default'])
VarDocs.__doc__ = """Named tuple for environment variable documentation
Parameters
----------
docstr : str
The environment variable docstring.
configurable : bool, optional
Flag for whether the environment variable is configurable or not.
default : str, optional
Custom docstring for the default value for complex defaults.
Is this is DefaultNotGiven, then the default will be looked up
from DEFAULT_VALUES and converted to a str.
"""
VarDocs.__new__.__defaults__ = (True, DefaultNotGiven) # iterates from back
# Please keep the following in alphabetic order - scopatz
DEFAULT_DOCS = {
'ANSICON': VarDocs('This is used on Windows to set the title, '
'if available.', configurable=ON_WINDOWS),
'AUTO_CD': VarDocs(
'Flag to enable changing to a directory by entering the dirname or '
'full path only (without the cd command).'),
'AUTO_PUSHD': VarDocs(
'Flag for automatically pushing directories onto the directory stack.'
),
'AUTO_SUGGEST': VarDocs(
'Enable automatic command suggestions based on history, like in fish '
'shell.\n\nPressing the right arrow key inserts the currently '
'displayed suggestion. Only usable with $SHELL_TYPE=prompt_toolkit.'),
'BASH_COMPLETIONS': VarDocs(
'This is a list (or tuple) of strings that specifies where the BASH '
'completion files may be found. The default values are platform '
'dependent, but sane. To specify an alternate list, do so in the run '
'control file.', default=(
"Normally this is:\n\n"
" ('/etc/bash_completion',\n"
" '/usr/share/bash-completion/completions/git')\n\n"
"But, on Mac it is:\n\n"
" ('/usr/local/etc/bash_completion',\n"
" '/opt/local/etc/profile.d/bash_completion.sh')\n\n"
"And on Arch Linux it is:\n\n"
" ('/usr/share/bash-completion/bash_completion',\n"
" '/usr/share/bash-completion/completions/git')\n\n"
"Other OS-specific defaults may be added in the future.")),
'CASE_SENSITIVE_COMPLETIONS': VarDocs(
'Sets whether completions should be case sensitive or case '
'insensitive.', default='True on Linux, False otherwise.'),
'CDPATH': VarDocs(
'A list of paths to be used as roots for a cd, breaking compatibility '
'with Bash, xonsh always prefer an existing relative path.'),
'COMPLETIONS_DISPLAY': VarDocs(
'Configure if and how Python completions are displayed by the '
'prompt_toolkit shell.\n\nThis option does not affect Bash '
'completions, auto-suggestions, etc.\n\nChanging it at runtime will '
'take immediate effect, so you can quickly disable and enable '
'completions during shell sessions.\n\n'
"- If $COMPLETIONS_DISPLAY is 'none' or 'false', do not display\n"
" those completions.\n"
"- If $COMPLETIONS_DISPLAY is 'single', display completions in a\n"
' single column while typing.\n'
"- If $COMPLETIONS_DISPLAY is 'multi' or 'true', display completions\n"
" in multiple columns while typing.\n\n"
'These option values are not case- or type-sensitive, so e.g.'
"writing \"$COMPLETIONS_DISPLAY = None\" and \"$COMPLETIONS_DISPLAY "
"= 'none'\" are equivalent. Only usable with "
"$SHELL_TYPE=prompt_toolkit"),
'COMPLETIONS_MENU_ROWS': VarDocs(
'Number of rows to reserve for tab-completions menu if '
"$COMPLETIONS_DISPLAY is 'single' or 'multi'. This only effects the "
'prompt-toolkit shell.'),
'DIRSTACK_SIZE': VarDocs('Maximum size of the directory stack.'),
'EXPAND_ENV_VARS': VarDocs(
'Toggles whether environment variables are expanded inside of strings '
'in subprocess mode.'),
'FORCE_POSIX_PATHS': VarDocs(
"Forces forward slashes ('/') on Windows systems when using auto "
'completion if set to anything truthy.', configurable=ON_WINDOWS),
'FORMATTER_DICT': VarDocs(
'Dictionary containing variables to be used when formatting $PROMPT '
"and $TITLE. See 'Customizing the Prompt' "
'http://xonsh.org/tutorial.html#customizing-the-prompt',
configurable=False, default='xonsh.environ.FORMATTER_DICT'),
'HISTCONTROL': VarDocs(
'A set of strings (comma-separated list in string form) of options '
'that determine what commands are saved to the history list. By '
"default all commands are saved. The option 'ignoredups' will not "
"save the command if it matches the previous command. The option "
"'ignoreerr' will cause any commands that fail (i.e. return non-zero "
"exit status) to not be added to the history list."),
'IGNOREEOF': VarDocs('Prevents Ctrl-D from exiting the shell.'),
'INDENT': VarDocs('Indentation string for multiline input'),
'LOADED_CONFIG': VarDocs('Whether or not the xonsh config file was loaded',
configurable=False),
'LOADED_RC_FILES': VarDocs(
'Whether or not any of the xonsh run control files were loaded at '
'startup. This is a sequence of bools in Python that is converted '
"to a CSV list in string form, ie [True, False] becomes 'True,False'.",
configurable=False),
'MOUSE_SUPPORT': VarDocs(
'Enable mouse support in the prompt_toolkit shell. This allows '
'clicking for positioning the cursor or selecting a completion. In '
'some terminals however, this disables the ability to scroll back '
'through the history of the terminal. Only usable with '
'$SHELL_TYPE=prompt_toolkit'),
'MULTILINE_PROMPT': VarDocs(
'Prompt text for 2nd+ lines of input, may be str or function which '
'returns a str.'),
'OLDPWD': VarDocs('Used to represent a previous present working directory.',
configurable=False),
'PATH': VarDocs(
'List of strings representing where to look for executables.'),
'PATHEXT': VarDocs('List of strings for filtering valid exeutables by.'),
'PROMPT': VarDocs(
'The prompt text. May contain keyword arguments which are '
"auto-formatted, see 'Customizing the Prompt' at "
'http://xonsh.org/tutorial.html#customizing-the-prompt.',
default='xonsh.environ.DEFAULT_PROMPT'),
'PROMPT_TOOLKIT_COLORS': VarDocs(
'This is a mapping of from color names to HTML color codes. Whenever '
'prompt-toolkit would color a word a particular color (in the prompt, '
'or in syntax highlighting), it will use the value specified here to '
'represent that color, instead of its default. If a color is not '
'specified here, prompt-toolkit uses the colors from '
"'xonsh.tools._PT_COLORS'.", configurable=False),
'PROMPT_TOOLKIT_STYLES': VarDocs(
'This is a mapping of user-specified styles for prompt-toolkit. See '
'the prompt-toolkit documentation for more details. If None, this is '
'skipped.', configurable=False),
'PUSHD_MINUS': VarDocs(
'Flag for directory pushing functionality. False is the normal '
'behaviour.'),
'PUSHD_SILENT': VarDocs(
'Whether or not to suppress directory stack manipulation output.'),
'SHELL_TYPE': VarDocs(
'Which shell is used. Currently two base shell types are supported:\n\n'
" - 'readline' that is backed by Python's readline module\n"
" - 'prompt_toolkit' that uses external library of the same name\n"
" - 'random' selects a random shell from the above on startup\n\n"
'To use the prompt_toolkit shell you need to have prompt_toolkit '
'(https://github.com/jonathanslenders/python-prompt-toolkit)'
'library installed. To specify which shell should be used, do so in '
'the run control file.', default=("'prompt_toolkit' if on Windows, "
"and 'readline' otherwise.")),
'SUGGEST_COMMANDS': VarDocs(
'When a user types an invalid command, xonsh will try to offer '
'suggestions of similar valid commands if this is True.'),
'SUGGEST_MAX_NUM': VarDocs(
'xonsh will show at most this many suggestions in response to an '
'invalid command. If negative, there is no limit to how many '
'suggestions are shown.'),
'SUGGEST_THRESHOLD': VarDocs(
'An error threshold. If the Levenshtein distance between the entered '
'command and a valid command is less than this value, the valid '
'command will be offered as a suggestion.'),
'TEEPTY_PIPE_DELAY': VarDocs(
'The number of [seconds] to delay a spawned process if it has '
'information being piped in via stdin. This value must be a float. '
'If a value less than or equal to zero is passed in, no delay is '
'used. This can be used to fix situations where a spawned process, '
'such as piping into \'grep\', exits too quickly for the piping '
'operation itself. TeePTY (and thus this variable) are currently '
'only used when $XONSH_STORE_STDOUT is True.', configurable=ON_LINUX),
'TERM': VarDocs(
'TERM is sometimes set by the terminal emulator. This is used (when '
"valid) to determine whether or not to set the title. Users shouldn't "
"need to set this themselves.", configurable=False),
'TITLE': VarDocs(
'The title text for the window in which xonsh is running. Formatted '
"in the same manner as $PROMPT, see 'Customizing the Prompt' "
'http://xonsh.org/tutorial.html#customizing-the-prompt.',
default='xonsh.environ.DEFAULT_TITLE'),
'VI_MODE': VarDocs(
"Flag to enable 'vi_mode' in the 'prompt_toolkit' shell."),
'XDG_CONFIG_HOME': VarDocs(
'Open desktop standard configuration home dir. This is the same '
'default as used in the standard.', configurable=False,
default="'~/.config'"),
'XDG_DATA_HOME': VarDocs(
'Open desktop standard data home dir. This is the same default as '
'used in the standard.', default="'~/.local/share'"),
'XONSHCONFIG': VarDocs(
'The location of the static xonsh configuration file, if it exists. '
'This is in JSON format.', configurable=False,
default="'$XONSH_CONFIG_DIR/config.json'"),
'XONSHRC': VarDocs(
'A tuple of the locations of run control files, if they exist. User '
'defined run control file will supercede values set in system-wide '
'control file if there is a naming collision.', default=(
"On Linux & Mac OSX: ('/etc/xonshrc', '~/.xonshrc')\n"
"On Windows: ('%ALLUSERSPROFILE%\\xonsh\\xonshrc', '~/.xonshrc')")),
'XONSH_CONFIG_DIR': VarDocs(
'This is location where xonsh configuration information is stored.',
configurable=False, default="'$XDG_CONFIG_HOME/xonsh'"),
'XONSH_DATA_DIR': VarDocs(
'This is the location where xonsh data files are stored, such as '
'history.', default="'$XDG_DATA_HOME/xonsh'"),
'XONSH_ENCODING': VarDocs(
'This is the that xonsh should use for subrpocess operations.',
default='sys.getdefaultencoding()'),
'XONSH_ENCODING_ERRORS': VarDocs(
'The flag for how to handle encoding errors should they happen. '
'Any string flag that has been previously registered with Python '
"is allowed. See the 'Python codecs documentation' "
"(https://docs.python.org/3/library/codecs.html#error-handlers) "
'for more information and available options.',
default="'surrogateescape'"),
'XONSH_HISTORY_FILE': VarDocs('Location of history file (deprecated).',
configurable=False, default="'~/.xonsh_history'"),
'XONSH_HISTORY_SIZE': VarDocs(
'Value and units tuple that sets the size of history after garbage '
'collection. Canonical units are:\n\n'
"- 'commands' for the number of past commands executed,\n"
"- 'files' for the number of history files to keep,\n"
"- 's' for the number of seconds in the past that are allowed, and\n"
"- 'b' for the number of bytes that history may consume.\n\n"
"Common abbreviations, such as '6 months' or '1 GB' are also allowed.",
default="(8128, 'commands') or '8128 commands'"),
'XONSH_INTERACTIVE': VarDocs(
'True if xonsh is running interactively, and False otherwise.',
configurable=False),
'XONSH_LOGIN': VarDocs(
'True if xonsh is running as a login shell, and False otherwise.',
configurable=False),
'XONSH_SHOW_TRACEBACK': VarDocs(
'Controls if a traceback is shown exceptions occur in the shell. '
'Set to True to always show or False to always hide. If undefined '
'then traceback is hidden but a notice is shown on how to enable the '
'traceback.'),
'XONSH_STORE_STDOUT': VarDocs(
'Whether or not to store the stdout and stderr streams in the '
'history files.', configurable=False),
}
#
# actual environment
#
@ -224,6 +466,7 @@ class Env(MutableMapping):
self._d = {}
self.ensurers = {k: Ensurer(*v) for k, v in DEFAULT_ENSURERS.items()}
self.defaults = DEFAULT_VALUES
self.docs = DEFAULT_DOCS
if len(args) == 0 and len(kwargs) == 0:
args = (os.environ, )
for key, val in dict(*args, **kwargs).items():
@ -281,6 +524,35 @@ class Env(MutableMapping):
self.ensurers[key] = ens
return ens
def get_docs(self, key, default=VarDocs('<no documentation>')):
"""Gets the documentation for the environment variable."""
vd = self.docs.get(key, None)
if vd is None:
return default
if vd.default is DefaultNotGiven:
dval = pformat(self.defaults.get(key, '<default not set>'))
vd = vd._replace(default=dval)
self.docs[key] = vd
return vd
@contextmanager
def swap(self, other):
"""Provides a context manager for temporarily swapping out certain
environment variables with other values. On exit from the context
manager, the original values are restored.
"""
old = {}
for k, v in other.items():
old[k] = self.get(k, NotImplemented)
self[k] = v
yield self
for k, v in old.items():
if v is NotImplemented:
del self[k]
else:
self[k] = v
#
# Mutable mapping interface
#
@ -604,6 +876,23 @@ DEFAULT_VALUES['FORMATTER_DICT'] = dict(FORMATTER_DICT)
_FORMATTER = string.Formatter()
def is_template_string(template, formatter_dict=None):
"""Returns whether or not the string is a valid template."""
template = template() if callable(template) else template
try:
included_names = set(i[1] for i in _FORMATTER.parse(template))
except ValueError:
return False
included_names.discard(None)
if formatter_dict is None:
fmtter = builtins.__xonsh_env__.get('FORMATTER_DICT', FORMATTER_DICT)
else:
fmtter = formatter_dict
known_names = set(fmtter.keys())
return included_names <= known_names
def format_prompt(template=DEFAULT_PROMPT, formatter_dict=None):
"""Formats a xonsh prompt template string."""
template = template() if callable(template) else template
@ -653,34 +942,41 @@ BASE_ENV = {
'XONSH_VERSION': XONSH_VERSION,
}
def load_static_config(ctx):
def load_static_config(ctx, config=None):
"""Loads a static configuration file from a given context, rather than the
current environment.
current environment. Optionally may pass in configuration file name.
"""
env = {}
env['XDG_CONFIG_HOME'] = ctx.get('XDG_CONFIG_HOME',
DEFAULT_VALUES['XDG_CONFIG_HOME'])
env['XONSH_CONFIG_DIR'] = ctx['XONSH_CONFIG_DIR'] if 'XONSH_CONFIG_DIR' in ctx \
else xonsh_config_dir(env)
env['XONSHCONFIG'] = ctx['XONSHCONFIG'] if 'XONSHCONFIG' in ctx \
else xonshconfig(env)
config = env['XONSHCONFIG']
if config is not None:
env['XONSHCONFIG'] = ctx['XONSHCONFIG'] = config
elif 'XONSHCONFIG' in ctx:
config = env['XONSHCONFIG'] = ctx['XONSHCONFIG']
else:
# don't set in ctx in order to maintain default
config = env['XONSHCONFIG'] = xonshconfig(env)
if os.path.isfile(config):
with open(config, 'r') as f:
conf = json.load(f)
ctx['LOADED_CONFIG'] = True
else:
conf = {}
ctx['LOADED_CONFIG'] = False
return conf
def xonshrc_context(rcfiles=None, execer=None):
"""Attempts to read in xonshrc file, and return the contents."""
if (rcfiles is None or execer is None
or sum([os.path.isfile(rcfile) for rcfile in rcfiles]) == 0):
loaded = builtins.__xonsh_env__['LOADED_RC_FILES'] = []
if (rcfiles is None or execer is None):
return {}
env = {}
for rcfile in rcfiles:
if not os.path.isfile(rcfile):
loaded.append(False)
continue
with open(rcfile, 'r') as f:
rc = f.read()
@ -690,7 +986,9 @@ def xonshrc_context(rcfiles=None, execer=None):
try:
execer.filename = rcfile
execer.exec(rc, glbs=env)
loaded.append(True)
except SyntaxError as err:
loaded.append(False)
msg = 'syntax error in xonsh run control file {0!r}: {1!s}'
warn(msg.format(rcfile, err), RuntimeWarning)
finally:
@ -717,12 +1015,12 @@ def windows_env_fixes(ctx):
ctx['PWD'] = _get_cwd()
def default_env(env=None):
def default_env(env=None, config=None):
"""Constructs a default xonsh environment."""
# in order of increasing precedence
ctx = dict(BASE_ENV)
ctx.update(os.environ)
conf = load_static_config(ctx)
conf = load_static_config(ctx, config=config)
ctx.update(conf.get('env', ()))
ctx.update(load_foreign_envs(shells=conf.get('foreign_shells', DEFAULT_SHELLS),
issue_warning=False))

View file

@ -16,11 +16,8 @@ from xonsh.built_ins import load_builtins, unload_builtins
class Execer(object):
"""Executes xonsh code in a context."""
def __init__(self,
filename='<xonsh-code>',
debug_level=0,
parser_args=None,
unload=True):
def __init__(self, filename='<xonsh-code>', debug_level=0, parser_args=None,
unload=True, config=None):
"""Parameters
----------
filename : str, optional
@ -31,6 +28,8 @@ class Execer(object):
Arguments to pass down to the parser.
unload : bool, optional
Whether or not to unload xonsh builtins upon deletion.
config : str, optional
Path to configuration file.
"""
parser_args = parser_args or {}
self.parser = Parser(**parser_args)
@ -38,7 +37,7 @@ class Execer(object):
self.debug_level = debug_level
self.unload = unload
self.ctxtransformer = ast.CtxAwareTransformer(self.parser)
load_builtins(execer=self)
load_builtins(execer=self, config=config)
def __del__(self):
if self.unload:

View file

@ -52,9 +52,10 @@ parser.add_argument('-l',
parser.add_argument('--config-path',
help='specify a custom static configuration file',
dest='config_path',
default=None,
type=path_argument)
parser.add_argument('--no-rc',
help="Do not load the .xonshrc file",
help="Do not load the .xonshrc files",
dest='norc',
action='store_true',
default=False)
@ -136,10 +137,10 @@ def premain(argv=None):
print(version)
exit()
shell_kwargs = {'shell_type': args.shell_type}
if args.config_path is None:
shell_kwargs['config'] = args.config_path
if args.norc:
shell_kwargs['ctx'] = {}
if args.config_path:
shell_kwargs['ctx']= {'XONSHCONFIG': args.config_path}
shell_kwargs['rc'] = ()
setattr(sys, 'displayhook', _pprint_displayhook)
shell = builtins.__xonsh_shell__ = Shell(**shell_kwargs)
from xonsh import imphooks
@ -182,6 +183,10 @@ def main(argv=None):
# otherwise, enter the shell
env['XONSH_INTERACTIVE'] = True
ignore_sigtstp()
if not env['LOADED_CONFIG'] and not any(env['LOADED_RC_FILES']):
print('Could not find xonsh configuration or run control files.')
code = '$[xonfig wizard --confirm]'
shell.execer.exec(code, mode='single', glbs=shell.ctx)
shell.cmdloop()
postmain(args)

View file

@ -38,6 +38,40 @@ class PromptToolkitShell(BaseShell):
enable_open_in_editor=True)
load_xonsh_bindings(self.key_bindings_manager)
def singleline(self, store_in_history=True, auto_suggest=None,
enable_history_search=True, multiline=True, **kwargs):
"""Reads a single line of input from the shell. The store_in_history
kwarg flags whether the input should be stored in PTK's in-memory
history.
"""
token_func, style_cls = self._get_prompt_tokens_and_style()
env = builtins.__xonsh_env__
mouse_support = env.get('MOUSE_SUPPORT')
if store_in_history:
history = self.history
else:
history = None
enable_history_search = False
auto_suggest = auto_suggest if env.get('AUTO_SUGGEST') else None
completions_display = env.get('COMPLETIONS_DISPLAY')
multicolumn = (completions_display == 'multi')
completer = None if completions_display == 'none' else self.pt_completer
with self.prompter:
line = self.prompter.prompt(
mouse_support=mouse_support,
auto_suggest=auto_suggest,
get_prompt_tokens=token_func,
style=style_cls,
completer=completer,
lexer=PygmentsLexer(XonshLexer),
multiline=multiline,
history=history,
enable_history_search=enable_history_search,
reserve_space_for_menu=0,
key_bindings_registry=self.key_bindings_manager.registry,
display_completions_in_columns=multicolumn)
return line
def push(self, line):
"""Pushes a line onto the buffer and compiles the code in a way that
enables multiline input.
@ -63,33 +97,10 @@ class PromptToolkitShell(BaseShell):
"""Enters a loop that reads and execute input from user."""
if intro:
print(intro)
_auto_suggest = AutoSuggestFromHistory()
auto_suggest = AutoSuggestFromHistory()
while not builtins.__xonsh_exit__:
try:
token_func, style_cls = self._get_prompt_tokens_and_style()
env = builtins.__xonsh_env__
mouse_support = env.get('MOUSE_SUPPORT')
if env.get('AUTO_SUGGEST'):
auto_suggest = _auto_suggest
else:
auto_suggest = None
completions_display = env.get('COMPLETIONS_DISPLAY')
multicolumn = (completions_display == 'multi')
completer = None if completions_display == 'none' else self.pt_completer
with self.prompter:
line = self.prompter.prompt(
mouse_support=mouse_support,
auto_suggest=auto_suggest,
get_prompt_tokens=token_func,
style=style_cls,
completer=completer,
lexer=PygmentsLexer(XonshLexer),
history=self.history,
multiline=True,
enable_history_search=True,
reserve_space_for_menu=0,
key_bindings_registry=self.key_bindings_manager.registry,
display_completions_in_columns=multicolumn)
line = self.singleline(auto_suggest=auto_suggest)
if not line:
self.emptyline()
else:

View file

@ -20,7 +20,7 @@ RL_DONE = None
def setup_readline():
"""Sets up the readline module and completion supression, if available."""
"""Sets up the readline module and completion suppression, if available."""
global RL_COMPLETION_SUPPRESS_APPEND, RL_LIB, RL_CAN_RESIZE
if RL_COMPLETION_SUPPRESS_APPEND is not None:
return
@ -101,6 +101,22 @@ class ReadlineShell(BaseShell, Cmd):
def __del__(self):
teardown_readline()
def singleline(self, store_in_history=True, **kwargs):
"""Reads a single line of input. The store_in_history kwarg
flags whether the input should be stored in readline's in-memory
history.
"""
if not store_in_history: # store current position to remove it later
try:
import readline
except ImportError:
store_in_history = True
pos = readline.get_current_history_length() - 1
rtn = input(self.prompt)
if not store_in_history:
readline.remove_history_item(pos)
return rtn
def parseline(self, line):
"""Overridden to no-op."""
return '', line, line
@ -198,7 +214,7 @@ class ReadlineShell(BaseShell, Cmd):
if inserter is not None:
readline.set_pre_input_hook(inserter)
try:
line = input(self.prompt)
line = self.singleline()
except EOFError:
if builtins.__xonsh_env__.get("IGNOREEOF"):
self.stdout.write('Use "exit" to leave the shell.'

View file

@ -9,6 +9,15 @@ from xonsh.environ import xonshrc_context
from xonsh.tools import XonshError
def is_readline_available():
"""Checks if readline is available to import."""
try:
import readline
return True
except ImportError:
return False
def is_prompt_toolkit_available():
"""Checks if prompt_toolkit is available to import."""
try:
@ -18,6 +27,12 @@ def is_prompt_toolkit_available():
return False
def prompt_toolkit_version():
"""Gets the prompt toolkit version."""
import prompt_toolkit
return getattr(prompt_toolkit, '__version__', '<0.57')
class Shell(object):
"""Main xonsh shell.
@ -25,8 +40,25 @@ class Shell(object):
readline version of shell should be used.
"""
def __init__(self, ctx=None, shell_type=None, **kwargs):
self._init_environ(ctx)
def __init__(self, ctx=None, shell_type=None, config=None, rc=None,
**kwargs):
"""
Parameters
----------
ctx : Mapping, optional
The execution context for the shell (e.g. the globals namespace).
If none, this is computed by loading the rc files. If not None,
this no additional context is computed and this is used
directly.
shell_type : str, optional
The shell type to start, such as 'readline', 'prompt_toolkit',
or 'random'.
config : str, optional
Path to configuration file.
rc : list of str, optional
Sequence of paths to run control files.
"""
self._init_environ(ctx, config, rc)
env = builtins.__xonsh_env__
# pick a valid shell
if shell_type is not None:
@ -57,13 +89,13 @@ class Shell(object):
"""Delegates calls to appropriate shell instance."""
return getattr(self.shell, attr)
def _init_environ(self, ctx):
self.execer = Execer()
def _init_environ(self, ctx, config, rc):
self.execer = Execer(config=config)
env = builtins.__xonsh_env__
if ctx is not None:
self.ctx = ctx
else:
rc = env.get('XONSHRC')
if ctx is None:
rc = env.get('XONSHRC') if rc is None else rc
self.ctx = xonshrc_context(rcfiles=rc, execer=self.execer)
else:
self.ctx = ctx
builtins.__xonsh_ctx__ = self.ctx
self.ctx['__name__'] = '__main__'

View file

@ -27,7 +27,7 @@ import traceback
import threading
import subprocess
from contextlib import contextmanager
from collections import OrderedDict, Sequence
from collections import OrderedDict, Sequence, Set
from warnings import warn
if sys.version_info[0] >= 3:
@ -553,6 +553,15 @@ def bool_to_str(x):
return '1' if x else ''
_BREAKS = frozenset(['b', 'break', 's', 'skip', 'q', 'quit'])
def to_bool_or_break(x):
if isinstance(x, string_types) and x.lower() in _BREAKS:
return 'break'
else:
return to_bool(x)
def ensure_int_or_slice(x):
"""Makes sure that x is list-indexable."""
if x is None:
@ -572,7 +581,7 @@ def is_string_set(x):
if isinstance(x, string_types):
return False
else:
return (isinstance(x, set) and
return (isinstance(x, Set) and
all([isinstance(a, string_types) for a in x]))
@ -589,6 +598,23 @@ def set_to_csv(x):
return ','.join(x)
def is_bool_seq(x):
"""Tests if an object is a sequence of bools."""
return isinstance(x, Sequence) and all(map(isinstance, x, [bool]*len(x)))
def csv_to_bool_seq(x):
"""Takes a comma-separated string and converts it into a list of bools."""
if len(x) == 0:
return []
return list(map(to_bool, x.split(',')))
def bool_seq_to_csv(x):
"""Converts a sequence of bools to a comma-separated string."""
return ','.join(map(str, x))
def is_completions_display_value(x):
return x in {'none', 'single', 'multi'}
@ -807,12 +833,29 @@ def format_prompt_for_prompt_toolkit(prompt):
return token_names, cstyles, strings
def format_color(string):
"""Formats strings that contain xonsh.tools.TERM_COLORS values."""
s = string.format(**TERM_COLORS).replace('\001', '').replace('\002', '')
return s
def print_color(string, file=sys.stdout):
"""Print strings that contain xonsh.tools.TERM_COLORS values. By default
`sys.stdout` is used as the output stream but an alternate can be specified
by the `file` keyword argument."""
print(string.format(**TERM_COLORS).replace('\001', '').replace('\002', ''),
file=file)
print(format_color(string), file=file)
def escape_color(string):
"""Escapes color formatting, ie '{RED}' becomes '{{RED}}'."""
s = string
for color in TERM_COLORS.keys():
if color in s:
bc = '{' + color + '}' # braced color
dbc = '{' + bc + '}' # double-braced color
s = s.replace(bc, dbc)
return s
_RE_STRING_START = "[bBrRuU]*"
_RE_STRING_TRIPLE_DOUBLE = '"""'
@ -1021,3 +1064,18 @@ def expandvars(path):
res += c
index += 1
return res
#
# File handling tools
#
def backup_file(fname):
"""Moves an existing file to a new name that has the current time right
before the extension.
"""
# lazy imports
import shutil
from datetime import datetime
base, ext = os.path.splitext(fname)
newfname = base + '.' + datetime.now().isoformat() + ext
shutil.move(fname, newfname)

614
xonsh/wizard.py Normal file
View file

@ -0,0 +1,614 @@
"""Tools for creating command-line and web-based wizards from a tree of nodes.
"""
import os
import ast
import json
import builtins
import textwrap
from pprint import pformat
from collections.abc import MutableSequence, Mapping, Sequence
from xonsh.tools import to_bool, to_bool_or_break, backup_file, print_color
#
# Nodes themselves
#
class Node(object):
"""Base type of all nodes."""
attrs = ()
def __str__(self):
return PrettyFormatter(self).visit()
def __repr__(self):
return str(self).replace('\n', '')
class Wizard(Node):
"""Top-level node in the tree."""
attrs = ('children', 'path')
def __init__(self, children, path=None):
self.children = children
self.path = path
class Pass(Node):
"""Simple do-nothing node"""
class Message(Node):
"""Contains a simple message to report to the user."""
attrs = ('message')
def __init__(self, message):
self.message = message
class Question(Node):
"""Asks a question and then chooses the next node based on the response.
"""
attrs = ('question', 'responses', 'converter', 'path')
def __init__(self, question, responses, converter=None, path=None):
"""
Parameters
----------
question : str
The question itself.
responses : dict with str keys and Node values
Mapping from user-input responses to nodes.
converter : callable, optional
Converts the string the user typed into another object
that serves as a key to the reponses dict.
path : str or sequence of str, optional
A path within the storage object.
"""
self.question = question
self.responses = responses
self.converter = converter
self.path = path
class Input(Node):
"""Gets input from the user."""
attrs = ('prompt', 'converter', 'show_conversion', 'confirm', 'path')
def __init__(self, prompt='>>> ', converter=None, show_conversion=False,
confirm=False, path=None):
"""
Parameters
----------
prompt : str, optional
Prompt string prior to input
converter : callable, optional
Converts the string the user typed into another object
prior to storage.
show_conversion : bool, optional
Flag for whether or not to show the results of the conversion
function if the conversion function was meaningfully executed.
Default False.
confirm : bool, optional
Whether the input should be confirmed until true or broken,
default False.
path : str or sequence of str, optional
A path within the storage object.
"""
self.prompt = prompt
self.converter = converter
self.show_conversion = show_conversion
self.confirm = confirm
self.path = path
class While(Node):
"""Computes a body while a condition function evaluates to true.
The condition function has the form cond(visitor=None, node=None) and
should return an object that is convertable to a bool. The beg attribute
specifies the number to start the loop iteration at.
"""
attrs = ('cond', 'body', 'idxname', 'beg', 'path')
def __init__(self, cond, body, idxname='idx', beg=0, path=None):
"""
Parameters
----------
cond : callable
Function that determines if the next loop iteration should
be executed. The condition function has the form
cond(visitor=None, node=None) and should return an object that
is convertable to a bool.
body : sequence of nodes
A list of node to execute on each iteration.
idxname : str, optional
The variable name for the index.
beg : int, optional
The first index value when evaluating path format strings.
path : str or sequence of str, optional
A path within the storage object.
"""
self.cond = cond
self.body = body
self.idxname = idxname
self.beg = beg
self.path = path
#
# Helper nodes
#
class YesNo(Question):
"""Represents a simple yes/no question."""
def __init__(self, question, yes, no, path=None):
"""
Parameters
----------
question : str
The question itself.
yes : Node
Node to execute if the response is True.
no : Node
Node to execute if the response is False.
path : str or sequence of str, optional
A path within the storage object.
"""
responses = {True: yes, False: no}
super().__init__(question, responses, converter=to_bool,
path=path)
class TrueFalse(Input):
"""Input node the returns a True or False value."""
def __init__(self, prompt='yes or no [default: no]? ', path=None):
super().__init__(prompt=prompt, converter=to_bool,
show_conversion=False, confirm=False, path=path)
class TrueFalseBreak(Input):
"""Input node the returns a True, False, or 'break' value."""
def __init__(self, prompt='yes, no, or break [default: no]? ', path=None):
super().__init__(prompt=prompt, converter=to_bool_or_break,
show_conversion=False, confirm=False, path=path)
class StoreNonEmpty(Input):
"""Stores the user input only if the input was not an empty string.
This works by wrapping the converter function.
"""
def __init__(self, prompt='>>> ', converter=None, show_conversion=False,
confirm=False, path=None):
def nonempty_converter(x):
"""Converts non-empty values and converts empty inputs to
Unstorable.
"""
if len(x) == 0:
x = Unstorable
elif converter is None:
pass
else:
x = converter(x)
return x
super().__init__(prompt=prompt, converter=nonempty_converter,
show_conversion=show_conversion, confirm=confirm,
path=path)
class StateFile(Input):
"""Node for repesenting the state as a JSON file under a default or user
given file name. This node type is likely not useful on its own.
"""
attrs = ('default_file', 'check')
def __init__(self, default_file=None, check=True):
"""
Parameters
----------
default_file : str, optional
The default filename to save the file as.
check : bool, optional
Whether to print the current state and ask if it should be
saved/loaded prior to asking for the file name and saving the
file, default=True.
"""
self._df = None
super().__init__(prompt='filename: ', converter=None,
confirm=False, path=None)
self.default_file = default_file
self.check = check
@property
def default_file(self):
return self._df
@default_file.setter
def default_file(self, val):
self._df = val
if val is None:
self.prompt = 'filename: '
else:
self.prompt = 'filename [default={0!r}]: '.format(val)
class Save(StateFile):
"""Node for saving the state as a JSON file under a default or user
given file name.
"""
class Load(StateFile):
"""Node for loading the state as a JSON file under a default or user
given file name.
"""
def create_truefalse_cond(prompt='yes or no [default: no]? ', path=None):
"""This creates a basic condition function for use with nodes like While
or other conditions. The condition function creates and visits a TrueFalse
node and returns the result. This TrueFalse node takes the prompt and
path that is passed in here.
"""
def truefalse_cond(visitor, node=None):
"""Prompts the user for a true/false condition."""
tf = TrueFalse(prompt=prompt, path=path)
rtn = visitor.visit(tf)
return rtn
return truefalse_cond
#
# Tools for trees of nodes.
#
_lowername = lambda cls: cls.__name__.lower()
class Visitor(object):
"""Super-class for all classes that should walk over a tree of nodes.
This implements the visit() method.
"""
def __init__(self, tree=None):
self.tree = tree
def visit(self, node=None):
"""Walks over a node. If no node is provided, the tree is used."""
if node is None:
node = self.tree
if node is None:
raise RuntimeError('no node or tree given!')
for clsname in map(_lowername, type.mro(node.__class__)):
meth = getattr(self, 'visit_' + clsname, None)
if callable(meth):
rtn = meth(node)
break
else:
msg = 'could not find valid visitor method for {0} on {1}'
nodename = node.__class__.__name__
selfname = self.__class__.__name__
raise AttributeError(msg.format(nodename, selfname))
return rtn
class PrettyFormatter(Visitor):
"""Formats a tree of nodes into a pretty string"""
def __init__(self, tree=None, indent=' '):
super().__init__(tree=tree)
self.level = 0
self.indent = indent
def visit_node(self, node):
s = node.__class__.__name__ + '('
if len(node.attrs) == 0:
return s + ')'
s += '\n'
self.level += 1
t = []
for aname in node.attrs:
a = getattr(node, aname)
t.append(self.visit(a) if isinstance(a, Node) else pformat(a))
t = ['{0}={1}'.format(n, x) for n, x in zip(node.attrs, t)]
s += textwrap.indent(',\n'.join(t), self.indent)
self.level -= 1
s += '\n)'
return s
def visit_wizard(self, node):
s = 'Wizard(children=['
if len(node.children) == 0:
if node.path is None:
return s + '])'
else:
return s + '], path={0!r})'.format(node.path)
s += '\n'
self.level += 1
s += textwrap.indent(',\n'.join(map(self.visit, node.children)),
self.indent)
self.level -= 1
if node.path is None:
s += '\n])'
else:
s += '{0}],\n{0}path={1!r}\n)'.format(self.indent, node.path)
return s
def visit_message(self, node):
return 'Message({0!r})'.format(node.message)
def visit_question(self, node):
s = node.__class__.__name__ + '(\n'
self.level += 1
s += self.indent + 'question={0!r},\n'.format(node.question)
s += self.indent + 'responses={'
if len(node.responses) == 0:
s += '}'
else:
s += '\n'
t = sorted(node.responses.items())
t = ['{0!r}: {1}'.format(k, self.visit(v)) for k, v in t]
s += textwrap.indent(',\n'.join(t), 2*self.indent)
s += '\n' + self.indent + '}'
if node.converter is not None:
s += ',\n' + self.indent + 'converter={0!r}'.format(node.converter)
if node.path is not None:
s += ',\n' + self.indent + 'path={0!r}'.format(node.path)
self.level -= 1
s += '\n)'
return s
def visit_input(self, node):
s = '{0}(prompt={1!r}'.format(node.__class__.__name__, node.prompt)
if node.converter is None and node.path is None:
return s + '\n)'
if node.converter is not None:
s += ',\n' + self.indent + 'converter={0!r}'.format(node.converter)
s += ',\n' + self.indent + 'show_conversion={0!r}'.format(node.show_conversion)
if node.path is not None:
s += ',\n' + self.indent + 'path={0!r}'.format(node.path)
s += '\n)'
return s
def visit_statefile(self, node):
s = '{0}(default_file={1!r}, check={2})'
s = s.format(node.__class__.__name__, node.default_file, node.check)
return s
def visit_while(self, node):
s = '{0}(cond={1!r}'.format(node.__class__.__name__, node.cond)
s += ',\n' + self.indent + 'body=['
if len(node.body) > 0:
s += '\n'
self.level += 1
s += textwrap.indent(',\n'.join(map(self.visit, node.body)),
self.indent)
self.level -= 1
s += '\n' + self.indent
s += ']'
s += ',\n' + self.indent + 'idxname={0!r}'.format(node.idxname)
s += ',\n' + self.indent + 'beg={0!r}'.format(node.beg)
if node.path is not None:
s += ',\n' + self.indent + 'path={0!r}'.format(node.path)
s += '\n)'
return s
def ensure_str_or_int(x):
"""Creates a string or int."""
if isinstance(x, int):
return x
x = x if isinstance(x, str) else str(x)
try:
x = ast.literal_eval(x)
except (ValueError, SyntaxError):
pass
if not isinstance(x, (int, str)):
msg = '{0!r} could not be converted to int or str'.format(x)
raise ValueError(msg)
return x
def canon_path(path, indices=None):
"""Returns the canonical form of a path, which is a tuple of str or ints.
Indices may be optionally passed in.
"""
if not isinstance(path, str):
return tuple(map(ensure_str_or_int, path))
if indices is not None:
path = path.format(**indices)
path = path[1:] if path.startswith('/') else path
path = path[:-1] if path.endswith('/') else path
if len(path) == 0:
return ()
return tuple(map(ensure_str_or_int, path.split('/')))
class UnstorableType(object):
"""Represents an unstorable return value for when no input was given
or such input was skipped. Typically represented by the Unstorable
singleton.
"""
_inst = None
def __new__(cls, *args, **kwargs):
if cls._inst is None:
cls._inst = super(UnstorableType, cls).__new__(cls, *args,
**kwargs)
return cls._inst
Unstorable = UnstorableType()
class StateVisitor(Visitor):
"""This class visits the nodes and stores the results in a top-level
dict of data according to the state path of the node. The the node
does not have a path or the path does not exist, the storage is skipped.
This class can be optionally initialized with an existing state.
"""
def __init__(self, tree=None, state=None, indices=None):
super().__init__(tree=tree)
self.state = {} if state is None else state
self.indices = {} if indices is None else indices
def visit(self, node=None):
if node is None:
node = self.tree
if node is None:
raise RuntimeError('no node or tree given!')
rtn = super().visit(node)
path = getattr(node, 'path', None)
if path is not None and rtn is not Unstorable:
self.store(path, rtn, indices=self.indices)
return rtn
def store(self, path, val, indices=None):
"""Stores a value at the path location."""
path = canon_path(path, indices=indices)
loc = self.state
for p, n in zip(path[:-1], path[1:]):
if isinstance(p, str) and p not in loc:
loc[p] = {} if isinstance(n, str) else []
elif isinstance(p, int) and abs(p) + (p >= 0) > len(loc):
i = abs(p) + (p >= 0) - len(loc)
if isinstance(n, str):
ex = [{} for _ in range(i)]
else:
ex = [[] for _ in range(i)]
loc.extend(ex)
loc = loc[p]
p = path[-1]
loc[p] = val
YN = "{GREEN}yes{NO_COLOR} or {RED}no{NO_COLOR} [default: no]? "
YNB = ('{GREEN}yes{NO_COLOR}, {RED}no{NO_COLOR}, or '
'{YELLOW}break{NO_COLOR} [default: no]? ')
class PromptVisitor(StateVisitor):
"""Visits the nodes in the tree via the a command-line prompt."""
def __init__(self, tree=None, state=None, **kwargs):
"""
Parameters
----------
tree : Node, optional
Tree of nodes to start visitor with.
state : dict, optional
Initial state to begin with.
kwargs : optional
Options that are passed through to the prompt via the shell's
singleline() method. See BaseShell for mor details.
"""
super().__init__(tree=tree, state=state)
self.env = builtins.__xonsh_env__
self.shell = builtins.__xonsh_shell__.shell
self.shell_kwargs = kwargs
def visit_wizard(self, node):
for child in node.children:
self.visit(child)
def visit_pass(self, node):
pass
def visit_message(self, node):
print_color(node.message)
def visit_question(self, node):
self.env['PROMPT'] = node.question
r = self.shell.singleline(**self.shell_kwargs)
if callable(node.converter):
r = node.converter(r)
self.visit(node.responses[r])
return r
def visit_input(self, node):
need_input = True
while need_input:
self.env['PROMPT'] = node.prompt
x = self.shell.singleline(**self.shell_kwargs)
if callable(node.converter):
x, raw = node.converter(x), x
if node.show_conversion and x is not Unstorable \
and str(x) != raw:
msg = '{{BOLD_PURPLE}}Converted{{NO_COLOR}} input {0!r} to {1!r}.'
print_color(msg.format(raw, x))
if node.confirm:
msg = 'Would you like to keep the input: {0}'
print(msg.format(pformat(x)))
confirmer = TrueFalseBreak(prompt=YNB)
status = self.visit(confirmer)
if isinstance(status, str) and status == 'break':
x = Unstorable
break
else:
need_input = not status
else:
need_input = False
return x
def visit_while(self, node):
rtns = []
origidx = self.indices.get(node.idxname, None)
self.indices[node.idxname] = idx = node.beg
while node.cond(visitor=self, node=node):
rtn = list(map(self.visit, node.body))
rtns.append(rtn)
idx += 1
self.indices[node.idxname] = idx
if origidx is None:
del self.indices[node.idxname]
else:
self.indices[node.idxname] = origidx
return rtns
def visit_save(self, node):
jstate = json.dumps(self.state, indent=1, sort_keys=True)
if node.check:
msg = 'The current state is:\n\n{0}\n'
print(msg.format(textwrap.indent(jstate, ' ')))
ap = 'Would you like to save this state, ' + YN
asker = TrueFalse(prompt=ap)
do_save = self.visit(asker)
if not do_save:
return Unstorable
fname = self.visit_input(node)
if fname is None or len(fname) == 0:
fname = node.default_file
if os.path.isfile(fname):
backup_file(fname)
else:
os.makedirs(os.path.dirname(fname), exist_ok=True)
with open(fname, 'w') as f:
f.write(jstate)
return fname
def visit_load(self, node):
if node.check:
ap = 'Would you like to load an existing file, ' + YN
asker = TrueFalse(prompt=ap)
do_load = self.visit(asker)
if not do_load:
return Unstorable
fname = self.visit_input(node)
if fname is None or len(fname) == 0:
fname = node.default_file
if os.path.isfile(fname):
with open(fname, 'r') as f:
self.state = json.load(f)
print_color('{{GREEN}}{0!r} loaded.{{NO_COLOR}}'.format(fname))
else:
print_color(('{{RED}}{0!r} could not be found, '
'continuing.{{NO_COLOR}}').format(fname))
return fname

289
xonsh/xonfig.py Normal file
View file

@ -0,0 +1,289 @@
"""The xonsh configuration (xonfig) utility."""
import os
import ast
import json
import textwrap
import builtins
import functools
from pprint import pformat
from argparse import ArgumentParser
import ply
from xonsh import __version__ as XONSH_VERSION
from xonsh import tools
from xonsh.environ import is_template_string
from xonsh.shell import (is_readline_available, is_prompt_toolkit_available,
prompt_toolkit_version)
from xonsh.wizard import (Wizard, Pass, Message, Save, Load, YesNo, Input,
PromptVisitor, While, StoreNonEmpty, create_truefalse_cond, YN)
HR = "'`-.,¸,.-*¯`-.,¸,.-*¯`-.,¸,.-*¯`-.,¸,.-*¯`-.,¸,.-*¯`-.,¸,.-*¯`-.,¸,.-*'"
WIZARD_HEAD = """
{{BOLD_WHITE}}Welcome to the xonsh configuration wizard!{{NO_COLOR}}
{{YELLOW}}------------------------------------------{{NO_COLOR}}
This will present a guided tour through setting up the xonsh static
config file. Xonsh will automatically ask you if you want to run this
wizard if the configuration file does not exist. However, you can
always rerun this wizard with the xonfig command:
$ xonfig wizard
This wizard will load an existing configuration, if it is available.
Also never fear when this wizard saves its results! It will create
a backup of any existing configuration automatically.
This wizard has two main phases: foreign shell setup and environment
variable setup. Each phase may be skipped in its entirety.
For the configuration to take effect, you will need to restart xonsh.
{hr}
""".format(hr=HR)
WIZARD_FS = """
{hr}
{{BOLD_WHITE}}Foreign Shell Setup{{NO_COLOR}}
{{YELLOW}}-------------------{{NO_COLOR}}
The xonsh shell has the ability to interface with foreign shells such
as Bash, zsh, or fish.
For configuration, this means that xonsh can load the environment,
aliases, and functions specified in the config files of these shells.
Naturally, these shells must be available on the system to work.
Being able to share configuration (and source) from foreign shells
makes it easier to transition to and from xonsh.
""".format(hr=HR)
WIZARD_ENV = """
{hr}
{{BOLD_WHITE}}Environment Variable Setup{{NO_COLOR}}
{{YELLOW}}--------------------------{{NO_COLOR}}
The xonsh shell also allows you to setup environment variables from
the static configuration file. Any variables set in this way are
superceded by the definitions in the xonshrc or on the command line.
Still, setting environment variables in this way can help define
options that are global to the system or user.
The following lists the environment variable name, its documentation,
the default value, and the current value. The default and current
values are presented as pretty repr strings of their Python types.
{{BOLD_GREEN}}Note:{{NO_COLOR}} Simply hitting enter for any environment variable
will accept the default value for that entry.
Would you like to set env vars now, """.format(hr=HR) + YN
WIZARD_TAIL = """
Thanks for using the xonsh configuration wizard!"""
def make_fs():
"""Makes the foreign shell part of the wizard."""
cond = create_truefalse_cond(prompt='Add a foreign shell, ' + YN)
fs = While(cond=cond, body=[
Input('shell name (e.g. bash): ', path='/foreign_shells/{idx}/shell'),
StoreNonEmpty('interactive shell [bool, default=True]: ',
converter=tools.to_bool,
show_conversion=True,
path='/foreign_shells/{idx}/interactive'),
StoreNonEmpty('login shell [bool, default=False]: ',
converter=tools.to_bool,
show_conversion=True,
path='/foreign_shells/{idx}/login'),
StoreNonEmpty("env command [str, default='env']: ",
path='/foreign_shells/{idx}/envcmd'),
StoreNonEmpty("alias command [str, default='alias']: ",
path='/foreign_shells/{idx}/aliascmd'),
StoreNonEmpty(("extra command line arguments [list of str, "
"default=[]]: "),
converter=ast.literal_eval,
show_conversion=True,
path='/foreign_shells/{idx}/extra_args'),
StoreNonEmpty('current environment [dict, default=None]: ',
converter=ast.literal_eval,
show_conversion=True,
path='/foreign_shells/{idx}/currenv'),
StoreNonEmpty('safely handle exceptions [bool, default=True]: ',
converter=tools.to_bool,
show_conversion=True,
path='/foreign_shells/{idx}/safe'),
StoreNonEmpty("pre-command [str, default='']: ",
path='/foreign_shells/{idx}/prevcmd'),
StoreNonEmpty("post-command [str, default='']: ",
path='/foreign_shells/{idx}/postcmd'),
StoreNonEmpty("foreign function command [str, default=None]: ",
path='/foreign_shells/{idx}/funcscmd'),
StoreNonEmpty("source command [str, default=None]: ",
path='/foreign_shells/{idx}/sourcer'),
Message(message='') # inserts a newline
])
return fs
def _wrap_paragraphs(text, width=70, **kwargs):
"""Wraps paragraphs instead."""
pars = text.split('\n')
pars = ['\n'.join(textwrap.wrap(p, width=width, **kwargs)) for p in pars]
s = '\n'.join(pars)
return s
ENVVAR_PROMPT = """
{{BOLD_CYAN}}${name}{{NO_COLOR}}
{docstr}
{{RED}}default value:{{NO_COLOR}} {default}
{{RED}}current value:{{NO_COLOR}} {current}
{{BOLD_GREEN}}>>>{{NO_COLOR}} """
def make_envvar(name):
"""Makes a StoreNonEmpty node for an environment variable."""
env = builtins.__xonsh_env__
vd = env.get_docs(name)
if not vd.configurable:
return
default = vd.default
if '\n' in default:
default = '\n' + _wrap_paragraphs(default, width=69)
curr = env.get(name)
if tools.is_string(curr) and is_template_string(curr):
curr = curr.replace('{', '{{').replace('}', '}}')
curr = pformat(curr, width=69)
if '\n' in curr:
curr = '\n' + curr
prompt = ENVVAR_PROMPT.format(name=name, default=default, current=curr,
docstr=_wrap_paragraphs(vd.docstr, width=69))
ens = env.get_ensurer(name)
path = '/env/' + name
node = StoreNonEmpty(prompt, converter=ens.convert, show_conversion=True,
path=path)
return node
def make_env():
"""Makes an environment variable wizard."""
kids = map(make_envvar, sorted(builtins.__xonsh_env__.docs.keys()))
kids = [k for k in kids if k is not None]
wiz = Wizard(children=kids)
return wiz
def make_wizard(default_file=None, confirm=False):
"""Makes a configuration wizard for xonsh config file.
Parameters
----------
default_file : str, optional
Default filename to save and load to. User will still be prompted.
confirm : bool, optional
Confirm that the main part of the wizard should be run.
"""
wiz = Wizard(children=[
Message(message=WIZARD_HEAD),
Load(default_file=default_file, check=True),
Message(message=WIZARD_FS),
make_fs(),
YesNo(question=WIZARD_ENV, yes=make_env(), no=Pass()),
Message(message='\n' + HR + '\n'),
Save(default_file=default_file, check=True),
Message(message=WIZARD_TAIL),
])
if confirm:
q = 'Would you like to run the xonsh configuration wizard now, ' + YN
wiz = YesNo(question=q, yes=wiz, no=Pass())
return wiz
def _wizard(ns):
env = builtins.__xonsh_env__
fname = env.get('XONSHCONFIG') if ns.file is None else ns.file
wiz = make_wizard(default_file=fname, confirm=ns.confirm)
tempenv = {'PROMPT': '', 'XONSH_STORE_STDOUT': False}
pv = PromptVisitor(wiz, store_in_history=False, multiline=False)
with env.swap(tempenv):
try:
pv.visit()
except (KeyboardInterrupt, EOFError):
pass
def _format_human(data):
wcol1 = wcol2 = 0
for key, val in data:
wcol1 = max(wcol1, len(key))
wcol2 = max(wcol2, len(str(val)))
hr = '+' + ('-'*(wcol1+2)) + '+' + ('-'*(wcol2+2)) + '+\n'
row = '| {key!s:<{wcol1}} | {val!s:<{wcol2}} |\n'
s = hr
for key, val in data:
s += row.format(key=key, wcol1=wcol1, val=val, wcol2=wcol2)
s += hr
return s
def _format_json(data):
data = {k.replace(' ', '_'): v for k, v in data}
s = json.dumps(data, sort_keys=True, indent=1) + '\n'
return s
def _info(ns):
data = [
('xonsh', XONSH_VERSION),
('Python', '.'.join(map(str, tools.VER_FULL))),
('PLY', ply.__version__),
('have readline', is_readline_available()),
('prompt toolkit', prompt_toolkit_version() if \
is_prompt_toolkit_available() else False),
('on posix', tools.ON_POSIX),
('on linux', tools.ON_LINUX),
('on arch', tools.ON_ARCH),
('on windows', tools.ON_WINDOWS),
('on mac', tools.ON_MAC),
('are root user', tools.IS_ROOT),
('default encoding', tools.DEFAULT_ENCODING),
]
formatter = _format_json if ns.json else _format_human
s = formatter(data)
return s
@functools.lru_cache()
def _create_parser():
p = ArgumentParser(prog='xonfig',
description='Manages xonsh configuration.')
subp = p.add_subparsers(title='action', dest='action')
info = subp.add_parser('info', help=('displays configuration information, '
'default action'))
info.add_argument('--json', action='store_true', default=False,
help='reports results as json')
wiz = subp.add_parser('wizard', help=('displays configuration information, '
'default action'))
wiz.add_argument('--file', default=None,
help='config file location, default=$XONSHCONFIG')
wiz.add_argument('--confirm', action='store_true', default=False,
help='confirm that the wizard should be run.')
return p
_MAIN_ACTIONS = {
'info': _info,
'wizard': _wizard,
}
def main(args=None):
"""Main xonfig entry point."""
if not args or (args[0] not in _MAIN_ACTIONS and
args[0] not in {'-h', '--help'}):
args.insert(0, 'info')
parser = _create_parser()
ns = parser.parse_args(args)
if ns.action is None: # apply default action
ns = parser.parse_args(['info'] + args)
return _MAIN_ACTIONS[ns.action](ns)
if __name__ == '__main__':
main()