Repository: spyder-ide/spyder

Issue:
ValueError when trying to get environment variables on Linux
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

In Spyder open Tools menu, then click Current user environment variables...

Doesnt open

### Traceback
```python-traceback
Traceback (most recent call last):
  File "/home/mark/tutorial_env/lib64/python3.10/site-packages/spyder/plugins/application/container.py", line 252, in show_user_env_variables
    self.dialog_manager.show(UserEnvDialog(self))
  File "/home/mark/tutorial_env/lib64/python3.10/site-packages/spyder/utils/environ.py", line 162, in __init__
    super().__init__(get_user_env(), parent, title, readonly)
  File "/home/mark/tutorial_env/lib64/python3.10/site-packages/spyder/utils/environ.py", line 71, in get_user_env
    env_dict = get_user_environment_variables()
  File "/home/mark/tutorial_env/lib64/python3.10/site-packages/spyder/utils/environ.py", line 63, in get_user_environment_variables
    k, v = kv.split('=', 1)
ValueError: not enough values to unpack (expected 2, got 1)
```

## Versions

* Spyder version: 5.4.0  (pip)
* Python version: 3.10.8 64-bit
* Qt version: 5.15.2
* PyQt5 version: 5.15.7
* Operating System: Linux 6.0.5-200.fc36.x86_64

### Dependencies

```
# Mandatory:
atomicwrites >=1.2.0          :  1.4.1 (OK)
chardet >=2.0.0               :  5.0.0 (OK)
cloudpickle >=0.5.0           :  2.2.0 (OK)
cookiecutter >=1.6.0          :  2.1.1 (OK)
diff_match_patch >=20181111   :  20200713 (OK)
intervaltree >=3.0.2          :  3.1.0 (OK)
IPython >=7.31.1;<8.0.0       :  7.34.0 (OK)
jedi >=0.17.2;<0.19.0         :  0.18.1 (OK)
jellyfish >=0.7               :  0.9.0 (OK)
jsonschema >=3.2.0            :  4.17.0 (OK)
keyring >=17.0.0              :  23.11.0 (OK)
nbconvert >=4.0               :  7.2.5 (OK)
numpydoc >=0.6.0              :  1.5.0 (OK)
parso >=0.7.0;<0.9.0          :  0.8.3 (OK)
pexpect >=4.4.0               :  4.8.0 (OK)
pickleshare >=0.4             :  0.7.5 (OK)
psutil >=5.3                  :  5.9.4 (OK)
pygments >=2.0                :  2.13.0 (OK)
pylint >=2.5.0;<3.0           :  2.15.6 (OK)
pylint_venv >=2.1.1           :  2.3.0 (OK)
pyls_spyder >=0.4.0           :  0.4.0 (OK)
pylsp >=1.6.0;<1.7.0          :  1.6.0 (OK)
pylsp_black >=1.2.0           :  1.2.1 (OK)
qdarkstyle >=3.0.2;<3.1.0     :  3.0.3 (OK)
qstylizer >=0.2.2             :  0.2.2 (OK)
qtawesome >=1.2.1             :  1.2.1 (OK)
qtconsole >=5.4.0;<5.5.0      :  5.4.0 (OK)
qtpy >=2.1.0                  :  2.3.0 (OK)
rtree >=0.9.7                 :  1.0.1 (OK)
setuptools >=49.6.0           :  59.6.0 (OK)
sphinx >=0.6.6                :  5.3.0 (OK)
spyder_kernels >=2.4.0;<2.5.0 :  2.4.0 (OK)
textdistance >=4.2.0          :  4.5.0 (OK)
three_merge >=0.1.1           :  0.1.1 (OK)
watchdog >=0.10.3             :  2.1.9 (OK)
xdg >=0.26                    :  0.28 (OK)
zmq >=22.1.0                  :  24.0.1 (OK)

# Optional:
cython >=0.21                 :  None (NOK)
matplotlib >=3.0.0            :  None (NOK)
numpy >=1.7                   :  None (NOK)
pandas >=1.1.1                :  None (NOK)
scipy >=0.17.0                :  None (NOK)
sympy >=0.7.3                 :  None (NOK)
```

