summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/error.rs42
-rw-r--r--src/main.rs62
-rw-r--r--src/path.rs145
-rw-r--r--src/path/error.rs35
4 files changed, 259 insertions, 25 deletions
diff --git a/src/error.rs b/src/error.rs
index 8e51880..018d812 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,4 +1,5 @@
 use crate::path::GenericPath;
+use crate::path::error::{FileNameError, DirectoryNameError};
 
 
 type ParseError<'a> =
@@ -8,10 +9,13 @@ type ParseError<'a> =
 pub enum Error {
   IO(std::io::Error),
   Parse(String),
+  FileName(FileNameError),
+  DirectoryName(DirectoryNameError),
   PathListHasEmptyComponents(String),
-  PathIsAFile(GenericPath),
-  PathIsRelative(GenericPath),
-  PathInvalid(GenericPath),
+  PathLexicallyDirectory(GenericPath),
+  PathLexicallyRelative(GenericPath),
+  PathLexicallyInvalid(GenericPath),
+  PathEmpiricallyFile(GenericPath),
 }
 
 impl std::error::Error for Error { }
@@ -21,26 +25,39 @@ impl std::fmt::Display for Error {
     match self {
       Error::IO(e) => e.fmt(f),
       Error::Parse(e) => e.fmt(f),
+      Error::FileName(e) => e.fmt(f),
+      Error::DirectoryName(e) => e.fmt(f),
       Error::PathListHasEmptyComponents(path_list) =>
         f.write_fmt(format_args!(
             "Path list has empty components: {}",
             path_list)),
-      Error::PathIsAFile(path) =>
+      Error::PathLexicallyDirectory(path) =>
         f.write_fmt(format_args!(
-            "There's a file at {}, not a directory.",
+            "The path {} ends in a slash, but is supposed to refer to a file, \
+             not a directory.",
             path)),
-      Error::PathIsRelative(path) =>
+      Error::PathLexicallyRelative(path) =>
         f.write_fmt(format_args!(
             "The path {} is relative, not absolute.",
             path)),
-      Error::PathInvalid(path) =>
+      Error::PathLexicallyInvalid(path) =>
         f.write_fmt(format_args!(
             "This isn't a valid path. {}",
             path)),
+      Error::PathEmpiricallyFile(path) =>
+        f.write_fmt(format_args!(
+            "There's a file at {}, not a directory.",
+            path)),
     }
   }
 }
 
+impl From<()> for Error {
+  fn from(_: ()) -> Error {
+    unreachable!()
+  }
+}
+
 impl From<std::io::Error> for Error {
   fn from(e: std::io::Error) -> Error {
     Error::IO(e)
@@ -53,3 +70,14 @@ impl From<ParseError<'_>> for Error {
   }
 }
 
+impl From<FileNameError> for Error {
+  fn from(e: FileNameError) -> Error {
+    Error::FileName(e)
+  }
+}
+
+impl From<DirectoryNameError> for Error {
+  fn from(e: DirectoryNameError) -> Error {
+    Error::DirectoryName(e)
+  }
+}
diff --git a/src/main.rs b/src/main.rs
index 60c14ca..3b7d26d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,9 @@
 use crate::prelude::*;
+use std::collections::HashMap;
+use std::collections::HashSet;
 use std::io;
 use std::io::prelude::*;
+use std::os::unix::fs::PermissionsExt;
 
 #[macro_use] extern crate lalrpop_util;
 
