]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/backup/catalog_shell.rs
cli: avoid useless .into()
[proxmox-backup.git] / src / backup / catalog_shell.rs
index d02572045ed1d2c63d08343c954d46f6e0d710c7..66b2529e32b55216f86c3c11646d64af108b19e3 100644 (file)
@@ -1,60 +1,87 @@
-use std::collections::HashSet;
-use std::ffi::{CStr, CString, OsStr};
+use std::cell::RefCell;
+use std::collections::{HashMap, HashSet};
+use std::ffi::{CString, OsStr};
 use std::io::Write;
 use std::os::unix::ffi::OsStrExt;
 use std::path::Path;
 
 use failure::*;
-use libc;
 
+use super::catalog::{CatalogReader, DirEntry};
 use crate::pxar::*;
+use crate::tools;
 
-use super::catalog::{CatalogReader, DirEntry};
-use super::readline::{Readline, Context};
+use proxmox::api::{cli::*, *};
 
-/// State of the shell instance
+const PROMPT_PREFIX: &str = "pxar:";
+const PROMPT: &str = ">";
+
+/// Interactive shell for interacton with the catalog.
 pub struct Shell {
-    /// Readline context
-    rl: Readline,
-    /// List of paths selected for a restore
-    selected: HashSet<Vec<u8>>,
-    /// Decoder instance for the current pxar archive
-    decoder: Decoder,
-    /// Root directory for the give archive as stored in the catalog
-    root: Vec<DirEntry>,
+    /// Readline instance handling input and callbacks
+    rl: rustyline::Editor<CliHelper>,
+    prompt: String,
 }
 
