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 pi@raspberrypi.local -
Using SSH to run a command remotely. For example, to list the content of your home directory:
$ ssh pi@raspberrypi.local 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
cronto 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/profileand/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
~/.bashrcpreamble; - otherwise put it in the
~/.bashrcpostamble.
Out of the box, PiBuilder:
-
Does not change your
~/.profileat 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_PROFILESto 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_ACTIONDOT_BASHRC_ACTION
Each variable can have the following values:
-
append. This is the default behaviour. Using~/.bashrcas the example, the 05 script will:$ cat /boot/scripts/support/home/pi/.bashrc >>$HOME/.bashrcIn the event that the Raspberry Pi Foundation changes the default versions of
~/.bashrcand~/.profile, appending means you inherit those changes on a clean build. -
replace. Using~/.bashrcas the example, the 05 script will:$ mv $HOME/.bashrc $HOME/.bashrc.bak $ cp /boot/scripts/support/home/pi/.bashrc $HOME/.bashrcUnlike
append, this approach will never inherit any changes to the the default versions of~/.bashrcand~/.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
appendorreplacewill bypass any~/.profileand/or~/.bashrcaction even if the relevant files are present in/boot/scripts/support/home/piwhen 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
~/.bashrcbefore the short-stop exit -
at the end of
~/.bashrc -
at the end of
~/.profilewhere 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
-Loperator tests for symbolic links andrealpathchases 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
-ntest 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
echoreturns 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 pi@raspberrypi.local 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
~/.bashrcassumes 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
~/.bashrcwrites to stdout or stderr,cronwill 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.
