37

The question says it all. I currently use Arch Linux and the zsh, but I'd like a solution that (at minimum) works both on VTs and in xterms and also (hopefully, preferably) will continue to work if I switch distros or shells.

I've heard wildly disparate answers to this question in different distros' docs. Ubuntu says "use .pam_environment". I think in Arch what they recommend depends on your shell. Currently I put everything in .profile and if a shell doesn't source that for some reason (e.g. bash if .bash_profile exists), I override that by manually sourcing it. But it seems like there must be a better way.

2
  • 2
    This has nothing to do with distro and everything to do with shell. Not sure there's a portable way to do it, though.
    – Joseph R.
    Aug 26, 2013 at 7:42
  • 1
    Hmm. Awesome.
    – strugee
    Aug 26, 2013 at 7:56

4 Answers 4

34

There is unfortunately no fully portable location to set environment variables. The two files that come closest are ~/.profile, which is the traditional location and works out of the box on many setups, and ~/.pam_environment, a modern, commonplace but limited alternative.

What to put in ~/.pam_environment

The file ~/.pam_environment is read by all login methods that use PAM and that have this file enabled. This covers most Linux systems nowadays.

The major advantage of ~/.pam_environment is that (when enabled) it is read before the user's shell starts, so it works regardless of the session type, login shell and other complexities. It even works for non-interactive logins such as su -c somecommand and ssh somecommand.

The major limitation of ~/.pam_environment is that you can only put simple assignments there, not complex shell syntax. The syntax of this file is as follows.

  • Files are parsed line by line.
  • Each line must have the form VAR=VALUE where VAR consists of letters, digits and underscores. The alternative form VAR DEFAULT=value allows expansions of environment variables using ${VAR} syntax and the special variables @{HOME} and @{SHELL}.
  • # starts a comment, it cannot appear in a value.
  • If VALUE is surrounded by ", then VAR is set to the string between the quotes.
  • \$ or \@ insert a literal $ or @ and long lines can be split by escaping the newline with a \.
  • If there is a syntax error such as no = or unquoted whitespace, the variable is removed from the environment.

So on the upside, ~/.pam_environment works in a large array of circumstances. On the downside, you cannot use the output of a command (e.g. test if a directory or program is present), and some characters (#", newline) are impossible or troublesome to put in the value.

What to put in ~/.profile

This file should have portable (POSIX) sh syntax. Only use ksh or bash extensions (arrays, [[ … ]], etc.) if you know that your system has these shells as /bin/sh.

This file may be read by scripts in automated applications, so it should not call programs that produce any output or call exec. If you want to do that on text-mode logins, do it only for interactive shells. Example:

case $- in *i*)
  # Display a message if I have new mail
  if mail -e; then echo 'You have new mail'; fi
  # If zsh is available, and this looks like a text-mode login, run zsh
  case "`ps $PPID` " in
    *" login "*)
      if type zsh >/dev/null 2>/dev/null; then exec zsh; fi;;
  esac
esac

This is an example of using /bin/sh as your login shell and switching to your favorite shell. See also how can I use bash as my login shell when my sysadmin refuses to let me change it

When is ~/.profile not read on non-graphical login?

Different login shells read different files.

If your login shell is bash

Bash reads ~/.bash_login or ~/.bash_profile if they exist instead of ~/.profile. Also bash does not read ~/.bashrc in a login shell even if it is interactive. To never have to remember these quirks again, create a ~/.bash_profile with the following two lines:

. ~/.profile
case $- in *i*) . ~/.bashrc;; esac

See also Which setup files should be used for setting up environment variables with bash?

If your login shell is zsh

Zsh reads ~/.zprofile and ~/.zlogin, but not ~/.profile. Zsh has a different syntax from sh, but can read ~/.profile in sh emulation mode. You can use this for your ~/.zprofile:

emulate sh -c '. ~/.profile'

See also Zsh not hitting ~/.profile

If your login shell is some other shell

There's not much you can do there, short of using /bin/sh as your login shell and your favorite shell (such as fish) as an interactive shell only. That's what I do with zsh. See above for an example of invoking another shell from ~/.profile.

Remote commands

When invoking a remote command without going through an interactive shell, not all shells read a startup file.

