diff --git a/src/filepath.gleam b/src/filepath.gleam index d77ff3b..dae0861 100644 --- a/src/filepath.gleam +++ b/src/filepath.gleam @@ -113,3 +113,51 @@ fn get_directory_name( pub fn is_absolute(path: String) -> Bool { string.starts_with(path, "/") } + +// TODO: document +// TODO: windows support +pub fn expand(path: String) -> Result(String, Nil) { + let is_absolute = is_absolute(path) + let result = + path + |> split + |> root_slash_to_empty + |> expand_segments([]) + + case is_absolute && result == Ok("") { + True -> Ok("/") + False -> result + } +} + +fn expand_segments( + path: List(String), + base: List(String), +) -> Result(String, Nil) { + case base, path { + // Going up past the root (empty string in this representation) + [""], ["..", ..] -> Error(Nil) + + // Going up past the top of a relative path + [], ["..", ..] -> Error(Nil) + + // Going up successfully + [_, ..base], ["..", ..path] -> expand_segments(path, base) + + // Discarding `.` + _, [".", ..path] -> expand_segments(path, base) + + // Adding a segment + _, [s, ..path] -> expand_segments(path, [s, ..base]) + + // Done! + _, [] -> Ok(string.join(list.reverse(base), "/")) + } +} + +fn root_slash_to_empty(segments: List(String)) -> List(String) { + case segments { + ["/", ..rest] -> ["", ..rest] + _ -> segments + } +} diff --git a/test/filepath_test.gleam b/test/filepath_test.gleam index 13b7656..cb6d92c 100644 --- a/test/filepath_test.gleam +++ b/test/filepath_test.gleam @@ -260,3 +260,88 @@ pub fn is_absolute_6_test() { filepath.is_absolute("/") |> should.equal(True) } + +pub fn expand_0_test() { + filepath.expand("one") + |> should.equal(Ok("one")) +} + +pub fn expand_1_test() { + filepath.expand("/one") + |> should.equal(Ok("/one")) +} + +pub fn expand_2_test() { + filepath.expand("/..") + |> should.equal(Error(Nil)) +} + +pub fn expand_3_test() { + filepath.expand("/one/two/..") + |> should.equal(Ok("/one")) +} + +pub fn expand_4_test() { + filepath.expand("/one/two/../..") + |> should.equal(Ok("/")) +} + +pub fn expand_5_test() { + filepath.expand("/one/two/../../..") + |> should.equal(Error(Nil)) +} + +pub fn expand_6_test() { + filepath.expand("/one/two/../../three") + |> should.equal(Ok("/three")) +} + +pub fn expand_7_test() { + filepath.expand("one") + |> should.equal(Ok("one")) +} + +pub fn expand_8_test() { + filepath.expand("..") + |> should.equal(Error(Nil)) +} + +pub fn expand_9_test() { + filepath.expand("one/two/..") + |> should.equal(Ok("one")) +} + +pub fn expand_10_test() { + filepath.expand("one/two/../..") + |> should.equal(Ok("")) +} + +pub fn expand_11_test() { + filepath.expand("one/two/../../..") + |> should.equal(Error(Nil)) +} + +pub fn expand_12_test() { + filepath.expand("one/two/../../three") + |> should.equal(Ok("three")) +} + +pub fn expand_13_test() { + filepath.expand("/one/.") + |> should.equal(Ok("/one")) +} + +pub fn expand_14_test() { + filepath.expand("/one/./two") + |> should.equal(Ok("/one/two")) +} + +pub fn expand_15_test() { + filepath.expand("/one/") + |> should.equal(Ok("/one")) +} + +pub fn expand_16_test() { + filepath.expand("/one/../") + |> should.equal(Ok("/")) +}