Need a basename function in Javascript

Viewed 76629

I need a short basename function (one-liner ?) for Javascript:

basename("/a/folder/file.a.ext") -> "file.a"
basename("/a/folder/file.ext") -> "file"
basename("/a/folder/file") -> "file"

That should strip the path and any extension.

Update: For dot at the beginning would be nice to treat as "special" files

basename("/a/folder/.file.a.ext") -> ".file.a"
basename("/a/folder/.file.ext") -> ".file"
basename("/a/folder/.file") -> ".file" # empty is Ok
basename("/a/folder/.fil") -> ".fil"  # empty is Ok
basename("/a/folder/.file..a..") -> # does'nt matter
19 Answers

Using modern (2020) js code:

function basename (path) {
  return path.substring(path.lastIndexOf('/') + 1)
}
console.log(basename('/home/user/file.txt'))

Contrary to misinformation provided above, regular expressions are extremely efficient. The caveat is that, when possible, they should be in a position so that they are compiled exactly once in the life of the program.

Here is a solution that gives both dirname and basename.

const rx1 = /(.*)\/+([^/]*)$/;    // (dir/) (optional_file)
const rx2 = /()(.*)$/;            // ()     (file)

function dir_and_file(path) {
  // result is array with
  //   [0]  original string
  //   [1]  dirname
  //   [2]  filename
  return rx1.exec(path) || rx2.exec(path);
}
// Single purpose versions.
function dirname(path) {
  return (rx1.exec(path) || rx2.exec(path))[1];
}
function basename(path) {
  return (rx1.exec(path) || rx2.exec(path))[2];
}

As for performance, I have not measured it, but I expect this solution to be in the same range as the fastest of the others on this page, but this solution does more. Helping the real-world performance is the fact that rx1 will match most actual paths, so rx2 is rarely executed.

Here is some test code.

function show_dir(parts) {
  console.log("Original str :"+parts[0]);
  console.log("Directory nm :"+parts[1]);
  console.log("File nm      :"+parts[2]);
  console.log();
}
show_dir(dir_and_file('/absolute_dir/file.txt'));
show_dir(dir_and_file('relative_dir////file.txt'));
show_dir(dir_and_file('dir_no_file/'));
show_dir(dir_and_file('just_one_word'));
show_dir(dir_and_file('')); // empty string
show_dir(dir_and_file(null)); 

And here is what the test code yields:

# Original str :/absolute_dir/file.txt
# Directory nm :/absolute_dir
# File nm      :file.txt
# 
# Original str :relative_dir////file.txt
# Directory nm :relative_dir
# File nm      :file.txt
# 
# Original str :dir_no_file/
# Directory nm :dir_no_file
# File nm      :
# 
# Original str :just_one_word
# Directory nm :
# File nm      :just_one_word
# 
# Original str :
# Directory nm :
# File nm      :
# 
# Original str :null
# Directory nm :
# File nm      :null

By the way, "node" has a built in module called "path" that has "dirname" and "basename". Node's "path.dirname()" function accurately imitates the behavior of the "bash" shell's "dirname," but is that good? Here's what it does:

  1. Produces '.' (dot) when path=="" (empty string).
  2. Produces '.' (dot) when path=="just_one_word".
  3. Produces '.' (dot) when path=="dir_no_file/".

I prefer the results of the function defined above.

Fast without regular expressions, suitable for both path types '/' and '\'. Gets the extension also:

function baseName(str)
{
    let li = Math.max(str.lastIndexOf('/'), str.lastIndexOf('\\'));
    return new String(str).substring(li + 1);
}

A nice one line, using ES6 arrow functions:

var basename = name => /([^\/\\]*|\.[^\/\\]*)\..*$/gm.exec(name)[1];
// In response to @IAM_AL_X's comments, even shorter and it
// works with files that don't have extensions:
var basename = name => /([^\/\\\.]*)(\..*)?$/.exec(name)[1];

UPDATE

An improved version which works with forward / and backslash \ single or double means either of the following

  • \\path\\to\\file
  • \path\to\file
  • //path//to//file
  • /path/to/file
  • http://url/path/file.ext
  • http://url/path/file

