Merge branch 'teepipe'

This commit is contained in:
Anthony Scopatz 2015-10-12 00:24:35 -04:00
commit bcd9f0cce0
23 changed files with 811 additions and 276 deletions

View file

@ -0,0 +1,10 @@
.. _xonsh_foreign_shells:
******************************************************
Foreign Shell Tools (``xonsh.foreign_shells``)
******************************************************
.. automodule:: xonsh.foreign_shells
:members:
:undoc-members:
:inherited-members:

View file

@ -51,5 +51,6 @@ For those of you who want the gritty details.
lazyjson lazyjson
teepty teepty
openpy openpy
foreign_shells
main main
pyghooks pyghooks

View file

@ -11,60 +11,68 @@ applicable.
* - variable * - variable
- default - default
- description - description
* - PROMPT * - ANSICON
- xonsh.environ.DEFAULT_PROMPT - No default set
- The prompt text. May contain keyword arguments which are auto-formatted, - This is used on Windows to set the title, if available.
see `Customizing the Prompt <tutorial.html#customizing-the-prompt>`_. * - AUTO_PUSHD
* - MULTILINE_PROMPT - ``False``
- ``'.'`` - Flag for automatically pushing directorties onto the directory stack.
- Prompt text for 2nd+ lines of input, may be str or function which returns a str. * - BASH_COMPLETIONS
* - TITLE - Normally this is ``('/etc/bash_completion',
- xonsh.environ.DEFAULT_TITLE '/usr/share/bash-completion/completions/git')``
- The title text for the window in which xonsh is running. Formatted in the same but on Mac is ``'/usr/local/etc/bash_completion',
manner as PROMPT, '/opt/local/etc/profile.d/bash_completion.sh')``.
see `Customizing the Prompt <tutorial.html#customizing-the-prompt>`_. - 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.
* - DIRSTACK_SIZE
- ``20``
- Maximum size of the directory stack.
* - FORCE_POSIX_PATHS
- ``False``
- Forces forward slashes (``/``) on Windows systems when using auto completion if
set to anything truthy.
* - FORMATTER_DICT * - FORMATTER_DICT
- xonsh.environ.FORMATTER_DICT - xonsh.environ.FORMATTER_DICT
- Dictionary containing variables to be used when formatting PROMPT and TITLE - Dictionary containing variables to be used when formatting PROMPT and TITLE
see `Customizing the Prompt <tutorial.html#customizing-the-prompt>`_. see `Customizing the Prompt <tutorial.html#customizing-the-prompt>`_.
* - XONSHRC * - INDENT
- ``'~/.xonshrc'`` - ``' '``
- Location of run control file. - Indentation string for multiline input
* - XONSH_HISTORY_SIZE * - MULTILINE_PROMPT
- ``(8128, 'commands')`` or ``'8128 commands'`` - ``'.'``
- Value and units tuple that sets the size of history after garbage collection. - Prompt text for 2nd+ lines of input, may be str or function which returns
Canonical units are ``'commands'`` for the number of past commands executed, a str.
``'files'`` for the number of history files to keep, ``'s'`` for the number of * - OLDPWD
seconds in the past that are allowed, and ``'b'`` for the number of bytes that - No default
are allowed for history to consume. Common abbreviations, such as ``6 months`` - Used to represent a previous present working directory.
or ``1 GB`` are also allowed. * - PATH
* - XONSH_HISTORY_FILE - ``()``
- ``'~/.xonsh_history'`` - List of strings representing where to look for executables.
- Location of history file (deprecated). * - PATHEXT
* - XONSH_STORE_STDOUT - ``()``
- 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_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`` - ``False``
- Whether or not to store the stdout and stderr streams in the history files. - Flag for directory pushing functionality. False is the normal behaviour.
* - XONSH_INTERACTIVE * - PUSHD_SILENT
- - ``False``
- ``True`` if xonsh is running interactively, and ``False`` otherwise. - Whether or not to supress directory stack manipulation output.
* - BASH_COMPLETIONS
- ``[] or ['/etc/...']``
- This is a list 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.
* - 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_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.
* - 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.
* - SHELL_TYPE * - SHELL_TYPE
- ``'readline'`` - ``'readline'``
- Which shell is used. Currently two shell types are supported: ``'readline'`` that - Which shell is used. Currently two shell types are supported: ``'readline'`` that
@ -74,27 +82,78 @@ applicable.
`prompt_toolkit <https://github.com/jonathanslenders/python-prompt-toolkit>`_ `prompt_toolkit <https://github.com/jonathanslenders/python-prompt-toolkit>`_
library installed. To specify which shell should be used, do so in the run library installed. To specify which shell should be used, do so in the run
control file. control file.
* - CDPATH * - SUGGEST_COMMANDS
- ``[]`` - ``True``
- A list of paths to be used as roots for a ``cd``, breaking compatibility with - When a user types an invalid command, xonsh will try to offer suggestions of
bash, xonsh always prefer an existing relative path. similar valid commands if this is ``True``.
* - XONSH_SHOW_TRACEBACK * - SUGGEST_MAX_NUM
- Not defined - ``5``
- Controls if a traceback is shown exceptions occur in the shell. Set ``'True'`` - xonsh will show at most this many suggestions in response to an invalid command.
to always show or ``'False'`` to always hide. If undefined then traceback is If negative, there is no limit to how many suggestions are shown.
hidden but a notice is shown on how to enable the traceback. * - SUGGEST_THRESHOLD
* - CASE_SENSITIVE_COMPLETIONS - ``3``
- ``True`` on Linux, otherwise ``False`` - An error threshold. If the Levenshtein distance between the entered command and
- Sets whether completions should be case sensitive or case insensitive. a valid command is less than this value, the valid command will be offered as a
* - FORCE_POSIX_PATHS suggestion.
- Not defined * - TEEPTY_PIPE_DELAY
- Forces forward slashes (``/``) on Windows systems when using auto completion if - ``0.01``
set to anything truthy. - The number of [seconds] to delay a spawned process if it has information
* - XONSH_DATA_DIR being piped in via stdin. This value must be a float. If a value less than
- ``$XDG_DATA_HOME/xonsh`` or equal to zero is passed in, no delay is used. This can be used to fix
- This is the location where xonsh data files are stored, such as history. 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>`_.
* - 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
- ``~/.xonshrc``
- Location of run control file.
* - XONSH_CONFIG_DIR * - XONSH_CONFIG_DIR
- ``$XDG_CONFIG_HOME/xonsh`` - ``$XDG_CONFIG_HOME/xonsh``
- This is location where xonsh configuration information is stored. - 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_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_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.

View file

@ -67,6 +67,7 @@ Contents
tutorial tutorial
tutorial_hist tutorial_hist
xonshrc xonshrc
xonshconfig
envvars envvars
aliases aliases
windows windows

12
docs/xonshconfig.json Normal file
View file

@ -0,0 +1,12 @@
{"env": {
"EDITOR": "xo",
"PAGER": "more"
},
"foreign_shells": [
{"shell": "bash",
"login": true,
"extra_args": ["--rcfile", "/path/to/rcfile"]
},
{"shell": "zsh"}
]
}

84
docs/xonshconfig.rst Normal file
View file

@ -0,0 +1,84 @@
Static Configuration File
=========================
In addition to the run control file, xonsh allows you to have a static config file.
This JSON-formatted file lives at ``$XONSH_CONFIG_DIR/config.json``, which is
normally ``~/.config/xonsh/config.json``. The purpose of this file is to allow
users to set runtime parameters *before* anything else happens. This inlcudes
loading data from various foreign shells or setting critical environment
variables.
This is a dictionary or JSON object at its top-level. It has the following
top-level keys. All top-level keys are optional.
``env``
--------
This is a simple string-keyed dictionary that lets you set environment
variables. For example,
.. code:: json
{"env": {
"EDITOR": "xo",
"PAGER": "more"
}
}
``foreign_shells``
--------------------
This is a list (JSON Array) of dicts (JSON objects) that represent the
foreign shells to inspect for extra start up information, such as environment
variables and aliases. The suite of data gathered may be expanded in the
future. Each shell dictionary unpacked and passed into the
``xonsh.foreign_shells.foreign_shell_data()`` function. Thus these dictionaries
have the following structure:
:shell: *str, required* - The name or path of the shell, such as "bash" or "/bin/sh".
:interactive: *bool, optional* - Whether the shell should be run in interactive mode.
``default=true``
:login: *bool, optional* - Whether the shell should be a login shell.
``default=false``
:envcmd: *str, optional* - The command to generate environment output with.
``default="env"``
:aliascmd: *str, optional* - The command to generate alais output with.
``default="alias"``
:extra_args: *list of str, optional* - Addtional command line options to pass
into the shell. ``default=[]``
:currenv: *dict or null, optional* - Manual override for the current environment.
``default=null``
:safe: *bool, optional* - Flag for whether or not to safely handle exceptions
and other errors. ``default=true``
Some examples can be seen below:
.. code:: json
# load bash then zsh
{"foreign_shells": [
{"shell": "/bin/bash"},
{"shell": "zsh"}
]
}
# load bash as a login shell with custom rcfile
{"foreign_shells": [
{"shell": "bash",
"login": true,
"extra_args": ["--rcfile", "/path/to/rcfile"]
}
]
}
# disable all foreign shell loading via an empty list
{"foreign_shells": []}
Putting it all together
-----------------------
The following ecample shows a fully fleshed out config file.
:download:`Download config.json <xonshxonfig.json>`
.. include:: xonshconfig.json
:code: json

5
tests/bashrc.sh Normal file
View file

@ -0,0 +1,5 @@
export EMERALD="SWORD"
alias ll='ls -a -lF'
alias la='ls -A'
export MIGHTY=WARRIOR
alias l='ls -CF'

View file

@ -0,0 +1,56 @@
"""Tests foreign shells."""
from __future__ import unicode_literals, print_function
import os
import subprocess
import nose
from nose.plugins.skip import SkipTest
from nose.tools import assert_equal, assert_true, assert_false
from xonsh.foreign_shells import foreign_shell_data, parse_env, parse_aliases
def test_parse_env():
exp = {'X': 'YES', 'Y': 'NO'}
s = ('some garbage\n'
'__XONSH_ENV_BEG__\n'
'Y=NO\n'
'X=YES\n'
'__XONSH_ENV_END__\n'
'more filth')
obs = parse_env(s)
assert_equal(exp, obs)
def test_parse_aliases():
exp = {'x': ['yes', '-1'], 'y': ['echo', 'no']}
s = ('some garbage\n'
'__XONSH_ALIAS_BEG__\n'
"alias x='yes -1'\n"
"alias y='echo no'\n"
'__XONSH_ALIAS_END__\n'
'more filth')
obs = parse_aliases(s)
assert_equal(exp, obs)
def test_foreign_bash_data():
expenv = {"EMERALD": "SWORD", 'MIGHTY': 'WARRIOR'}
expaliases = {
'l': ['ls', '-CF'],
'la': ['ls', '-A'],
'll': ['ls', '-a', '-lF'],
}
rcfile = os.path.join(os.path.dirname(__file__), 'bashrc.sh')
try:
obsenv, obsaliases = foreign_shell_data('bash', currenv=(),
extra_args=('--rcfile', rcfile),
safe=False)
except (subprocess.CalledProcessError, FileNotFoundError):
raise SkipTest
for key, expval in expenv.items():
yield assert_equal, expval, obsenv.get(key, False)
yield assert_equal, expaliases, obsaliases
if __name__ == '__main__':
nose.runmodule()

View file

@ -9,7 +9,7 @@ from xonsh.lexer import Lexer
from xonsh.tools import subproc_toks, subexpr_from_unbalanced, is_int, \ from xonsh.tools import subproc_toks, subexpr_from_unbalanced, is_int, \
always_true, always_false, ensure_string, is_env_path, str_to_env_path, \ always_true, always_false, ensure_string, is_env_path, str_to_env_path, \
env_path_to_str, escape_windows_title_string, is_bool, to_bool, bool_to_str, \ env_path_to_str, escape_windows_title_string, is_bool, to_bool, bool_to_str, \
ensure_int_or_slice ensure_int_or_slice, is_float
LEXER = Lexer() LEXER = Lexer()
LEXER.build() LEXER.build()
@ -154,6 +154,10 @@ def test_is_int():
yield assert_true, is_int(42) yield assert_true, is_int(42)
yield assert_false, is_int('42') yield assert_false, is_int('42')
def test_is_float():
yield assert_true, is_float(42.0)
yield assert_false, is_float('42.0')
def test_always_true(): def test_always_true():
yield assert_true, always_true(42) yield assert_true, always_true(42)
yield assert_true, always_true('42') yield assert_true, always_true('42')

View file

@ -1,10 +1,8 @@
"""Aliases for the xonsh shell.""" """Aliases for the xonsh shell."""
import os import os
import sys
import shlex import shlex
import builtins import builtins
import subprocess import subprocess
import datetime
from warnings import warn from warnings import warn
from argparse import ArgumentParser from argparse import ArgumentParser
@ -75,6 +73,7 @@ def xexec(args, stdin=None):
_BANG_N_PARSER = None _BANG_N_PARSER = None
def bang_n(args, stdin=None): def bang_n(args, stdin=None):
"""Re-runs the nth command as specified in the argument.""" """Re-runs the nth command as specified in the argument."""
global _BANG_N_PARSER global _BANG_N_PARSER
@ -101,37 +100,6 @@ def bang_bang(args, stdin=None):
return bang_n(['-1']) return bang_n(['-1'])
def bash_aliases():
"""Computes a dictionary of aliases based on Bash's aliases."""
try:
s = subprocess.check_output(['bash', '-i', '-l'],
input='alias',
stderr=subprocess.PIPE,
universal_newlines=True)
except (subprocess.CalledProcessError, FileNotFoundError):
s = ''
items = [line.split('=', 1) for line in s.splitlines() if '=' in line]
aliases = {}
for key, value in items:
try:
key = key[6:] # lstrip 'alias '
# undo bash's weird quoting of single quotes (sh_single_quote)
value = value.replace('\'\\\'\'', '\'')
# strip one single quote at the start and end of value
if value[0] == '\'' and value[-1] == '\'':
value = value[1:-1]
value = shlex.split(value)
except ValueError as exc:
warn('could not parse Bash alias "{0}": {1!r}'.format(key, exc),
RuntimeWarning)
continue
aliases[key] = value
return aliases
DEFAULT_ALIASES = { DEFAULT_ALIASES = {
'cd': cd, 'cd': cd,
'pushd': pushd, 'pushd': pushd,
@ -186,5 +154,3 @@ elif ON_MAC:
else: else:
DEFAULT_ALIASES['grep'] = ['grep', '--color=auto'] DEFAULT_ALIASES['grep'] = ['grep', '--color=auto']
DEFAULT_ALIASES['ls'] = ['ls', '--color=auto', '-v'] DEFAULT_ALIASES['ls'] = ['ls', '--color=auto', '-v']

View file

@ -119,7 +119,7 @@ class BaseShell(object):
return return
hist = builtins.__xonsh_history__ hist = builtins.__xonsh_history__
ts1 = None ts1 = None
tee = Tee() if builtins.__xonsh_env__.get('XONSH_STORE_STDOUT', False) \ tee = Tee() if builtins.__xonsh_env__.get('XONSH_STORE_STDOUT') \
else io.StringIO() else io.StringIO()
try: try:
ts0 = time.time() ts0 = time.time()
@ -181,9 +181,8 @@ class BaseShell(object):
term = env.get('TERM', None) term = env.get('TERM', None)
if term is None or term == 'linux': if term is None or term == 'linux':
return return
if 'TITLE' in env: t = env.get('TITLE')
t = env['TITLE'] if t is None:
else:
return return
t = format_prompt(t) t = format_prompt(t)
if ON_WINDOWS and 'ANSICON' not in env: if ON_WINDOWS and 'ANSICON' not in env:
@ -204,14 +203,11 @@ class BaseShell(object):
self.mlprompt = '<multiline prompt error> ' self.mlprompt = '<multiline prompt error> '
return self.mlprompt return self.mlprompt
env = builtins.__xonsh_env__ env = builtins.__xonsh_env__
if 'PROMPT' in env: p = env.get('PROMPT')
p = env['PROMPT'] try:
try: p = format_prompt(p)
p = format_prompt(p) except Exception:
except Exception: print_exception()
print_exception()
else:
p = "set '$PROMPT = ...' $ "
self.settitle() self.settitle()
return p return p

View file

@ -22,10 +22,11 @@ from xonsh.tools import suggest_commands, XonshError, ON_POSIX, ON_WINDOWS, \
string_types string_types
from xonsh.inspectors import Inspector from xonsh.inspectors import Inspector
from xonsh.environ import Env, default_env from xonsh.environ import Env, default_env
from xonsh.aliases import DEFAULT_ALIASES, bash_aliases from xonsh.aliases import DEFAULT_ALIASES
from xonsh.jobs import add_job, wait_for_active_job from xonsh.jobs import add_job, wait_for_active_job
from xonsh.proc import ProcProxy, SimpleProcProxy, TeePTYProc from xonsh.proc import ProcProxy, SimpleProcProxy, TeePTYProc
from xonsh.history import History from xonsh.history import History
from xonsh.foreign_shells import load_foreign_aliases
ENV = None ENV = None
BUILTINS_LOADED = False BUILTINS_LOADED = False
@ -268,20 +269,17 @@ RE_SHEBANG = re.compile(r'#![ \t]*(.+?)$')
def _get_runnable_name(fname): def _get_runnable_name(fname):
if os.path.isfile(fname) and fname != os.path.basename(fname): if os.path.isfile(fname) and fname != os.path.basename(fname):
return fname return fname
for d in builtins.__xonsh_env__['PATH']: for d in builtins.__xonsh_env__.get('PATH'):
if os.path.isdir(d): if os.path.isdir(d):
files = os.listdir(d) files = os.listdir(d)
if ON_WINDOWS: if ON_WINDOWS:
PATHEXT = builtins.__xonsh_env__.get('PATHEXT', []) PATHEXT = builtins.__xonsh_env__.get('PATHEXT')
for dirfile in files: for dirfile in files:
froot, ext = os.path.splitext(dirfile) froot, ext = os.path.splitext(dirfile)
if fname == froot and ext.upper() in PATHEXT: if fname == froot and ext.upper() in PATHEXT:
return os.path.join(d, dirfile) return os.path.join(d, dirfile)
if fname in files: if fname in files:
return os.path.join(d, fname) return os.path.join(d, fname)
return None return None
@ -666,7 +664,7 @@ def load_builtins(execer=None):
builtins.execx = None if execer is None else execer.exec builtins.execx = None if execer is None else execer.exec
builtins.compilex = None if execer is None else execer.compile builtins.compilex = None if execer is None else execer.compile
builtins.default_aliases = builtins.aliases = Aliases(DEFAULT_ALIASES) builtins.default_aliases = builtins.aliases = Aliases(DEFAULT_ALIASES)
builtins.aliases.update(bash_aliases()) builtins.aliases.update(load_foreign_aliases(issue_warning=False))
# history needs to be started after env and aliases # history needs to be started after env and aliases
# would be nice to actually include non-detyped versions. # would be nice to actually include non-detyped versions.
builtins.__xonsh_history__ = History(env=ENV.detype(), #aliases=builtins.aliases, builtins.__xonsh_history__ = History(env=ENV.detype(), #aliases=builtins.aliases,

View file

@ -38,10 +38,6 @@ COMP_CWORD={n}
for ((i=0;i<${{#COMPREPLY[*]}};i++)) do echo ${{COMPREPLY[i]}}; done for ((i=0;i<${{#COMPREPLY[*]}};i++)) do echo ${{COMPREPLY[i]}}; done
""" """
get_env = lambda name, default=None: builtins.__xonsh_env__.get(name, default)
def startswithlow(x, start, startlow=None): def startswithlow(x, start, startlow=None):
"""True if x starts with a string or its lowercase version. The lowercase """True if x starts with a string or its lowercase version. The lowercase
version may be optionally be provided. version may be optionally be provided.
@ -73,7 +69,7 @@ def _normpath(p):
if trailing_slash: if trailing_slash:
p = os.path.join(p, '') p = os.path.join(p, '')
if ON_WINDOWS and get_env('FORCE_POSIX_PATHS', False): if ON_WINDOWS and builtins.__xonsh_env__.get('FORCE_POSIX_PATHS'):
p = p.replace(os.sep, os.altsep) p = p.replace(os.sep, os.altsep)
return p return p
@ -125,7 +121,7 @@ class Completer(object):
ctx = ctx or {} ctx = ctx or {}
prefixlow = prefix.lower() prefixlow = prefix.lower()
cmd = line.split(' ', 1)[0] cmd = line.split(' ', 1)[0]
csc = get_env('CASE_SENSITIVE_COMPLETIONS', True) csc = builtins.__xonsh_env__.get('CASE_SENSITIVE_COMPLETIONS')
startswither = startswithnorm if csc else startswithlow startswither = startswithnorm if csc else startswithlow
if begidx == 0: if begidx == 0:
# the first thing we're typing; could be python or subprocess, so # the first thing we're typing; could be python or subprocess, so
@ -172,7 +168,7 @@ class Completer(object):
def _add_env(self, paths, prefix): def _add_env(self, paths, prefix):
if prefix.startswith('$'): if prefix.startswith('$'):
csc = get_env('CASE_SENSITIVE_COMPLETIONS', True) csc = builtins.__xonsh_env__.get('CASE_SENSITIVE_COMPLETIONS')
startswither = startswithnorm if csc else startswithlow startswither = startswithnorm if csc else startswithlow
key = prefix[1:] key = prefix[1:]
keylow = key.lower() keylow = key.lower()
@ -186,8 +182,9 @@ class Completer(object):
def _add_cdpaths(self, paths, prefix): def _add_cdpaths(self, paths, prefix):
"""Completes current prefix using CDPATH""" """Completes current prefix using CDPATH"""
csc = get_env('CASE_SENSITIVE_COMPLETIONS', True) env = builtins.__xonsh_env__
for cdp in get_env("CDPATH", []): csc = env.get('CASE_SENSITIVE_COMPLETIONS')
for cdp in env.get('CDPATH'):
test_glob = os.path.join(cdp, prefix) + '*' test_glob = os.path.join(cdp, prefix) + '*'
for s in iglobpath(test_glob, ignore_case=(not csc)): for s in iglobpath(test_glob, ignore_case=(not csc)):
if os.path.isdir(s): if os.path.isdir(s):
@ -197,7 +194,7 @@ class Completer(object):
"""Completes a command name based on what is on the $PATH""" """Completes a command name based on what is on the $PATH"""
space = ' ' space = ' '
cmdlow = cmd.lower() cmdlow = cmd.lower()
csc = get_env('CASE_SENSITIVE_COMPLETIONS', True) csc = builtins.__xonsh_env__.get('CASE_SENSITIVE_COMPLETIONS')
startswither = startswithnorm if csc else startswithlow startswither = startswithnorm if csc else startswithlow
return {s + space return {s + space
for s in self._all_commands() for s in self._all_commands()
@ -207,7 +204,7 @@ class Completer(object):
"""Completes a name of a module to import.""" """Completes a name of a module to import."""
prefixlow = prefix.lower() prefixlow = prefix.lower()
modules = set(sys.modules.keys()) modules = set(sys.modules.keys())
csc = get_env('CASE_SENSITIVE_COMPLETIONS', True) csc = builtins.__xonsh_env__.get('CASE_SENSITIVE_COMPLETIONS')
startswither = startswithnorm if csc else startswithlow startswither = startswithnorm if csc else startswithlow
return {s for s in modules if startswither(s, prefix, prefixlow)} return {s for s in modules if startswither(s, prefix, prefixlow)}
@ -217,7 +214,7 @@ class Completer(object):
slash = '/' slash = '/'
tilde = '~' tilde = '~'
paths = set() paths = set()
csc = get_env('CASE_SENSITIVE_COMPLETIONS', True) csc = builtins.__xonsh_env__.get('CASE_SENSITIVE_COMPLETIONS')
if prefix.startswith("'") or prefix.startswith('"'): if prefix.startswith("'") or prefix.startswith('"'):
prefix = prefix[1:] prefix = prefix[1:]
for s in iglobpath(prefix + '*', ignore_case=(not csc)): for s in iglobpath(prefix + '*', ignore_case=(not csc)):
@ -279,9 +276,10 @@ class Completer(object):
def _source_completions(self): def _source_completions(self):
srcs = [] srcs = []
for f in builtins.__xonsh_env__.get('BASH_COMPLETIONS', ()): for f in builtins.__xonsh_env__.get('BASH_COMPLETIONS'):
if os.path.isfile(f): if os.path.isfile(f):
if ON_WINDOWS: # We need to "Unixify" Windows paths for Bash to understand # We need to "Unixify" Windows paths for Bash to understand
if ON_WINDOWS:
f = RE_WIN_DRIVE.sub(lambda m: '/{0}/'.format(m.group(1).lower()), f).replace('\\', '/') f = RE_WIN_DRIVE.sub(lambda m: '/{0}/'.format(m.group(1).lower()), f).replace('\\', '/')
srcs.append('source ' + f) srcs.append('source ' + f)
return srcs return srcs
@ -346,7 +344,7 @@ class Completer(object):
if len(attr) == 0: if len(attr) == 0:
opts = [o for o in opts if not o.startswith('_')] opts = [o for o in opts if not o.startswith('_')]
else: else:
csc = get_env('CASE_SENSITIVE_COMPLETIONS', True) csc = builtins.__xonsh_env__.get('CASE_SENSITIVE_COMPLETIONS')
startswither = startswithnorm if csc else startswithlow startswither = startswithnorm if csc else startswithlow
attrlow = attr.lower() attrlow = attr.lower()
opts = [o for o in opts if startswither(o, attrlow)] opts = [o for o in opts if startswither(o, attrlow)]
@ -407,7 +405,7 @@ class ManCompleter(object):
def option_complete(self, prefix, cmd): def option_complete(self, prefix, cmd):
"""Completes an option name, basing on content of man page.""" """Completes an option name, basing on content of man page."""
csc = get_env('CASE_SENSITIVE_COMPLETIONS', True) csc = builtins.__xonsh_env__.get('CASE_SENSITIVE_COMPLETIONS')
startswither = startswithnorm if csc else startswithlow startswither = startswithnorm if csc else startswithlow
if cmd not in self._options.keys(): if cmd not in self._options.keys():
try: try:

View file

@ -6,10 +6,7 @@ from glob import iglob
from argparse import ArgumentParser from argparse import ArgumentParser
DIRSTACK = [] DIRSTACK = []
""" """A list containing the currently remembered directories."""
A list containing the currently remembered directories.
"""
def _get_cwd(): def _get_cwd():
try: try:
@ -42,7 +39,7 @@ def _try_cdpath(apath):
# in bash a full $ cd ./xonsh is needed. # in bash a full $ cd ./xonsh is needed.
# In xonsh a relative folder is allways preferred. # In xonsh a relative folder is allways preferred.
env = builtins.__xonsh_env__ env = builtins.__xonsh_env__
cdpaths = env.get('CDPATH', []) cdpaths = env.get('CDPATH')
for cdp in cdpaths: for cdp in cdpaths:
for cdpath_prefixed_path in iglob(os.path.join(cdp, apath)): for cdpath_prefixed_path in iglob(os.path.join(cdp, apath)):
return cdpath_prefixed_path return cdpath_prefixed_path
@ -92,15 +89,14 @@ def cd(args, stdin=None):
if not os.path.isdir(d): if not os.path.isdir(d):
return '', 'cd: {0} is not a directory\n'.format(d) return '', 'cd: {0} is not a directory\n'.format(d)
# now, push the directory onto the dirstack if AUTO_PUSHD is set # now, push the directory onto the dirstack if AUTO_PUSHD is set
if cwd is not None and env.get('AUTO_PUSHD', False): if cwd is not None and env.get('AUTO_PUSHD'):
pushd(['-n', '-q', cwd]) pushd(['-n', '-q', cwd])
_change_working_directory(os.path.abspath(d)) _change_working_directory(os.path.abspath(d))
return None, None return None, None
def pushd(args, stdin=None): def pushd(args, stdin=None):
""" """xonsh command: pushd
xonsh command: pushd
Adds a directory to the top of the directory stack, or rotates the stack, Adds a directory to the top of the directory stack, or rotates the stack,
making the new top of the stack the current working directory. making the new top of the stack the current working directory.
@ -165,11 +161,11 @@ def pushd(args, stdin=None):
else: else:
DIRSTACK.insert(0, os.path.expanduser(os.path.abspath(new_pwd))) DIRSTACK.insert(0, os.path.expanduser(os.path.abspath(new_pwd)))
maxsize = env.get('DIRSTACK_SIZE', 20) maxsize = env.get('DIRSTACK_SIZE')
if len(DIRSTACK) > maxsize: if len(DIRSTACK) > maxsize:
DIRSTACK = DIRSTACK[:maxsize] DIRSTACK = DIRSTACK[:maxsize]
if not args.quiet and not env.get('PUSHD_SILENT', False): if not args.quiet and not env.get('PUSHD_SILENT'):
return dirs([], None) return dirs([], None)
return None, None return None, None
@ -190,7 +186,7 @@ def popd(args, stdin=None):
env = builtins.__xonsh_env__ env = builtins.__xonsh_env__
if env.get('PUSHD_MINUS', False): if env.get('PUSHD_MINUS'):
BACKWARD = '-' BACKWARD = '-'
FORWARD = '+' FORWARD = '+'
else: else:
@ -238,15 +234,14 @@ def popd(args, stdin=None):
if args.cd: if args.cd:
_change_working_directory(os.path.abspath(new_pwd)) _change_working_directory(os.path.abspath(new_pwd))
if not args.quiet and not env.get('PUSHD_SILENT', False): if not args.quiet and not env.get('PUSHD_SILENT'):
return dirs([], None) return dirs([], None)
return None, None return None, None
def dirs(args, stdin=None): def dirs(args, stdin=None):
""" """xonsh command: dirs
xonsh command: dirs
Displays the list of currently remembered directories. Can also be used Displays the list of currently remembered directories. Can also be used
to clear the directory stack. to clear the directory stack.
@ -261,7 +256,7 @@ def dirs(args, stdin=None):
env = builtins.__xonsh_env__ env = builtins.__xonsh_env__
if env.get('PUSHD_MINUS', False): if env.get('PUSHD_MINUS'):
BACKWARD = '-' BACKWARD = '-'
FORWARD = '+' FORWARD = '+'
else: else:

View file

@ -1,6 +1,7 @@
"""Environment for the xonsh shell.""" """Environment for the xonsh shell."""
import os import os
import re import re
import json
import socket import socket
import string import string
import locale import locale
@ -14,8 +15,9 @@ from xonsh import __version__ as XONSH_VERSION
from xonsh.tools import TERM_COLORS, ON_WINDOWS, ON_MAC, ON_LINUX, string_types, \ from xonsh.tools import TERM_COLORS, ON_WINDOWS, ON_MAC, ON_LINUX, string_types, \
is_int, always_true, always_false, ensure_string, is_env_path, str_to_env_path, \ is_int, always_true, always_false, ensure_string, is_env_path, str_to_env_path, \
env_path_to_str, is_bool, to_bool, bool_to_str, is_history_tuple, to_history_tuple, \ env_path_to_str, is_bool, to_bool, bool_to_str, is_history_tuple, to_history_tuple, \
history_tuple_to_str history_tuple_to_str, is_float
from xonsh.dirstack import _get_cwd from xonsh.dirstack import _get_cwd
from xonsh.foreign_shells import DEFAULT_SHELLS, load_foreign_envs
LOCALE_CATS = { LOCALE_CATS = {
'LC_CTYPE': locale.LC_CTYPE, 'LC_CTYPE': locale.LC_CTYPE,
@ -59,8 +61,103 @@ DEFAULT_ENSURERS = {
'XONSH_STORE_STDOUT': (is_bool, to_bool, bool_to_str), 'XONSH_STORE_STDOUT': (is_bool, to_bool, bool_to_str),
'CASE_SENSITIVE_COMPLETIONS': (is_bool, to_bool, bool_to_str), 'CASE_SENSITIVE_COMPLETIONS': (is_bool, to_bool, bool_to_str),
'BASH_COMPLETIONS': (is_env_path, str_to_env_path, env_path_to_str), 'BASH_COMPLETIONS': (is_env_path, str_to_env_path, env_path_to_str),
'TEEPTY_PIPE_DELAY': (is_float, float, str),
} }
#
# Defaults
#
def default_value(f):
"""Decorator for making callable default values."""
f._xonsh_callable_default = True
return f
def is_callable_default(x):
"""Checks if a value is a callable default."""
return callable(x) and getattr(x, '_xonsh_callable_default', False)
DEFAULT_PROMPT = ('{BOLD_GREEN}{user}@{hostname}{BOLD_BLUE} '
'{cwd}{branch_color}{curr_branch} '
'{BOLD_BLUE}${NO_COLOR} ')
DEFAULT_TITLE = '{user}@{hostname}: {cwd} | xonsh'
@default_value
def xonsh_data_dir(env):
"""Ensures and returns the $XONSH_DATA_DIR"""
xdd = os.path.join(env.get('XDG_DATA_HOME'), 'xonsh')
os.makedirs(xdd, exist_ok=True)
return xdd
@default_value
def xonsh_config_dir(env):
"""Ensures and returns the $XONSH_CONFIG_DIR"""
xcd = os.path.join(env.get('XDG_CONFIG_HOME'), 'xonsh')
os.makedirs(xcd, exist_ok=True)
return xcd
@default_value
def xonshconfig(env):
"""Ensures and returns the $XONSHCONFIG"""
xcd = env.get('XONSH_CONFIG_DIR')
xc = os.path.join(xcd, 'config.json')
return xc
# Default values should generally be immutable, that way if a user wants
# to set them they have to do a copy and write them to the environment.
# try to keep this sorted.
DEFAULT_VALUES = {
'AUTO_PUSHD': False,
'BASH_COMPLETIONS': ('/usr/local/etc/bash_completion',
'/opt/local/etc/profile.d/bash_completion.sh') if ON_MAC \
else ('/etc/bash_completion',
'/usr/share/bash-completion/completions/git'),
'CASE_SENSITIVE_COMPLETIONS': ON_LINUX,
'CDPATH': (),
'DIRSTACK_SIZE': 20,
'FORCE_POSIX_PATHS': False,
'INDENT': ' ',
'LC_CTYPE': locale.setlocale(locale.LC_CTYPE),
'LC_COLLATE': locale.setlocale(locale.LC_COLLATE),
'LC_TIME': locale.setlocale(locale.LC_TIME),
'LC_MONETARY': locale.setlocale(locale.LC_MONETARY),
'LC_NUMERIC': locale.setlocale(locale.LC_NUMERIC),
'MULTILINE_PROMPT': '.',
'PATH': (),
'PATHEXT': (),
'PROMPT': DEFAULT_PROMPT,
'PROMPT_TOOLKIT_STYLES': None,
'PUSHD_MINUS': False,
'PUSHD_SILENT': False,
'SHELL_TYPE': 'readline',
'SUGGEST_COMMANDS': True,
'SUGGEST_MAX_NUM': 5,
'SUGGEST_THRESHOLD': 3,
'TEEPTY_PIPE_DELAY': 0.01,
'TITLE': DEFAULT_TITLE,
'XDG_CONFIG_HOME': os.path.expanduser(os.path.join('~', '.config')),
'XDG_DATA_HOME': os.path.expanduser(os.path.join('~', '.local', 'share')),
'XONSHCONFIG': xonshconfig,
'XONSHRC': os.path.expanduser('~/.xonshrc'),
'XONSH_CONFIG_DIR': xonsh_config_dir,
'XONSH_DATA_DIR': xonsh_data_dir,
'XONSH_HISTORY_FILE': os.path.expanduser('~/.xonsh_history.json'),
'XONSH_HISTORY_SIZE': (8128, 'commands'),
'XONSH_SHOW_TRACEBACK': False,
'XONSH_STORE_STDOUT': False,
}
class DefaultNotGivenType(object):
"""Singleton for representing when no default value is given."""
DefaultNotGiven = DefaultNotGivenType()
#
# actual environment
#
class Env(MutableMapping): class Env(MutableMapping):
"""A xonsh environment, whose variables have limited typing """A xonsh environment, whose variables have limited typing
@ -82,6 +179,7 @@ class Env(MutableMapping):
"""If no initial environment is given, os.environ is used.""" """If no initial environment is given, os.environ is used."""
self._d = {} self._d = {}
self.ensurers = {k: Ensurer(*v) for k, v in DEFAULT_ENSURERS.items()} self.ensurers = {k: Ensurer(*v) for k, v in DEFAULT_ENSURERS.items()}
self.defaults = DEFAULT_VALUES
if len(args) == 0 and len(kwargs) == 0: if len(args) == 0 and len(kwargs) == 0:
args = (os.environ, ) args = (os.environ, )
for key, val in dict(*args, **kwargs).items(): for key, val in dict(*args, **kwargs).items():
@ -168,6 +266,20 @@ class Env(MutableMapping):
del self._d[key] del self._d[key]
self._detyped = None self._detyped = None
def get(self, key, default=DefaultNotGiven):
"""The environment will look up default values from its own defaults if a
default is not given here.
"""
if key in self:
val = self[key]
elif default is DefaultNotGiven:
val = self.defaults.get(key, None)
if is_callable_default(val):
val = val(self)
else:
val = default
return val
def __iter__(self): def __iter__(self):
yield from self._d yield from self._d
@ -382,12 +494,6 @@ def branch_color():
TERM_COLORS['BOLD_GREEN']) TERM_COLORS['BOLD_GREEN'])
DEFAULT_PROMPT = ('{BOLD_GREEN}{user}@{hostname}{BOLD_BLUE} '
'{cwd}{branch_color}{curr_branch} '
'{BOLD_BLUE}${NO_COLOR} ')
DEFAULT_TITLE = '{user}@{hostname}: {cwd} | xonsh'
def _replace_home(x): def _replace_home(x):
if ON_WINDOWS: if ON_WINDOWS:
home = (builtins.__xonsh_env__['HOMEDRIVE'] + home = (builtins.__xonsh_env__['HOMEDRIVE'] +
@ -419,10 +525,10 @@ FORMATTER_DICT = dict(
curr_branch=current_branch, curr_branch=current_branch,
branch_color=branch_color, branch_color=branch_color,
**TERM_COLORS) **TERM_COLORS)
DEFAULT_VALUES['FORMATTER_DICT'] = dict(FORMATTER_DICT)
_FORMATTER = string.Formatter() _FORMATTER = string.Formatter()
def format_prompt(template=DEFAULT_PROMPT, formatter_dict=None): def format_prompt(template=DEFAULT_PROMPT, formatter_dict=None):
"""Formats a xonsh prompt template string.""" """Formats a xonsh prompt template string."""
template = template() if callable(template) else template template = template() if callable(template) else template
@ -447,10 +553,9 @@ def format_prompt(template=DEFAULT_PROMPT, formatter_dict=None):
RE_HIDDEN = re.compile('\001.*?\002') RE_HIDDEN = re.compile('\001.*?\002')
def multiline_prompt(): def multiline_prompt():
"""Returns the filler text for the prompt in multiline scenarios.""" """Returns the filler text for the prompt in multiline scenarios."""
curr = builtins.__xonsh_env__.get('PROMPT', "set '$PROMPT = ...' $ ") curr = builtins.__xonsh_env__.get('PROMPT')
curr = format_prompt(curr) curr = format_prompt(curr)
line = curr.rsplit('\n', 1)[1] if '\n' in curr else curr line = curr.rsplit('\n', 1)[1] if '\n' in curr else curr
line = RE_HIDDEN.sub('', line) # gets rid of colors line = RE_HIDDEN.sub('', line) # gets rid of colors
@ -460,7 +565,7 @@ def multiline_prompt():
# tail is the trailing whitespace # tail is the trailing whitespace
tail = line if headlen == 0 else line.rsplit(head[-1], 1)[1] tail = line if headlen == 0 else line.rsplit(head[-1], 1)[1]
# now to constuct the actual string # now to constuct the actual string
dots = builtins.__xonsh_env__.get('MULTILINE_PROMPT', '.') dots = builtins.__xonsh_env__.get('MULTILINE_PROMPT')
dots = dots() if callable(dots) else dots dots = dots() if callable(dots) else dots
if dots is None or len(dots) == 0: if dots is None or len(dots) == 0:
return '' return ''
@ -469,58 +574,37 @@ def multiline_prompt():
BASE_ENV = { BASE_ENV = {
'XONSH_VERSION': XONSH_VERSION, 'XONSH_VERSION': XONSH_VERSION,
'INDENT': ' ',
'FORMATTER_DICT': dict(FORMATTER_DICT),
'PROMPT': DEFAULT_PROMPT,
'TITLE': DEFAULT_TITLE,
'MULTILINE_PROMPT': '.',
'XONSHRC': os.path.expanduser('~/.xonshrc'),
'XONSH_HISTORY_SIZE': (8128, 'commands'),
'XONSH_HISTORY_FILE': os.path.expanduser('~/.xonsh_history.json'),
'XONSH_STORE_STDOUT': False,
'LC_CTYPE': locale.setlocale(locale.LC_CTYPE), 'LC_CTYPE': locale.setlocale(locale.LC_CTYPE),
'LC_COLLATE': locale.setlocale(locale.LC_COLLATE), 'LC_COLLATE': locale.setlocale(locale.LC_COLLATE),
'LC_TIME': locale.setlocale(locale.LC_TIME), 'LC_TIME': locale.setlocale(locale.LC_TIME),
'LC_MONETARY': locale.setlocale(locale.LC_MONETARY), 'LC_MONETARY': locale.setlocale(locale.LC_MONETARY),
'LC_NUMERIC': locale.setlocale(locale.LC_NUMERIC), 'LC_NUMERIC': locale.setlocale(locale.LC_NUMERIC),
'SHELL_TYPE': 'readline',
'CASE_SENSITIVE_COMPLETIONS': ON_LINUX,
} }
try: try:
BASE_ENV['LC_MESSAGES'] = locale.setlocale(locale.LC_MESSAGES) BASE_ENV['LC_MESSAGES'] = DEFAULT_VALUES['LC_MESSAGES'] = \
locale.setlocale(locale.LC_MESSAGES)
except AttributeError: except AttributeError:
pass pass
def load_static_config(ctx):
if ON_MAC: """Loads a static configuration file from a given context, rather than the
BASE_ENV['BASH_COMPLETIONS'] = [ current environment.
'/usr/local/etc/bash_completion', """
'/opt/local/etc/profile.d/bash_completion.sh' env = {}
] env['XDG_CONFIG_HOME'] = ctx.get('XDG_CONFIG_HOME',
else: DEFAULT_VALUES['XDG_CONFIG_HOME'])
BASE_ENV['BASH_COMPLETIONS'] = [ env['XONSH_CONFIG_DIR'] = ctx['XONSH_CONFIG_DIR'] if 'XONSH_CONFIG_DIR' in ctx \
'/etc/bash_completion', '/usr/share/bash-completion/completions/git' else xonsh_config_dir(env)
] env['XONSHCONFIG'] = ctx['XONSHCONFIG'] if 'XONSHCONFIG' in ctx \
else xonshconfig(env)
config = env['XONSHCONFIG']
def bash_env(): if os.path.isfile(config):
"""Attempts to compute the bash envinronment variables.""" with open(config, 'r') as f:
currenv = None conf = json.load(f)
if hasattr(builtins, '__xonsh_env__'): else:
currenv = builtins.__xonsh_env__.detype() conf = {}
try: return conf
s = subprocess.check_output(['bash', '-i', '-l'],
input='env',
env=currenv,
stderr=subprocess.PIPE,
universal_newlines=True)
except (subprocess.CalledProcessError, FileNotFoundError):
s = ''
items = [line.split('=', 1) for line in s.splitlines() if '=' in line]
env = dict(items)
return env
def xonshrc_context(rcfile=None, execer=None): def xonshrc_context(rcfile=None, execer=None):
@ -544,19 +628,23 @@ def xonshrc_context(rcfile=None, execer=None):
return env return env
def recursive_base_env_update(env): def windows_env_fixes(ctx):
"""Updates the environment with members that may rely on previously defined """Environment fixes for Windows. Operates in-place."""
members. Takes an env as its argument. # Windows default prompt doesn't work.
""" ctx['PROMPT'] = DEFAULT_PROMPT
home = os.path.expanduser('~') # remove these bash variables which only cause problems.
if 'XONSH_DATA_DIR' not in env: for ev in ['HOME', 'OLDPWD']:
xdgdh = env.get('XDG_DATA_HOME', os.path.join(home, '.local', 'share')) if ev in ctx:
env['XONSH_DATA_DIR'] = xdd = os.path.join(xdgdh, 'xonsh') del ctx[ev]
os.makedirs(xdd, exist_ok=True) # Override path-related bash variables; on Windows bash uses
if 'XONSH_CONFIG_DIR' not in env: # /c/Windows/System32 syntax instead of C:\\Windows\\System32
xdgch = env.get('XDG_CONFIG_HOME', os.path.join(home, '.config')) # which messes up these environment variables for xonsh.
env['XONSH_CONFIG_DIR'] = xcd = os.path.join(xdgch, 'xonsh') for ev in ['PATH', 'TEMP', 'TMP']:
os.makedirs(xcd, exist_ok=True) if ev in os.environ:
ctx[ev] = os.environ[ev]
elif ev in ctx:
del ctx[ev]
ctx['PWD'] = _get_cwd()
def default_env(env=None): def default_env(env=None):
@ -564,28 +652,13 @@ def default_env(env=None):
# in order of increasing precedence # in order of increasing precedence
ctx = dict(BASE_ENV) ctx = dict(BASE_ENV)
ctx.update(os.environ) ctx.update(os.environ)
ctx.update(bash_env()) conf = load_static_config(ctx)
ctx.update(conf.get('env', ()))
ctx.update(load_foreign_envs(shells=conf.get('foreign_shells', DEFAULT_SHELLS),
issue_warning=False))
if ON_WINDOWS: if ON_WINDOWS:
# Windows default prompt doesn't work. windows_env_fixes(ctx)
ctx['PROMPT'] = DEFAULT_PROMPT
# remove these bash variables which only cause problems.
for ev in ['HOME', 'OLDPWD']:
if ev in ctx:
del ctx[ev]
# Override path-related bash variables; on Windows bash uses
# /c/Windows/System32 syntax instead of C:\\Windows\\System32
# which messes up these environment variables for xonsh.
for ev in ['PATH', 'TEMP', 'TMP']:
if ev in os.environ:
ctx[ev] = os.environ[ev]
elif ev in ctx:
del ctx[ev]
ctx['PWD'] = _get_cwd()
# finalize env # finalize env
recursive_base_env_update(ctx)
if env is not None: if env is not None:
ctx.update(env) ctx.update(env)
return ctx return ctx

236
xonsh/foreign_shells.py Normal file
View file

@ -0,0 +1,236 @@
"""Tools to help interface with foreign shells, such as Bash."""
import os
import re
import json
import shlex
import builtins
import subprocess
from warnings import warn
from functools import lru_cache
from collections import MutableMapping, Mapping, Sequence
from xonsh.tools import to_bool, ensure_string
COMMAND = """
echo __XONSH_ENV_BEG__
{envcmd}
echo __XONSH_ENV_END__
echo __XONSH_ALIAS_BEG__
{aliascmd}
echo __XONSH_ALIAS_END__
""".strip()
@lru_cache()
def foreign_shell_data(shell, interactive=True, login=False, envcmd='env',
aliascmd='alias', extra_args=(), currenv=None,
safe=True):
"""Extracts data from a foreign (non-xonsh) shells. Currently this gets
the environment and aliases, but may be extended in the future.
Parameters
----------
shell : str
The name of the shell, such as 'bash' or '/bin/sh'.
interactive : bool, optional
Whether the shell should be run in interactive mode.
login : bool, optional
Whether the shell should be a login shell.
envcmd : str, optional
The command to generate environment output with.
aliascmd : str, optional
The command to generate alais output with.
extra_args : tuple of str, optional
Addtional command line options to pass into the shell.
currenv : tuple of items or None, optional
Manual override for the current environment.
safe : bool, optional
Flag for whether or not to safely handle exceptions and other errors.
Returns
-------
env : dict
Dictionary of shell's environment
aliases : dict
Dictionary of shell's alaiases.
"""
cmd = [shell]
cmd.extend(extra_args) # needs to come here for GNU long options
if interactive:
cmd.append('-i')
if login:
cmd.append('-l')
cmd.append('-c')
cmd.append(COMMAND.format(envcmd=envcmd, aliascmd=aliascmd))
if currenv is None and hasattr(builtins, '__xonsh_env__'):
currenv = builtins.__xonsh_env__.detype()
elif currenv is not None:
currenv = dict(currenv)
try:
s = subprocess.check_output(cmd,stderr=subprocess.PIPE, env=currenv,
universal_newlines=True)
except (subprocess.CalledProcessError, FileNotFoundError):
if not safe:
raise
return {}, {}
env = parse_env(s)
aliases = parse_aliases(s)
return env, aliases
ENV_RE = re.compile('__XONSH_ENV_BEG__\n(.*)__XONSH_ENV_END__', flags=re.DOTALL)
def parse_env(s):
"""Parses the environment portion of string into a dict."""
m = ENV_RE.search(s)
if m is None:
return {}
g1 = m.group(1)
items = [line.split('=', 1) for line in g1.splitlines() if '=' in line]
env = dict(items)
return env
ALIAS_RE = re.compile('__XONSH_ALIAS_BEG__\n(.*)__XONSH_ALIAS_END__',
flags=re.DOTALL)
def parse_aliases(s):
"""Parses the aliases portion of string into a dict."""
m = ALIAS_RE.search(s)
if m is None:
return {}
g1 = m.group(1)
items = [line.split('=', 1) for line in g1.splitlines() if \
line.startswith('alias ') and '=' in line]
aliases = {}
for key, value in items:
try:
key = key[6:] # lstrip 'alias '
# undo bash's weird quoting of single quotes (sh_single_quote)
value = value.replace('\'\\\'\'', '\'')
# strip one single quote at the start and end of value
if value[0] == '\'' and value[-1] == '\'':
value = value[1:-1]
value = shlex.split(value)
except ValueError as exc:
warn('could not parse alias "{0}": {1!r}'.format(key, exc),
RuntimeWarning)
continue
aliases[key] = value
return aliases
VALID_SHELL_PARAMS = frozenset(['shell', 'interactive', 'login', 'envcmd',
'aliascmd', 'extra_args', 'currenv', 'safe'])
def ensure_shell(shell):
"""Ensures that a mapping follows the shell specification."""
if not isinstance(shell, MutableMapping):
shell = dict(shell)
shell_keys = set(shell.keys())
if not (shell_keys <= VALID_SHELL_PARAMS):
msg = 'unknown shell keys: {0}'
raise KeyError(msg.format(shell_keys - VALID_SHELL_PARAMS))
shell['shell'] = ensure_string(shell['shell'])
if 'interactive' in shell_keys:
shell['interactive'] = to_bool(shell['interactive'])
if 'login' in shell_keys:
shell['login'] = to_bool(shell['login'])
if 'envcmd' in shell_keys:
shell['envcmd'] = eunsure_string(shell['envcmd'])
if 'aliascmd' in shell_keys:
shell['aliascmd'] = eunsure_string(shell['aliascmd'])
if 'extra_args' in shell_keys and not isinstance(shell['extra_args'], tuple):
shell['extra_args'] = tuple(map(ensure_string, shell['extra_args']))
if 'currenv' in shell_keys and not isinstance(shell['currenv'], tuple):
ce = shell['currenv']
if isinstance(ce, Mapping):
ce = tuple([(ensure_string(k), v) for k, v in ce.items()])
elif isinstance(ce, Sequence):
ce = tuple([(ensure_string(k), v) for k, v in ce])
else:
raise RuntimeError('unrecognized type for currenv')
shell['currenv'] = ce
if 'safe' in shell_keys:
shell['safe'] = to_bool(shell['safe'])
return shell
DEFAULT_SHELLS = ({'shell': 'bash'},)
def _get_shells(shells=None, config=None, issue_warning=True):
if shells is not None and config is not None:
raise RuntimeError('Only one of shells and config may be non-None.')
elif shells is not None:
pass
else:
if config is None:
config = builtins.__xonsh_env__.get('XONSHCONFIG')
if os.path.isfile(config):
with open(config, 'r') as f:
conf = json.load(f)
shells = conf.get('foreign_shells', DEFAULT_SHELLS)
else:
if issue_warning:
msg = 'could not find xonsh config file ($XONSHCONFIG) at {0!r}'
warn(msg.format(config), RuntimeWarning)
shells = DEFAULT_SHELLS
return shells
def load_foreign_envs(shells=None, config=None, issue_warning=True):
"""Loads environments from foreign shells.
Parameters
----------
shells : sequence of dicts, optional
An iterable of dicts that can be passed into foreign_shell_data() as
keyword arguments. Not compatible with config not being None.
config : str of None, optional
Path to the static config file. Not compatible with shell not being None.
If both shell and config is None, then it will be read from the
$XONSHCONFIG environment variable.
issue_warning : bool, optional
Issues warnings if config file cannot be found.
Returns
-------
env : dict
A dictionary of the merged environments.
"""
shells = _get_shells(shells=shells, config=config, issue_warning=issue_warning)
env = {}
for shell in shells:
shell = ensure_shell(shell)
shenv, _ = foreign_shell_data(**shell)
env.update(shenv)
return env
def load_foreign_aliases(shells=None, config=None, issue_warning=True):
"""Loads aliases from foreign shells.
Parameters
----------
shells : sequence of dicts, optional
An iterable of dicts that can be passed into foreign_shell_data() as
keyword arguments. Not compatible with config not being None.
config : str of None, optional
Path to the static config file. Not compatible with shell not being None.
If both shell and config is None, then it will be read from the
$XONSHCONFIG environment variable.
issue_warning : bool, optional
Issues warnings if config file cannot be found.
Returns
-------
aliases : dict
A dictionary of the merged aliases.
"""
shells = _get_shells(shells=shells, config=config, issue_warning=issue_warning)
aliases = {}
for shell in shells:
shell = ensure_shell(shell)
_, shaliases = foreign_shell_data(**shell)
aliases.update(shaliases)
return aliases

View file

@ -29,7 +29,7 @@ class HistoryGC(Thread):
time.sleep(0.01) time.sleep(0.01)
env = builtins.__xonsh_env__ env = builtins.__xonsh_env__
if self.size is None: if self.size is None:
hsize, units = env.get('XONSH_HISTORY_SIZE', (8128, 'commands')) hsize, units = env.get('XONSH_HISTORY_SIZE')
else: else:
hsize, units = to_history_tuple(self.size) hsize, units = to_history_tuple(self.size)
files = self.unlocked_files() files = self.unlocked_files()
@ -79,7 +79,7 @@ class HistoryGC(Thread):
"""Finds the history files and returns the ones that are unlocked, this is """Finds the history files and returns the ones that are unlocked, this is
sorted by the last closed time. Returns a list of (timestamp, file) tuples. sorted by the last closed time. Returns a list of (timestamp, file) tuples.
""" """
xdd = os.path.abspath(builtins.__xonsh_env__['XONSH_DATA_DIR']) xdd = os.path.abspath(builtins.__xonsh_env__.get('XONSH_DATA_DIR'))
fs = [f for f in iglob(os.path.join(xdd, 'xonsh-*.json'))] fs = [f for f in iglob(os.path.join(xdd, 'xonsh-*.json'))]
files = [] files = []
for f in fs: for f in fs:
@ -209,7 +209,7 @@ class History(object):
""" """
self.sessionid = sid = uuid.uuid4() if sessionid is None else sessionid self.sessionid = sid = uuid.uuid4() if sessionid is None else sessionid
if filename is None: if filename is None:
self.filename = os.path.join(builtins.__xonsh_env__['XONSH_DATA_DIR'], self.filename = os.path.join(builtins.__xonsh_env__.get('XONSH_DATA_DIR'),
'xonsh-{0}.json'.format(sid)) 'xonsh-{0}.json'.format(sid))
else: else:
self.filename = filename self.filename = filename

View file

@ -10,6 +10,7 @@ import io
import os import os
import sys import sys
import time import time
import builtins
from threading import Thread from threading import Thread
from collections import Sequence from collections import Sequence
from subprocess import Popen, PIPE, DEVNULL, STDOUT, TimeoutExpired from subprocess import Popen, PIPE, DEVNULL, STDOUT, TimeoutExpired
@ -370,7 +371,9 @@ class TeePTYProc(object):
self._tpty = tpty = TeePTY() self._tpty = tpty = TeePTY()
if preexec_fn is not None: if preexec_fn is not None:
preexec_fn() preexec_fn()
tpty.spawn(args, env=env, stdin=stdin) delay = builtins.__xonsh_env__.get('TEEPTY_PIPE_DELAY') if \
hasattr(builtins, '__xonsh_env__') else None
tpty.spawn(args, env=env, stdin=stdin, delay=delay)
@property @property
def pid(self): def pid(self):

View file

@ -18,8 +18,7 @@ from xonsh.prompt_toolkit_key_bindings import load_xonsh_bindings
def setup_history(): def setup_history():
"""Creates history object.""" """Creates history object."""
env = builtins.__xonsh_env__ env = builtins.__xonsh_env__
hfile = env.get('XONSH_HISTORY_FILE', hfile = env.get('XONSH_HISTORY_FILE')
os.path.expanduser('~/.xonsh_history'))
history = LimitedFileHistory() history = LimitedFileHistory()
try: try:
history.read_history_file(hfile) history.read_history_file(hfile)
@ -31,9 +30,8 @@ def setup_history():
def teardown_history(history): def teardown_history(history):
"""Tears down the history object.""" """Tears down the history object."""
env = builtins.__xonsh_env__ env = builtins.__xonsh_env__
hsize = env.get('XONSH_HISTORY_SIZE', (8128, 'commands'))[0] hsize = env.get('XONSH_HISTORY_SIZE')[0]
hfile = env.get('XONSH_HISTORY_FILE', hfile = env.get('XONSH_HISTORY_FILE')
os.path.expanduser('~/.xonsh_history'))
try: try:
history.save_history_to_file(hfile, hsize) history.save_history_to_file(hfile, hsize)
except PermissionError: except PermissionError:
@ -97,7 +95,8 @@ class PromptToolkitShell(BaseShell):
# update with the prompt styles # update with the prompt styles
styles.update({t: s for (t, s) in zip(tokens, cstyles)}) styles.update({t: s for (t, s) in zip(tokens, cstyles)})
# Update with with any user styles # Update with with any user styles
userstyle = builtins.__xonsh_env__.get('PROMPT_TOOLKIT_STYLES', {}) userstyle = builtins.__xonsh_env__.get('PROMPT_TOOLKIT_STYLES')
styles.update(userstyle) if userstyle is not None:
styles.update(userstyle)
return get_tokens, CustomStyle return get_tokens, CustomStyle

View file

@ -47,7 +47,7 @@ def setup_readline():
readline.parse_and_bind('"\e[B": history-search-forward') readline.parse_and_bind('"\e[B": history-search-forward')
readline.parse_and_bind('"\e[A": history-search-backward') readline.parse_and_bind('"\e[A": history-search-backward')
# Setup Shift-Tab to indent # Setup Shift-Tab to indent
readline.parse_and_bind('"\e[Z": "{0}"'.format(env.get('INDENT', ''))) readline.parse_and_bind('"\e[Z": "{0}"'.format(env.get('INDENT')))
# handle tab completion differences found in libedit readline compatibility # handle tab completion differences found in libedit readline compatibility
# as discussed at http://stackoverflow.com/a/7116997 # as discussed at http://stackoverflow.com/a/7116997
@ -139,12 +139,12 @@ class ReadlineShell(BaseShell, Cmd):
self._current_indent = '' self._current_indent = ''
elif line.rstrip()[-1] == ':': elif line.rstrip()[-1] == ':':
ind = line[:len(line) - len(line.lstrip())] ind = line[:len(line) - len(line.lstrip())]
ind += builtins.__xonsh_env__.get('INDENT', '') ind += builtins.__xonsh_env__.get('INDENT')
readline.set_pre_input_hook(_insert_text_func(ind, readline)) readline.set_pre_input_hook(_insert_text_func(ind, readline))
self._current_indent = ind self._current_indent = ind
elif line.split(maxsplit=1)[0] in DEDENT_TOKENS: elif line.split(maxsplit=1)[0] in DEDENT_TOKENS:
env = builtins.__xonsh_env__ env = builtins.__xonsh_env__
ind = self._current_indent[:-len(env.get('INDENT', ''))] ind = self._current_indent[:-len(env.get('INDENT'))]
readline.set_pre_input_hook(_insert_text_func(ind, readline)) readline.set_pre_input_hook(_insert_text_func(ind, readline))
self._current_indent = ind self._current_indent = ind
else: else:

View file

@ -26,25 +26,26 @@ class Shell(object):
def __init__(self, ctx=None, shell_type=None, **kwargs): def __init__(self, ctx=None, shell_type=None, **kwargs):
self._init_environ(ctx) self._init_environ(ctx)
env = builtins.__xonsh_env__ env = builtins.__xonsh_env__
# pick a valid shell
if shell_type is not None: if shell_type is not None:
env['SHELL_TYPE'] = shell_type env['SHELL_TYPE'] = shell_type
if env['SHELL_TYPE'] == 'prompt_toolkit': shell_type = env.get('SHELL_TYPE')
if shell_type == 'prompt_toolkit':
if not is_prompt_toolkit_available(): if not is_prompt_toolkit_available():
warn('prompt_toolkit is not available, using readline instead.') warn('prompt_toolkit is not available, using readline instead.')
env['SHELL_TYPE'] = 'readline' shell_type = env['SHELL_TYPE'] = 'readline'
# actually make the shell
if env['SHELL_TYPE'] == 'prompt_toolkit': if shell_type == 'prompt_toolkit':
from xonsh.prompt_toolkit_shell import PromptToolkitShell from xonsh.prompt_toolkit_shell import PromptToolkitShell
self.shell = PromptToolkitShell(execer=self.execer, self.shell = PromptToolkitShell(execer=self.execer,
ctx=self.ctx, **kwargs) ctx=self.ctx, **kwargs)
elif env['SHELL_TYPE'] == 'readline': elif shell_type == 'readline':
from xonsh.readline_shell import ReadlineShell from xonsh.readline_shell import ReadlineShell
self.shell = ReadlineShell(execer=self.execer, self.shell = ReadlineShell(execer=self.execer,
ctx=self.ctx, **kwargs) ctx=self.ctx, **kwargs)
else: else:
raise XonshError('{} is not recognized as a shell type'.format( raise XonshError('{} is not recognized as a shell type'.format(
env['SHELL_TYPE'])) shell_type))
# allows history garbace colector to start running # allows history garbace colector to start running
builtins.__xonsh_history__.gc.wait_for_shell = False builtins.__xonsh_history__.gc.wait_for_shell = False
@ -58,7 +59,7 @@ class Shell(object):
if ctx is not None: if ctx is not None:
self.ctx = ctx self.ctx = ctx
else: else:
rc = env.get('XONSHRC', None) rc = env.get('XONSHRC')
self.ctx = xonshrc_context(rcfile=rc, execer=self.execer) self.ctx = xonshrc_context(rcfile=rc, execer=self.execer)
builtins.__xonsh_ctx__ = self.ctx builtins.__xonsh_ctx__ = self.ctx
self.ctx['__name__'] = '__main__' self.ctx['__name__'] = '__main__'

View file

@ -11,6 +11,7 @@ import os
import sys import sys
import tty import tty
import pty import pty
import time
import array import array
import fcntl import fcntl
import select import select
@ -78,7 +79,7 @@ class TeePTY(object):
self._temp_stdin.close() self._temp_stdin.close()
self._temp_stdin = None self._temp_stdin = None
def spawn(self, argv=None, env=None, stdin=None): def spawn(self, argv=None, env=None, stdin=None, delay=None):
"""Create a spawned process. Based on the code for pty.spawn(). """Create a spawned process. Based on the code for pty.spawn().
This cannot be used except from the main thread. This cannot be used except from the main thread.
@ -88,6 +89,12 @@ class TeePTY(object):
Arguments to pass in as subprocess. In None, will execute $SHELL. Arguments to pass in as subprocess. In None, will execute $SHELL.
env : Mapping, optional env : Mapping, optional
Environment to pass execute in. Environment to pass execute in.
delay : float, optional
Delay timing before executing process if piping in data. The value
is passed into time.sleep() so it is in [seconds]. If delay is None,
its value will attempted to be looked up from the environment
variable $TEEPTY_PIPE_DELAY, from the passed in env or os.environ.
If not present or not positive valued, no delay is used.
Returns Returns
------- -------
@ -104,6 +111,10 @@ class TeePTY(object):
self.pid = pid self.pid = pid
self.master_fd = master_fd self.master_fd = master_fd
if pid == pty.CHILD: if pid == pty.CHILD:
# determine if a piping delay is needed.
if self._temp_stdin is not None:
self._delay_for_pipe(env=env, delay=delay)
# ok, go
if env is None: if env is None:
os.execvp(argv[0], argv) os.execvp(argv[0], argv)
else: else:
@ -255,8 +266,35 @@ class TeePTY(object):
tsi.flush() tsi.flush()
else: else:
raise ValueError('stdin not understood {0!r}'.format(stdin)) raise ValueError('stdin not understood {0!r}'.format(stdin))
def _delay_for_pipe(self, env=None, delay=None):
# This delay is sometimes needed because the temporary stdin file that
# is being written (the pipe) may not have even hits its first flush()
# call by the time the spawned process starts up and determines there
# is nothing in the file. The spawn can thus exit, without doing any
# real work. Consider the case of piping something into grep:
#
# $ ps aux | grep root
#
# grep will exit on EOF and so there is a race between the buffersize
# and flushing the temporary file and grep. However, this race is not
# always meaningful. Pagers, for example, update when the file is written
# to. So what is important is that we start the spawned process ASAP:
#
# $ ps aux | less
#
# So there is a push-and-pull between the the competing objectives of
# not blocking and letting the spawned process have enough to work with
# such that it doesn't exit prematurely. Unfortunately, there is no
# way to know a priori how big the file is, how long the spawned process
# will run for, etc. Thus as user-definable delay let's the user
# find something that works for them.
if delay is None:
delay = (env or os.environ).get('TEEPTY_PIPE_DELAY', -1.0)
delay = float(delay)
if 0.0 < delay:
time.sleep(delay)
if __name__ == '__main__': if __name__ == '__main__':
tpty = TeePTY() tpty = TeePTY()
@ -266,4 +304,4 @@ if __name__ == '__main__':
print('-=-'*10) print('-=-'*10)
print(tpty) print(tpty)
print('-=-'*10) print('-=-'*10)
print('Returned with status {0}'.format(tpty.rtn)) print('Returned with status {0}'.format(tpty.returncode))

View file

@ -342,11 +342,11 @@ def command_not_found(cmd):
def suggest_commands(cmd, env, aliases): def suggest_commands(cmd, env, aliases):
"""Suggests alternative commands given an environment and aliases.""" """Suggests alternative commands given an environment and aliases."""
suggest_cmds = env.get('SUGGEST_COMMANDS', True) suggest_cmds = env.get('SUGGEST_COMMANDS')
if not suggest_cmds: if not suggest_cmds:
return return
thresh = env.get('SUGGEST_THRESHOLD', 3) thresh = env.get('SUGGEST_THRESHOLD')
max_sugg = env.get('SUGGEST_MAX_NUM', 5) max_sugg = env.get('SUGGEST_MAX_NUM')
if max_sugg < 0: if max_sugg < 0:
max_sugg = float('inf') max_sugg = float('inf')
@ -357,7 +357,7 @@ def suggest_commands(cmd, env, aliases):
if levenshtein(a.lower(), cmd, thresh) < thresh: if levenshtein(a.lower(), cmd, thresh) < thresh:
suggested[a] = 'Alias' suggested[a] = 'Alias'
for d in filter(os.path.isdir, env.get('PATH', [])): for d in filter(os.path.isdir, env.get('PATH')):
for f in os.listdir(d): for f in os.listdir(d):
if f not in suggested: if f not in suggested:
if levenshtein(f.lower(), cmd, thresh) < thresh: if levenshtein(f.lower(), cmd, thresh) < thresh:
@ -387,8 +387,8 @@ def print_exception():
"""Print exceptions with/without traceback.""" """Print exceptions with/without traceback."""
if 'XONSH_SHOW_TRACEBACK' not in builtins.__xonsh_env__: if 'XONSH_SHOW_TRACEBACK' not in builtins.__xonsh_env__:
sys.stderr.write('xonsh: For full traceback set: ' sys.stderr.write('xonsh: For full traceback set: '
'$XONSH_SHOW_TRACEBACK=True\n') '$XONSH_SHOW_TRACEBACK = True\n')
if builtins.__xonsh_env__.get('XONSH_SHOW_TRACEBACK', False): if builtins.__xonsh_env__.get('XONSH_SHOW_TRACEBACK'):
traceback.print_exc() traceback.print_exc()
else: else:
exc_type, exc_value, exc_traceback = sys.exc_info() exc_type, exc_value, exc_traceback = sys.exc_info()
@ -401,15 +401,12 @@ def print_exception():
def levenshtein(a, b, max_dist=float('inf')): def levenshtein(a, b, max_dist=float('inf')):
"""Calculates the Levenshtein distance between a and b.""" """Calculates the Levenshtein distance between a and b."""
n, m = len(a), len(b) n, m = len(a), len(b)
if abs(n - m) > max_dist: if abs(n - m) > max_dist:
return float('inf') return float('inf')
if n > m: if n > m:
# Make sure n <= m, to use O(min(n,m)) space # Make sure n <= m, to use O(min(n,m)) space
a, b = b, a a, b = b, a
n, m = m, n n, m = m, n
current = range(n + 1) current = range(n + 1)
for i in range(1, m + 1): for i in range(1, m + 1):
previous, current = current, [i] + [0] * n previous, current = current, [i] + [0] * n
@ -419,7 +416,6 @@ def levenshtein(a, b, max_dist=float('inf')):
if a[j - 1] != b[i - 1]: if a[j - 1] != b[i - 1]:
change = change + 1 change = change + 1
current[j] = min(add, delete, change) current[j] = min(add, delete, change)
return current[n] return current[n]
@ -441,7 +437,6 @@ def escape_windows_title_string(s):
""" """
for c in '^&<>|': for c in '^&<>|':
s = s.replace(c, '^' + c) s = s.replace(c, '^' + c)
s = s.replace('/?', '/.') s = s.replace('/?', '/.')
return s return s
@ -474,6 +469,11 @@ def is_int(x):
return isinstance(x, int) return isinstance(x, int)
def is_float(x):
"""Tests if something is a float"""
return isinstance(x, float)
def always_true(x): def always_true(x):
"""Returns True""" """Returns True"""
return True return True