Skip to content

Instantly share code, notes, and snippets.

@creationix
Created November 12, 2013 18:10
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save creationix/7435851 to your computer and use it in GitHub Desktop.
Save creationix/7435851 to your computer and use it in GitHub Desktop.
Simple path join and dirname functions for generic javascript
// Joins path segments. Preserves initial "/" and resolves ".." and "."
// Does not support using ".." to go above/outside the root.
// This means that join("foo", "../../bar") will not resolve to "../bar"
function join(/* path segments */) {
// Split the inputs into a list of path commands.
var parts = [];
for (var i = 0, l = arguments.length; i < l; i++) {
parts = parts.concat(arguments[i].split("/"));
}
// Interpret the path commands to get the new resolved path.
var newParts = [];
for (i = 0, l = parts.length; i < l; i++) {
var part = parts[i];
// Remove leading and trailing slashes
// Also remove "." segments
if (!part || part === ".") continue;
// Interpret ".." to pop the last segment
if (part === "..") newParts.pop();
// Push new path segments.
else newParts.push(part);
}
// Preserve the initial slash if there was one.
if (parts[0] === "") newParts.unshift("");
// Turn back into a single string path.
return newParts.join("/") || (newParts.length ? "/" : ".");
}
// A simple function to get the dirname of a path
// Trailing slashes are ignored. Leading slash is preserved.
function dirname(path) {
return join(path, "..");
}
@creationix
Copy link
Author

This had some interesting design constraints that allowed it to be very simple.

  • I don't want/need to support things like join("foo", "../../bar") that would resolve to "../bar"
  • I only want to treat the first string as absolute if it starts with a slash, this means join("some/path", "/looks/absolute") will resolve to "some/path/looks/absolute" and not "/looks/absolute".

@japj
Copy link

japj commented Nov 12, 2013

sorry to ask, but what about the OS with the 'other' slashes? ;)

@medikoo
Copy link

medikoo commented Nov 13, 2013

@japj +1 ;-) Otherwise it's strictly network (not FS) dedicated version.

@creationix
Copy link
Author

@medikoo, @japj, that was also a design constraint. I'm not using this for real FS paths, but only network paths and require where it's always "/". (You don't see people write require("foo\\bar\\bar.js") do you?)

@amcgregor
Copy link

Is there a license you could mention with this? Public domain doesn't exist everywhere as a concept and unattributed works can't be utilized without serious stares from legal and threats of the attack sharks. MIT, and I'd love you forever?

@jslegers
Copy link

jslegers commented Oct 25, 2017

Some issues I experienced when trying out this approach :

  • Trailing / isn't preserved
  • Double // are replaced with / when the first path is an absolute path that starts with the protocol (eg. blob://, file://, http://, https://, ...)
  • If your first path starts with ../, it is stripped away

For me, that makes this approach pretty much unusable.

@legrady
Copy link

legrady commented Mar 29, 2018

MSDOS cmd.exe and powershell accept '/' as well as '\'. It's only obscenely obsolete systems that reject '/' as a file separator. You might have a real reason for using a 386 system running MSDOS 3.1, but it probably doesn't involve running javascript :-)

@wallabra
Copy link

Do you have a license, or at least allow other people to use this? It's beautiful, great and simple. Maybe you also want to check for escaped slashes (\/ on Unix), although URL encoding usually takes care of that well. :)

@catamphetamine
Copy link

catamphetamine commented Apr 8, 2021

My variant:

/**
 * A simple analog of Node.js's `path.join(...)`.
 * https://gist.github.com/creationix/7435851#gistcomment-3698888
 * @param  {...string} segments
 * @return {string}
 */
export default function joinPath(...segments) {
  const parts = segments.reduce((parts, segment) => {
    // Remove leading slashes from non-first part.
    if (parts.length > 0) {
      segment = segment.replace(/^\//, '')
    }
    // Remove trailing slashes.
    segment = segment.replace(/\/$/, '')
    return parts.concat(segment.split('/'))
  }, [])
  const resultParts = []
  for (const part of parts) {
    if (part === '.') {
      continue
    }
    if (part === '..') {
      resultParts.pop()
      continue
    }
    resultParts.push(part)
  }
  return resultParts.join('/')
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment