Why does bashrc check whether the current shell is interactive?

28,985

Solution 1

This is a question that I was going to post here a few weeks ago. Like terdon, I understood that a .bashrc is only sourced for interactive Bash shells so there should be no need for .bashrc to check if it is running in an interactive shell. Confusingly, all the distributions I use (Ubuntu, RHEL and Cygwin) had some type of check (testing $- or $PS1) to ensure the current shell is interactive. I don’t like cargo cult programming so I set about understanding the purpose of this code in my .bashrc.

Bash has a special case for remote shells

After researching the issue, I discovered that remote shells are treated differently. While non-interactive Bash shells don’t normally run ~/.bashrc commands at start-up, a special case is made when the shell is Invoked by remote shell daemon:

Bash attempts to determine when it is being run with its standard input connected to a network connection, as when executed by the remote shell daemon, usually rshd, or the secure shell daemon sshd. If Bash determines it is being run in this fashion, it reads and executes commands from ~/.bashrc, if that file exists and is readable. It will not do this if invoked as sh. The --norc option may be used to inhibit this behavior, and the --rcfile option may be used to force another file to be read, but neither rshd nor sshd generally invoke the shell with those options or allow them to be specified.

Example

Insert the following at the start of a remote .bashrc. (If .bashrc is sourced by .profile or .bash_profile, temporarily disable this while testing):

echo bashrc
fun()
{
    echo functions work
}

Run the following commands locally:

$ ssh remote_host 'echo $- $0'
bashrc
hBc bash
  • No i in $- indicates that the shell is non-interactive.
  • No leading - in $0 indicates that the shell is not a login shell.

Shell functions defined in the remote .bashrc can also be run:

$ ssh remote_host fun
bashrc
functions work

I noticed that the ~/.bashrc is only sourced when a command is specified as the argument for ssh. This makes sense: when ssh is used to start a regular login shell, .profile or .bash_profile are run (and .bashrc is only sourced if explicitly done so by one of these files).

The main benefit I can see to having .bashrc sourced when running a (non-interactive) remote command is that shell functions can be run. However, most of the commands in a typical .bashrc are only relevant in an interactive shell, e.g., aliases aren’t expanded unless the shell is interactive.

Remote file transfers can fail

This isn’t usually a problem when rsh or ssh are used to start an interactive login shell or when non-interactive shells are used to run commands. However, it can be a problem for programs such as rcp, scp and sftp that use remote shells for transferring data.

It turns out that the remote user’s default shell (like Bash) is implicitly started when using the scp command. There’s no mention of this in the man page – only a mention that scp uses ssh for its data transfer. This has the consequence that if the .bashrc contains any commands that print to standard output, file transfers will fail, e.g, scp fails without error.

See also this related Red Hat bug report from 15 years ago, scp breaks when there's an echo command in /etc/bashrc (which was eventually closed as WONTFIX).

Why scp and sftp fail

SCP (Secure copy) and SFTP (Secure File Transfer Protocol) have their own protocols for the local and remote ends to exchange information about the file(s) being transferred. Any unexpected text from the remote end is (wrongly) interpreted as part of the protocol and the transfer fails. According to a FAQ from the Snail Book

What often happens, though, is that there are statements in either the system or per-user shell startup files on the server (.bashrc, .profile, /etc/csh.cshrc, .login, etc.) which output text messages on login, intended to be read by humans (like fortune, echo "Hi there!", etc.).

Such code should only produce output on interactive logins, when there is a tty attached to standard input. If it does not make this test, it will insert these text messages where they don't belong: in this case, polluting the protocol stream between scp2/sftp and sftp-server.

The reason the shell startup files are relevant at all, is that sshd employs the user's shell when starting any programs on the user's behalf (using e.g. /bin/sh -c "command"). This is a Unix tradition, and has advantages:

  • The user's usual setup (command aliases, environment variables, umask, etc.) are in effect when remote commands are run.
  • The common practice of setting an account's shell to /bin/false to disable it will prevent the owner from running any commands, should authentication still accidentally succeed for some reason.

SCP protocol details

For those interested in the details of how SCP works, I found interesting information in How the SCP protocol works which includes details on Running scp with talkative shell profiles on the remote side?:

For example, this can happen if you add this to your shell profile on the remote system:

echo ""

Why it just hangs? That comes from the way how scp in source mode waits for the confirmation of the first protocol message. If it's not binary 0, it expects that it's a notification of a remote problem and waits for more characters to form an error message until the new line arrives. Since you didn't print another new line after the first one, your local scp just stays in a loop, blocked on read(2). In the meantime, after the shell profile was processed on the remote side, scp in sink mode was started, which also blocks on read(2), waiting for a binary zero denoting the start of the data transfer.

Conclusion / TLDR

Most of the statements in a typical .bashrc are only useful for an interactive shell – not when running remote commands with rsh or ssh. In most such situations, setting shell variables, aliases and defining functions isn’t desired – and printing any text to standard out is actively harmful if transferring files using programs such as scp or sftp. Exiting after verifying that the current shell is non-interactive is the safest behaviour for .bashrc.

Solution 2

The man page neglects to mention that bash also sources .bashrc for non-interactive remote shells, as in

ssh hostname command

http://git.savannah.gnu.org/cgit/bash.git/tree/shell.c#n1010

 COMMAND        EXECUTE BASHRC
 --------------------------------
 bash -c foo        NO
 bash foo           NO
 foo                NO
 rsh machine ls     YES (for rsh, which calls 'bash -c')
 rsh machine foo    YES (for shell started by rsh) NO (for foo!)
 echo ls | bash     NO
 login              NO
 bash               YES

