Repository: xonsh/xonsh

Issue:
Do not load `~/.xonshrc` in not interactive mode
### Motivation

In #5099 was introduced the logic where all RC files are loaded in interactive and non-interactive modes. This logic is not working good for home based `~/.xonshrc` file.

First of all `~/.xonshrc` is the buffer of settings focused on interactive mode. Most tools with integration with xonsh (e.g. `conda`, `zoxide`, etc) offer to just do `echo "init_tool()" >> ~/.xonshrc` (`conda` has around 20 lines of init code) to start using the tool and users are doing this without any doubts.

But because of after 5099 `~/.xonshrc` is executed in non-interactive mode the adding code to it leads to unexpected and unintended side effects: 

* If you run a script with shebang (e.g. `#!/usr/bin/env xonsh` or [xonsh-awesome-cli-app](https://github.com/anki-code/xonsh-awesome-cli-app)) or just from `xonsh script.xsh` the code will be unexpected and unintended slower.

* If you're using xonsh-based tools (e.g. you install them using pip) and run them in environment that has no packages that initiated in `~/.xonshrc` you will see epic errors.

* Additional context: 
  * Bash and Zsh do not load `~/.bashrc` and `~/.zshrc` in non-interactive mode by the same reasons.
  * We have welcome message `Create ~/.xonshrc file manually or use xonfig to suppress the welcome message` and we don't want to make the process of creating this file complex.

All of this leads to bad unexpected and unintended experience. This PR is to fix this.

### Expectation

By doing this fix we assume that experienced user who wants to build good repeatable run control files will use another ways to create config files and this has [reflection in docs](https://github.com/xonsh/xonsh/blob/8860f2bd5273d5f3fc08ccf6be6af8163bfec0bd/docs/xonshrc.rst). In the nutshell if you want to create the RC files that affect every run of code you should use one or many of these ways:

* Cross-desktop group (XDG) compliant `~/.config/xonsh/rc.xsh` control file.
* The system-wide control file `/etc/xonsh/xonshrc` for Linux and OSX and in `%ALLUSERSPROFILE%\xonsh\xonshrc` on Windows. It controls options that are applied to all users of Xonsh on a given system.
* The home-based directory `~/.config/xonsh/rc.d/` and system `/etc/xonsh/rc.d/` can contain .xsh files. They will be executed at startup in order. This allows for drop-in configuration where your configuration can be split across scripts and common and local configurations more easily separated.

In your configs you need to check `$XONSH_INTERACTIVE` and `$XONSH_LOGIN` explicitly.

### Before

`~/.xonshrc` is used in non-interactive mode.

```xsh
echo "echo RC" >> ~/.xonshrc
cd /tmp
echo "echo Script" > script.xsh
xonsh script.xsh
# RC
# Script
```
```xsh
cd /tmp
echo 'echo RC' >> ~/.xonshrc
echo '#!/usr/bin/env xonsh' > myscript
chmod +x myscript

./myscript
# RC
```

### After

`~/.xonshrc` is not used in non-interactive mode. Use `-i` if you need it.

```xsh
echo "echo RC" >> ~/.xonshrc
cd /tmp
echo "echo Script" > script.xsh
xonsh script.xsh
# Script

xonsh -i script.xsh
# RC
# Script
```
```xsh
cd /tmp
echo 'echo RC' >> ~/.xonshrc
echo '#!/usr/bin/env xonsh' > myscript
chmod +x myscript

./myscript
```
Closes #5488 #4096 #5496

### Fun

I want to leave here some nice representation of how it works in bash/sh/zsh from [twitter post](https://twitter.com/paxx39/status/1742768007154479109):

![image](https://github.com/xonsh/xonsh/assets/1708680/cd7b3803-483f-4d5d-bf9d-baa61c794f68)

[How it works in xonsh](https://github.com/xonsh/xonsh/blob/20081b5a5179afa82b587e0bd3795b72caa2a2a5/docs/xonshrc.rst):
* Non-interactive ->  `$XONSHRC` and `$XONSHRC_DIR`
* Interactive -> The same + `~/.xonshrc`

And all logic inside files ;)

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**



Discussion:
@gforsyth I've added tests that work for nix and windows. It's ready to review.
Hey @bestlem -- I think you're on board with these changes, yes?
@gforsyth yes - https://github.com/xonsh/xonsh/issues/4096#issuecomment-2165690767
Thanks for review @gforsyth ! I believe it was fixed in https://github.com/xonsh/xonsh/pull/5497 :)

Resolving patch:
diff --git a/xonsh/main.py b/xonsh/main.py
--- a/xonsh/main.py
+++ b/xonsh/main.py
@@ -309,7 +309,7 @@ def _get_rc_files(shell_kwargs: dict, args, env):
     rc = env.get("XONSHRC")
     rcd = env.get("XONSHRC_DIR")
 
-    if not env.get("XONSH_INTERACTIVE", False) or not args.force_interactive:
+    if not env.get("XONSH_INTERACTIVE", False):
         """
         Home based ``~/.xonshrc`` file has special meaning and history. The ecosystem around shells treats this kind of files
         as the place where interactive tools can add configs. To avoid unintended and unexpected affection


Test patch:
diff --git a/tests/test_integrations.py b/tests/test_integrations.py
--- a/tests/test_integrations.py
+++ b/tests/test_integrations.py
@@ -40,6 +40,16 @@
     shutil.which("sleep") is None, reason="sleep command not on PATH"
 )
 
+base_env = {
+    "PATH": PATH,
+    "XONSH_DEBUG": "0",
+    "XONSH_SHOW_TRACEBACK": "1",
+    "RAISE_SUBPROC_ERROR": "0",
+    "FOREIGN_ALIASES_SUPPRESS_SKIP_MESSAGE": "1",
+    "PROMPT": "",
+    "TERM": "linux",  # disable ansi escape codes
+}
+
 
 def run_xonsh(
     cmd,
@@ -52,35 +62,31 @@ def run_xonsh(
     path=None,
     args=None,
     timeout=20,
-    add_env=None,
+    env=None,
 ):
-    env = dict(os.environ)
-    if path is None:
-        env["PATH"] = PATH
-    else:
-        env["PATH"] = path
-    env["XONSH_DEBUG"] = "0"  # was "1"
-    env["XONSH_SHOW_TRACEBACK"] = "1"
-    env["RAISE_SUBPROC_ERROR"] = "0"
-    env["FOREIGN_ALIASES_SUPPRESS_SKIP_MESSAGE"] = "1"
-    env["PROMPT"] = ""
-    # disable ansi escape codes
-    env["TERM"] = "linux"
-
-    if add_env:
-        env |= add_env
-
+    # Env
+    popen_env = dict(os.environ)
+    popen_env |= base_env
+    if path:
+        popen_env["PATH"] = path
+    if env:
+        popen_env |= env
+
+    # Args
     xonsh = shutil.which("xonsh", path=PATH)
     popen_args = [xonsh]
+
     if not args:
         popen_args += ["--no-rc"]
     else:
         popen_args += args
+
     if interactive:
         popen_args.append("-i")
         if cmd and isinstance(cmd, str) and not cmd.endswith("\n"):
             # In interactive mode we need to emulate "Press Enter".
             cmd += "\n"
+
     if single_command:
         popen_args += ["-c", cmd]
         input = None
@@ -89,7 +95,7 @@ def run_xonsh(
 
     proc = sp.Popen(
         popen_args,
-        env=env,
+        env=popen_env,
         stdin=stdin,
         stdout=stdout,
         stderr=stderr,
@@ -1349,102 +1355,77 @@ def test_alias_stability_exception():
     assert "Bad file descriptor" not in out
 
 
-@pytest.mark.flaky(reruns=3, reruns_delay=2)
-def test_rc_no_xonshrc_for_non_interactive(tmpdir):
-    """Testing no ``~/.xonshrc`` execution in non-interactive commands (#5491)."""
-    rc_dir = tmpdir.mkdir("rc_dir")
-    user_home_dir = tmpdir.mkdir("user_home")
-    user_not_home_dir = tmpdir.mkdir("user_not_home")
-
-    (rc_dir / "rc_dir.xsh").write_text("echo RC_DIR", encoding="utf8")
-    (user_home_dir / ".xonshrc").write_text("echo RC_HOME", encoding="utf8")
-    (script := user_home_dir / "script.xsh").write_text("echo SCRIPT", encoding="utf8")
-    user_home_rc_path_crossplatform = str(
-        (Path(user_home_dir) / ".xonshrc").expanduser()
+@pytest.mark.parametrize(
+    "cmd,exp",
+    [
+        ["-i", ".*CONFIG_XONSH_RC_XSH.*HOME_XONSHRC.*CONFIG_XONSH_RCD.*"],
+        ["--rc rc.xsh", ".*RCXSH.*"],
+        ["-i --rc rc.xsh", ".*RCXSH.*"],
+        ["-c print('CMD')", ".*CONFIG_XONSH_RC_XSH.*CONFIG_XONSH_RCD.*CMD.*"],
+        [
+            "-i -c print('CMD')",
+            ".*CONFIG_XONSH_RC_XSH.*HOME_XONSHRC.*CONFIG_XONSH_RCD.*CMD.*",
+        ],
+        ["script.xsh", ".*CONFIG_XONSH_RC_XSH.*CONFIG_XONSH_RCD.*SCRIPT.*"],
+        [
+            "-i script.xsh",
+            ".*CONFIG_XONSH_RC_XSH.*HOME_XONSHRC.*CONFIG_XONSH_RCD.*SCRIPT.*",
+        ],
+        ["--rc rc.xsh -- script.xsh", ".*RCXSH.*SCRIPT.*"],
+        ["-i --rc rc.xsh -- script.xsh", ".*RCXSH.*SCRIPT.*"],
+        ["--no-rc --rc rc.xsh -- script.xsh", ".*SCRIPT.*"],
+        ["-i --no-rc --rc rc.xsh -- script.xsh", ".*SCRIPT.*"],
+    ],
+)
+# @pytest.mark.flaky(reruns=3, reruns_delay=2)
+def test_xonshrc(tmpdir, cmd, exp):
+    # ~/.xonshrc
+    home = tmpdir.mkdir("home")
+    (home / ".xonshrc").write_text("echo HOME_XONSHRC", encoding="utf8")
+    home_xonsh_rc_path = str(  # crossplatform path
+        (Path(home) / ".xonshrc").expanduser()
+    )
+
+    # ~/.config/xonsh/rc.xsh
+    home_config_xonsh = tmpdir.mkdir("home_config_xonsh")
+    (home_config_xonsh_rc_xsh := home_config_xonsh / "rc.xsh").write_text(
+        "echo CONFIG_XONSH_RC_XSH", encoding="utf8"
     )
-    (user_not_home_rc := user_not_home_dir / "rc.xsh").write_text(
-        "echo RC_NOT_HOME", encoding="utf8"
+
+    # ~/.config/xonsh/rc.d/
+    home_config_xonsh_rcd = tmpdir.mkdir("home_config_xonsh_rcd")
+    (home_config_xonsh_rcd / "rcd1.xsh").write_text(
+        "echo CONFIG_XONSH_RCD", encoding="utf8"
     )
-    xonshrc_files = [str(user_not_home_rc), str(user_home_rc_path_crossplatform)]
-    xonshrc_dir = [str(rc_dir)]
 
-    # Here `eval()` is needed in Windows case where the path contains `:` symbol (e.g. `C:\\path`) and treated as list delimiter.
+    # ~/home/rc.xsh
+    (rc_xsh := home / "rc.xsh").write_text("echo RCXSH", encoding="utf8")
+    (script_xsh := home / "script.xsh").write_text("echo SCRIPT_XSH", encoding="utf8")
+
+    # Construct $XONSHRC and $XONSHRC_DIR
+    xonshrc_files = [str(home_config_xonsh_rc_xsh), str(home_xonsh_rc_path)]
+    xonshrc_dir = [str(home_config_xonsh_rcd)]
+
     args = [
-        f'-DHOME="{str(user_home_dir)}"',
+        f'-DHOME="{str(home)}"',
         f'-DXONSHRC="{os.pathsep.join(xonshrc_files)}"',
         f'-DXONSHRC_DIR="{os.pathsep.join(xonshrc_dir)}"',
     ]
-    cmd = "print(42+42)"
-    add_env = {"HOME": str(user_home_dir)}
+    env = {"HOME": str(home)}
+
+    cmd = cmd.replace("rc.xsh", str(rc_xsh)).replace("script.xsh", str(script_xsh))
+    args = args + cmd.split()
 
     # xonsh
     out, err, ret = run_xonsh(
         cmd=None,
-        stdin=None,
-        single_command=False,
-        interactive=True,
         args=args,
-        add_env=add_env,
-    )
-
-    exp = ".*RC_NOT_HOME.*RC_HOME.*RC_DIR.*"
-    assert re.match(
-        exp,
-        out,
-        re.MULTILINE | re.DOTALL,
-    ), f"Expected: {exp!r},\nResult: {out!r},\nargs={args!r}"
-
-    # xonsh -c "cmd"
-    out, err, ret = run_xonsh(cmd=cmd, interactive=False, args=args, add_env=add_env)
-    exp = ".*RC_NOT_HOME.*RC_DIR.*84.*"
-    assert re.match(
-        exp,
-        out,
-        re.MULTILINE | re.DOTALL,
-    ), f"Expected: {exp!r},\nResult: {out!r},\nargs={args!r}"
-
-    # xonsh -i -c "cmd"
-    out, err, ret = run_xonsh(
-        cmd=cmd + "\n", interactive=True, args=args, add_env=add_env
-    )
-
-    exp = ".*RC_NOT_HOME.*RC_HOME.*RC_DIR.*"
-    if not ON_WINDOWS:
-        # On Windows we well have `NoConsoleScreenBufferError` in interactive mode so avoid checking interactive output of the command.
-        exp += "84.*"
-
-    assert re.match(
-        exp,
-        out,
-        re.MULTILINE | re.DOTALL,
-    ), f"Expected: {exp!r},\nResult: {out!r},\nargs={args!r}"
-
-    # xonsh script.xsh
-    out, err, ret = run_xonsh(
-        cmd=cmd + "\n",
-        interactive=False,
-        single_command=False,
-        args=args + ["--", script],
-        add_env=add_env,
+        env=env,
     )
-    exp = ".*RC_NOT_HOME.*RC_DIR.*SCRIPT.*"
-    assert re.match(
-        exp,
-        out,
-        re.MULTILINE | re.DOTALL,
-    ), f"Expected: {exp!r},\nResult: {out!r},\nargs={args!r}"
 
-    # xonsh -i script.xsh
-    out, err, ret = run_xonsh(
-        cmd=None,
-        interactive=False,
-        single_command=False,
-        args=args + ["-i", "--", script],
-        add_env=add_env,
-    )
-    exp = ".*RC_NOT_HOME.*RC_HOME.*RC_DIR.*SCRIPT.*"
+    exp = exp
     assert re.match(
         exp,
         out,
         re.MULTILINE | re.DOTALL,
-    ), f"Expected: {exp!r},\nResult: {out!r},\nargs={args!r}"
+    ), f"Case: xonsh {cmd},\nExpected: {exp!r},\nResult: {out!r},\nargs={args!r}"