-/// All supported commands of the shell
-enum Command<'a> {
-    /// List the content of the current dir or in path, if provided
-    List(&'a [u8]),
-    /// Stat of the provided path
-    Stat(&'a [u8]),
-    /// Select the given entry for a restore
-    Select(&'a [u8]),
-    /// Remove the entry from the list of entries to restore
-    Deselect(&'a [u8]),
-    /// Restore an archive to the provided target, can be limited to files
-    /// matching the provided match pattern
-    Restore(&'a [u8], &'a [u8]),
-    /// Restore the selected entries to the provided target
-    RestoreSelected(&'a [u8]),
-    /// List the entries currently selected for restore
-    ListSelected,
-    /// Change the current working directory
-    ChangeDir(&'a [u8]),
-    /// Print the working directory
-    PrintWorkingDir,
-    /// Terminate the shell loop, returns from the shell
-    Quit,
-    /// Empty line from readline
-    Empty,
+/// This list defines all the shell commands and their properties
+/// using the api schema
+pub fn catalog_shell_cli() -> CommandLineInterface {
+
+    let map = CliCommandMap::new()
+        .insert("pwd", CliCommand::new(&API_METHOD_PWD_COMMAND))
+        .insert(
+            "cd",
+            CliCommand::new(&API_METHOD_CD_COMMAND)
+                .arg_param(&["path"])
+                .completion_cb("path", Shell::complete_path)
+        )
+        .insert(
+            "ls",
+            CliCommand::new(&API_METHOD_LS_COMMAND)
+                .arg_param(&["path"])
+                .completion_cb("path", Shell::complete_path)
+         )
+        .insert(
+            "stat",
+            CliCommand::new(&API_METHOD_STAT_COMMAND)
+                .arg_param(&["path"])
+                .completion_cb("path", Shell::complete_path)
+         )
+        .insert(
+            "select",
+            CliCommand::new(&API_METHOD_SELECT_COMMAND)
+                .arg_param(&["path"])
+                .completion_cb("path", Shell::complete_path)
+        )
+        .insert(
+            "deselect",
+            CliCommand::new(&API_METHOD_DESELECT_COMMAND)
+                .arg_param(&["path"])
+                .completion_cb("path", Shell::complete_path)
+        )
+        .insert(
+            "restore-selected",
+            CliCommand::new(&API_METHOD_RESTORE_SELECTED_COMMAND)
+                .arg_param(&["target"])
+                .completion_cb("target", tools::complete_file_name)
+        )
+        .insert(
+            "list-selected",
+            CliCommand::new(&API_METHOD_LIST_SELECTED_COMMAND),
+        )
+        .insert(
+            "restore",
+            CliCommand::new(&API_METHOD_RESTORE_COMMAND)
+                .arg_param(&["target"])
+                .completion_cb("target", tools::complete_file_name)
+        )
+        .insert_help();
+
+    CommandLineInterface::Nested(map)
 }
 
-const PROMPT_PREFIX: &str = "pxar:";
-const PROMPT_POST: &str = " > ";
-
 impl Shell {
+    /// Create a new shell for the given catalog and pxar archive.
     pub fn new(
         mut catalog: CatalogReader<std::fs::File>,
         archive_name: &str,
@@ -64,135 +91,123 @@ impl Shell {
         // The root for the given archive as stored in the catalog
         let archive_root = catalog.lookup(&catalog_root, archive_name.as_bytes())?;
         let root = vec![archive_root];
-        Ok(Self {
-            rl: Readline::new(
-                Self::generate_prompt(b"/"),
-                root.clone(),
-                Box::new(complete),
+
+        CONTEXT.with(|handle| {
+            let mut ctx = handle.borrow_mut();
+            *ctx = Some(Context {
                 catalog,
-            ),
-            selected: HashSet::new(),
-            decoder,
-            root,
+                selected: HashSet::new(),
+                decoder,
+                root: root.clone(),
+                current: root,
+            });
+        });
+
+        let cli_helper = CliHelper::new(catalog_shell_cli());
+        let mut rl = rustyline::Editor::<CliHelper>::new();
+        rl.set_helper(Some(cli_helper));
+
+        Context::with(|ctx| {
+            Ok(Self {
+                rl,
+                prompt: ctx.generate_prompt()?,
+            })
         })
     }
 
-    fn generate_prompt(path: &[u8]) -> CString {
-        let mut buffer = Vec::new();
-        buffer.extend_from_slice(PROMPT_PREFIX.as_bytes());
-        buffer.extend_from_slice(path);
-        buffer.extend_from_slice(PROMPT_POST.as_bytes());
-        unsafe { CString::from_vec_unchecked(buffer) }
-    }
-
     /// Start the interactive shell loop
     pub fn shell(mut self) -> Result<(), Error> {
-        while let Some(line) = self.rl.readline() {
-            let res = match self.parse_command(&line) {
-                Ok(Command::List(path)) => self.list(path).and_then(|list| {
-                    Self::print_list(&list).map_err(|err| format_err!("{}", err))?;
-                    Ok(())
-                }),
-                Ok(Command::ChangeDir(path)) => self.change_dir(path),
-                Ok(Command::Restore(target, pattern)) => self.restore(target, pattern),
-                Ok(Command::Select(path)) => self.select(path),
-                Ok(Command::Deselect(path)) => self.deselect(path),
-                Ok(Command::RestoreSelected(target)) => self.restore_selected(target),
-                Ok(Command::Stat(path)) => self.stat(&path).and_then(|(item, attr, size)| {
-                    Self::print_stat(&item, &attr, size)?;
-                    Ok(())
-                }),
-                Ok(Command::ListSelected) => {
-                    self.list_selected().map_err(|err| format_err!("{}", err))
+        while let Ok(line) = self.rl.readline(&self.prompt) {
+            let helper = self.rl.helper().unwrap();
+            let args = match shellword_split(&line) {
+                Ok(args) => args,
+                Err(err) => {
+                    println!("Error: {}", err);
+                    continue;
                 }
-                Ok(Command::PrintWorkingDir) => self.pwd().and_then(|pwd| {
-                    Self::print_pwd(&pwd).map_err(|err| format_err!("{}", err))?;
-                    Ok(())
-                }),
-                Ok(Command::Quit) => break,
-                Ok(Command::Empty) => continue,
-                Err(err) => Err(err),
             };
-            if let Err(err) = res {
-                println!("error: {}", err);
-            }
+            let _ = handle_command(helper.cmd_def(), "", args);
+            self.rl.add_history_entry(line);
+            self.update_prompt()?;
         }
         Ok(())
     }
 
-    /// Command parser mapping the line returned by readline to a command.
-    fn parse_command<'a>(&self, line: &'a [u8]) -> Result<Command<'a>, Error> {
-        // readline already handles tabs, so here we only split on spaces
-        let args: Vec<&[u8]> = line
-            .split(|b| *b == b' ')
-            .filter(|word| !word.is_empty())
-            .collect();
+    /// Update the prompt to the new working directory
+    fn update_prompt(&mut self) -> Result<(), Error> {
+        Context::with(|ctx| {
+            self.prompt = ctx.generate_prompt()?;
+            Ok(())
+        })
+    }
+
+    /// Completions for paths by lookup in the catalog
+    fn complete_path(complete_me: &str, _map: &HashMap<String, String>) -> Vec<String> {
+        Context::with(|ctx| {
+            let (base, to_complete) = match complete_me.rfind('/') {
+                // Split at ind + 1 so the slash remains on base, ok also if
+                // ends in slash as split_at accepts up to length as index.
+                Some(ind) => complete_me.split_at(ind + 1),
+                None => ("", complete_me),
+            };
 
-        if args.is_empty() {
-            return Ok(Command::Empty);
-        }
+            let current = if base.is_empty() {
+                ctx.current.clone()
+            } else {
+                ctx.canonical_path(base)?
+            };
 
-        match args[0] {
-            b"quit" => Ok(Command::Quit),
-            b"exit" => Ok(Command::Quit),
-            b"ls" => match args.len() {
-                1 => Ok(Command::List(&[])),
-                2 => Ok(Command::List(args[1])),
-                _ => bail!("To many parameters!"),
-            },
-            b"pwd" => Ok(Command::PrintWorkingDir),
-            b"restore" => match args.len() {
-                1 => bail!("no target provided"),
-                2 => Ok(Command::Restore(args[1], &[])),
-                4 => if args[2] == b"-p" {
-                    Ok(Command::Restore(args[1], args[3]))
-                } else {
-                    bail!("invalid parameter")
+            let entries = match ctx.catalog.read_dir(&current.last().unwrap()) {
+                Ok(entries) => entries,
+                Err(_) => return Ok(Vec::new()),
+            };
+
+            let mut list = Vec::new();
+            for entry in &entries {
+                let mut name = String::from(base);
+                if entry.name.starts_with(to_complete.as_bytes()) {
+                    name.push_str(std::str::from_utf8(&entry.name)?);
+                    if entry.is_directory() {
+                        name.push('/');
+                    }
+                    list.push(name);
                 }
-                _ => bail!("to many parameters"),
-            },
-            b"cd" => match args.len() {
-                1 => Ok(Command::ChangeDir(&[])),
-                2 => Ok(Command::ChangeDir(args[1])),
-                _ => bail!("to many parameters"),
-            },
-            b"stat" => match args.len() {
-                1 => bail!("no path provided"),
-                2 => Ok(Command::Stat(args[1])),
-                _ => bail!("to many parameters"),
-            },
-            b"select" => match args.len() {
-                1 => bail!("no path provided"),
-                2 => Ok(Command::Select(args[1])),
-                _ => bail!("to many parameters"),
-            },
-            b"deselect" => match args.len() {
-                1 => bail!("no path provided"),
-                2 => Ok(Command::Deselect(args[1])),
-                _ => bail!("to many parameters"),
-            },
-            b"selected" => match args.len() {
-                1 => Ok(Command::ListSelected),
-                _ => bail!("to many parameters"),
-            },
-            b"restore-selected" => match args.len() {
-                1 => bail!("no path provided"),
-                2 => Ok(Command::RestoreSelected(args[1])),
-                _ => bail!("to many parameters"),
-            },
-            _ => bail!("command not known"),
-        }
+            }
+            Ok(list)
+        })
+        .unwrap_or_default()
     }
+}
 
-    /// Get a mut ref to the context in order to be able to access the
-    /// catalog and the directory stack for the current working directory.
-    fn context(&mut self) -> &mut Context {
-        self.rl.context()
-    }
+#[api(input: { properties: {} })]
+/// List the current working directory.
+fn pwd_command() -> Result<(), Error> {
+    Context::with(|ctx| {
+        let path = Context::generate_cstring(&ctx.current)?;
+        let mut out = std::io::stdout();
+        out.write_all(&path.as_bytes())?;
+        out.write_all(&[b'\n'])?;
+        out.flush()?;
+        Ok(())
+    })
+}
 
-    /// Change the current working directory to the new directory
-    fn change_dir(&mut self, path: &[u8]) -> Result<(), Error> {
-        let mut path = self.canonical_path(path)?;
+#[api(
+    input: {
+        properties: {
+            path: {
+                type: String,
+                optional: true,
+                description: "target path."
+            }
+        }
+    }
+)]
+/// Change the current working directory to the new directory
+fn cd_command(path: Option<String>) -> Result<(), Error> {
+    Context::with(|ctx| {
+        let path = path.unwrap_or_default();
+        let mut path = ctx.canonical_path(&path)?;
         if !path
             .last()
             .ok_or_else(|| format_err!("invalid path component"))?
@@ -202,181 +217,186 @@ impl Shell {
             path.pop();
             eprintln!("not a directory, fallback to parent directory");
         }
-        self.context().current = path;
-        // Update the directory displayed in the prompt
-        let prompt = Self::generate_prompt(self.pwd()?.as_slice());
-        self.rl.update_prompt(prompt);
+        ctx.current = path;
         Ok(())
-    }
+    })
+}
 
-    /// List the content of a directory.
-    ///
-    /// Executed on files it returns the DirEntry of the file as single element
-    /// in the list.
-    fn list(&mut self, path: &[u8]) -> Result<Vec<DirEntry>, Error> {
-        let parent = if !path.is_empty() {
-            self.canonical_path(path)?
+#[api(
+    input: {
+        properties: {
+            path: {
+                type: String,
+                optional: true,
+                description: "target path."
+            }
+        }
+    }
+)]
+/// List the content of working directory or given path.
+fn ls_command(path: Option<String>) -> Result<(), Error> {
+    Context::with(|ctx| {
+        let parent = if let Some(path) = path {
+            ctx.canonical_path(&path)?
                 .last()
                 .ok_or_else(|| format_err!("invalid path component"))?
                 .clone()
         } else {
-            self.context().current.last().unwrap().clone()
+            ctx.current.last().unwrap().clone()
         };
 
         let list = if parent.is_directory() {
-            self.context().catalog.read_dir(&parent)?
+            ctx.catalog.read_dir(&parent)?
         } else {
             vec![parent]
         };
-        Ok(list)
-    }
 
-    /// Return the current working directory as string
-    fn pwd(&mut self) -> Result<Vec<u8>, Error> {
-        Self::to_path(&self.context().current.clone())
-    }
-
-    /// Generate an absolute path from a directory stack.
-    fn to_path(dir_stack: &[DirEntry]) -> Result<Vec<u8>, Error> {
-        let mut path = vec![b'/'];
-        // Skip the archive root, '/' is displayed for it
-        for item in dir_stack.iter().skip(1) {
-            path.extend_from_slice(&item.name);
-            if item.is_directory() {
-                path.push(b'/');
-            }
-        }
-        Ok(path)
-    }
-
-    /// Resolve the indirect path components and return an absolute path.
-    ///
-    /// This will actually navigate the filesystem tree to check that the
-    /// path is vaild and exists.
-    /// This does not include following symbolic links.
-    /// If None is given as path, only the root directory is returned.
-    fn canonical_path(&mut self, path: &[u8]) -> Result<Vec<DirEntry>, Error> {
-        if path == b"/" {
-            return Ok(self.root.clone());
+        if list.is_empty() {
+            return Ok(());
         }
-
-        let mut path_slice = if path.is_empty() {
-            // Fallback to root if no path was provided
-            return Ok(self.root.clone());
-        } else {
-            path
+        let max = list.iter().max_by(|x, y| x.name.len().cmp(&y.name.len()));
+        let max = match max {
+            Some(dir_entry) => dir_entry.name.len() + 1,
+            None => 0,
         };
 
-        let mut dir_stack = if path_slice.starts_with(&[b'/']) {
-            // Absolute path, reduce view of slice and start from root
-            path_slice = &path_slice[1..];
-            self.root.clone()
-        } else {
-            // Relative path, start from current working directory
-            self.context().current.clone()
-        };
-        let should_end_dir = if path_slice.ends_with(&[b'/']) {
-            path_slice = &path_slice[0..path_slice.len() - 1];
-            true
-        } else {
-            false
-        };
-        for name in path_slice.split(|b| *b == b'/') {
-            match name {
-                b"." => continue,
-                b".." => {
-                    // Never pop archive root from stack
-                    if dir_stack.len() > 1 {
-                        dir_stack.pop();
-                    }
-                }
-                _ => {
-                    let entry = self.context().catalog.lookup(dir_stack.last().unwrap(), name)?;
-                    dir_stack.push(entry);
-                }
+        let (_rows, mut cols) = Context::get_terminal_size();
+        cols /= max;
+
+        let mut out = std::io::stdout();
+        for (index, item) in list.iter().enumerate() {
+            out.write_all(&item.name)?;
+            // Fill with whitespaces
+            out.write_all(&vec![b' '; max - item.name.len()])?;
+            if index % cols == (cols - 1) {
+                out.write_all(&[b'\n'])?;
             }
         }
-        if should_end_dir && !dir_stack.last()
-            .ok_or_else(|| format_err!("invalid path component"))?
-            .is_directory()
-        {
-            bail!("entry is not a directory");
+        // If the last line is not complete, add the newline
+        if list.len() % cols != cols - 1 {
+            out.write_all(&[b'\n'])?;
         }
+        out.flush()?;
+        Ok(())
+    })
+}
 
-        Ok(dir_stack)
+#[api(
+    input: {
+        properties: {
+            path: {
+                type: String,
+                description: "target path."
+            }
+        }
     }
-
-    /// Read the metadata for a given directory entry.
-    ///
-    /// This is expensive because the data has to be read from the pxar `Decoder`,
-    /// which means reading over the network.
-    fn stat(&mut self, path: &[u8]) -> Result<(DirectoryEntry, PxarAttributes, u64), Error> {
+)]
+/// Read the metadata for a given directory entry.
+///
+/// This is expensive because the data has to be read from the pxar `Decoder`,
+/// which means reading over the network.
+fn stat_command(path: String) -> Result<(), Error> {
+    Context::with(|ctx| {
         // First check if the file exists in the catalog, therefore avoiding
-        // expensive calls to the decoder just to find out that there could be no
-        // such entry. This is done by calling canonical_path(), which returns
-        // the full path if it exists, error otherwise.
-        let path = self.canonical_path(path)?;
-        self.lookup(&path)
-    }
+        // expensive calls to the decoder just to find out that there maybe is
+        // no such entry.
+        // This is done by calling canonical_path(), which returns the full path
+        // if it exists, error otherwise.
+        let path = ctx.canonical_path(&path)?;
+        let (item, _attr, size) = ctx.lookup(&path)?;
+        let mut out = std::io::stdout();
+        out.write_all(b"File: ")?;
+        out.write_all(item.filename.as_bytes())?;
+        out.write_all(&[b'\n'])?;
+        out.write_all(format!("Size: {}\n", size).as_bytes())?;
+        out.write_all(b"Type: ")?;
+        match item.entry.mode as u32 & libc::S_IFMT {
+            libc::S_IFDIR => out.write_all(b"directory\n")?,
+            libc::S_IFREG => out.write_all(b"regular file\n")?,
+            libc::S_IFLNK => out.write_all(b"symbolic link\n")?,
+            libc::S_IFBLK => out.write_all(b"block special file\n")?,
+            libc::S_IFCHR => out.write_all(b"character special file\n")?,
+            _ => out.write_all(b"unknown\n")?,
+        };
+        out.write_all(format!("Uid: {}\n", item.entry.uid).as_bytes())?;
+        out.write_all(format!("Gid: {}\n", item.entry.gid).as_bytes())?;
+        out.flush()?;
+        Ok(())
+    })
+}
 
-    /// Look up the entry given by a canonical absolute `path` in the archive.
-    ///
-    /// This will actively navigate the archive by calling the corresponding decoder
-    /// functionalities and is therefore very expensive.
-    fn lookup(
-        &mut self,
-        absolute_path: &[DirEntry],
-    ) -> Result<(DirectoryEntry, PxarAttributes, u64), Error> {
-        let mut current = self.decoder.root()?;
-        let (_, _, mut attr, mut size) = self.decoder.attributes(0)?;
-        // Ignore the archive root, don't need it.
-        for item in absolute_path.iter().skip(1) {
-            match self.decoder.lookup(&current, &OsStr::from_bytes(&item.name))? {
-                Some((item, item_attr, item_size)) => {
-                    current = item;
-                    attr = item_attr;
-                    size = item_size;
-                }
-                // This should not happen if catalog an archive are consistent.
-                None => bail!("no such file or directory in archive"),
+#[api(
+    input: {
+        properties: {
+            path: {
+                type: String,
+                description: "target path."
             }
         }
-        Ok((current, attr, size))
     }
-
-    /// Select an entry for restore.
-    ///
-    /// This will return an error if the entry is already present in the list or
-    /// if an invalid path was provided.
-    fn select(&mut self, path: &[u8]) -> Result<(), Error> {
+)]
+/// Select an entry for restore.
+///
+/// This will return an error if the entry is already present in the list or
+/// if an invalid path was provided.
+fn select_command(path: String) -> Result<(), Error> {
+    Context::with(|ctx| {
         // Calling canonical_path() makes sure the provided path is valid and
         // actually contained within the catalog and therefore also the archive.
-        let path = self.canonical_path(path)?;
-        if self.selected.insert(Self::to_path(&path)?) {
+        let path = ctx.canonical_path(&path)?;
+        if ctx
+            .selected
+            .insert(Context::generate_cstring(&path)?.into_bytes())
+        {
             Ok(())
         } else {
             bail!("entry already selected for restore")
         }
-    }
+    })
+}
 
-    /// Deselect an entry for restore.
-    ///
-    /// This will return an error if the entry was not found in the list of entries
-    /// selected for restore.
-    fn deselect(&mut self, path: &[u8]) -> Result<(), Error> {
-        if self.selected.remove(path) {
+#[api(
+    input: {
+        properties: {
+            path: {
+                type: String,
+                description: "path to entry to remove from list."
+            }
+        }
+    }
+)]
+/// Deselect an entry for restore.
+///
+/// This will return an error if the entry was not found in the list of entries
+/// selected for restore.
+fn deselect_command(path: String) -> Result<(), Error> {
+    Context::with(|ctx| {
+        let path = ctx.canonical_path(&path)?;
+        if ctx.selected.remove(&Context::generate_cstring(&path)?.into_bytes()) {
             Ok(())
         } else {
             bail!("entry not selected for restore")
         }
-    }
+    })
+}
 
-    /// Restore the selected entries to the given target path.
-    ///
-    /// Target must not exist on the clients filesystem.
-    fn restore_selected(&mut self, target: &[u8]) -> Result<(), Error> {
+#[api(
+    input: {
+        properties: {
+            target: {
+                type: String,
+                description: "target path for restore on local filesystem."
+            }
+        }
+    }
+)]
+/// Restore the selected entries to the given target path.
+///
+/// Target must not exist on the clients filesystem.
+fn restore_selected_command(target: String) -> Result<(), Error> {
+    Context::with(|ctx| {
         let mut list = Vec::new();
-        for path in &self.selected {
+        for path in &ctx.selected {
             let pattern = MatchPattern::from_line(path)?
                 .ok_or_else(|| format_err!("encountered invalid match pattern"))?;
             list.push(pattern);
@@ -387,114 +407,192 @@ impl Shell {
 
         // Entry point for the restore is always root here as the provided match
         // patterns are relative to root as well.
-        let start_dir = self.decoder.root()?;
-        let target: &OsStr = OsStrExt::from_bytes(target);
-        self.decoder.restore(&start_dir, &Path::new(target), &list)?;
+        let start_dir = ctx.decoder.root()?;
+        ctx.decoder
+            .restore(&start_dir, &Path::new(&target), &list)?;
         Ok(())
-    }
+    })
+}
 
-    /// List entries currently selected for restore.
-    fn list_selected(&self) -> Result<(), std::io::Error> {
+#[api( input: { properties: {} })]
+/// List entries currently selected for restore.
+fn list_selected_command() -> Result<(), Error> {
+    Context::with(|ctx| {
         let mut out = std::io::stdout();
-        for entry in &self.selected {
+        for entry in &ctx.selected {
             out.write_all(entry)?;
             out.write_all(&[b'\n'])?;
         }
         out.flush()?;
         Ok(())
-    }
+    })
+}
 
-    /// Restore the sub-archive given by the current working directory to target.
-    ///
-    /// By further providing a pattern, the restore can be limited to a narrower
-    /// subset of this sub-archive.
-    /// If pattern is an empty slice, the full dir is restored.
-    fn restore(&mut self, target: &[u8], pattern: &[u8]) -> Result<(), Error> {
-        let match_pattern = match pattern {
-            b"" | b"/" | b"." => Vec::new(),
-            _ => vec![MatchPattern::from_line(pattern)?.unwrap()],
+#[api(
+    input: {
+        properties: {
+            target: {
+                type: String,
+                description: "target path for restore on local filesystem."
+            },
+            pattern: {
+                type: String,
+                optional: true,
+                description: "match pattern to limit files for restore."
+            }
+        }
+    }
+)]
+/// Restore the sub-archive given by the current working directory to target.
+///
+/// By further providing a pattern, the restore can be limited to a narrower
+/// subset of this sub-archive.
+/// If pattern is not present or empty, the full archive is restored to target.
+fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error> {
+    Context::with(|ctx| {
+        let pattern = pattern.unwrap_or_default();
+        let match_pattern = match pattern.as_str() {
+            "" | "/" | "." => Vec::new(),
+            _ => vec![MatchPattern::from_line(pattern.as_bytes())?.unwrap()],
         };
-        // Entry point for the restore.
-        let start_dir = if pattern.starts_with(&[b'/']) {
-            self.decoder.root()?
+        // Decoder entry point for the restore.
+        let start_dir = if pattern.starts_with("/") {
+            ctx.decoder.root()?
         } else {
             // Get the directory corresponding to the working directory from the
             // archive.
-            let cwd = self.context().current.clone();
-            let (dir, _, _) = self.lookup(&cwd)?;
+            let cwd = ctx.current.clone();
+            let (dir, _, _) = ctx.lookup(&cwd)?;
             dir
         };
 
-        let target: &OsStr = OsStrExt::from_bytes(target);
-        self.decoder.restore(&start_dir, &Path::new(target), &match_pattern)?;
+        ctx.decoder
+            .restore(&start_dir, &Path::new(&target), &match_pattern)?;
         Ok(())
+    })
+}
+
+std::thread_local! {
+    static CONTEXT: RefCell<Option<Context>> = RefCell::new(None);
+}
+
+/// Holds the context needed for access to catalog and decoder
+struct Context {
+    /// Calalog reader instance to navigate
+    catalog: CatalogReader<std::fs::File>,
+    /// List of selected paths for restore
+    selected: HashSet<Vec<u8>>,
+    /// Decoder instance for the current pxar archive
+    decoder: Decoder,
+    /// Root directory for the give archive as stored in the catalog
+    root: Vec<DirEntry>,
+    /// Stack of directories up to the current working directory
+    /// used for navigation and path completion.
+    current: Vec<DirEntry>,
+}
+
+impl Context {
+    /// Execute `call` within a context providing a mut ref to `Context` instance.
+    fn with<T, F>(call: F) -> Result<T, Error>
+    where
+        F: FnOnce(&mut Context) -> Result<T, Error>,
+    {
+        CONTEXT.with(|cell| {
+            let mut ctx = cell.borrow_mut();
+            call(&mut ctx.as_mut().unwrap())
+        })
     }
 
-    fn print_list(list: &Vec<DirEntry>) -> Result<(), std::io::Error> {
-        let max = list
-            .iter()
-            .max_by(|x, y| x.name.len().cmp(&y.name.len()));
-        let max = match max {
-            Some(dir_entry) => dir_entry.name.len() + 1,
-            None => 0,
+    /// Generate CString from provided stack of `DirEntry`s.
+    fn generate_cstring(dir_stack: &[DirEntry]) -> Result<CString, Error> {
+        let mut path = vec![b'/'];
+        // Skip the archive root, the '/' is displayed for it instead
+        for component in dir_stack.iter().skip(1) {
+            path.extend_from_slice(&component.name);
+            if component.is_directory() {
+                path.push(b'/');
+            }
+        }
+        Ok(unsafe { CString::from_vec_unchecked(path) })
+    }
+
+    /// Resolve the indirect path components and return an absolute path.
+    ///
+    /// This will actually navigate the filesystem tree to check that the
+    /// path is vaild and exists.
+    /// This does not include following symbolic links.
+    /// If None is given as path, only the root directory is returned.
+    fn canonical_path(&mut self, path: &str) -> Result<Vec<DirEntry>, Error> {
+        if path == "/" {
+            return Ok(self.root.clone());
+        }
+
+        let mut path_slice = if path.is_empty() {
+            // Fallback to root if no path was provided
+            return Ok(self.root.clone());
+        } else {
+            path
         };
-        let (_rows, mut cols) = Self::get_terminal_size();
-        cols /= max;
-        let mut out = std::io::stdout();
 
-        for (index, item) in list.iter().enumerate() {
-            out.write_all(&item.name)?;
-            // Fill with whitespaces
-            out.write_all(&vec![b' '; max - item.name.len()])?;
-            if index % cols == (cols - 1)  {
-                out.write_all(&[b'\n'])?;
+        let mut dir_stack = if path_slice.starts_with("/") {
+            // Absolute path, reduce view of slice and start from root
+            path_slice = &path_slice[1..];
+            self.root.clone()
+        } else {
+            // Relative path, start from current working directory
+            self.current.clone()
+        };
+        let should_end_dir = if path_slice.ends_with("/") {
+            path_slice = &path_slice[0..path_slice.len() - 1];
+            true
+        } else {
+            false
+        };
+        for name in path_slice.split('/') {
+            match name {
+                "." => continue,
+                ".." => {
+                    // Never pop archive root from stack
+                    if dir_stack.len() > 1 {
+                        dir_stack.pop();
+                    }
+                }
+                _ => {
+                    let entry = self.catalog.lookup(dir_stack.last().unwrap(), name.as_bytes())?;
+                    dir_stack.push(entry);
+                }
             }
         }
-        // If the last line is not complete, add the newline
-        if list.len() % cols != cols - 1 {
-            out.write_all(&[b'\n'])?;
+        if should_end_dir
+            && !dir_stack
+                .last()
+                .ok_or_else(|| format_err!("invalid path component"))?
+                .is_directory()
+        {
+            bail!("entry is not a directory");
         }
-        out.flush()?;
-        Ok(())
-    }
 
-    fn print_pwd(pwd: &[u8]) -> Result<(), std::io::Error> {
-        let mut out = std::io::stdout();
-        out.write_all(pwd)?;
-        out.write_all(&[b'\n'])?;
-        out.flush()?;
-        Ok(())
+        Ok(dir_stack)
     }
 
-    fn print_stat(item: &DirectoryEntry, _attr: &PxarAttributes, size: u64) -> Result<(), std::io::Error> {
-        let mut out = std::io::stdout();
-        out.write_all("File: ".as_bytes())?;
-        out.write_all(&item.filename.as_bytes())?;
-        out.write_all(&[b'\n'])?;
-        out.write_all(format!("Size: {}\n", size).as_bytes())?;
-        let mode = match item.entry.mode as u32 & libc::S_IFMT {
-            libc::S_IFDIR => "directory".as_bytes(),
-            libc::S_IFREG => "regular file".as_bytes(),
-            libc::S_IFLNK => "symbolic link".as_bytes(),
-            libc::S_IFBLK => "block special file".as_bytes(),
-            libc::S_IFCHR => "character special file".as_bytes(),
-            _ => "unknown".as_bytes(),
-        };
-        out.write_all("Type: ".as_bytes())?;
-        out.write_all(&mode)?;
-        out.write_all(&[b'\n'])?;
-        out.write_all(format!("Uid: {}\n", item.entry.uid).as_bytes())?;
-        out.write_all(format!("Gid: {}\n", item.entry.gid).as_bytes())?;
-        out.flush()?;
-        Ok(())
+    /// Generate the CString to display by readline based on
+    /// PROMPT_PREFIX, PROMPT and the current working directory.
+    fn generate_prompt(&self) -> Result<String, Error> {
+        let prompt = format!(
+            "{}{} {} ",
+            PROMPT_PREFIX,
+            Self::generate_cstring(&self.current)?.to_string_lossy(),
+            PROMPT,
+        );
+        Ok(prompt)
     }
 
     /// Get the current size of the terminal
+    /// # Safety
     ///
-    /// uses tty_ioctl, see man tty_ioctl(2)
+    /// uses unsafe call to tty_ioctl, see man tty_ioctl(2)
     fn get_terminal_size() -> (usize, usize) {
-
-        const TIOCGWINSZ: libc::c_ulong = 0x00005413;
+        const TIOCGWINSZ: libc::c_ulong = 0x5413;
 
         #[repr(C)]
         struct WinSize {
@@ -513,62 +611,32 @@ impl Shell {
         unsafe { libc::ioctl(libc::STDOUT_FILENO, TIOCGWINSZ, &mut winsize) };
         (winsize.ws_row as usize, winsize.ws_col as usize)
     }
-}
 
-/// Filename completion callback for the shell
-// TODO: impl command completion. For now only filename completion.
-fn complete(
-    ctx: &mut Context,
-    text: &CStr,
-    _start: usize,
-    _end: usize
-) -> Vec<CString> {
-    let slices: Vec<_> = text
-        .to_bytes()
-        .split(|b| *b == b'/')
-        .collect();
-    let to_complete = match slices.last() {
-        Some(last) => last,
-        None => return Vec::new(),
-    };
-    let mut current = ctx.current.clone();
-    let (prefix, entries) = {
-        let mut prefix = Vec::new();
-        if slices.len() > 1 {
-            for component in &slices[..slices.len() - 1] {
-                if component == b"." {
-                    continue;
-                } else if component == b".." {
-                    // Never leave the current archive in the catalog
-                    if current.len() > 1 { current.pop(); }
-                } else {
-                    match ctx.catalog.lookup(current.last().unwrap(), component) {
-                        Err(_) => return Vec::new(),
-                        Ok(dir) => current.push(dir),
-                    }
+    /// Look up the entry given by a canonical absolute `path` in the archive.
+    ///
+    /// This will actively navigate the archive by calling the corresponding
+    /// decoder functionalities and is therefore very expensive.
+    fn lookup(
+        &mut self,
+        absolute_path: &[DirEntry],
+    ) -> Result<(DirectoryEntry, PxarAttributes, u64), Error> {
+        let mut current = self.decoder.root()?;
+        let (_, _, mut attr, mut size) = self.decoder.attributes(0)?;
+        // Ignore the archive root, don't need it.
+        for item in absolute_path.iter().skip(1) {
+            match self
+                .decoder
+                .lookup(&current, &OsStr::from_bytes(&item.name))?
+            {
+                Some((item, item_attr, item_size)) => {
+                    current = item;
+                    attr = item_attr;
+                    size = item_size;
                 }
-                prefix.extend_from_slice(component);
-                prefix.push(b'/');
-            }
-        }
-        let entries = match ctx.catalog.read_dir(&current.last().unwrap()) {
-            Ok(entries) => entries,
-            Err(_) => return Vec::new(),
-        };
-        (prefix, entries)
-    };
-    // Create a list of completion strings which outlives this function
-    let mut list = Vec::new();
-    for entry in &entries {
-        if entry.name.starts_with(to_complete) {
-            let mut name_buf = prefix.clone();
-            name_buf.extend_from_slice(&entry.name);
-            if entry.is_directory() {
-                name_buf.push(b'/');
+                // This should not happen if catalog an archive are consistent.
+                None => bail!("no such file or directory in archive - inconsistent catalog"),
             }
-            let name = unsafe { CString::from_vec_unchecked(name_buf) };
-            list.push(name);
         }
+        Ok((current, attr, size))
     }
-    list
 }