Oak

path.oak

← Standard library See on GitHub ↗

// libpath implements utilities for working with UNIX style paths on file
// systems and in URIs

{
	default: default
	slice: slice
	filter: filter
	reduce: reduce
} := import('std')
{
	join: strJoin
	split: strSplit
	trimEnd: trimEnd
} := import('str')

// abs? reports whether a path is absolute
fn abs?(path) path.0 = '/'

// rel? reports whether a path is relative
fn rel?(path) path.0 != '/'

// internal helper, returns the last occurrence of '/' in a string or 0 if it
// does not appear.
fn _lastSlash(path) if path {
	'' -> 0
	_ -> {
		fn sub(i) if path.(i) {
			?, '/' -> i
			_ -> sub(i - 1)
		}
		sub(len(path) - 1)
	}
}

// dir returns the portion of the a path that represents the directory
// containing it. In effect, this is all but the last part of a path.
fn dir(path) {
	path := path |> trimEnd('/')
	path |> slice(0, _lastSlash(path))
}

// base returns the last element of a path, which is typically the file or
// directory referred to by the path.
fn base(path) {
	path := path |> trimEnd('/')
	path |> slice(_lastSlash(path) + 1)
}

// cut returns a [dir, base] pair representing both parts of a path
fn cut(path) {
	path := path |> trimEnd('/')
	lastSlash := _lastSlash(path)
	[
		path |> slice(0, lastSlash)
		path |> slice(lastSlash + 1)
	]
}

// clean returns a path normalized with the following transformations
//
// 1. Remove consecutive slashes not at the beginning
// 2. Remove '.'
// 3. Remove '..' and the (parent) directory right before it, if such parent
//    directory is in the path
fn clean(path) {
	rooted := path.0 = '/'
	cleaned := path |>
		strSplit('/') |>
		reduce([], fn(parts, part, i) if part {
			// remove consecutive slashes and '.'
			'', '.' -> parts
			// remove '..' and its leading dir
			'..' -> if i {
				0 -> parts << part
				_ -> parts |> slice(0, len(parts) - 1)
			}
			_ -> parts << part
	}) |> strJoin('/')
	if rooted {
		true -> '/' << cleaned
		_ -> cleaned
	}
}

// join joins multiple paths together into a single valid cleaned path
fn join(parts...) parts |> reduce('', fn(base, path) if base {
	// if we simply return `path`, path will be used as `base` next iteration
	// which might mutate `path`.
	'' -> '' << path
	_ -> base << '/' << path
}) |> clean()

// split returns a list of each element of the path, ignoring the trailing
// slash. If the path is absolute, the first item is an empty string.
fn split(path) if path |> trimEnd('/') {
	'' -> []
	_ -> path |> strSplit('/') |> filter(fn(s) s != '')
}

// resolve takes a path and returns an equivalent cleaned, absolute path, using
// the given base path as the root, or using the current working directory if
// no base path is given.
fn resolve(path, base) if abs?(path) {
	true -> clean(path)
	_ -> join(base |> default(env().PWD), path)
}