http://git.savannah.gnu.org/cgit/bash.git/tree/shell.c#n1050

          /* If we were run by sshd or we think we were run by rshd, execute
             ~/.bashrc if we are a top-level shell. */
          if ((run_by_ssh || isnetconn (fileno (stdin))) && shell_level < 2)
            {
              maybe_execute_file (SYS_BASHRC, 1);
              maybe_execute_file (bashrc_file, 1);

Solution 3

By convention, .bashrc is place where user store the customize configuration for the shell.

These customize configuration can be environment variables, aliases, fancy prompt. With a non-interactive shell, those short of things are meaningless. Moreover, a non-interactive shell can be call in many contexts, you're not sure those environment variables can lead to false negative case, or even security vulnerable.

A closest example is an alias like:

alias cp='cp -i'

Then it hang your non-interactive shell forever.

So the check perform at the top of .bashrc to ensure that we won't get trouble.


Because the shell can be called as non-interactive login shell, so explicitly block sourcing *bashrc make no sense.

When the shell was called as non-interactive login shell, it source /etc/profile, then source the first one found in ~/.bash_profile, ~/.bash_login, and ~/.profile:

When bash is invoked as an interactive login shell, or as a non-interactive shell with the --login option, it first reads and executes commands from the file /etc/profile, if that file exists. After reading that file, it looks for ~/.bash_profile, ~/.bash_login, and ~/.profile, in that order, and reads and executes commands from the first one that exists and is readable.

Nothing prevent those files from sourcing .bashrc itself, so doing the check inside .bashrc is safer and make things simple.

Share:
28,985

Related videos on Youtube

terdon
Author by

terdon

Elected moderator on Unix &amp; Linux. I've been using Linux since the late '90s and have gone through a variety of distributions. At one time or another, I've been a user of Mandrake, SuSe, openSuSe, Fedora, RedHat, Ubuntu, Mint, Linux Mint Debian Edition (basically Debian testing but more green) and, for the past few years, Arch. My Linux expertise, such as it is, is mostly on manipulating text and regular expressions since that represents a large chunk of my daily work.

Updated on September 18, 2022

Comments

  • terdon
    terdon almost 2 years

    On my Arch install, /etc/bash.bashrc and /etc/skel/.bashrc contain these lines:

    # If not running interactively, don't do anything
    [[ $- != *i* ]] && return
    

    On Debian, /etc/bash.bashrc has:

    # If not running interactively, don't do anything
    [ -z "$PS1" ] && return
    

    And /etc/skel/.bashrc:

    # If not running interactively, don't do anything
    case $- in
        *i*) ;;
          *) return;;
    esac
    

    According to man bash, however, non-interactive shells don't even read these files:

    When bash is started non-interactively, to run a shell script, for example, it looks for the variable BASH_ENV in the environment, expands its value if it appears there, and uses the expanded value as the name of a file to read and execute. Bash behaves as if the following commands were executed: if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi but the value of the PATH variable is not used to search for the filename.

    If I understand correctly, the *.bashrc files will only be read if BASH_ENV is set to point to them. This is something that can't happen by chance and will only occur if someone has explicitly set the variable accordingly.

    That seems to break the possibility of having scripts source a user's .bashrc automatically by setting BASH_ENV, something that could come in handy. Given that bash will never read these files when run non-interactively unless explicitly told to do so, why do the default *bashrc files disallow it?

  • Angel Todorov
    Angel Todorov over 8 years
    Do you source your .bashrc from your .bash_profile (or .profile)?
  • Mikel
    Mikel over 8 years
    Yes, but that's not the point. An empty .bash_profile would exhibit the same behavior.
  • terdon
    terdon over 8 years
    Not sure what that's showing me. Are you saying that rsh (which I've never used) sources ~/.bashrc? And that, by extension, Linux distros block it for bash just in case anyone ever tries to run a script with rsh? ssh server shouldn't touch .bashrc since that's a login shell anyway. Not unless your system is one of those that sources ~/.bashrc` from ~/.profile. And ssh server command shouldn't even do that. Certainly doesn't on my system.
  • Mikel
    Mikel over 8 years
    No. I linked you to the bash source, not the rsh source. :) The comment applies to ssh as well, it was just written a very long time ago. See updated answer with more source.
  • terdon
    terdon over 8 years
    Ah, that's helpful, thank you. How would I test this though? I tried adding an echo at the very top of /etc/bash.bashrc and ~/.bashrc (before the interactive shell tests) and then ssh into the target machine with ssh server hostname and while hostname was run, I didn't see either of my echoes. Is it that my shell_level (whatever that is) is not <2?
  • terdon
    terdon over 8 years
    Yes, I know that. This is why non-interactive shells don't source *bashrc files at all. My question is why do various Linux vendors go to the trouble of explicitly blocking non-interactive shells from reading these files if these files aren't read by non-interactive shells anyway.
  • cuonglm
    cuonglm over 8 years
    @terdon: Because a shell can be called as non-interactive login shell, then a login shell will source .bash_profile, which can be source .bashrc.
  • terdon
    terdon over 8 years
    No, in non-interactive shells, the only thing that's read is $BASH_ENV. Both *profile and *bashrc are ignored. Or, at least, that's what the man page says. However, as Mikel has shown the man page might be lying.
  • terdon
    terdon over 8 years
    Wow. You're absolutely right. I've read that part about --login 100 times but, apparently, it had never registered. Thanks!
  • Joshua
    Joshua over 8 years
    Also non-interactive login shell is easy to get with ssh.
  • terdon
    terdon over 8 years
    @Joshua but that doesn't seem to read bashrc. That's precisely what had confused me.
  • dbncourt
    dbncourt over 6 years
    nice article. but I should do with that? i do have check in by .bashrc but still scp exit with 0 code and do not copy anything to remote box
  • Anthony Geoghegan
    Anthony Geoghegan over 6 years
    @ses It'd be best to ask a new question where you can describe your situation in greater detail.