*Kind of* Making my own shell
In this article, we'll explore how I created a simple, custom shell specifically for Linux systems using Python. We'll cover what a shell is, why building one can be rewarding, and step-by-step instructions on handling user input, executing commands, and adding custom commands. Note that this project was made as a fun side endeavor, so the code may be somewhat unpolished but effective.
Introduction
In this article, we'll go through how I (kind of) made my own shell, I'll get you to know how it works, and why it's not actually an entirely new shell, note that this shell will be specifically for Linux systems.
NOTE: This shell is pretty bad and unprofessional, I only made it as a side project that i thought would be fun, the code is pretty sloppy but the results are kinda good.
What is a shell?
A shell is a user interface that provides access to various services of an operating system. It can be either command-line based (CLI) or graphical (GUI). In the context of a command-line interface, a shell interprets and executes user commands, allowing users to interact with the operating system by typing commands. The shell processes these commands, executes them, and returns the output to the user. Common examples of shells include Bash (Bourne Again Shell), Zsh (Z Shell), and the Windows Command Prompt.
Why build a shell?
Building your own shell or anything for that matter is kinda fun, using something you created yourself is so rewarding, and aside from that, you can add whatever features you want to it, without waiting for the dev of your favorite program to finally read your github issue and add a feature or fix a bug
Prerequisites
Python V3
A Linux installation
A text editor like Visual Studio Code
User Input
Getting the input:
Using the input()
function in python, I can easily get the user's input.
def main() -> None:
while True:
cmd = input(">")
if __name__ == "__main__"
main()
For now, we'll leave the prompt character as >
but that will change later, this code defines a function called main where most of our code will be located, inside that function a while loop is responsible for preventing the program from exiting after command execution, then it takes the user's input and stores it in the cmd
variable and lastly, the if statement makes sure that the main function is only ran when the file is executed directly and not when being imported by a different file.
Handling input:
Now that we have the command that user typed in, we can split it into it's two components, the command itself, and it's arguments.
def main() -> None:
while True:
cmd = input(">")
if " " in cmd:
cmdSplit, args = cmd.split(maxsplit=1)
else:
cmdSplit = cmd
args = ""
if __name__ == "__main__":
main()
Running commands
Okay, we know what the user wants to do, but how do we execute it? This is where it becomes debatable whether the shell is truly ours, since we will be using the system's shell as a base for our own. Using the subprocess
module, we can use it's built-in function run
to run our command in bash or whatever the system's default shell is.
import subprocess
def setup() -> None:
commandsFile = open(f"{os.path.expanduser('~')}/.config/commands.txt", "a")
commandsFile.close()
def main() -> None:
setup()
with open(f"{os.path.expanduser('~')}/.config/commands.txt", "r") as readCmdFile:
commands = readCmdFile.read().split(",\n")
while True:
cmd = input(">")
if " " in cmd:
cmdSplit, args = cmd.split(maxsplit=1)
else:
cmdSplit = cmd
args = ""
if cmdSplit not in commands:
subprocess.run(cmd, shell=True)
if __name__ == "__main__":
main()
This one is a bit complicated. First, we import the subprocess
module. Then, we define a function called setup
that opens and closes a file named commands.txt
in the user's .config
directory. This ensures the file exists, and if it doesn't, the program creates it. Inside the main
function, we open the commands file again and read its contents, assigning each command to a list variable called commands
. This list will contain the custom commands we add. We then check if the input command is in that list, and if it's not then that means the command either doesn't exist or is a system command, so it gets run by the system's shell. This is beneficial because the system's shell handles error management, command execution, and other tasks.
NOTE: Some system commands won't run properly with this code since we are using subprocesses and not actually running the bash commands in the python process, to avoid this, you can add those commands as custom commands and program them yourself, if you can't do it, check out the full version of the shell here
Adding custom commands
After the last section, i guess you can see where this is going, to add a custom command then you just have to add it's name to the commands file, and make an if statement for it.
import subprocess
def setup() -> None:
commandsFile = open(f"{os.path.expanduser('~')}/.config/commands.txt", "a")
commandsFile.close()
def main() -> None:
setup()
with open(f"{os.path.expanduser('~')}/.config/commands.txt", "r") as readCmdFile:
commands = readCmdFile.read().split(",\n")
while True:
cmd = input(">")
if " " in cmd:
cmdSplit, args = cmd.split(maxsplit=1)
else:
cmdSplit = cmd
args = ""
if cmdSplit not in commands:
subprocess.run(cmd, shell=True)
elif cmdSplit == "mkcd":
if not args:
print(f"Error: Invalid arguments")
else:
try:
os.makedirs(args, exist_ok=True)
os.chdir(args)
except:
print(f"Error: Unknown")
if __name__ == "__main__":
main()
And just like that, we added a custom command. An if statement checks whether the command mkcd
(which stands for "make directory and change directory") matches the input command. If it does, the code checks for any arguments. If there are no arguments, the command outputs an "invalid arguments" error. If arguments are provided, which in this case would be the name of the directory, the command creates a directory with that name and then changes the current working directory to the newly created one.
Conclusion
Creating your own shell can be a fun and rewarding project, even if it's not perfect or professional. It allows you to understand the inner workings of command-line interfaces and gives you the freedom to add custom features tailored to your needs. However, it's important to be aware of the security risks involved. The lack of proper security measures can make the shell susceptible to command injection attacks, which can lead to unauthorized access, system compromise, data exfiltration, service disruption, privilege escalation, and malware installation. While this shell may not replace your default system shell, it serves as a great learning experience and a stepping stone for more advanced projects. If you're interested in exploring the full version of the shell, you can check it out here. Happy coding!