Creating A Shell: Difference between revisions

m
Bot: Replace deprecated source tag with syntaxhighlight
[unchecked revision][unchecked revision]
(Created page with "Hello, this is a guide for writing a simple shell. Our shell with need a few things: 1. A working bootloader/GRUB 2. An IDT and IRQ handler 3. A GDT 4. A keyboard dri...")
 
m (Bot: Replace deprecated source tag with syntaxhighlight)
 
(9 intermediate revisions by 8 users not shown)
Line 1:
{{In_Progress}}
Hello, this is a guide for writing a simple shell. Our shell with need a few things:
 
Please note that the contents of this article may not be applicable to all operating system designs, and is modeled predominantly after the shells found on Windows and UNIX systems. All code in this article is based around a typical userspace API, using common or understandable names for the API functions.
 
=What is a shell?=
A shell is a userspace application that provides a simple interface to an operating system kernel. The simplest shell performs the following tasks:
 
# Execute other userspace binaries
# Provide input data for the binaries executed
# Provide a means for the executing binaries to output data
 
A shell may also offer other features depending on the operating system targeted, such as a means to set and use environment variables if such a concept exists in the kernel. The way that a shell is designed depends heavily on the operating system targeted, for example if the operating system has the concept of a "working directory" then the shell may want to maintain its own working directory which will become the working directory of any binaries executed (and provide commands to change the working directory, which may be a built-in command - a command handled by the shell itself, rather than an external binary - such as the <code>cd</code> command in the BASH shell).
 
Depending on the design of the kernel, the shell may provide input and output facilities through the "standard input" and "standard output" streams provided by the kernel to each executing binary. In this case, the shell will typically offer some form of output redirection, whereby input may be read from a file and output may be written to a file or passed as input to a second binary; unredirected input will typically be read from the shell's own "standard input" stream and unredirected output will typically be written to the shell's own "standard output" stream.
 
As a shell is a form of user interface, it will typically read input from the user (which may, as aforementioned, occur through its "standard input" stream) and display output to the user. It's important, however, to get the distinction between the physical hardware used to interact with the user and the shell's interface with the user. For example, the user may be working at a physical keyboard and monitor attached to the computer, or they may be connected over a network, or they may be using a serial console. A versatile shell should be able to work in all of these situations, reading input and sending output through whatever device the user is using (of course, this may not be possible in all operating systems, unless a concept analogous to the UNIX "standard input" and "standard output" is present in the kernel, or the kernel provides some other way of abstracting input devices or emulating one input device as another).
 
Typically, thus, a simple shell will do the following for each binary executed:
 
# Read a command from the user, which becomes the name of the binary to execute plus any command-line arguments (if applicable)
# Tell the kernel to execute the binary
# Read input from the user and send it to the binary
# Read output from the binary and send it to the user
# Repeat steps 3 and 4 until the binary terminates
 
In order to do this, the shell will require the following:
 
# A kernel with an API call for executing a binary
# A way to get input from the user (typically, this requires a keyboard driver for the kernel and an API call for reading from the keyboard - may take the form of "standard input")
# A way to send output to the user (typically, this requires a video driver for the kernel and an API call for printing text to the screen - may take the form of "standard output")
 
It's probably also desirable for the user to be able to edit their commands without having to type them all over again, so a set of string editing functions (ala GNU readline) are also preferable.
 
=Outline of shell=
Based on the explanations above, here is the main function for a simple shell:
 
<syntaxhighlight lang="c">
void main(int argc, char* argv[]) // edit as appropriate for your kernel
{
while (true) // you may want to provide a built-in "exit" command
{
char* command;
int proc;
standard I/O streams
output_prompt(); // display a prompt
command = input_line(); // get a line of input, which will become the command to execute
proc = process_start(command); // start a process from the command
free(command);
 
while (process_executing(proc))
{
if (input_line_waiting())
{
char* line;
line = input_line(); // read input from user
process_send_input_line(proc, line); // send input to process
free(line);
}
if (process_output_line_waiting(proc))
{
char* output;
output = process_get_output_line(proc); // get output from process
output_line(output); // write output to user
free(output);
}
}
}
}
</syntaxhighlight>
 
=Implementation=
The implementation of the functions referred to in the above code snippet depends heavily on the design of your operating system, but here are some typical examples:
 
<syntaxhighlight lang="c">
static void output_prompt()
{
output_line(">");
}
 
static void output_line(char* line)
{
printf("%s\n", line);
}
 
static int input_line_waiting()
{
// TODO: perhaps someone more familiar with the C library could write a better example?
}
 
static char* input_line()
{
// TODO: perhaps someone more familiar with the C library could write a better example?
// TODO: this function will allocate a char buffer containing the line read
}
 
static int process_start(char* command)
{
exec(command);
// TODO: needs to return an int identifying this process
}
 
static int process_executing(int proc)
{
// TODO: what would be a good example here?
}
 
static void process_send_input_line(int proc, char* line)
{
// TODO: what would be a good example here?
}
 
static int process_output_line_waiting(int proc)
{
// TODO: what would be a good example here?
}
 
static char* process_get_output_line(int proc)
{
// TODO: what would be a good example here?
// TODO: this function will allocate a char buffer containing the line read
}
</syntaxhighlight>
 
=TODO=
* Where to include string editing functions?
* How to implement working directory, I/O redirection/piping, etc.?
 