@@ -71,10 +74,29 @@ fn execute(input: &str) -> Result<()> {
   println!("{}", input);
 
   match invocation.as_slice() {
-    ["paths", path_list, ..] => {
-      let paths = path::parse_path_list(path_list)?;
-      for path in &paths {
-        println!("{}", path);
+    ["environment", ..] => {
+      let environment = read_environment()?;
+      println!("{:?}", environment);
+    }
+    ["which", command_name, ..] => {
+      let file_name: path::FileName = command_name.parse()?;
+      let search_paths = get_search_paths()?;
+      let mut executable_path: Option<path::AbsoluteFilePath> = None;
+      for search_path in &search_paths {
+        let candidate_path = search_path.concat_file_name(&file_name);
+        match candidate_path.to_sys_path().metadata() {
+          Ok(metadata) => {
+            if metadata.is_file()
+              && metadata.permissions().mode() & 0o111 != 0
+            {
+              println!("{} {:?}", candidate_path, metadata);
+              executable_path = Some(candidate_path);
+              break;
+            }
+          },
+          Err(_) => { },
+        }
+
       }
     },
     _ => {
@@ -84,3 +106,35 @@ fn execute(input: &str) -> Result<()> {
 
   Ok(())
 }
+
+
+fn read_environment() -> Result<HashMap<String,String>> {
+  Ok(std::env::vars().collect())
+}
+
+
+fn get_environment(variable_name: &str) -> Result<Option<String>> {
+  Ok(std::env::vars()
+     .find(|(key, _)| key == variable_name)
+     .map(|(_, value)| value))
+}
+
+
+fn get_search_paths() -> Result<Vec<path::AbsoluteDirectoryPath>> {
+  let paths = get_environment("PATH")?.unwrap_or_default();
+  let paths = path::parse_path_list(&paths)?;
+
+  let mut result = Vec::new();
+  let mut seen = HashSet::new();
+  for path in paths {
+    if seen.contains(&path) {
+      continue;
+    }
+
+    seen.insert(path.clone());
+    result.push(path);
+  }
+
+  Ok(result)
+}
+
diff --git a/src/path.rs b/src/path.rs
index 56855ac..2e599bf 100644
--- a/src/path.rs
+++ b/src/path.rs
@@ -7,29 +7,37 @@
  */
 
 use crate::prelude::*;
+use crate::path::error::*;
 
 lalrpop_mod!(pub parser, "/path/parser.rs");
+pub mod error;
 
 
-#[derive(Debug)]
+#[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)]
 pub struct AbsoluteDirectoryPath {
-  components: Vec<DirectoryName>,
+  directory_names: Vec<DirectoryName>,
 }
 
-#[derive(Debug)]
+#[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)]
+pub struct AbsoluteFilePath {
+  directory_names: Vec<DirectoryName>,
+  file_name: FileName,
+}
+
+#[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)]
 pub struct FileName(String);
 
-#[derive(Debug)]
+#[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)]
 pub struct DirectoryName(String);
 
-#[derive(Debug)]
+#[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)]
 pub struct GenericPath {
   components: Vec<GenericPathComponent>,
   starts_with_slash: bool,
   ends_with_slash: bool,
 }
 
-#[derive(Debug)]
+#[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)]
 pub enum GenericPathComponent {
   FileOrDirectoryName(String),
   CurrentDirectory,
@@ -39,13 +47,29 @@ pub enum GenericPathComponent {
 
 impl std::fmt::Display for AbsoluteDirectoryPath {
   fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-    for component in &self.components {
+    for directory_name in &self.directory_names {
       f.write_str("/")?;
-      component.fmt(f)?;
+      directory_name.fmt(f)?;
+    }
+
+    f.write_str("/")?;
+
+    Ok(())
+  }
+}
+
+
+impl std::fmt::Display for AbsoluteFilePath {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    for directory_name in &self.directory_names {
+      f.write_str("/")?;
+      directory_name.fmt(f)?;
     }
 
     f.write_str("/")?;
 
+    self.file_name.fmt(f)?;
+
     Ok(())
   }
 }
@@ -125,6 +149,57 @@ impl std::fmt::Display for GenericPathComponent {
 }
 
 
+impl std::str::FromStr for FileName {
+  type Err = FileNameError;
+
+  fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
+    if input.find('/').is_some() {
+      Err(FileNameError::ContainsSlash(input.to_string()))
+    } else {
+      Ok(FileName(input.to_string()))
+    }
+  }
+}
+
+
+impl std::str::FromStr for DirectoryName {
+  type Err = DirectoryNameError;
+
+  fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
+    if input.find('/').is_some() {
+      Err(DirectoryNameError::ContainsSlash(input.to_string()))
+    } else {
+      Ok(DirectoryName(input.to_string()))
+    }
+  }
+}
+
+
+impl AbsoluteDirectoryPath {
+  pub fn to_sys_path(&self) -> std::path::PathBuf {
+    let mut result = std::path::PathBuf::new();
+    result.push(format!("{}", self));
+    result
+  }
+
+  pub fn concat_file_name(&self, file_name: &FileName) -> AbsoluteFilePath {
+    AbsoluteFilePath {
+      directory_names: self.directory_names.clone(),
+      file_name: file_name.clone(),
+    }
+  }
+}
+
+
+impl AbsoluteFilePath {
+  pub fn to_sys_path(&self) -> std::path::PathBuf {
+    let mut result = std::path::PathBuf::new();
+    result.push(format!("{}", self));
+    result
+  }
+}
+
+
 pub fn parse_path_list(path_list: &str)
   -> Result<Vec<AbsoluteDirectoryPath>>
 {
@@ -157,7 +232,7 @@ pub fn absolute_directory_path(generic_path: GenericPath)
   -> Result<AbsoluteDirectoryPath>
 {
   if !generic_path.starts_with_slash {
-    return Err(Error::PathIsRelative(generic_path));
+    return Err(Error::PathLexicallyRelative(generic_path));
   }
 
   let mut flattened_components = Vec::new();
@@ -168,7 +243,7 @@ pub fn absolute_directory_path(generic_path: GenericPath)
         if flattened_components.len() > 0 {
           flattened_components.pop();
         } else {
-          return Err(Error::PathInvalid(generic_path));
+          return Err(Error::PathLexicallyInvalid(generic_path));
         }
       },
       GenericPathComponent::FileOrDirectoryName(name) => {
@@ -177,12 +252,54 @@ pub fn absolute_directory_path(generic_path: GenericPath)
     }
   }
 
-  if flattened_components.len() == 0 {
-    return Err(Error::PathInvalid(generic_path));
+  Ok(AbsoluteDirectoryPath {
+    directory_names: flattened_components,
+  })
+}
+
+
+pub fn absolute_file_path(generic_path: GenericPath)
+  -> Result<AbsoluteFilePath>
+{
+  if !generic_path.starts_with_slash {
+    return Err(Error::PathLexicallyRelative(generic_path));
   }
 
-  Ok(AbsoluteDirectoryPath {
-    components: flattened_components,
+  if generic_path.ends_with_slash {
+    return Err(Error::PathLexicallyDirectory(generic_path));
+  }
+
+  let mut iterator = generic_path.components.iter();
+
+  let file_name = match iterator.next_back() {
+    Some(GenericPathComponent::FileOrDirectoryName(name)) => {
+      FileName(name.to_string())
+    }
+    _ => {
+      return Err(Error::PathLexicallyInvalid(generic_path));
+    }
+  };
+
+  let mut flattened_components = Vec::new();
+  for component in &generic_path.components {
+    match component {
+      GenericPathComponent::CurrentDirectory => { },
+      GenericPathComponent::ParentDirectory => {
+        if flattened_components.len() > 0 {
+          flattened_components.pop();
+        } else {
+          return Err(Error::PathLexicallyInvalid(generic_path));
+        }
+      },
+      GenericPathComponent::FileOrDirectoryName(name) => {
+        flattened_components.push(DirectoryName(name.to_string()));
+      },
+    }
+  }
+
+  Ok(AbsoluteFilePath {
+    directory_names: flattened_components,
+    file_name: file_name,
   })
 }
 
diff --git a/src/path/error.rs b/src/path/error.rs
new file mode 100644
index 0000000..1162a3b
--- /dev/null
+++ b/src/path/error.rs
@@ -0,0 +1,35 @@
+#[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)]
+pub enum FileNameError {
+  ContainsSlash(String),
+}
+
+#[derive(Clone,Debug,Eq,Hash,Ord,PartialEq,PartialOrd)]
+pub enum DirectoryNameError {
+  ContainsSlash(String),
+}
+
+
+impl std::error::Error for FileNameError { }
+
+impl std::error::Error for DirectoryNameError { }
+
+impl std::fmt::Display for FileNameError {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    match self {
+      FileNameError::ContainsSlash(s) =>
+        f.write_fmt(format_args!(
+            "File names cannot contain slashes, but {:?} does.", s)),
+    }
+  }
+}
+
+impl std::fmt::Display for DirectoryNameError {
+  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+    match self {
+      DirectoryNameError::ContainsSlash(s) =>
+        f.write_fmt(format_args!(
+            "File names cannot contain slashes, but {:?} does.", s)),
+    }
+  }
+}
+