diff --git a/.gitignore b/.gitignore index e028ddb9d..fdb215611 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ pip-selfcheck.json bin/ /lib/ include/ +venv/ # Mac .DS_Store diff --git a/news/extended-exec.rst b/news/extended-exec.rst new file mode 100644 index 000000000..7f9be2186 --- /dev/null +++ b/news/extended-exec.rst @@ -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:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/tests/aliases/test_xexec.py b/tests/aliases/test_xexec.py new file mode 100644 index 000000000..d7891a397 --- /dev/null +++ b/tests/aliases/test_xexec.py @@ -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"] == {} diff --git a/xonsh/aliases.py b/xonsh/aliases.py index 2300047c1..74d726d32 100644 --- a/xonsh/aliases.py +++ b/xonsh/aliases.py @@ -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): diff --git a/xonsh/main.py b/xonsh/main.py index 45ab31a9c..3eb38abdb 100644 --- a/xonsh/main.py +++ b/xonsh/main.py @@ -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"] = ()