Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: January 23, 2024
Shells are one of the main and most fundamental ways to control Linux. After an interactive login through a given terminal, users are presented with their preconfigured shell so they can further interact with and command the system.
In this tutorial, we’ll talk about shells and come up with a method to get the number of current running shell processes in a stable and reliable way, executing a command in each one. First, we summarize our knowledge of shells and how to check the available ones in a given Linux system. Next, we discuss how commands run in the shell. After that, we go over the login process hierarchy. Then, we explore shell spawning in general. Finally, we count the number of shells in a system and create a script to run a command in each one.
We tested the code on Debian 12 (Bookworm) with GNU Bash 5.2.15. It should work in most POSIX-compliant environments unless otherwise specified.
We already know that a shell is a process that receives commands, interprets each, and then makes the necessary calls so the commands get executed by the operating system (OS). Shells can also handle all input and output during this process.
Many shells exist, but only some are considered major:
However, we can usually get a full list of shells available to a given system from the /etc/shells file:
$ cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/bin/bash
/usr/bin/bash
/bin/rbash
/usr/bin/rbash
/bin/dash
/usr/bin/dash
/usr/bin/tmux
/bin/zsh
/usr/bin/zsh
/usr/bin/screen
/bin/ksh93
/usr/bin/ksh93
/bin/rksh93
/usr/bin/rksh93
/bin/tcsh
/usr/bin/tcsh
Here, we see different paths and alternative names for the shells we discussed, as well as additional shells, e.g., provided by the terminal multiplexers tmux and screen.
To get only the list of shell paths, we use grep to filter out any lines that don’t start with a / forward slash, i.e., an absolute path:
$ cat /etc/shells | grep '^/'
/bin/sh
/bin/bash
/usr/bin/bash
[...]
/usr/bin/tmux
[...]
Further, we can exclude terminal multiplexers since they also need an actual shell within them:
$ cat /etc/shells | grep '^/' | grep -v 'tmux\|screen'
/bin/sh
/bin/bash
/usr/bin/bash
[...]
Finally, if needed, there is a way to only get the unique list of shell executable filenames via basename and sort:
for shell in $(cat /etc/shells | grep '^/' | grep -v 'tmux\|screen'); do basename $shell; done | sort -u
bash
dash
ksh93
rbash
rksh93
sh
tcsh
zsh
Naturally, we can identify processes as shells by several criteria:
While we won’t delve into the last part because hashes change with versions, hash verification is one of the most robust ways to ensure a given file is the one we expect. Importantly, verifying the hash also prevents the use of infected and maliciously modified files, but, as usual, may cause inconvenience after automatic updates.
Importantly, commands can be input in shells in two main ways:
Of course, the first way requires user input as the keyboard is usually stdin. Similarly, the user is often the intended output recipient since stdout is the terminal by default.
To use non-interactive command input, we can employ a shell-specific switch (commonly -c) and supply commands when starting the shell:
$ sh -c '<COMMANDS>'
Here, we spawn the sh shell and supply some COMMANDS to it in the form of a string argument.
Alternatively, we can use a more advanced method such as GNU Debugger (GDB) to attach to and inject a command directly within the shell process input stream:
$ gdb -p <SHELL_PID> -batch-silent -ex 'p system("<COMMANDS>")' 2>/dev/null
In this case, since we don’t run the command within the target shell and don’t spawn a new one, we need to supply the SHELL_PID shell process ID (PID) in addition to the COMMANDS.
To begin with, let’s explore the terminal process hierarchy that forms after a basic interactive login:
xost login: baeldung
Password:
[...]
$ tty
/dev/pts/0
$ ps -H -t /dev/pts/0
PID TTY TIME CMD
666 pts/0 00:00:01 bash
667 pts/0 00:00:00 ps
$ echo $SHELL
/bin/bash
$ echo $$
666
In this case, we log in as the baeldung user. After logging in, we perform several operations:
At this point, we understand that a single shell runs right after the login process. Thus, we can expect at least one session in progress for every logged-in terminal.
Of course, we can spawn a shell at any time by either logging in at another terminal or simply invoking the shell process. Naturally, when using the command line interface (CLI), we do the latter within a currently running shell:
$ /bin/bash
$ ps -H -t /dev/pts/0
PID TTY TIME CMD
666 pts/0 00:00:01 bash
668 pts/0 00:00:00 bash
[...]
After running another Bash process, we see it in the hierarchy. Now, we have two running shells within the same terminal. Although we directly interact with only one of them, both are active.
If we exit the new current shell, we get back to the other one:
$ exit
$ ps -H -t /dev/pts/0
PID TTY TIME CMD
666 pts/0 00:00:01 bash
670 pts/0 00:00:00 ps
So, the shell count can’t be accurately deduced from the number of users or terminals.
After getting to know shells and how they spawn, let’s create a simple script to find the current shell processes:
$ cat shellspid.sh
#!/usr/bin/env bash
# get current shells
shells=$(cat /etc/shells | grep '^/' | grep -v 'screen\|tmux')
# shell processes array
shellpids=()
# for each shell, add PID to shell processes
for shell in $shells; do
shellpids+=($(pidof $shell))
done
# exclude script shell PID and leave only unique values
printf '%s\n' ${shellpids[@]} | grep -v $$ | sort -u
Here, we use /etc/shells to get the available shells in the system, excluding terminal multiplexers. Consequently, we store the filtered list of available shells in the shells variable via command substitution. After that, we define the shellpids array and populate it in a for loop over shells, where we use pidof to convert each shell path to its respective PID.
Finally, we only output the unique values from shellpids and exclude the current shell PID with grep.
Let’s make the script executable via chmod:
$ chmod +x shellspid.sh
Now, we can see two sample runs:
$ ./shellspid.sh
666
$ bash
$ ./shellspid.sh
666
668
The first run shows PID 666 as the only shell process, while the second also adds PID 668 for the new shell we spawned within the first.
Naturally, we can use wc to count the total number of shell processes, i.e., –lines:
$ ./shellspid.sh | wc --lines
2
Since the main part of our script is ready, we can add one more function to it.
Now, let’s copy and expand our script to run a command within each of the shells:
$ cat shellsexec.sh
#!/usr/bin/env bash
# get current shells
shells=$(cat /etc/shells | grep '^/' | grep -v 'screen\|tmux')
# shell processes array
shellpids=()
# for each shell, add PID to shell processes
for shell in $shells; do
shellpids+=($(pidof $shell))
done
# exclude script shell PID and leave only unique values
shellpids=($(printf '%s\n' ${shellpids[@]} | grep -v $$ | sort -u))
# run the supplied parameters as a command for each shell
for shellpid in "${shellpids[@]}"; do
gdb -p $shellpid -batch-silent -ex 'p system("'"$*"'")' 2>/dev/null
done
Next, we make the shellsexec.sh executable:
$ chmod +x shellsexec.sh
At this point, using the shellsexec.sh script, we can execute any command line we pass to it within all currently running shells:
$ ./shellsexec echo Test.
Test.
This can be helpful in different scenarios, such as running a screensaver after a timeout.
In this article, we talked about shells, how to see which ones are available, count the running ones, and execute commands within each.
In conclusion, the shell is the primary command execution environment for Linux systems and, as such, is a very powerful tool, control over which is critical.