See a working demo below

let urlHelper = {};
urlHelper.basename = (path) => {
  let isForwardSlash = path.match(/\/{1,2}/g) !== null;
  let isBackSlash = path.match(/\\{1,2}/g) !== null;

  if (isForwardSlash) {
    return path.split('/').reverse().filter(function(el) {
      return el !== '';
    })[0];
  } else if (isBackSlash) {
    return path.split('\\').reverse().filter(function(el) {
      return el !== '';
    })[0];
  }
  return path;
};

$('em').each(function() {
  var text = $(this).text();
  $(this).after(' --> <strong>' + urlHelper.basename(text) + '</strong><br>');
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<em>http://folder/subfolder/file.ext</em><br>
<em>http://folder/subfolder/subfolder2</em><br>
<em>/folder/subfolder</em><br>
<em>/file.ext</em><br>
<em>file.ext</em><br>
<em>/folder/subfolder/</em><br>
<em>//folder//subfolder//</em><br>
<em>//folder//subfolder</em><br>
<em>\\folder\\subfolder\\</em><br>
<em>\\folder\\subfolder\\file.ext</em><br>
<em>\folder\subfolder\</em><br>
<em>\\folder\\subfolder</em><br>
<em>\\folder\\subfolder\\file.ext</em><br>
<em>\folder\subfolder</em><br>


A more simpler solution could be

function basename(path) {
      return path.replace(/\/+$/, "").replace( /.*\//, "" );
}

Input                           basename()
/folder/subfolder/file.ext   --> file.ext
/folder/subfolder            --> subfolder
/file.ext                    --> file.ext
file.ext                     --> file.ext
/folder/subfolder/           --> subfolder

Working example: https://jsfiddle.net/Smartik/2c20q0ak/1/

This is my implementation which I use in my fundamental js file.

// BASENAME

Window.basename = function() {
    var basename = window.location.pathname.split(/[\\/]/);
    return basename.pop() || basename.pop();
}

JavaScript Functions for basename and also dirname:

function basename(path) {
     return path.replace(/.*\//, '');
}

function dirname(path) {
     return path.match(/.*\//);
}

Sample:

Input                       dirname()           basename()
/folder/subfolder/file.ext  /folder/subfolder/  file.ext
/folder/subfolder           /folder/            subfolder
/file.ext                   file.ext            /
file.ext                    file.ext            null

See reference.

Defining a flexible basename implementation

Despite all the answers, I still had to produce my own solution which fits the following criteria:

  1. Is fully portable and works in any environment (thus Node's path.basename won't do)
  2. Works with both kinds of separators (/ and \)
  3. Allows for mixing separators - e.g. a/b\c (this is different from Node's implementation which respects the underlying system's separator instead)
  4. Does not return an empty path if path ends on separator (i.e. getBaseName('a/b/c/') === 'c')

Code

(make sure to open the console before running the Snippet)

/**
 * Flexible `basename` implementation
 * @see https://stackoverflow.com/a/59907288/2228771
 */
function getBasename(path) {
  // make sure the basename is not empty, if string ends with separator
  let end = path.length-1;
  while (path[end] === '/' || path[end] === '\\') {
    --end;
  }

  // support mixing of Win + Unix path separators
  const i1 = path.lastIndexOf('/', end);
  const i2 = path.lastIndexOf('\\', end);

  let start;
  if (i1 === -1) {
    if (i2 === -1) {
      // no separator in the whole thing
      return path;
    }
    start = i2;
  }
  else if (i2 === -1) {
    start = i1;
  }
  else {
    start = Math.max(i1, i2);
  }
  return path.substring(start+1, end+1);
}

// tests
console.table([
  ['a/b/c', 'c'],
  ['a/b/c//', 'c'],
  ['a\\b\\c', 'c'],
  ['a\\b\\c\\', 'c'],
  ['a\\b\\c/', 'c'],
  ['a/b/c\\', 'c'],
  ['c', 'c']
].map(([input, expected]) => {
  const result = getBasename(input);
  return {
    input, 
    result,
    expected,
    good: result === expected ? '✅' : '❌'
  };
}));

Related