=External Links=
* http://stephen-brennan.com/2015/01/16/write-a-shell-in-c/
 
<!-- Hello, this is a guide for writing a simple shell. Our shell will need a few things:
 
1. A working bootloader/GRUB
Line 5 ⟶ 135:
3. A GDT
4. A keyboard driver
5. A working text output/input/output system.
6. A set of string editing functions
 
First we need to make a file, shell.c, this file will contain all our functions and shell managers. The first function we need to write is our initialization function then our actual shell function.
 
<syntaxhighlight lang="c">
/* shell.c */
/* shell.c */
void init_shell()
void init_shell()
{
{
}
}
void shell()
{
{
}
</syntaxhighlight>
}
 
Now you need to add "init_shell()" to your setup routine in your kernel.c file or whatever you named it, followed by a while loop running shell until the boolean "exit_status" is true.
Line 25 ⟶ 155:
Ok, now we need to write a few things. The first is command table, or a way to store our commands. I do this with a structure as follows:
 
<syntaxhighlight lang="c">
/* shell.h */
/* shell.h */
#define MAX_COMMANDS 100
#define MAX_COMMANDS 100
 
typedef struct
typedef struct
{
{
char *name;
char *descriptionname;
voidchar *functiondescription;
}void command_table_t*function;
} command_table_t;
</syntaxhighlight>
 
Now we have a way to store commands, but how do we use this? Well lets add a few variables to shell.c:
 
<syntaxhighlight lang="c">
/* shell.c */
/* shell.c */
command_table_t CommandTable[MAX_COMMANDS];
command_table_t CommandTable[MAX_COMMANDS];
</syntaxhighlight>
 
This goes right above the function definitions. The first is a way to keep track of how many commands there are. The second is our command table. Ok, so now we have a "Command Table", but how to we access it? Well in order to add a command we need a function to do it. Add this below the other functions:
 
<syntaxhighlight lang="c">
/* shell.c */
/* shell.c */
void add_new_command(char *name, char* description, void *function)
void add_new_command(char *name, char* description, void *function)
{
if(NumberOfCommands + 1 < MAX_COMMANDS)
{
if( NumberOfCommands + 1 < MAX_COMMANDS)+;
 
{
CommandTable[NumberOfCommands].name = name;
NumberOfCommands++;
CommandTable[NumberOfCommands].description = description;
CommandTable[NumberOfCommands].namefunction = namefunction;
}
CommandTable[NumberOfCommands].description = description;
return;
CommandTable[NumberOfCommands].function = function;
}
}
</syntaxhighlight>
return;
}
 
Ok, now we are getting somewhere. We can add a command. But what about getting command line input? Thats up to our "shell()" function. But before we code that, lets add another variable: "char* input_string". Now for the shell function:
 
<syntaxhighlight lang="c">
/* shell.c */
/* shell.c */
void shell()
void shell()
{
{
puts("\nMy_Prompt>");
getsputs(input_string"\nMy_Prompt>");
gets(input_string);
 
void (*command_run)(void);
void (*command_run)(void);
}
}
</syntaxhighlight>
 
Ok, now we can get a string. But what about finding what command the user typed? Well we rely on old trusty "findCommand()" function for that one:
 
<syntaxhighlight lang="c">
/* shell.c */
/* shell.c */
int findCommand(char* command)
int findCommand(char* command)
{
int i;
int ret;
 
for(i = 0; i < NumberOfCommands + 1; i++)
{
ret = strcmp(command, CommandTable[i].name);
int i;
 
int ret;
if(ret == 0)
for(i = 0; i < NumberOfCommands +return 1i; i++)
{else
retreturn = strcmp(command, CommandTable[i].name)-1;
if(ret == 0)
{
return i;
}
else
{
return -1;
}
}
}
}
</syntaxhighlight>
 
Now we need to add a little something right below "gets(input_string)", but above "void (*command_function)(void)":
 
<syntaxhighlight lang="c">
/* shell.c */
/* shell.c */
int i = findCommand(input_string);
int i = findCommand(input_string);
 
if(i >= 0)
if(i >= 0)
{
{
command_function = CommandTable[i].function
command_function = CommandTable[i].function
command_function();
command_function();
}
}
else
else
{
{
return;
}
return;
}
return;
</syntaxhighlight>
 
Now in our "init_shell()" function we need to add a few commands:
 
<syntaxhighlight lang="c">
/* shell.c */
/* shell.c */
add_new_command("help", "You code this one.", help_command);
add_new_command("help", "You code this one.", empty_commandhelp_command);
add_new_command("", "", empty_command);
</syntaxhighlight>
 
Now in our shell.h file:
 
<syntaxhighlight lang="c">
extern void add_new_command();
extern void help_commandadd_new_command();
extern void empty_commandhelp_command();
extern void empty_command();
</syntaxhighlight>
 
Now you need to code a void help_command function and a empty_command that manages null input, all it needs is a definition:
 
<syntaxhighlight lang="c">
void empty_command()
void empty_command()
{
{
}
}
</syntaxhighlight>
 
An idea is to loop through all the command table and print the command name and description - for the help command.
Line 132 ⟶ 276:
 
- wxwsk8er
 
==External Links==
* http://stephen-brennan.com/2015/01/16/write-a-shell-in-c/ Write a Shell in C - similar concepts, but for user space. -->