Error when running Numpy, Scipy, Pandas code with Windows installer
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

I'm running pybamm using DFN model but I get this error No module named 'numpy.core._multiarray_umath, I have updated python version to 10.8. Why I'm getting this issue? any resolution ? thanks 



## Versions

* Spyder version: 5.3.3  (conda)
* Python version: 3.10.8 64-bit
* Qt version: 5.15.2
* PyQt5 version: 5.15.7
* Operating System: Windows 10

### Dependencies

```
# Mandatory:
atomicwrites >=1.2.0          :  1.4.0 (OK)
chardet >=2.0.0               :  4.0.0 (OK)
cloudpickle >=0.5.0           :  2.0.0 (OK)
cookiecutter >=1.6.0          :  1.7.3 (OK)
diff_match_patch >=20181111   :  20200713 (OK)
intervaltree >=3.0.2          :  3.1.0 (OK)
IPython >=7.31.1;<8.0.0       :  7.31.1 (OK)
jedi >=0.17.2;<0.19.0         :  0.18.1 (OK)
jellyfish >=0.7               :  0.9.0 (OK)
jsonschema >=3.2.0            :  4.16.0 (OK)
keyring >=17.0.0              :  23.4.0 (OK)
nbconvert >=4.0               :  6.5.4 (OK)
numpydoc >=0.6.0              :  1.5.0 (OK)
paramiko >=2.4.0              :  2.8.1 (OK)
parso >=0.7.0;<0.9.0          :  0.8.3 (OK)
pexpect >=4.4.0               :  4.8.0 (OK)
pickleshare >=0.4             :  0.7.5 (OK)
psutil >=5.3                  :  5.9.0 (OK)
pygments >=2.0                :  2.11.2 (OK)
pylint >=2.5.0;<3.0           :  2.14.5 (OK)
pyls_spyder >=0.4.0           :  0.4.0 (OK)
pylsp >=1.5.0;<1.6.0          :  1.5.0 (OK)
pylsp_black >=1.2.0           :  1.2.1 (OK)
qdarkstyle >=3.0.2;<3.1.0     :  3.0.2 (OK)
qstylizer >=0.1.10            :  0.1.10 (OK)
qtawesome >=1.0.2             :  1.0.3 (OK)
qtconsole >=5.3.2;<5.4.0      :  5.3.2 (OK)
qtpy >=2.1.0                  :  2.2.0 (OK)
rtree >=0.9.7                 :  0.9.7 (OK)
setuptools >=49.6.0           :  65.5.0 (OK)
sphinx >=0.6.6                :  5.0.2 (OK)
spyder_kernels >=2.3.3;<2.4.0 :  2.3.3 (OK)
textdistance >=4.2.0          :  4.2.1 (OK)
three_merge >=0.1.1           :  0.1.1 (OK)
watchdog >=0.10.3             :  2.1.6 (OK)
zmq >=22.1.0                  :  23.2.0 (OK)

# Optional:
cython >=0.21                 :  None (NOK)
matplotlib >=3.0.0            :  None (NOK)
numpy >=1.7                   :  None (NOK)
pandas >=1.1.1                :  None (NOK)
scipy >=0.17.0                :  None (NOK)
sympy >=0.7.3                 :  1.11.1 (OK)
```



Discussion:
Hey @markmcv, thanks for reporting. Could you open a terminal and post the contents you get after running the command

    printenv

