Loading rc in non-interactive login shell (#3422)

* Setting -l flag loads environment on non-interactive shell
* Added checking dash existance in sys.argv[0]
* Fixed -a switch behaviour
* Added tests for xexec
* Added test for python<3.7 on Windows
* Testing non existent command with mocked execvpe
This commit is contained in:
Jerzy Drozdz 2020-03-01 18:56:23 +01:00 committed by GitHub
parent 1b1c668049
commit b37675a6e8
Failed to generate hash of commit
5 changed files with 156 additions and 13 deletions

1
.gitignore vendored
View file

@ -36,6 +36,7 @@ pip-selfcheck.json
bin/
/lib/
include/
venv/
# Mac
.DS_Store

24
news/extended-exec.rst Normal file
View file

@ -0,0 +1,24 @@
**Added:**
* Added ``-l``, ``-c`` and ``-a`` options to ``xexec``, works now like ``exec``
in bash/zsh
**Changed:**
* ``-l`` switch works like bash, loads environment in non-interactive shell
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

View file

@ -0,0 +1,84 @@
import os
import inspect
import pytest
import sys
from xonsh.aliases import xexec
@pytest.fixture
def mockexecvpe(monkeypatch):
def mocked_execvpe(_command, _args, _env):
pass
monkeypatch.setattr(os, "execvpe", mocked_execvpe)
def test_noargs(mockexecvpe):
assert xexec([]) == (None, "xonsh: exec: no args specified\n", 1)
def test_missing_command(mockexecvpe):
assert xexec(["-a", "foo"]) == (None, "xonsh: exec: no command specified\n", 1)
assert xexec(["-c"]) == (None, "xonsh: exec: no command specified\n", 1)
assert xexec(["-l"]) == (None, "xonsh: exec: no command specified\n", 1)
def test_command_not_found(monkeypatch):
dummy_error_msg = "This is dummy error message, file not found or something like that"
command = "non_existing_command"
def mocked_execvpe(_command, _args, _env):
raise FileNotFoundError(2, dummy_error_msg)
monkeypatch.setattr(os, "execvpe", mocked_execvpe)
assert xexec([command]) == (None,
"xonsh: exec: file not found: {}: {}" "\n".format(dummy_error_msg, command),
1)
def test_help(mockexecvpe):
assert xexec(["-h"]) == inspect.getdoc(xexec)
assert xexec(["--help"]) == inspect.getdoc(xexec)
def test_a_switch(monkeypatch):
called = {}
def mocked_execvpe(command, args, env):
called.update({"command": command, "args": args, "env": env})
monkeypatch.setattr(os, "execvpe", mocked_execvpe)
proc_name = "foo"
command = "bar"
command_args = ["1"]
xexec(["-a", proc_name, command] + command_args)
assert called["command"] == command
assert called["args"][0] == proc_name
assert len(called["args"]) == len([command] + command_args)
def test_l_switch(monkeypatch):
called = {}
def mocked_execvpe(command, args, env):
called.update({"command": command, "args": args, "env": env})
monkeypatch.setattr(os, "execvpe", mocked_execvpe)
command = "bar"
xexec(["-l", command, "1"])
assert called["args"][0].startswith("-")
def test_c_switch(monkeypatch):
called = {}
def mocked_execvpe(command, args, env):
called.update({"command": command, "args": args, "env": env})
monkeypatch.setattr(os, "execvpe", mocked_execvpe)
command = "sleep"
xexec(["-c", command, "1"])
assert called["env"] == {}

View file

@ -610,7 +610,7 @@ def source_cmd(args, stdin=None):
def xexec(args, stdin=None):
"""exec [-h|--help] command [args...]
"""exec [-h|--help] [-cl] [-a name] command [args...]
exec (also aliased as xexec) uses the os.execvpe() function to
replace the xonsh process with the specified program. This provides
@ -620,6 +620,13 @@ def xexec(args, stdin=None):
bash $
The '-h' and '--help' options print this message and exit.
If the '-l' option is supplied, the shell places a dash at the
beginning of the zeroth argument passed to command to simulate login
shell.
The '-c' option causes command to be executed with an empty environment.
If '-a' is supplied, the shell passes name as the zeroth argument
to the executed command.
Notes
-----
@ -633,18 +640,40 @@ def xexec(args, stdin=None):
"""
if len(args) == 0:
return (None, "xonsh: exec: no args specified\n", 1)
elif args[0] == "-h" or args[0] == "--help":
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("-h", "--help", action="store_true")
parser.add_argument("-l", dest="login", action="store_true")
parser.add_argument("-c", dest="clean", action="store_true")
parser.add_argument("-a", dest="name", nargs="?")
parser.add_argument("command", nargs=argparse.REMAINDER)
args = parser.parse_args(args)
if args.help:
return inspect.getdoc(xexec)
else:
if len(args.command) == 0:
return (None, "xonsh: exec: no command specified\n", 1)
command = args.command[0]
if args.name is not None:
args.command[0] = args.name
if args.login:
args.command[0] = "-{}".format(args.command[0])
denv = {}
if not args.clean:
denv = builtins.__xonsh__.env.detype()
try:
os.execvpe(args[0], args, denv)
except FileNotFoundError as e:
return (
None,
"xonsh: exec: file not found: {}: {}" "\n".format(e.args[1], args[0]),
1,
)
try:
os.execvpe(command, args.command, denv)
except FileNotFoundError as e:
return (
None,
"xonsh: exec: file not found: {}: {}"
"\n".format(e.args[1], args.command[0]),
1,
)
class AWitchAWitch(argparse.Action):

View file

@ -292,7 +292,11 @@ def start_services(shell_kwargs, args):
env = builtins.__xonsh__.env
rc = shell_kwargs.get("rc", None)
rc = env.get("XONSHRC") if rc is None else rc
if args.mode != XonshMode.interactive and not args.force_interactive:
if (
args.mode != XonshMode.interactive
and not args.force_interactive
and not args.login
):
# Don't load xonshrc if not interactive shell
rc = None
events.on_pre_rc.fire()
@ -329,7 +333,8 @@ def premain(argv=None):
"cacheall": args.cacheall,
"ctx": builtins.__xonsh__.ctx,
}
if args.login:
if args.login or sys.argv[0].startswith("-"):
args.login = True
shell_kwargs["login"] = True
if args.norc:
shell_kwargs["rc"] = ()