Repository: xonsh/xonsh

Issue:
Unintended loading `xonshrc` file in script mode
My xonshrc is pretty silent and I suddenly noticed that xonshrc is loaded in script mode:

```xsh
cd /tmp
echo 'echo RC' >> ~/.xonshrc
echo '#!/usr/bin/env xonsh' > myscript
chmod +x myscript

./myscript
# RC

xonsh myscript.xsh
# RC
```

When I run any xonsh script I have no any expectation that my huge `.xonshrc` with features for prompt will be loaded.

Bash/zsh also don't load RC files when you run script:

```xsh
cd /tmp
echo 'echo RC' >> ~/.bashrc
echo 'echo 1' > script.sh
bash script.sh
# 1  # No RC

echo 'echo RC' >> ~/.zshrc
echo 'echo 1' > script.zsh
zsh script.zsh
# 1  # No RC
```

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**



Discussion:
I think this is the result of #5099 (@cosineFish @bestlem).

@gforsyth I think we need to fix/revert this in 0.17.0 because:
* All xonsh scripts are affected.
* If you have many libs in xonshrc and want to run xonsh script in env you will face with unintended errors about missing libs. It's bad experience.

The user own xonshrc that is mostly for using it in prompt must NOT be loaded in script mode.
> The user own xonshrc that is mostly for using it in prompt must NOT be loaded in script mode.

I mean, that's the bash approach, but `zsh` and `fish` do load those files in script mode.