? Thanks!
Thanks for your reply.
Output of printenv command:
```
SHELL=/bin/bash
IMSETTINGS_INTEGRATE_DESKTOP=yes
SESSION_MANAGER=local/unix:@/tmp/.ICE-unix/1694,unix/unix:/tmp/.ICE-unix/1694
WINDOWID=50331654
COLORTERM=truecolor
HISTCONTROL=ignoredups
GTK_IM_MODULE=gtk-im-context-simple
HISTSIZE=1000
HOSTNAME=toshiba
SSH_AUTH_SOCK=/run/user/1000/keyring/ssh
XMODIFIERS=@im=none
DESKTOP_SESSION=mate
SSH_AGENT_PID=1919
EDITOR=/usr/bin/nano
XDG_SEAT=seat0
MATE_DESKTOP_SESSION_ID=this-is-deprecated
PWD=/home/mark/tutorial_env
LOGNAME=mark
XDG_SESSION_DESKTOP=mate
XDG_SESSION_TYPE=x11
SYSTEMD_EXEC_PID=1554
XAUTHORITY=/run/user/1000/gdm/Xauthority
SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS=0
WINDOWPATH=2
GDM_LANG=en_GB.UTF-8
HOME=/home/mark
USERNAME=mark
LANG=en_GB.UTF-8
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.m4a=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.oga=01;36:*.opus=01;36:*.spx=01;36:*.xspf=01;36:
XDG_CURRENT_DESKTOP=MATE
VTE_VERSION=6800
IMSETTINGS_MODULE=none
STEAM_FRAME_FORCE_CLOSE=1
MOZ_GMP_PATH=/usr/lib64/mozilla/plugins/gmp-gmpopenh264/system-installed
XDG_SESSION_CLASS=user
TERM=xterm-256color
GTK_OVERLAY_SCROLLING=0
LESSOPEN=||/usr/bin/lesspipe.sh %s
USER=mark
DISPLAY=:0
SHLVL=1
QT_IM_MODULE=xim
XDG_VTNR=2
XDG_SESSION_ID=2
XDG_RUNTIME_DIR=/run/user/1000
DEBUGINFOD_URLS=https://debuginfod.fedoraproject.org/ 
which_declare=declare -f
GTK3_MODULES=xapp-gtk3-module
XDG_DATA_DIRS=/home/mark/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share/:/usr/share/
PATH=/home/mark/.local/bin:/home/mark/bin:/home/mark/.cargo/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin
GDMSESSION=mate
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
MAIL=/var/spool/mail/mark
QT_FONT_DPI=96
QT_SCALE_FACTOR=1
BASH_FUNC_which%%=() {  ( alias;
 eval ${which_declare} ) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot $@
}
_=/usr/bin/printenv
OLDPWD=/home/mark/tutorial_env/share
```

Thanks @markmcv!

@mrclary, could you take a look at this one? Thanks!
@markmcv, thank you for posting the results of `printenv`; that is helpful.
Could you also post the contents of `os.environ` from Spyder's internal console?
View->Panes->Internal console, then
```python
>>> import os
>>> os.environ
```

@markmcv, also, how did you create your `tutorial_env` environment?
I'm just trying to reproduce your traceback.


> @markmcv, thank you for posting the results of `printenv`; that is helpful. Could you also post the contents of `os.environ` from Spyder's internal console? View->Panes->Internal console, then
> 
> ```python
> >>> import os
> >>> os.environ
> ```

