Get a file name from a path

The Simplest way in C++17 is:

use the #include <filesystem> and filename() for filename with extension and stem() without extension.

#include <iostream>
#include <string>
#include <filesystem>
namespace fs = std::filesystem;

int main()
{
  std::string filename = "C:\\MyDirectory\\MyFile.bat";

  std::cout << fs::path(filename).filename() << '\n'
    << fs::path(filename).stem() << '\n'
    << fs::path("/foo/bar.txt").filename() << '\n'
    << fs::path("/foo/bar.txt").stem() << '\n'
    << fs::path("/foo/.bar").filename() << '\n'
    << fs::path("/foo/bar/").filename() << '\n'
    << fs::path("/foo/.").filename() << '\n'
    << fs::path("/foo/..").filename() << '\n'
    << fs::path(".").filename() << '\n'
    << fs::path("..").filename() << '\n'
    << fs::path("/").filename() << '\n';
}

Which can be compiled with g++ -std=c++17 main.cpp -lstdc++fs, and outputs:

"MyFile.bat"
"MyFile"
"bar.txt"
"bar"
".bar"
""
"."
".."
"."
".."
"/"

Reference: cppreference


The task is fairly simple as the base filename is just the part of the string starting at the last delimeter for folders:

std::string base_filename = path.substr(path.find_last_of("/\\") + 1)

If the extension is to be removed as well the only thing to do is find the last . and take a substr to this point

std::string::size_type const p(base_filename.find_last_of('.'));
std::string file_without_extension = base_filename.substr(0, p);

Perhaps there should be a check to cope with files solely consisting of extensions (ie .bashrc...)

If you split this up into seperate functions you're flexible to reuse the single tasks:

template<class T>
T base_name(T const & path, T const & delims = "/\\")
{
  return path.substr(path.find_last_of(delims) + 1);
}
template<class T>
T remove_extension(T const & filename)
{
  typename T::size_type const p(filename.find_last_of('.'));
  return p > 0 && p != T::npos ? filename.substr(0, p) : filename;
}

The code is templated to be able to use it with different std::basic_string instances (i.e. std::string & std::wstring...)

The downside of the templation is the requirement to specify the template parameter if a const char * is passed to the functions.

So you could either:

A) Use only std::string instead of templating the code

std::string base_name(std::string const & path)
{
  return path.substr(path.find_last_of("/\\") + 1);
}

B) Provide wrapping function using std::string (as intermediates which will likely be inlined / optimized away)

inline std::string string_base_name(std::string const & path)
{
  return base_name(path);
}

C) Specify the template parameter when calling with const char *.

std::string base = base_name<std::string>("some/path/file.ext");

Result

std::string filepath = "C:\\MyDirectory\\MyFile.bat";
std::cout << remove_extension(base_name(filepath)) << std::endl;

Prints

MyFile

A possible solution:

string filename = "C:\\MyDirectory\\MyFile.bat";

// Remove directory if present.
// Do this before extension removal incase directory has a period character.
const size_t last_slash_idx = filename.find_last_of("\\/");
if (std::string::npos != last_slash_idx)
{
    filename.erase(0, last_slash_idx + 1);
}

// Remove extension if present.
const size_t period_idx = filename.rfind('.');
if (std::string::npos != period_idx)
{
    filename.erase(period_idx);
}

Tags:

C++

Visual C++