diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..83c5ccc6b49cbee9d31080b7faeba93a22191755
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+### Rust ###
+# Generated by Cargo
+# will have compiled files and executables
+debug/
+target/
+
+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
+Cargo.lock
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..09cf6e97d88f9dab05e6fcbeb305f506d7c73ccb
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,26 @@
+image:
+  name: cr.weirdboi.dev/base/rust:1-82
+  pull_policy: always
+stages:
+  - test
+cache: &global_cache
+  key: ${CI_COMMIT_REF_SLUG}
+  paths:
+    - .cargo/bin
+    - .cargo/registry/index
+    - .cargo/registry/cache
+    - target/debug/deps
+    - target/debug/build
+  policy: pull-push
+
+variables:
+  CARGO_HOME: ${CI_PROJECT_DIR}/.cargo
+
+test:
+  stage: test
+  script:
+    - cargo +nightly test --workspace -- --format=json -Z unstable-options --report-time | junitify --out $CI_PROJECT_DIR/tests/
+  artifacts:
+    when: always
+    reports:
+      junit: $CI_PROJECT_DIR/tests/*.xml
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..13566b81b018ad684f3a35fee301741b2734c8f4
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/envish.iml b/.idea/envish.iml
new file mode 100644
index 0000000000000000000000000000000000000000..bbe0a70f71c5b253c29e73ecf956e6f6ee2bfb13
--- /dev/null
+++ b/.idea/envish.iml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="EMPTY_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/target" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e7b282bfbac670d848dad1aa9b5d542ecb54a38e
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/envish.iml" filepath="$PROJECT_DIR$/.idea/envish.iml" />
+    </modules>
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000000000000000000000000000000000000..94a25f7f4cb416c083d265558da75d457237d671
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..5a3411970fd4e04ee06ebc49546f89902f014c33
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "envish"
+description = "A library for sourcing, reading, and interacting with .env files, including interpolation support"
+version = "0.1.0"
+edition = "2021"
+
+[features]
+default = ["fs"]
+fs = []
+
+[dependencies]
+nom = "7.1.3"
+nom_locate = "4.2.0"
+thiserror = "2.0.3"
diff --git a/src/env_file.rs b/src/env_file.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c013c255468d54cc7f7167cd81ef75f25c1ecf9b
--- /dev/null
+++ b/src/env_file.rs
@@ -0,0 +1,287 @@
+use std::env;
+use std::env::VarError;
+use crate::parser::{file, FileLine};
+use nom_locate::LocatedSpan;
+use std::str::FromStr;
+use std::string::ParseError;
+
+#[derive(Clone, Debug, Default)]
+pub struct EnvironmentFile {
+	lines: Vec<FileLine>,
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum EnvironmentFileError {
+	#[error("Unable to determine eof marker")]
+	InvalidEof,
+	#[error("Parse error at line {line}, column {column}: {kind:?}")]
+	ParseError {
+		line: usize,
+		column: usize,
+		kind: nom::error::ErrorKind,
+	},
+	#[error(transparent)]
+	StdParseError(#[from] ParseError),
+	#[error("Value could not be determined to be valid UTF-8")]
+	InvalidValue,
+}
+
+impl FromStr for EnvironmentFile {
+	type Err = EnvironmentFileError;
+
+	fn from_str(s: &str) -> Result<Self, Self::Err> {
+		let (_, lines) = file(LocatedSpan::new(s)).map_err(|err| match err {
+			nom::Err::Incomplete(_) => EnvironmentFileError::InvalidEof,
+			nom::Err::Error(err) | nom::Err::Failure(err) => EnvironmentFileError::ParseError {
+				column: err.input.get_column(),
+				line: err.input.location_line() as usize,
+				kind: err.code,
+			},
+		})?;
+
+		Ok(Self::new(lines))
+	}
+}
+
+fn is_kv_line(line: &FileLine) -> bool {
+	matches!(line, FileLine::KeyValue { .. })
+}
+
+fn is_comment_line(line: &FileLine) -> bool {
+	matches!(line, FileLine::Comment(_))
+}
+
+impl EnvironmentFile {
+	pub fn new(lines: Vec<FileLine>) -> Self {
+		Self { lines }
+	}
+
+	pub fn parse(value: impl AsRef<str>) -> Result<Self, EnvironmentFileError> {
+		value.as_ref().parse()
+	}
+
+	pub fn iter(&self) -> EnvFileIterator {
+		EnvFileIterator {
+			lines: &self.lines,
+			current: 0,
+		}
+	}
+
+	pub fn lines_kv(&self) -> SubTypeFileIterator {
+		SubTypeFileIterator {
+			lines: &self.lines,
+			current: 0,
+			predicate: is_kv_line,
+		}
+	}
+
+	pub fn lines_comment(&self) -> SubTypeFileIterator {
+		SubTypeFileIterator {
+			lines: &self.lines,
+			current: 0,
+			predicate: is_comment_line,
+		}
+	}
+
+	pub fn get_raw(&self, key: &str) -> Option<String> {
+		for line in &self.lines {
+			if let FileLine::KeyValue { key: k, value } = line {
+				if k == key {
+					return Some(value.iter().map(|part| part.to_string()).collect::<String>());
+				}
+			}
+		}
+
+		None
+	}
+
+	pub fn len(&self) -> usize {
+		self.lines.len()
+	}
+
+	pub fn is_empty(&self) -> bool {
+		self.lines.is_empty()
+	}
+
+	pub fn apply(&self) -> Result<(), EnvironmentFileError> {
+		set_from_file(self)
+	}
+}
+
+pub struct EnvFileIterator<'a> {
+	lines: &'a [FileLine],
+	current: usize,
+}
+
+impl<'a> Iterator for EnvFileIterator<'a> {
+	type Item = &'a FileLine;
+
+	fn next(&mut self) -> Option<Self::Item> {
+		if self.current >= self.lines.len() {
+			None
+		} else {
+			let line = &self.lines[self.current];
+			self.current += 1;
+			Some(line)
+		}
+	}
+}
+
+impl ExactSizeIterator for EnvFileIterator<'_> {
+	fn len(&self) -> usize {
+		self.lines.len() - self.current
+	}
+}
+
+pub struct SubTypeFileIterator<'a> {
+	lines: &'a [FileLine],
+	current: usize,
+	predicate: fn(&FileLine) -> bool,
+}
+
+impl<'a> Iterator for SubTypeFileIterator<'a> {
+	type Item = &'a FileLine;
+
+	fn next(&mut self) -> Option<Self::Item> {
+		let mut found = None;
+
+		while self.current < self.lines.len() {
+			let line = &self.lines[self.current];
+			self.current += 1;
+			if (self.predicate)(line) {
+				found = Some(line);
+				break;
+			}
+		}
+
+		found
+	}
+}
+
+fn is_missing_key(key: &str) -> Result<bool, EnvironmentFileError> {
+	match env::var(key) {
+		Ok(_) => Ok(false),
+		Err(VarError::NotPresent) => Ok(true),
+		Err(_) => Err(EnvironmentFileError::InvalidValue),
+	}
+}
+
+fn set_from_file(file: &EnvironmentFile) -> Result<(), EnvironmentFileError> {
+	let mut defferred = Vec::with_capacity(file.len());
+
+	for line in file.lines_kv() {
+		if let FileLine::KeyValue { key, .. } = &line {
+			if is_missing_key(key)? {
+				if line.is_complete() {
+					env::set_var(key, line.assemble_value());
+				} else {
+					defferred.push(line);
+				}
+			}
+		}
+	}
+
+	for line in defferred {
+		if let FileLine::KeyValue { key, .. } = line {
+			env::set_var(key, line.assemble_value());
+		}
+	}
+
+	Ok(())
+}
+
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+	use crate::parser::ValuePart;
+
+	#[test]
+	fn test_lines_kv_iterator() {
+		let env_file = EnvironmentFile::new(vec![
+			FileLine::empty(),
+			FileLine::comment("# This is a comment"),
+			FileLine::key_value("KEY1".to_string(), vec![ValuePart::Static("VALUE1".to_string())]),
+			FileLine::empty(),
+			FileLine::key_value("KEY2".to_string(), vec![ValuePart::Static("VALUE2".to_string())]),
+		]);
+
+		let kv_lines: Vec<&FileLine> = env_file.lines_kv().collect();
+
+		assert_eq!(kv_lines.len(), 2);
+		assert!(
+			matches!(kv_lines[0], FileLine::KeyValue { key, value } if key == "KEY1" && value == &vec![ValuePart::Static("VALUE1".to_string())])
+		);
+		assert!(
+			matches!(kv_lines[1], FileLine::KeyValue { key, value } if key == "KEY2" && value == &vec![ValuePart::Static("VALUE2".to_string())])
+		);
+	}
+
+	#[test]
+	fn test_lines_comment_iterator() {
+		let env_file = EnvironmentFile::new(vec![
+			FileLine::empty(),
+			FileLine::comment(" Comment 1"),
+			FileLine::key_value("KEY1".to_string(), vec![ValuePart::Static("VALUE1".to_string())]),
+			FileLine::comment(" Comment 2"),
+			FileLine::key_value("KEY2".to_string(), vec![ValuePart::Static("VALUE2".to_string())]),
+			FileLine::comment(" Comment 3"),
+		]);
+
+		let comment_lines: Vec<&FileLine> = env_file.lines_comment().collect();
+
+		assert_eq!(comment_lines.len(), 3);
+		assert!(matches!(comment_lines[0], FileLine::Comment(comment) if comment == " Comment 1"));
+		assert!(matches!(comment_lines[1], FileLine::Comment(comment) if comment == " Comment 2"));
+		assert!(matches!(comment_lines[2], FileLine::Comment(comment) if comment == " Comment 3"));
+	}
+
+	#[test]
+	fn test_iter_returns_all_lines() {
+		let env_file = EnvironmentFile::new(vec![
+			FileLine::empty(),
+			FileLine::comment("This is a comment"),
+			FileLine::key_value("KEY1".to_string(), vec![ValuePart::Static("VALUE1".to_string())]),
+			FileLine::empty(),
+			FileLine::key_value("KEY2".to_string(), vec![ValuePart::Static("VALUE2".to_string())]),
+		]);
+
+		let all_lines: Vec<&FileLine> = env_file.iter().collect();
+
+		assert_eq!(all_lines.len(), 5);
+		assert!(matches!(all_lines[0], FileLine::Empty));
+		assert!(matches!(all_lines[1], FileLine::Comment(comment) if comment == "This is a comment"));
+		assert!(
+			matches!(all_lines[2], FileLine::KeyValue { key, value } if key == "KEY1" && value == &vec![ValuePart::Static("VALUE1".to_string())])
+		);
+		assert!(matches!(all_lines[3], FileLine::Empty));
+		assert!(
+			matches!(all_lines[4], FileLine::KeyValue { key, value } if key == "KEY2" && value == &vec![ValuePart::Static("VALUE2".to_string())])
+		);
+	}
+
+	#[test]
+	fn test_lines_kv_maintains_order() {
+		let env_file = EnvironmentFile::new(vec![
+			FileLine::empty(),
+			FileLine::key_value("KEY1".to_string(), vec![ValuePart::Static("VALUE1".to_string())]),
+			FileLine::comment("Comment"),
+			FileLine::key_value("KEY2".to_string(), vec![ValuePart::Static("VALUE2".to_string())]),
+			FileLine::key_value("KEY3".to_string(), vec![ValuePart::Static("VALUE3".to_string())]),
+			FileLine::empty(),
+		]);
+
+		let kv_lines: Vec<&FileLine> = env_file.lines_kv().collect();
+
+		assert_eq!(kv_lines.len(), 3);
+		assert!(
+			matches!(kv_lines[0], FileLine::KeyValue { key, value } if key == "KEY1" && value == &vec![ValuePart::Static("VALUE1".to_string())])
+		);
+		assert!(
+			matches!(kv_lines[1], FileLine::KeyValue { key, value } if key == "KEY2" && value == &vec![ValuePart::Static("VALUE2".to_string())])
+		);
+		assert!(
+			matches!(kv_lines[2], FileLine::KeyValue { key, value } if key == "KEY3" && value == &vec![ValuePart::Static("VALUE3".to_string())])
+		);
+	}
+}
diff --git a/src/filesystem.rs b/src/filesystem.rs
new file mode 100644
index 0000000000000000000000000000000000000000..af50b9f5c720952614b73dad5fc3ca48b245c7e1
--- /dev/null
+++ b/src/filesystem.rs
@@ -0,0 +1,47 @@
+use crate::{EnvironmentFile};
+use std::fmt::Display;
+use std::fs::File;
+use std::io::Read;
+use std::path::Path;
+
+#[derive(Debug, thiserror::Error)]
+#[allow(clippy::enum_variant_names)]
+pub enum EnvFsError {
+	#[error(transparent)]
+	IoError(#[from] std::io::Error),
+	#[error(transparent)]
+	ParseError(#[from] crate::EnvironmentFileError),
+	#[error("The target environment variable exists, but is not Unicode")]
+	EnvironmentError,
+}
+
+pub fn env_file() -> Result<EnvironmentFile, EnvFsError> {
+	env_file_from_path(".env")
+}
+
+pub fn env_file_suffix(environment: impl Display) -> Result<EnvironmentFile, EnvFsError> {
+	env_file_from_path(format!(".env.{}", environment))
+}
+
+pub fn env_file_from_path(path: impl AsRef<Path>) -> Result<EnvironmentFile, EnvFsError> {
+	let mut file = File::open(path)?;
+	let mut buffer = String::new();
+	file.read_to_string(&mut buffer)?;
+	Ok(buffer.parse()?)
+}
+
+pub fn dotenv() -> Result<(), EnvFsError> {
+	let file = env_file()?;
+	file.apply().map_err(|_| EnvFsError::EnvironmentError)
+}
+
+pub fn dotenv_suffix(environment: impl Display) -> Result<(), EnvFsError> {
+	let file = env_file_suffix(environment)?;
+	file.apply().map_err(|_| EnvFsError::EnvironmentError)
+}
+
+pub fn dotenv_from(path: impl AsRef<Path>) -> Result<(), EnvFsError> {
+	let file = env_file_from_path(path)?;
+	file.apply().map_err(|_| EnvFsError::EnvironmentError)
+}
+
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..82f59c5ca4aca804c4c0244938c53264d586be73
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,12 @@
+#![allow(unused_labels)]
+
+mod env_file;
+#[cfg(feature = "fs")]
+mod filesystem;
+mod parser;
+
+pub use env_file::{EnvironmentFile, EnvironmentFileError};
+pub use parser::{FileLine, ValuePart};
+
+#[cfg(feature = "fs")]
+pub use filesystem::{dotenv, dotenv_from, dotenv_suffix};
diff --git a/src/parser.rs b/src/parser.rs
new file mode 100644
index 0000000000000000000000000000000000000000..efb5b59c57b7572331c03b1095d9a309e8995550
--- /dev/null
+++ b/src/parser.rs
@@ -0,0 +1,655 @@
+use nom::bytes::complete::take_while1;
+use nom::character::complete::space0;
+use nom::error::{context, ErrorKind};
+use nom::multi::separated_list0;
+use nom::sequence::{delimited, separated_pair};
+use nom::{
+	branch::alt,
+	bytes::complete::{is_not, tag},
+	combinator::map,
+	multi::many0,
+	sequence::preceded,
+	IResult, InputTake,
+};
+use std::fmt::Display;
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum ValuePart {
+	Static(String),
+	Variable(String),
+	Comment(String),
+}
+
+impl Display for ValuePart {
+	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+		match self {
+			Self::Static(static_value) => write!(f, "{}", static_value),
+			Self::Variable(variable_name) => write!(f, "${{{}}}", variable_name),
+			Self::Comment(comment_value) => write!(f, "#{}", comment_value),
+		}
+	}
+}
+
+impl ValuePart {
+	pub fn new(value: impl ToString) -> Self {
+		Self::Static(value.to_string())
+	}
+	pub fn variable(value: impl ToString) -> Self {
+		Self::Variable(value.to_string())
+	}
+	pub fn comment(value: impl ToString) -> Self {
+		Self::Comment(value.to_string())
+	}
+
+	fn var_from_span(span: Span) -> Self {
+		Self::Variable(span.fragment().to_string())
+	}
+
+	fn static_from_span(span: Span) -> Self {
+		Self::Static(span.fragment().to_string())
+	}
+
+	#[allow(dead_code)]
+	fn static_from_string(span: String) -> Self {
+		Self::Static(span)
+	}
+
+	fn comment_from_span(span: Span) -> Self {
+		Self::Comment(span.fragment().to_string())
+	}
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum FileLine {
+	Empty,
+	Comment(String),
+	KeyValue { key: String, value: Vec<ValuePart> },
+}
+
+impl Display for FileLine {
+	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+		match self {
+			Self::Empty => write!(f, ""),
+			Self::Comment(comment) => write!(f, "#{}", comment),
+			Self::KeyValue { key, value } => {
+				write!(f, "{}={}", key, value.iter().map(|part| part.to_string()).collect::<String>())
+			}
+		}
+	}
+}
+
+impl FileLine {
+	pub fn empty() -> Self {
+		Self::Empty
+	}
+
+	pub fn comment(comment: impl ToString) -> Self {
+		Self::Comment(comment.to_string())
+	}
+
+	pub fn key_value(key: impl ToString, value: impl ToOwned<Owned = Vec<ValuePart>>) -> Self {
+		Self::KeyValue {
+			key: key.to_string(),
+			value: value.to_owned(),
+		}
+	}
+
+	fn empty_from_span(_span: Span) -> Self {
+		Self::Empty
+	}
+	fn key_value_form_span_pair((key_span, value): (Span, Vec<ValuePart>)) -> Self {
+		Self::KeyValue {
+			key: key_span.fragment().to_string(),
+			value,
+		}
+	}
+	fn comment_from_span(span: Span) -> Self {
+		Self::Comment(span.fragment().to_string())
+	}
+
+	/// Whether this line is entirely self contained. If the line is a kv pair that requires
+	/// interpolation, it is not considered complete.
+	pub fn is_complete(&self) -> bool {
+		match self {
+			Self::KeyValue { value, .. } => value.iter().all(|part| !matches!(part, &ValuePart::Variable(..))),
+			_ => true,
+		}
+	}
+
+	pub fn strip_comments(&self) -> Self {
+		match self {
+			Self::Empty => Self::Empty,
+			Self::Comment(_) => Self::Empty,
+			Self::KeyValue { key, value } => Self::KeyValue {
+				key: key.clone(),
+				value: value
+					.into_iter()
+					.filter(|part| !matches!(part, ValuePart::Comment(..)))
+					.cloned()
+					.collect(),
+			},
+		}
+	}
+
+	/// Convert the line into a complete value string
+	pub fn assemble_value(&self) -> String {
+		match self {
+			Self::Empty => String::new(),
+			Self::Comment(_) => String::new(),
+			Self::KeyValue { value, .. } => value
+				.iter()
+				.map(|part| match part {
+					ValuePart::Static(val) => val.clone(),
+					ValuePart::Variable(var) => std::env::var(var).unwrap_or_default(),
+					ValuePart::Comment(_) => String::new(),
+				})
+				.collect::<String>(),
+		}
+	}
+}
+
+type Span<'a> = nom_locate::LocatedSpan<&'a str>;
+
+/// Takes everything until the end of line.
+fn take_until_eol(input: Span) -> IResult<Span, Span> {
+	if input.fragment().starts_with('\n') {
+		return Ok((input, unsafe {
+			Span::new_from_raw_offset(input.location_offset(), input.location_line(), "", ())
+		}));
+	}
+	is_not("\n")(input)
+}
+
+fn is_ident_first_char(c: char) -> bool {
+	c.is_alphabetic() || c == '_'
+}
+fn is_ident_any_char(c: char) -> bool {
+	c.is_alphanumeric() || c == '_'
+}
+
+fn identifier(input: Span) -> IResult<Span, Span> {
+	let mut last_index = 0;
+	let mut input_chars = input.chars().peekable();
+	let mut valid = match input_chars.next() {
+		Some(c) => is_ident_first_char(c),
+		None => false,
+	};
+
+	'checkloop: while valid {
+		last_index += 1;
+		let next_char = input_chars.next();
+		if next_char.is_none() {
+			break 'checkloop;
+		}
+		valid = next_char.map(is_ident_any_char).unwrap_or(false);
+	}
+
+	if last_index == 0 {
+		Err(nom::Err::Error(nom::error::Error::new(input, ErrorKind::Fail)))
+	} else {
+		Ok(input.take_split(last_index))
+	}
+}
+
+fn value_separator(input: Span) -> IResult<Span, Span> {
+	context("Value Separator", delimited(space0, tag("="), space0))(input)
+}
+
+fn values(input: Span) -> IResult<Span, Vec<ValuePart>> {
+	context(
+		"Environment Variable Values",
+		many0(alt((
+			preceded(tag("#"), map(take_until_eol, ValuePart::comment_from_span)),
+			preceded(tag("$"), map(identifier, ValuePart::var_from_span)),
+			delimited(tag("${"), map(identifier, ValuePart::var_from_span), tag("}")),
+			map(
+				take_while1(|c: char| c != '$' && c != '\n' && c != '#'),
+				ValuePart::static_from_span,
+			),
+		))),
+	)(input)
+}
+
+fn line(input: Span) -> IResult<Span, FileLine> {
+	context(
+		"Environment Variable Line",
+		delimited(
+			space0,
+			alt((
+				map(preceded(tag("#"), take_until_eol), FileLine::comment_from_span),
+				map(
+					separated_pair(identifier, value_separator, values),
+					FileLine::key_value_form_span_pair,
+				),
+				map(take_until_eol, FileLine::empty_from_span), // Empty line
+			)),
+			space0,
+		),
+	)(input)
+}
+
+pub(crate) fn file(input: Span) -> IResult<Span, Vec<FileLine>> {
+	context("Environment File", separated_list0(tag("\n"), line))(input)
+}
+
+#[cfg(test)]
+mod tests {
+	use super::*;
+
+	#[test]
+	fn test_take_until_eol_empty_line() {
+		let input = Span::new(" \n");
+		let result = take_until_eol(input);
+		let (remaining, parsed) = result.expect("Failed to parse");
+
+		assert_eq!(parsed.fragment(), &" ");
+		assert_eq!(remaining.fragment(), &"\n");
+	}
+
+	#[test]
+	fn test_take_until_eol_whitespace_only() {
+		let input = Span::new("    \t  ");
+		let result = take_until_eol(input);
+		let (remaining, parsed) = result.expect("Failed to parse");
+
+		assert_eq!(parsed.fragment(), &"    \t  ");
+		assert_eq!(remaining.fragment(), &"");
+	}
+
+	#[test]
+	fn test_identifier_alphanumeric() {
+		let input = Span::new("abc123_XYZ");
+		let result = identifier(input);
+		let (remaining, parsed) = result.expect("Failed to parse identifier");
+
+		assert_eq!(parsed.fragment(), &"abc123_XYZ");
+		assert_eq!(remaining.fragment(), &"");
+	}
+
+	#[test]
+	fn test_identifier_starting_with_number() {
+		let input = Span::new("123abc");
+		let result = identifier(input);
+		assert!(result.is_err(), "Expected an error for identifier starting with a number");
+	}
+
+	#[test]
+	fn test_identifier_mixed_case() {
+		let input = Span::new("abcDEF123");
+		let result = identifier(input);
+		let (remaining, parsed) = result.expect("Failed to parse identifier");
+
+		assert_eq!(parsed.fragment(), &"abcDEF123");
+		assert_eq!(remaining.fragment(), &"");
+	}
+
+	#[test]
+	fn test_identifier_at_end_of_input() {
+		let input = Span::new("ABC_123\n");
+		let result = identifier(input);
+		let (remaining, parsed) = result.expect("Failed to parse identifier");
+
+		assert_eq!(parsed.fragment(), &"ABC_123");
+		assert_eq!(remaining.fragment(), &"\n");
+	}
+
+	#[test]
+	fn test_identifier_longest_valid() {
+		let input = Span::new("abc123_XYZ@invalid");
+		let result = identifier(input);
+		let (remaining, parsed) = result.expect("Failed to parse identifier");
+
+		assert_eq!(parsed.fragment(), &"abc123_XYZ");
+		assert_eq!(remaining.fragment(), &"@invalid");
+	}
+
+	#[test]
+	fn test_value_separator_multiple_spaces_before() {
+		let input = Span::new("   =   value");
+		let result = value_separator(input);
+		let (remaining, parsed) = result.expect("Failed to parse value separator");
+
+		assert_eq!(parsed.fragment(), &"=");
+		assert_eq!(remaining.fragment(), &"value");
+	}
+
+	#[test]
+	fn test_value_separator_multiple_spaces_after() {
+		let input = Span::new("= \t  ");
+		let result = value_separator(input);
+		let (remaining, parsed) = result.expect("Failed to parse value separator");
+
+		assert_eq!(parsed.fragment(), &"=");
+		assert_eq!(remaining.fragment(), &"");
+	}
+
+	#[test]
+	fn test_value_separator_missing_equals() {
+		let input = Span::new("KEY value");
+		let result = value_separator(input);
+		assert!(result.is_err(), "Expected an error for input without '=' separator");
+	}
+
+	#[test]
+	fn test_value_separator_at_end_of_file() {
+		let input = Span::new("=");
+		let result = value_separator(input);
+		let (remaining, parsed) = result.expect("Failed to parse value separator");
+
+		assert_eq!(parsed.fragment(), &"=");
+		assert_eq!(remaining.fragment(), &"");
+	}
+
+	#[test]
+	fn test_values_comment() {
+		let input = Span::new("# This is a comment");
+		let result = values(input);
+		let (remaining, parsed) = result.expect("Failed to parse values");
+
+		assert_eq!(parsed.len(), 1);
+		match &parsed[0] {
+			ValuePart::Comment(comment) => assert_eq!(comment, " This is a comment"),
+			_ => panic!("Expected a Comment ValuePart"),
+		}
+		assert_eq!(remaining.fragment(), &"");
+	}
+
+	#[test]
+	fn test_values_variable_with_dollar_prefix() {
+		let input = Span::new("$VAR_NAME");
+		let result = values(input);
+		let (remaining, parsed) = result.expect("Failed to parse values");
+
+		assert_eq!(parsed.len(), 1);
+		match &parsed[0] {
+			ValuePart::Variable(var_name) => assert_eq!(var_name, "VAR_NAME"),
+			_ => panic!("Expected a Variable ValuePart"),
+		}
+		assert_eq!(remaining.fragment(), &"");
+	}
+
+	#[test]
+	fn test_values_variable_with_curly_braces() {
+		let input = Span::new("${VAR_NAME}");
+		let result = values(input);
+		let (remaining, parsed) = result.expect("Failed to parse values");
+
+		assert_eq!(parsed.len(), 1);
+		match &parsed[0] {
+			ValuePart::Variable(var_name) => assert_eq!(var_name, "VAR_NAME"),
+			_ => panic!("Expected a Variable ValuePart"),
+		}
+		assert_eq!(remaining.fragment(), &"");
+	}
+
+	#[test]
+	fn test_values_static_part() {
+		let example_input = "THis is aosidnon409ansd0 9j0an oians**d6a7d98odin on";
+		let input = Span::new(example_input);
+		let result = values(input);
+		let (remaining, parsed) = result.expect("Failed to parse values");
+
+		assert_eq!(parsed.len(), 1);
+		match &parsed[0] {
+			ValuePart::Static(value) => assert_eq!(value, example_input),
+			_ => panic!("Expected a Static ValuePart"),
+		}
+		assert_eq!(remaining.fragment(), &"");
+	}
+
+	#[test]
+	fn test_values_multiple_parts() {
+		let input = Span::new("static1 $VAR1 ${VAR2} static2 # Comment");
+		let result = values(input);
+		let (_, parsed) = result.expect("Failed to parse values");
+
+		assert_eq!(
+			&parsed,
+			&[
+				ValuePart::Static(String::from("static1 ")),
+				ValuePart::Variable(String::from("VAR1")),
+				ValuePart::Static(String::from(" ")),
+				ValuePart::Variable(String::from("VAR2")),
+				ValuePart::Static(String::from(" static2 ")),
+				ValuePart::Comment(String::from(" Comment")),
+			]
+		);
+	}
+
+	#[test]
+	fn test_values_only_whitespace() {
+		let input = Span::new("   \t  ");
+		let result = values(input);
+		let (remaining, parsed) = result.expect("Failed to parse values");
+
+		assert_eq!(parsed.len(), 1);
+		match &parsed[0] {
+			ValuePart::Static(value) => assert_eq!(value, "   \t  "),
+			_ => panic!("Expected a Static ValuePart"),
+		}
+		assert_eq!(remaining.fragment(), &"");
+	}
+
+	#[test]
+	fn test_values_consecutive_variables() {
+		let input = Span::new("$VAR1${VAR2}$VAR3");
+		let result = values(input);
+		let (remaining, parsed) = result.expect("Failed to parse values");
+
+		assert_eq!(
+			parsed,
+			vec![
+				ValuePart::Variable("VAR1".to_string()),
+				ValuePart::Variable("VAR2".to_string()),
+				ValuePart::Variable("VAR3".to_string()),
+			]
+		);
+		assert_eq!(remaining.fragment(), &"");
+	}
+	#[test]
+	fn test_values_consecutive_variables_same_type() {
+		let input = Span::new("$VAR1$VAR2$VAR3");
+		let result = values(input);
+		let (remaining, parsed) = result.expect("Failed to parse values");
+
+		assert_eq!(
+			parsed,
+			vec![
+				ValuePart::Variable("VAR1".to_string()),
+				ValuePart::Variable("VAR2".to_string()),
+				ValuePart::Variable("VAR3".to_string()),
+			]
+		);
+		assert_eq!(remaining.fragment(), &"");
+	}
+
+	#[test]
+	fn test_line_comment_with_whitespace() {
+		let input = Span::new("  # This is a comment  \n");
+		let result = line(input);
+		let (remaining, parsed) = result.expect("Failed to parse line");
+
+		match parsed {
+			FileLine::Comment(comment) => assert_eq!(comment, " This is a comment  "),
+			_ => panic!("Expected a Comment FileLine"),
+		}
+		assert_eq!(remaining.fragment(), &"\n");
+	}
+
+	#[test]
+	fn test_line_key_value_pair_empty_value() {
+		let input = Span::new("KEY=");
+		let result = line(input);
+		let (remaining, parsed) = result.expect("Failed to parse line");
+
+		assert_eq!(remaining.fragment(), &"");
+		match parsed {
+			FileLine::KeyValue { key, value } => {
+				assert_eq!(key, "KEY");
+				assert!(value.is_empty());
+			}
+			_ => panic!("Expected KeyValue FileLine"),
+		}
+	}
+	#[test]
+	fn test_line_key_value_with_multiple_variables() {
+		let input = Span::new("KEY = value_${VAR1}_${VAR2}_$VAR3");
+		let result = line(input);
+		let (remaining, parsed) = result.expect("Failed to parse line");
+
+		assert_eq!(remaining.fragment(), &"");
+		match parsed {
+			FileLine::KeyValue { key, value } => {
+				assert_eq!(key, "KEY");
+				assert_eq!(value.len(), 6);
+				assert_eq!(value[0], ValuePart::Static("value_".to_string()));
+				assert_eq!(value[1], ValuePart::Variable("VAR1".to_string()));
+				assert_eq!(value[2], ValuePart::Static("_".to_string()));
+				assert_eq!(value[3], ValuePart::Variable("VAR2".to_string()));
+				assert_eq!(value[4], ValuePart::Static("_".to_string()));
+				assert_eq!(value[5], ValuePart::Variable("VAR3".to_string()));
+			}
+			other => panic!("Expected KeyValue FileLine, got {other:?}"),
+		}
+	}
+
+	#[test]
+	fn test_file_empty_input() {
+		let input = Span::new("");
+		let result = file(input);
+		let (remaining, parsed) = result.expect("Failed to parse empty file");
+
+		assert!(parsed.is_empty(), "Expected empty Vec for an empty input");
+		assert_eq!(remaining.fragment(), &"", "Expected remaining input to be empty");
+	}
+
+	#[test]
+	fn test_file_multiple_lines() {
+		let input = Span::new("KEY1=value1\n# Comment\nKEY2=value2\n");
+		let result = file(input);
+		let (remaining, parsed) = result.expect("Failed to parse file");
+
+		assert_eq!(remaining.fragment(), &"\n");
+		assert_eq!(parsed.len(), 3);
+
+		assert_eq!(
+			&parsed[0],
+			&FileLine::key_value("KEY1", vec![ValuePart::Static("value1".to_string())])
+		);
+		assert_eq!(&parsed[1], &FileLine::comment(" Comment"));
+		assert_eq!(
+			&parsed[2],
+			&FileLine::key_value("KEY2", vec![ValuePart::Static("value2".to_string())])
+		);
+	}
+
+	#[test]
+	fn test_file_only_empty_lines() {
+		let input = Span::new("\n\n\n");
+		let result = file(input);
+		let (remaining, parsed) = result.expect("Failed to parse file with only empty lines");
+
+		assert_eq!(remaining.fragment(), &"\n");
+		assert_eq!(parsed.len(), 3);
+
+		assert_eq!(parsed, vec![FileLine::empty(), FileLine::empty(), FileLine::empty()]);
+	}
+
+	#[test]
+	fn test_file_with_empty_line_in_middle() {
+		let input = Span::new("KEY1=value1\n\nKEY2=value2");
+		let result = file(input);
+		let (remaining, parsed) = result.expect("Failed to parse file with empty line");
+
+		assert_eq!(remaining.fragment(), &"");
+		assert_eq!(parsed.len(), 3);
+
+		assert_eq!(
+			&parsed[0],
+			&FileLine::key_value("KEY1", vec![ValuePart::Static("value1".to_string())])
+		);
+		assert_eq!(&parsed[1], &FileLine::empty());
+		assert_eq!(
+			&parsed[2],
+			&FileLine::key_value("KEY2", vec![ValuePart::Static("value2".to_string())])
+		);
+	}
+
+	#[test]
+	fn test_file_only_comments() {
+		let input = Span::new("# Comment 1\n# Comment 2\n# Comment 3");
+		let result = file(input);
+		let (remaining, parsed) = result.expect("Failed to parse file with only comments");
+
+		assert_eq!(remaining.fragment(), &"");
+		assert_eq!(parsed.len(), 3);
+
+		assert_eq!(&parsed[0], &FileLine::comment(" Comment 1"));
+		assert_eq!(&parsed[1], &FileLine::comment(" Comment 2"));
+		assert_eq!(&parsed[2], &FileLine::comment(" Comment 3"));
+	}
+
+	#[test]
+	fn test_file_with_duplicate_keys() {
+		let input = Span::new("KEY1=value1\nKEY1=value2\nKEY2=value3\nKEY1=value4");
+		let result = file(input);
+		let (remaining, parsed) = result.expect("Failed to parse file with duplicate keys");
+
+		assert_eq!(remaining.fragment(), &"");
+		assert_eq!(parsed.len(), 4);
+
+		assert_eq!(
+			&parsed[0],
+			&FileLine::key_value("KEY1", vec![ValuePart::Static("value1".to_string())])
+		);
+		assert_eq!(
+			&parsed[1],
+			&FileLine::key_value("KEY1", vec![ValuePart::Static("value2".to_string())])
+		);
+		assert_eq!(
+			&parsed[2],
+			&FileLine::key_value("KEY2", vec![ValuePart::Static("value3".to_string())])
+		);
+		assert_eq!(
+			&parsed[3],
+			&FileLine::key_value("KEY1", vec![ValuePart::Static("value4".to_string())])
+		);
+	}
+
+	#[test]
+	fn test_file_key_without_value() {
+		let input = Span::new("KEY_ONLY=\n");
+		let result = file(input);
+		let (remaining, parsed) = result.expect("Failed to parse file");
+
+		assert_eq!(remaining.fragment(), &"\n");
+		assert_eq!(parsed.len(), 1);
+		match &parsed[0] {
+			FileLine::KeyValue { key, value } => {
+				assert_eq!(key, "KEY_ONLY");
+				assert!(value.is_empty(), "Expected empty value for key without equals sign");
+			}
+			_ => panic!("Expected KeyValue FileLine"),
+		}
+	}
+	#[test]
+	fn test_file_mixed_lines_with_empty_values() {
+		let input = Span::new("KEY1=value1\nKEY2=\nKEY3=value3\nKEY4=");
+		let result = file(input);
+		let (remaining, parsed) = result.expect("Failed to parse file");
+
+		assert_eq!(remaining.fragment(), &"");
+		assert_eq!(parsed.len(), 4);
+
+		assert_eq!(
+			&parsed[0],
+			&FileLine::key_value("KEY1", vec![ValuePart::Static("value1".to_string())])
+		);
+		assert_eq!(&parsed[1], &FileLine::key_value("KEY2", Vec::new()));
+		assert_eq!(
+			&parsed[2],
+			&FileLine::key_value("KEY3", vec![ValuePart::Static("value3".to_string())])
+		);
+		assert_eq!(&parsed[3], &FileLine::key_value("KEY4", Vec::new()));
+	}
+}
diff --git a/tests/integration.rs b/tests/integration.rs
new file mode 100644
index 0000000000000000000000000000000000000000..bbdd0a4bf3b743003d30b62f264249de7b4cec5e
--- /dev/null
+++ b/tests/integration.rs
@@ -0,0 +1,44 @@
+use envish::EnvironmentFile;
+
+#[test]
+fn it_parses_basic_dotenv_file() {
+    let file_contents = r#"
+    # This value won't be set in the test, and this comment will be ignored
+    MY_BEST_VARIABLE=some_value
+    # This variable is also not defined, and it'll still be a string,
+    # because all environment variables are strings without being converted to other data types
+    SOME_OTHER_VARIABLE=1234
+    "#;
+
+    std::env::var("MY_BEST_VARIABLE").expect_err("MY_BEST_VARIABLE should not be set");
+    std::env::var("SOME_OTHER_VARIABLE").expect_err("SOME_OTHER_VARIABLE should not be set");
+
+    let file = EnvironmentFile::parse(file_contents).expect("Failed to parse environment file");
+    file.apply().expect("Failed to apply environment file");
+
+    assert_eq!(std::env::var("MY_BEST_VARIABLE").unwrap(), "some_value");
+    assert_eq!(std::env::var("SOME_OTHER_VARIABLE").unwrap(), "1234");
+}
+
+fn it_parses_dotenv_file_with_interpolation() {
+    let file_contents = r#"
+    # This value won't be set in the test, and this comment will be ignored
+    MY_BEST_VARIABLE=some_value
+    # This variable is also not defined, and it'll still be a string,
+    # because all environment variables are strings without being converted to other data types
+    SOME_OTHER_VARIABLE=1234
+    # This variable contains an interpolated value
+    INTERPOLATED_VARIABLE=${SOME_OTHER_VARIABLE}567
+    "#;
+
+    std::env::var("MY_BEST_VARIABLE").expect_err("MY_BEST_VARIABLE should not be set");
+    std::env::var("SOME_OTHER_VARIABLE").expect_err("SOME_OTHER_VARIABLE should not be set");
+    std::env::var("INTERPOLATED_VARIABLE").expect_err("INTERPOLATED_VARIABLE should not be set");
+
+    let file = EnvironmentFile::parse(file_contents).expect("Failed to parse environment file");
+    file.apply().expect("Failed to apply environment file");
+
+    assert_eq!(std::env::var("MY_BEST_VARIABLE").unwrap(), "some_value");
+    assert_eq!(std::env::var("SOME_OTHER_VARIABLE").unwrap(), "1234");
+    assert_eq!(std::env::var("INTERPOLATED_VARIABLE").unwrap(), "1234567");
+}
\ No newline at end of file