```
environ({'SHELL': '/bin/bash', 'IMSETTINGS_INTEGRATE_DESKTOP': 'yes', 'SESSION_MANAGER': 'local/unix:@/tmp/.ICE-unix/1673,unix/unix:/tmp/.ICE-unix/1673', 'WINDOWID': '48234502', 'COLORTERM': 'truecolor', 'HISTCONTROL': 'ignoredups', 'GTK_IM_MODULE': 'gtk-im-context-simple', 'HISTSIZE': '1000', 'HOSTNAME': 'toshiba', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'XMODIFIERS': '@im=none', 'DESKTOP_SESSION': 'mate', 'SSH_AGENT_PID': '1888', 'EDITOR': '/usr/bin/nano', 'XDG_SEAT': 'seat0', 'MATE_DESKTOP_SESSION_ID': 'this-is-deprecated', 'PWD': '/home/mark/tutorial_env/bin', 'LOGNAME': 'mark', 'XDG_SESSION_DESKTOP': 'mate', 'XDG_SESSION_TYPE': 'x11', 'SYSTEMD_EXEC_PID': '1540', 'XAUTHORITY': '/run/user/1000/gdm/Xauthority', 'SDL_VIDEO_MINIMIZE_ON_FOCUS_LOSS': '0', 'WINDOWPATH': '2', 'GDM_LANG': 'en_GB.UTF-8', 'HOME': '/home/mark', 'USERNAME': 'mark', 'LANG': 'en_GB.UTF-8', 'LS_COLORS': 'rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.m4a=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.oga=01;36:*.opus=01;36:*.spx=01;36:*.xspf=01;36:', 'XDG_CURRENT_DESKTOP': 'MATE', 'VIRTUAL_ENV': '/home/mark/tutorial_env', 'VTE_VERSION': '6800', 'IMSETTINGS_MODULE': 'none', 'STEAM_FRAME_FORCE_CLOSE': '1', 'MOZ_GMP_PATH': '/usr/lib64/mozilla/plugins/gmp-gmpopenh264/system-installed', 'XDG_SESSION_CLASS': 'user', 'TERM': 'xterm-256color', 'GTK_OVERLAY_SCROLLING': '0', 'LESSOPEN': '||/usr/bin/lesspipe.sh %s', 'USER': 'mark', 'DISPLAY': ':0', 'SHLVL': '1', 'QT_IM_MODULE': 'xim', 'XDG_VTNR': '2', 'XDG_SESSION_ID': '2', 'VIRTUAL_ENV_PROMPT': '(tutorial_env) ', 'XDG_RUNTIME_DIR': '/run/user/1000', 'PS1': '(tutorial_env) [\\u@\\h \\W]\\$ ', 'DEBUGINFOD_URLS': 'https://debuginfod.fedoraproject.org/ ', 'which_declare': 'declare -f', 'GTK3_MODULES': 'xapp-gtk3-module', 'XDG_DATA_DIRS': '/home/mark/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share/:/usr/share/', 'PATH': '/home/mark/tutorial_env/bin:/home/mark/.local/bin:/home/mark/bin:/home/mark/.cargo/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin', 'GDMSESSION': 'mate', 'DBUS_SESSION_BUS_ADDRESS': 'unix:path=/run/user/1000/bus', 'MAIL': '/var/spool/mail/mark', 'QT_FONT_DPI': '96', 'QT_SCALE_FACTOR': '', 'OLDPWD': '/home/mark/tutorial_env', 'BASH_FUNC_which%%': '() {  ( alias;\n eval ${which_declare} ) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot $@\n}', '_': '/home/mark/tutorial_env/bin/spyder', 'LANGUAGE': 'en', 'SPYDER_ARGS': '[]', 'QT_SCREEN_SCALE_FACTORS': '', 'SPYDER_DEBUG_FILE': '/home/mark/.config/spyder-py3/spyder-debug.log', 'QT_API': 'pyqt5'})
```
> @markmcv, also, how did you create your `tutorial_env` environment? I'm just trying to reproduce your traceback.

python3 -m venv tutorial-env
The issue is being caused by an environment variable that has newline characters in its value. I'll update the parsing to be more robust.
@markmcv, in your environment variables (listed from the `printenv` bash command) you have a variable that is actually a function, `BASH_FUNC_which%%`. How is this defined in your bash startup scripts?
Exported functions in bash will generate environment variables with newlines.

Example:
```
$ myfunc() { echo Hello; }
$ export -f myfunc
$ printenv
[...]
BASH_FUNC_myfunc%%=() {  echo Hello
}
```

Using `os.environ` is safer than trying to parse the output of the `printenv` command.

Attached is the fix I made to have Spyder working again for me.

