There are many ways to execute commands on your Raspberry Pi. The most common methods are:
-
Opening a window in the Terminal application from the desktop (console or via VNC).
-
Using SSH to connect to the Raspberry Pi from another host:
$ ssh [email protected]
-
Using SSH to run a command remotely. For example, to list the content of your home directory:
$ ssh [email protected] ls -l
-
Using SCP (secure copy) to copy files between hosts. SCP does not "execute commands" in the same way as SSH but, for the purposes of this tutorial, the way its environment is set up is similar to using SSH to run a command remotely.
-
Using
cron
to trigger a command at a predetermined time.
With the exception of cron
, each method triggers setup scripts behind the scenes. This all happens before your commands are run. Collectively, these scripts establish your environment:
/etc/profile
/etc/bash.bashrc
~/.profile
~/.bashrc
It's your Raspberry Pi so you can edit the files in /etc
if you wish but best practice is to leave those alone and only modify the ones in your home directory.
The table below summarises when the two scripts in your home directory are run:
The same pattern applies to
/etc/profile
and/etc/bash.bashrc
In the ssh «hostname»
case, the default version of ~/.profile
contains these lines:
# if running bash
if [ -n "$BASH_VERSION" ]; then
# include .bashrc if it exists
if [ -f "$HOME/.bashrc" ]; then
. "$HOME/.bashrc"
fi
fi
If you remove those lines from ~/.profile
then ~/.bashrc
will not run when you SSH to the host. It is unwise to do this because most of your PATH is set up by ~/.bashrc
. Many commands will not execute as you expect if your PATH is not set up in the usual way.
The "short-stop" behaviour of these cases depends on the following lines of code in the default version of ~/.bashrc
:
… preamble …
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return ;;
esac
… postamble …
The return
statement is executed for any non-interactive shell.
The overall effect of the case
statement is to divide ~/.bashrc
into two parts:
- the preamble which is always executed; and
- the postamble which is only executed for interactive shells.
If you ever wondered why you need to use full pathnames to run commands in your ~/.local/bin
when using ssh host command
syntax, it is due to the combined effect of ~/.profile
not running and ~/.bashrc
stopping before adding ~/.local/bin
to your PATH.
The preamble section of the default version of ~/.bashrc
does not contain any commands but that doesn't mean you can't put commands into the preamble if you wish. One very good candidate for the preamble is adding ~/.local/bin
to your PATH.
Each new user account is initialised by copying these files:
/etc/skel/.bashrc
/etc/skel/.profile
If you ever make a mess of your own ~/.profile
or ~/.bashrc
, you can always obtain pristine copies from /etc/skel
.
As you can see from the table above, ~/.bashrc
has better coverage than ~/.profile
so that makes ~/.bashrc
far more useful.
It then boils down to when you want a given command to be run:
- if you always want a command to run then put it in the
~/.bashrc
preamble; - otherwise put it in the
~/.bashrc
postamble.
Out of the box, PiBuilder:
-
Does not change your
~/.profile
at all; but -
Appends to your
~/.bashrc
. The changes it makes include:- Sourcing IOTstackAliases if that package is installed (which PiBuilder does by default).
- Enabling
DOCKER_BUILDKIT=1
- see enable buildkit builds. - Setting
COMPOSE_PROFILES
to your host name - see compose profiles.
Remembering that ~/PiBuilder
means "the path to the directory where you have cloned the PiBuilder repository from GitHub onto your support host", if you simply create a file at:
~/PiBuilder/boot/scripts/support/home/pi/.profile
then the contents of that file will be appended to the default ~/.profile
when the 05 script runs.
PiBuilder supports two environment variables:
DOT_PROFILE_ACTION
DOT_BASHRC_ACTION
Each variable can have the following values:
-
append
. This is the default behaviour. Using~/.bashrc
as the example, the 05 script will:$ cat /boot/scripts/support/home/pi/.bashrc >>$HOME/.bashrc
In the event that the Raspberry Pi Foundation changes the default versions of
~/.bashrc
and~/.profile
, appending means you inherit those changes on a clean build. -
replace
. Using~/.bashrc
as the example, the 05 script will:$ mv $HOME/.bashrc $HOME/.bashrc.bak $ cp /boot/scripts/support/home/pi/.bashrc $HOME/.bashrc
Unlike
append
, this approach will never inherit any changes to the the default versions of~/.bashrc
and~/.profile
.I don't want to overstate this problem. Changes to the defaults are rare. You won't need to keep a constant watch on
/etc/skel
. -
any value other than
append
orreplace
will bypass any~/.profile
and/or~/.bashrc
action even if the relevant files are present in/boot/scripts/support/home/pi
when the 05 script runs.
The PiBuilder tutorials folder contains two reference implementations which you are welcome to adopt, adapt to your own circumstances, and then have PiBuilder implement them automatically on each build by setting the action variables to replace
.
~/PiBuilder/tutorials/reference/reference.bashrc
~/PiBuilder/tutorials/reference/reference.profile
Between them, these two scripts mean your shell environment will always be as similar as is possible, no matter how you connect to your Raspberry Pi.
Both ~/.profile
and ~/.bashrc
(and their corresponding versions in /etc
) change your PATH variable several times. Your PATH usually winds up with duplicates for /usr/bin
and /usr/sbin
, and it is easy to find yourself having added ~/.local/bin
more than once.
Over time, you may also find your PATH contains references to things that are no longer installed.
Rather than expending effort in trying to eliminate duplicates and non-existent components by hand, it is better to accept that PATH construction is messy, and automate the cleanup.
The following function can be a useful addition to your ~/.bashrc
. It should be defined in the preamble section (it is included in the reference.bashrc
):
tidyPATH() {
local REPATH CKPATH P PE
for P in ${PATH//:/ }; do
PE=$(eval echo "$P")
[ -L "$PE" ] && PE=$(realpath "$PE")
if [[ ! "$CKPATH" =~ ":$PE:" && -d "$PE" ]] ; then
[ -n "$REPATH" ] && REPATH="$REPATH:$PE" || REPATH="$PE"
CKPATH="$CKPATH:$PE:"
fi
done
echo "$REPATH"
}
This bash
function works on both Raspberry Pi OS and macOS. Usage is:
export PATH=$(tidyPATH)
It can be called:
-
in
~/.bashrc
before the short-stop exit -
at the end of
~/.bashrc
-
at the end of
~/.profile
where it should be gated like this:[ "$(type -t tidyPATH)" = "function" ] && export PATH=$(tidyPATH)
How it works:
- The
${PATH//:/ }
construct starts with the existing PATH variable and replaces the colon separators with spaces. This makes it for loop-friendly and capable of iteration to decompose it back to its constituent parts. $(eval echo "$P")
expands any embedded shortcuts like~
.- The
-L
operator tests for symbolic links andrealpath
chases symlink chains to arrive at the actual final library folder, if it exists. - The line with the
=~
operator checks for duplicates and ensures the candidate both exists and is a directory. - The
-n
test either initialises the new path with the first candidate to emerge or appends subsequent candidates to the new path, separated by colons, maintaining the original order. - The final
echo
returns the updated path.
This is a special case. No profile scripts are run when cron
spawns a job. You can set environment variables in the header section of your crontab
but variable expansion (eg expecting $HOME
to expand to /home/pi
) does not work and neither does sourcing scripts from the header section.
At a pinch, you can set up cron
jobs like this:
*/5 * * * * . /home/pi/.bashrc ; run-some-command.sh
That will create conditions for run-some-command.sh
that are similar to invoking it via SSH:
$ ssh [email protected] run-some-command.sh
In other words, run-some-command.sh
will inherit whatever is set up by ~/.bashrc
before it short-stops. There are a few provisos:
- If code in your
~/.bashrc
assumes certain variables will be set, you have to meet that condition in the header section of yourcrontab
. The most common example is setting HOME correctly. - If code in your
~/.bashrc
writes to stdout or stderr,cron
will send you an email about it for each invocation. In other words, you may also have to think about redirection to a log file.
Having said all that, if your sole objective is to add ~/.local/bin
to your PATH, you can do that in the crontab
preamble. Or you can simply use the absolute path, as in:
*/5 * * * * ./home/pi/.local/bin/run-some-command.sh
A good example of the use of profiles is a "live" vs "test" scenario. Suppose you have two Raspberry Pis, both running IOTstack:
- The "live" machine is named "iot-hub".
- The "test" machine is named "iot-test"
Assume ~/.bashrc
contains this line:
export COMPOSE_PROFILES=$(hostname -s)
Consider the following service definitions:
services:
red:
container_name: red
image: red
ports:
- "9001:80"
restart: unless-stopped
green:
container_name: green
image: green
profiles:
- iot-hub
ports:
- "9002:80"
restart: unless-stopped
blue:
container_name: blue
image: blue
profiles:
- iot-test
ports:
- "9003:80"
restart: unless-stopped
magenta:
container_name: magenta
image: blue
profiles:
- manual
ports:
- "9004:80"
restart: unless-stopped
"manual" is not a reserved word, it is simply a string that is not one of the host names.
Running docker-compose up -d
will have the effect:
- On the "live" machine, the red and green services will start but blue and magenta will not.
- On the "test" machine, the red and blue services will start but green and magenta will not.
On the "live" machine, the blue and magenta services can be forced to start by naming them explicitly:
$ docker-compose up -d blue magenta
Naming a container explicitly overrides the profile. You can use the same technique to force green and magenta to start on the "test" machine.
Using "profiles" makes it easy to have a single docker-compose.yml
which is used on multiple machines. Adjusting profile names is also a good deal simpler than adding, removing or commenting-in/out whole service definitions as you test changes.
See the Docker documentation for another example of using Compose Profiles.