Shell implemented in C. Built as part of the Operating Systems course at the RUG.
Go to file
Djairo Hougee 00d7dcfa88 assignment 1 2024-02-16 15:34:28 +01:00
assets assignment 1 2024-02-16 15:34:28 +01:00
Makefile assignment 1 2024-02-16 15:34:28 +01:00
README.md assignment 1 2024-02-16 15:34:28 +01:00
cmd.c assignment 1 2024-02-16 15:34:28 +01:00
cmd.h assignment 1 2024-02-16 15:34:28 +01:00
main.c assignment 1 2024-02-16 15:34:28 +01:00
scanner.c assignment 1 2024-02-16 15:34:28 +01:00
scanner.h assignment 1 2024-02-16 15:34:28 +01:00
shell assignment 1 2024-02-16 15:34:28 +01:00
shell.c assignment 1 2024-02-16 15:34:28 +01:00
shell.h assignment 1 2024-02-16 15:34:28 +01:00

README.md

general code structure

The main objective of this assignment was to tokenize given inputs and process the resulting tokens according to the given grammar. We decided to seperate the two steps of tokenizing and processing almost entirely, which decouples the input parser from the execution very neatly. The one drawback of this method is that each input is, more or less, processed twice - but we considered this a fair trade-off, as the input for a shell can generally be expected to not be incredulously large.

The crux of our approach lies in a slight addition to the ListNode struct: we have added a single Enum Type attribute which places each token within one of a few categories, roughly mapping to the grammar we are implementing.

The possible values of the Type enum are:

Value Definition meaning
EXECUTABLE any non-operator A command in either $CWD or $PATH.
OPTION any non-operator Any options to pass to an EXECUTABLE or BUILTIN.
COMPOSITION '&&', '||', ';', '\n' Special characters which signal the end of a chain.
BUILTIN a builtin One of a few pre-defined builtin executables.
PIPELINE '|' A pipeline, indicating the preceding and following commands ought to be piped together.
REDIRECT '<', '>' Signals stdin and/or stdout should be handled differently.
BACKGROUND '&' Tells the shell to run a given chain as non-blocking (run in the background).
FILENAME any non-operator A file (for redirection).

builtins

A builtin for our shell means any specific command which should be handled by the shell internally, as opposed to calling an external executable. So far, we have implemented:

Builtin Result
status prints the return status of the last run command.
exit exits the shell.
true sets status to 0.
false sets status to -1.
cd changes the current working directory.
debug prints debugging information to stdout.

operators

An operator is a special token which tells the shell to process the preceding (and following) chain differently. The standard operators are (or will be) implemented by this shell:

Operator Name Effect
"&" Background operator Tells the shell ro run a chain in the background.
"&&" Conditional and Tells the shell to execute the next chain iff the status of the previous chain equals 0.
"||" Conditional or Tells the shell to execute the next chain iff the status of the previous chain does not equal 0.
";" Seperator Tells the shell when a chain ends.
"<" Redirect in Tells the shell to take input from a non-stdin stream.
">" Redirect out Tells the shell to print output to a non-stdout stream.
"|" Pipe operator Tells the shell to chain commands together.

processing

After parsing a complete line and assigning each token a Type, the next step of the program is the actual processing. To aid in this, we have constructed a few different types - defined in cmd.h - which model various parts of a typical shell execution.

The Command struct models one executable and its' options, arranged in char * and char ** to easily pass them to an exec() system call.

typedef struct Command {
  char **arguments;
  char *command;
  int capacity;
  int numArguments;
} Command;

The CommandList struct models a list of Commands, and will be used when multiple commands are to be executed together (as is the case when piping commands, or using the background operator).

typedef struct CommandList {
  Command *commands;
  int capacity;
  int numCommands;
} CommandList;

The Chain struct models one complete chain to be executed. It is currently a bit barebones, but will be expanded when we implement piping, redirection, and running in the background.

typedef struct Chain {
  CommandList commands;
  bool runInBackground;
} Chain;

These structs all come with some library functions for ease of use, which allow us to build up commands and chains, and execute them appropriately.

main

The driver code behind the program lives (obviously) in main().

//TODO you were writing here

bonuses

We have enhanced the functionality of our shell by implementing a few bonuses. These are described in this section.

cd builtin

Any self-respecting shell needs a way to change the current working directory. Luckily for the end-user, we have implemented exactly that! Use the builtin cd to move around the system. We even added support for moving to the $HOME directory - accomplished by witholding any command options (just cd will get you home).

Note that this builtin has side-effects, and might therefore edit status. If changing directories fails for whatever reason (insufficient permissions, destination doesn't exist, etc), status will be set to -1.

This builtin provides interactions like these ('>' is input, added in post):

>realpath .
/home/djairoh/Nextcloud/opsys/assignments/1/src
>cd /home/djairoh/Documents
>realpath .
/home/djairoh/Documents
>cd
>realpath .
/home/djairoh

debug mode

To get an insight into how the shell works under the hood, we've added a custom builtin: with the debug flag, all executed commands will be laid out in glorious JSON format.

This builtin is a toggle, meaning a second use will disable it once more. Provided below is some example input/output with this builtin on. Lines starting with '>' are input (the '>' was manually added post-fact).

>debug
Toggled debug to 1.
>echo "hello, world!"
{
"commandList": {
    "commands": [
      {
        "arguments": [
          "echo",
          "hello, world!",
          "(null)"
        ],
        "command": "echo",
        "capacity": 4,
        "numArguments": 3
      }],
    "capacity": 4,
    "numCommands": 1
  },
"runInBackground": 0
}
hello, world!

shell prompt (coloured)

Standard in any shell is a prompt. These can range from minimalist (the sh prompt only shows the current version of the shell), to things as complicated and customizable as the Starship prompt - of which an example is provided here:

An example of a Starship prompt

While we didn't quite get around to this level of complexity, we have implemented a little prompt consisting of three components:

  • the username of the person who started the shell
  • the current working directory
  • the status/status of the last command, indicated by '>'

The end result looks like this:

Our own (less impressive) prompt

true / false

The smallest of our extra implementations, we have added two builtins which directly modify status.

  • The builtin true will set status to 0.
  • The builtin false will set status to -1.

Really this was more of an implementation to allow us to do proper debugging, but it's still something we added to the shell that was not specifically part of the assignment, so we decided we may as well document it here.