A user can run using `--no-rc` to avoid loading their `xonshrc` file
@gforsyth 
1. zsh does not do this (see example in the first message).
2. I believe that if I have CLI xonsh `script.xsh` with shebang I should not do anything with `--no-rc`. I expect that it just works. See also [xonsh-awesome-cli-app](https://github.com/anki-code/xonsh-awesome-cli-app).

So I'm completely for disabling load `~/.xonshrc` for scripts accordingly with expectations and bash, zsh behavior. 
If user wants to create xonshrc to load forever on user base level we already have `~/.config/xonsh/rc.xsh` ([doc](https://xon.sh/xonshrc.html)). 
ah, right, `zsh` does load `zshenv` though -- I don't want to get into having a million special cased startup files, though.

I'm fine with this either way.  If we do disable loading `rc` files in script mode, then we should ensure that a user can force loading them by passing `-i`
I understand the idea from [here](https://github.com/xonsh/xonsh/issues/4096#issuecomment-792035141) to have this in every xonshrc file:
```xsh
if $XONSH_INTERACTIVE:
    # ...
```
It looks clear BUT you forget about software around (e.g. `conda`, `zoxide`, `xonsh wizard`, `xonsh web`, etc) that just do:
```xsh
echo "some_init_code()" >> ~/.xonshrc
```
And this kind of tools don't know about `$XONSH_INTERACTIVE`. And when they add init code into xonshrc directly ALL scripts will be affected by this and will produce errors and slow timings! This is why I think it's a not so good idea @gforsyth .

We need more complex behavior. I'm thinking about at least push out `~/.xonshrc` to interactive mode.
And one moment. I want to remember you all that we have already stepped on this rake when we had loading history on every xonsh run - #4178. Before this fix we observed all kinds of unexpected errors and bad experience with scripts.
Do not do unexpected unintended work undercover.
> I understand the idea from [here](https://github.com/xonsh/xonsh/issues/4096#issuecomment-792035141) to have this in every xonshrc file:
> 
> ```python
> if $XONSH_INTERACTIVE:
>     # ...
> ```
> 
> It looks clear BUT you forget about software around (e.g. `conda`, `zoxide`, `xonsh wizard`, `xonsh web`, etc) that just do:
> 
> ```python
> echo "some_init_code()" >> ~/.xonshrc
> ```
> 
> And this kind of tools don't know about `$XONSH_INTERACTIVE`. And when they add init code into xonshrc directly ALL scripts will be affected by this and will produce errors and slow timings! This is why I think it's a not so good idea @gforsyth .
> 
> We need more complex behavior. I'm thinking about at least push out `~/.xonshrc` to interactive mode.

This is exactly how fish does it - load the same config.fish in all cases but have test for --is-interactive and --is-login Why is this not an issue for fish? 

Resolving patch:
diff --git a/xonsh/environ.py b/xonsh/environ.py
--- a/xonsh/environ.py
+++ b/xonsh/environ.py
@@ -15,6 +15,7 @@
 import typing as tp
 import warnings
 from collections import ChainMap
+from pathlib import Path
 
 import xonsh.prompt.base as prompt
 from xonsh import __version__ as XONSH_VERSION
@@ -624,6 +625,11 @@ def xonshconfig(env):
     return xc
 
 
+def get_home_xonshrc_path():
+    """Cross-platform implementation of getting ``~/.xonshrc`` path."""
+    return str((Path("~") / ".xonshrc").expanduser())
+
+
 @default_value
 def default_xonshrc(env) -> "tuple[str, ...]":
     """
@@ -633,7 +639,7 @@ def default_xonshrc(env) -> "tuple[str, ...]":
     dxrc = (
         os.path.join(xonsh_sys_config_dir(env), "xonshrc"),
         os.path.join(xonsh_config_dir(env), "rc.xsh"),
-        os.path.expanduser("~/.xonshrc"),
+        get_home_xonshrc_path(),
     )
     # Check if old config file exists and issue warning
     old_config_filename = xonshconfig(env)
@@ -1156,7 +1162,9 @@ class GeneralSetting(Xettings):
     )
     XONSH_INTERACTIVE = Var.with_default(
         True,
-        "``True`` if xonsh is running interactively, and ``False`` otherwise.",
+        "``True`` if xonsh is running interactively, and ``False`` otherwise. "
+        "It's highly recommended to use this variable in your ``xonshrc`` files "
+        "to split the code execution for interactive and non-interactive modes.",
         is_configurable=False,
     )
     XONSH_LOGIN = Var.with_default(
diff --git a/xonsh/main.py b/xonsh/main.py
--- a/xonsh/main.py
+++ b/xonsh/main.py
@@ -13,7 +13,7 @@
 from xonsh import __version__
 from xonsh.built_ins import XSH
 from xonsh.codecache import run_code_with_cache, run_script_with_cache
-from xonsh.environ import make_args_env, xonshrc_context
+from xonsh.environ import get_home_xonshrc_path, make_args_env, xonshrc_context
 from xonsh.events import events
 from xonsh.execer import Execer
 from xonsh.imphooks import install_import_hooks
@@ -308,6 +308,16 @@ def _get_rc_files(shell_kwargs: dict, args, env):
     # otherwise, get the RC files from XONSHRC, and RC dirs from XONSHRC_DIR
     rc = env.get("XONSHRC")
     rcd = env.get("XONSHRC_DIR")
+
+    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
+        of this file to non-interactive behavior we remove this file in non-interactive mode e.g. script with shebang.
+        """
+        home_xonshrc = get_home_xonshrc_path()
+        rc = tuple(c for c in rc if c != home_xonshrc)
+
     return rc, rcd
 
 
diff --git a/xonsh/webconfig/file_writes.py b/xonsh/webconfig/file_writes.py
--- a/xonsh/webconfig/file_writes.py
+++ b/xonsh/webconfig/file_writes.py
@@ -4,6 +4,8 @@
 import re
 import typing as tp
 
+from xonsh.environ import get_home_xonshrc_path
+
 
 def write_value(value: str, _) -> str:
     return f"{value!r}"
@@ -50,7 +52,7 @@ def config_to_xonsh(
     yield suffix
 
 
-RC_FILE = "~/.xonshrc"
+RC_FILE = get_home_xonshrc_path()
 
 
 def insert_into_xonshrc(


Test patch:
diff --git a/tests/test_integrations.py b/tests/test_integrations.py
--- a/tests/test_integrations.py
+++ b/tests/test_integrations.py
@@ -50,8 +50,9 @@ def run_xonsh(
     single_command=False,
     interactive=False,
     path=None,
-    add_args: list = None,
+    args=None,
     timeout=20,
+    add_env=None,
 ):
     env = dict(os.environ)
     if path is None:
@@ -65,20 +66,29 @@ def run_xonsh(
     env["PROMPT"] = ""
     # disable ansi escape codes
     env["TERM"] = "linux"
+
+    if add_env:
+        env |= add_env
+
     xonsh = shutil.which("xonsh", path=PATH)
-    args = [xonsh, "--no-rc"]
+    popen_args = [xonsh]
+    if not args:
+        popen_args += ["--no-rc"]
+    else:
+        popen_args += args
     if interactive:
-        args.append("-i")
+        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:
-        args += ["-c", cmd]
+        popen_args += ["-c", cmd]
         input = None
     else:
         input = cmd
-    if add_args:
-        args += add_args
 
     proc = sp.Popen(
-        args,
+        popen_args,
         env=env,
         stdin=stdin,
         stdout=stdout,
@@ -692,6 +702,8 @@ def _callme(args):
 @pytest.mark.flaky(reruns=3, reruns_delay=2)
 def test_script(case):
     script, exp_out, exp_rtn = case
+    if ON_DARWIN:
+        script = script.replace("tests/bin", str(Path(__file__).parent / "bin"))
     out, err, rtn = run_xonsh(script)
     if callable(exp_out):
         assert exp_out(out)
@@ -1257,7 +1269,7 @@ def test_main_d():
     assert out == "json\n"
 
     out, err, ret = run_xonsh(
-        add_args=["-DXONSH_HISTORY_BACKEND='dummy'"],
+        args=["--no-rc", "-DXONSH_HISTORY_BACKEND='dummy'"],
         cmd="print($XONSH_HISTORY_BACKEND)",
         single_command=True,
     )
@@ -1335,3 +1347,53 @@ def test_alias_stability_exception():
         re.MULTILINE | re.DOTALL,
     )
     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")
+    user_home_rc_path_crossplatform = str(
+        (Path(user_home_dir) / ".xonshrc").expanduser()
+    )
+    (user_not_home_rc := user_not_home_dir / "rc.xsh").write_text(
+        "echo RC_NOT_HOME", 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.
+    args = [
+        f'-DHOME="{str(user_home_dir)}"',
+        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)}
+    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}"
+
+    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}"
diff --git a/tests/test_pipelines.py b/tests/test_pipelines.py
--- a/tests/test_pipelines.py
+++ b/tests/test_pipelines.py
@@ -74,6 +74,7 @@ def patched_events(monkeypatch, xonsh_events, xonsh_session):
         ("!(echo hi | grep x)", "", "", ""),
     ),
 )
+@pytest.mark.flaky(reruns=3, reruns_delay=2)
 def test_command_pipeline_capture(cmdline, stdout, stderr, raw_stdout, xonsh_execer):
     pipeline: CommandPipeline = xonsh_execer.eval(cmdline)
     assert pipeline.out == stdout