[environ.txt](https://github.com/spyder-ide/spyder/files/10336673/environ.txt)

Thanks for your interest in solving this issue @slugan. The problem with using `os.environ` is that Spyder won't be able to grab env vars defined in `~/.bashrc` or `/etc/profile` unless it's started from a terminal.

But we're working to find other ways to fix this problem and preserve the current functionality. Hopefully, they'll be available in our next version (5.4.2), to be released in February.
@slugan, thank you very much for explaining the exported function. I couldn't figure out how the OP got that defined in their environment variables. We'll work on a fix for 5.4.2. However, in the mean time, the 5.4.1 release assets for the standalone macOS installers, [Spyder.dmg](https://github.com/spyder-ide/spyder/releases/download/v5.4.1/Spyder.dmg) and [Spyder-Lite.dmg](https://github.com/spyder-ide/spyder/releases/download/v5.4.1/Spyder-Lite.dmg), should have a temporary fix that should work.
Hi @minatouma seems like you don't have `numpy` installed in the env Spyder is runing. Could you try to install and check again?

Resolving patch:
diff --git a/spyder/utils/environ.py b/spyder/utils/environ.py
--- a/spyder/utils/environ.py
+++ b/spyder/utils/environ.py
@@ -10,6 +10,9 @@
 
 # Standard library imports
 import os
+from pathlib import Path
+import re
+import sys
 try:
     import winreg
 except Exception:
@@ -19,7 +22,7 @@
 from qtpy.QtWidgets import QMessageBox
 
 # Local imports
-from spyder.config.base import _
+from spyder.config.base import _, running_in_ci
 from spyder.widgets.collectionseditor import CollectionsEditor
 from spyder.utils.icon_manager import ima
 from spyder.utils.programs import run_shell_command
@@ -51,26 +54,25 @@ def get_user_environment_variables():
     env_var : dict
         Key-value pairs of environment variables.
     """
-    if os.name == 'nt':
-        cmd = "set"
-    else:
-        cmd = "printenv"
-    proc = run_shell_command(cmd)
-    stdout, stderr = proc.communicate()
-
     try:
-        res = stdout.decode().strip().split(os.linesep)
+        if os.name == 'nt':
+            key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment")
+            num_values = winreg.QueryInfoKey(key)[1]
+            env_var = dict(
+                [winreg.EnumValue(key, k)[:2] for k in range(num_values)]
+            )
+        else:
+            shell = os.environ.get("SHELL", "/bin/bash")
+            cmd = (
+                f"{shell} -l -c"
+                f""" "{sys.executable} -c 'import os; print(dict(os.environ))'" """
+            )
+            proc = run_shell_command(cmd, env={}, text=True)
+            stdout, stderr = proc.communicate()
+            env_var = eval(stdout, None)
     except Exception:
         return {}
 
-    env_var = {}
-    for kv in res:
-        try:
-            k, v = kv.split('=', 1)
-            env_var[k] = v
-        except Exception:
-            pass
-
     return env_var
 
 
@@ -81,35 +83,80 @@ def get_user_env():
 
 
 def set_user_env(env, parent=None):
-    """Set HKCU (current user) environment variables"""
-    if os.name != 'nt':
-        raise NotImplementedError("Not implemented for %s platforms", os.name)
-
-    reg = listdict2envdict(env)
-    types = dict()
-    key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment")
-    for name in reg:
+    """
+    Set user environment variables via HKCU (Windows) or shell startup file
+    (Unix).
+    """
+    env_dict = listdict2envdict(env)
+
+    if os.name == 'nt':
+        types = dict()
+        key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment")
+        for name in env_dict:
+            try:
+                _x, types[name] = winreg.QueryValueEx(key, name)
+            except WindowsError:
+                types[name] = winreg.REG_EXPAND_SZ
+        key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment", 0,
+                             winreg.KEY_SET_VALUE)
+        for name in env_dict:
+            winreg.SetValueEx(key, name, 0, types[name], env_dict[name])
         try:
-            _x, types[name] = winreg.QueryValueEx(key, name)
-        except WindowsError:
-            types[name] = winreg.REG_EXPAND_SZ
-    key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Environment", 0,
-                         winreg.KEY_SET_VALUE)
-    for name in reg:
-        winreg.SetValueEx(key, name, 0, types[name], reg[name])
-    try:
-        from win32gui import SendMessageTimeout
-        from win32con import (HWND_BROADCAST, WM_SETTINGCHANGE,
-                              SMTO_ABORTIFHUNG)
-        SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0,
-                           "Environment", SMTO_ABORTIFHUNG, 5000)
-    except Exception:
-        QMessageBox.warning(
-            parent, _("Warning"),
-            _("Module <b>pywin32 was not found</b>.<br>"
-              "Please restart this Windows <i>session</i> "
-              "(not the computer) for changes to take effect.")
-        )
+            from win32gui import SendMessageTimeout
+            from win32con import (HWND_BROADCAST, WM_SETTINGCHANGE,
+                                  SMTO_ABORTIFHUNG)
+            SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0,
+                               "Environment", SMTO_ABORTIFHUNG, 5000)
+        except Exception:
+            QMessageBox.warning(
+                parent, _("Warning"),
+                _("Module <b>pywin32 was not found</b>.<br>"
+                  "Please restart this Windows <i>session</i> "
+                  "(not the computer) for changes to take effect.")
+            )
+    elif os.name == 'posix' and running_in_ci():
+        text = "\n".join([f"export {k}={v}" for k, v in env_dict.items()])
+        amend_user_shell_init(text)
+    else:
+        raise NotImplementedError("Not implemented for platform %s", os.name)
+
+
+def amend_user_shell_init(text="", restore=False):
+    """Set user environment variable for pytests on Unix platforms"""
+    if os.name == "nt":
+        return
+
+    HOME = Path(os.environ["HOME"])
+    SHELL = os.environ.get("SHELL", "/bin/bash")
+    if "bash" in SHELL:
+        init_files = [".bash_profile", ".bash_login", ".profile"]
+    elif "zsh" in SHELL:
+        init_files = [".zprofile", ".zshrc"]
+    else:
+        raise Exception(f"{SHELL} not supported.")
+
+    for file in init_files:
+        init_file = HOME / file
+        if init_file.exists():
+            break
+
+    script = init_file.read_text() if init_file.exists() else ""
+    m1 = "# <<<< Spyder Environment <<<<"
+    m2 = "# >>>> Spyder Environment >>>>"
+    if restore:
+        if init_file.exists() and (m1 in script and m2 in script):
+            new_text = ""
+        else:
+            return
+    else:
+        new_text = f"{m1}\n" + text + f"\n{m2}"
+
+    if m1 in script and m2 in script:
+        _script = re.sub(f"{m1}(.*){m2}", new_text, script, flags=re.DOTALL)
+    else:
+        _script = script.rstrip() + "\n\n" + new_text
+
+    init_file.write_text(_script.rstrip() + "\n")
 
 
 def clean_env(env_vars):


Test patch:
diff --git a/spyder/app/tests/conftest.py b/spyder/app/tests/conftest.py
--- a/spyder/app/tests/conftest.py
+++ b/spyder/app/tests/conftest.py
@@ -24,11 +24,13 @@
 
 # Spyder imports
 from spyder.app import start
-from spyder.config.base import get_home_dir
+from spyder.config.base import get_home_dir, running_in_ci
 from spyder.config.manager import CONF
 from spyder.plugins.ipythonconsole.utils.kernelspec import SpyderKernelSpec
 from spyder.plugins.projects.api import EmptyProject
 from spyder.utils import encoding
+from spyder.utils.environ import (get_user_env, set_user_env,
+                                  amend_user_shell_init)
 
 
 # =============================================================================
@@ -536,3 +538,20 @@ def threads_condition():
                     window = None
                     CONF.reset_to_defaults(notification=False)
                     raise
+
+
+@pytest.fixture
+def restore_user_env():
+    """Set user environment variables and restore upon test exit"""
+    if not running_in_ci():
+        pytest.skip("Skipped because not in CI.")
+
+    if os.name == "nt":
+        orig_env = get_user_env()
+
+    yield
+
+    if os.name == "nt":
+        set_user_env(orig_env)
+    else:
+        amend_user_shell_init(restore=True)
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -59,6 +59,7 @@
 from spyder.plugins.layout.layouts import DefaultLayouts
 from spyder.plugins.toolbar.api import ApplicationToolbars
 from spyder.py3compat import qbytearray_to_str, to_text_string
+from spyder.utils.environ import set_user_env
 from spyder.utils.misc import remove_backslashes
 from spyder.utils.clipboard_helper import CLIPBOARD_HELPER
 from spyder.widgets.dock import DockTitleBar
@@ -718,7 +719,7 @@ def test_dedicated_consoles(main_window, qtbot):
 
     # --- Set run options for this file ---
     rc = RunConfiguration().get()
-    
+
     # A dedicated console is used when these three options are False
     rc['default'] = rc['current'] = rc['systerm'] = False
     rc['clear_namespace'] = False
@@ -1401,7 +1402,7 @@ def test_run_code(main_window, qtbot, tmpdir):
     qtbot.waitUntil(lambda: nsb.editor.source_model.rowCount() == 1,
                     timeout=EVAL_TIMEOUT)
     assert shell.get_value('li') == [1, 2, 3]
-    
+
     # try running cell without file name
     shell.clear()
     # Clean namespace
@@ -5342,7 +5343,8 @@ def test_switch_to_plugin(main_window, qtbot):
 
 
 @flaky(max_runs=5)
-def test_PYTHONPATH_in_consoles(main_window, qtbot, tmp_path):
+def test_PYTHONPATH_in_consoles(main_window, qtbot, tmp_path,
+                                restore_user_env):
     """
     Test that PYTHONPATH is passed to IPython consoles under different
     scenarios.
@@ -5356,7 +5358,7 @@ def test_PYTHONPATH_in_consoles(main_window, qtbot, tmp_path):
     # Add a new directory to PYTHONPATH
     new_dir = tmp_path / 'new_dir'
     new_dir.mkdir()
-    os.environ['PYTHONPATH'] = str(new_dir)
+    set_user_env({"PYTHONPATH": str(new_dir)})
 
     # Open Pythonpath dialog to detect new_dir
     ppm = main_window.get_plugin(Plugins.PythonpathManager)
diff --git a/spyder/utils/tests/test_environ.py b/spyder/utils/tests/test_environ.py
--- a/spyder/utils/tests/test_environ.py
+++ b/spyder/utils/tests/test_environ.py
@@ -8,6 +8,9 @@
 Tests for environ.py
 """
 
+# Standard library imports
+import os
+
 # Test library imports
 import pytest
 
@@ -15,8 +18,10 @@
 from qtpy.QtCore import QTimer
 
 # Local imports
-from spyder.utils.environ import get_user_environment_variables, UserEnvDialog
+from spyder.utils.environ import (get_user_environment_variables,
+                                  UserEnvDialog, amend_user_shell_init)
 from spyder.utils.test import close_message_box
+from spyder.app.tests.conftest import restore_user_env
 
 
 @pytest.fixture
@@ -31,13 +36,23 @@ def environ_dialog(qtbot):
 
 def test_get_user_environment_variables():
     """Test get_user_environment_variables function"""
-
     # All platforms should have a path environment variable, but
     # Windows may have mixed case.
     keys = {k.lower() for k in get_user_environment_variables()}
     assert "path" in keys
 
 
+@pytest.mark.skipif(os.name == "nt", reason="Does not apply to Windows")
+def test_get_user_env_newline(restore_user_env):
+    # Test variable value with newline characters.
+    # Regression test for spyder-ide#20097
+    text = "myfunc() {  echo hello;\n echo world\n}\nexport -f myfunc"
+    amend_user_shell_init(text)
+    user_env = get_user_environment_variables()
+
+    assert user_env['BASH_FUNC_myfunc%%'] in text
+
+
 def test_environ(environ_dialog, qtbot):
     """Test the environment variables dialog."""
     environ_dialog.show()