Ksh reads the file specified by the ENV variable, if you manage to pass it.

Bash reads ~/.bashrc if it is not interactive (!) and its parent process is called rshd or sshd. So you can start your ~/.bashrc with

if [[ $- != *i* ]]; then
  . ~/.profile
  return
fi

Zsh always reads ~/.zshenv when it starts. Use with caution, since this is read by every single instance of zsh, even when it is a subshell where you've set other variables. If zsh is your login shell and you want to use it to set variables only for remote commands, use a guard: set some variable in ~/.profile, such as MY_ENVIRONMENT_HAS_BEEN_SET=yes, and check this guard before reading ~/.profile.

if [[ -z $MY_ENVIRONMENT_HAS_BEEN_SET ]]; then emulate sh -c '~/.profile'; fi

The case of graphical logins

Many distributions, display managers and desktop environments arrange to run ~/.profile, either by explicitly sourcing it from the startup scripts or by running a login shell.

Unfortunately, there is no general method to handle distro/DM/DE combinations where ~/.profile is not read.

If you use a traditional session started by ~/.xsession, this is the place where you should set your environment variables; do it by sourcing ~/.profile (i.e. . ~/.profile). Note that in some setups, the desktop environment startup scripts will source ~/.profile again.

8
  • what does case $- in *i*) do?
    – qodeninja
    Feb 5, 2014 at 0:04
  • 2
    @qodeninja Executes the following instructions (until the matching ;; or esac) if $- matches the pattern *i*, i.e. if $- contains i, i.e. if the shell is interactive. Feb 5, 2014 at 0:06
  • $- is a string of the currently-set shell options. (like set -x). i means interactive shell. Apr 19, 2015 at 15:12
  • Can't you just source a common file, say ~/.config/env, even without emulation? Dec 29, 2015 at 21:34
  • 1
    @StéphaneChazelas That's a purist view. I keep my .profile compliant with fairly old Bourne shells, but I recognize that some people just don't care. I have nothing against people assuming that sh = bash for their own files, I only care if they publish #!/bin/sh script that use bash features. Aug 3, 2017 at 16:53
4

As far as I know there exists no distro and shell agnostic standard how to set environment variables.

The most common and de facto standard seems to be /etc/profile and ~/.profile. The second most common seems to be /etc/environment and ~/.pam_environment.

It seems to me that all documentation that I found you also already found. I list them here anyway for the other readers.

  • Debian recommends /etc/profile and ~/.profile (link).
  • Ubuntu recommends /etc/environment and ~/.pam_environment (link).
  • Arch Linux mentions, among others, /etc/profile and /etc/environment (link).

Bonus: a text questioning the use and/or misuse of /etc/environment in debian (link, last update 2008).

3
  • Regardless of the file you use you would still bump into incompatible syntax between different shells.
    – Joseph R.
    Aug 26, 2013 at 13:25
  • 1
    @JosephR. don't most shells keep backwards-compatibility with sh? So long as you stick to POSIX, I would have thought you'd be fine...
    – evilsoup
    Aug 26, 2013 at 15:56
  • 1
    AFAIK you can't assign variables in csh and friends the POSIX way (you need something like set or setenv)
    – Joseph R.
    Aug 26, 2013 at 17:22
1

If you use systemd-homed, you should be able to use homectl to set environment variables for your user account regardless of your shell. The following command sets the EDITOR environment variable this way.

homectl update username --setenv=EDITOR=/usr/bin/vim
0

I added the following script ~/bin/agnostic_setenv:

#!/bin/csh -f
set args = ($*)
if ($#args == 1) then
   echo "export $args[1]="
   exit 0
endif

if ($#args == 2) then
   if ("$args[1]" =~ *csh*) then 
      echo "setenv $args[2]"
      exit 0
   else
      echo "export $args[1]=$args[2]"
      exit 0
   endif
endif

echo "setenv $args[2] $args[3]"

And in ~/.perl-homedir I use:

eval `${HOME}/bin/agnostic_setenv $shell PERL_HOMEDIR 0`

A Similiar script for agnostic_unsetenv:

#!/bin/csh -f
set args = ($*)
if ($#args == 1) then
   echo "export $args[1]"
   exit 0
endif

echo "unsetenv $args[2]"
exit 0

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .