Ion

Ion is the underlying library for shells and command execution in Redox, as well as the default shell. Ion has it's own manual, which you can find here.

1. The default shell in Redox

What is a shell?

A shell is a layer around operating system kernel and libraries, that allows users to interact with operating system. That means a shell can be used on any operating system (Ion runs on both Linux and Redox) or implementation of a standard library as long as the provided API is the same. Shells can either be graphical (GUI) or command-line (CLI).

Text shells

Text shells are programs that provide interactive user interface with an operating system. A shell reads from users as they type and performs operations according to the input. This is similar to read-eval-print loop (REPL) found in many programming languages (e.g. Python).

Typical *nix shells

Probably the most famous shell is Bash, which can be found in vast majority of Linux distributions, and also in macOS (formerly known as Mac OS X). On the other hand, FreeBSD uses tcsh by default.

There are many more shell implementations, but these two form the base of two fundamentally different sets:

  • Bourne shell syntax (bash, sh, zsh)
  • C shell syntax (csh, tcsh)

Of course these two groups are not exhaustive; it is worth mentioning at least the fish shell and xonsh. These shells are trying to abandon some features of old-school shell to make the language safer and more sane.

Fancy features

Writing commands without any help from the shell would be very exhausting and impossible to use for everyday work. Therefore, most shells (including Ion of course!) include features such as command history, autocompletion based on history or man pages, shortcuts to speed-up typing, etc.

2. A scripting language

Ion can also be used to write simple scripts for common tasks or system configuration after startup. It is not meant as a fully-featured programming language, but more like a glue to connect other programs together.

Relation to terminals

Early terminals were devices used to communicate with large computer systems like IBM mainframes. Nowadays Unix operating systems usually implement so called virtual terminals (tty stands for teletypewriter ... whoa!) and terminal emulators (e.g. xterm, gnome-terminal).

Terminals are used to read input from a keyboard and display textual output of the shell and other programs running inside it. This means that a terminal converts key strokes into control codes that are further used by the shell. The shell provides the user with a command line prompt (for instance: user name and working directory), line editing capabilities (Ctrl + a,e,u,k...), history, and the ability to run other programs (ls, uname, vim, etc.) according to user's input.

TODO: In Linux we have device files like /dev/tty, how is this concept handled in Redox?

Shell

When ion is called without "-c", it starts a main loop, which can be found inside Shell.execute().


#![allow(unused)]
fn main() {
        self.print_prompt();
        while let Some(command) = readln() {
            let command = command.trim();
            if !command.is_empty() {
                self.on_command(command, &commands);
            }
            self.update_variables();
            self.print_prompt();
        }
}

self.print_prompt(); is used to print the shell prompt.

The readln() function is the input reader. The code can be found in crates/ion/src/input_editor.

The documentation about trim() can be found here. If the command is not empty, the on_command method will be called. Then, the shell will update variables, and reprint the prompt.


#![allow(unused)]
fn main() {
fn on_command(&mut self, command_string: &str, commands: &HashMap<&str, Command>) {
    self.history.add(command_string.to_string(), &self.variables);

    let mut pipelines = parse(command_string);

    // Execute commands
    for pipeline in pipelines.drain(..) {
        if self.flow_control.collecting_block {
            // TODO move this logic into "end" command
            if pipeline.jobs[0].command == "end" {
                self.flow_control.collecting_block = false;
                let block_jobs: Vec<Pipeline> = self.flow_control
                                               .current_block
                                               .pipelines
                                               .drain(..)
                                               .collect();
                match self.flow_control.current_statement.clone() {
                    Statement::For(ref var, ref vals) => {
                        let variable = var.clone();
                        let values = vals.clone();
                        for value in values {
                            self.variables.set_var(&variable, &value);
                            for pipeline in &block_jobs {
                                self.run_pipeline(&pipeline, commands);
                            }
                        }
                    },
                    Statement::Function(ref name, ref args) => {
                        self.functions.insert(name.clone(), Function { name: name.clone(), pipelines: block_jobs.clone(), args: args.clone() });
                    },
                    _ => {}
                }
                self.flow_control.current_statement = Statement::Default;
            } else {
                self.flow_control.current_block.pipelines.push(pipeline);
            }
        } else {
            if self.flow_control.skipping() && !is_flow_control_command(&pipeline.jobs[0].command) {
                continue;
            }
            self.run_pipeline(&pipeline, commands);
        }
    }
}
}

First, on_command adds the commands to the shell history with self.history.add(command_string.to_string(), &self.variables);.

Then the script will be parsed. The parser code is in crates/ion/src/peg.rs. The parse will return a set of pipelines, with each pipeline containing a set of jobs. Each job represents a single command with its arguments. You can take a look in crates/ion/src/peg.rs.


#![allow(unused)]
fn main() {
pub struct Pipeline {
    pub jobs: Vec<Job>,
    pub stdout: Option<Redirection>,
    pub stdin: Option<Redirection>,
}
pub struct Job {
    pub command: String,
    pub args: Vec<String>,
    pub background: bool,
}
}

What Happens Next:

  • If the current block is a collecting block (a for loop or a function declaration) and the current command is ended, we close the block:
    • If the block is a for loop we run the loop.
    • If the block is a function declaration we push the function to the functions list.
  • If the current block is a collecting block but the current command is not ended, we add the current command to the block.
  • If the current block is not a collecting block, we simply execute the current command.

The code blocks are defined in crates/ion/src/flow_control.rs.

pub struct CodeBlock {
    pub pipelines: Vec<Pipeline>,
}

The function code can be found in crates/ion/src/functions.rs.

The execution of pipeline content will be executed in run_pipeline().

The Command class inside crates/ion/src/main.rs maps each command with a description and a method to be executed. For example:


#![allow(unused)]
fn main() {
commands.insert("cd",
                Command {
                    name: "cd",
                    help: "Change the current directory\n    cd <path>",
                    main: box |args: &[String], shell: &mut Shell| -> i32 {
                        shell.directory_stack.cd(args, &shell.variables)
                    },
                });
}

cd is described by "Change the current directory\n cd <path>", and when called the method shell.directory_stack.cd(args, &shell.variables) will be used. You can see its code in crates/ion/src/directory_stack.rs.