The shell used in Redox is ion.

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

        while let Some(command) = readln() {
            let command = command.trim();
            if !command.is_empty() {
                self.on_command(command, &commands);

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.

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[0].command == "end" {
                self.flow_control.collecting_block = false;
                let block_jobs: Vec<Pipeline> = self.flow_control
                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 {
        } else {
            if self.flow_control.skipping() && !is_flow_control_command(&[0].command) {
            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/ 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/

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/

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

The function code can be found in crates/ion/src/

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

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

                Command {
                    name: "cd",
                    help: "Change the current directory\n    cd <path>",
                    main: box |args: &[String], shell: &mut Shell| -> i32 {
              , &shell.variables)

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