Click here to Skip to main content
13,804,175 members
Click here to Skip to main content
Add your own
alternative version


66 bookmarked
Posted 23 Dec 2003
Licenced CPOL

A class to make it easy to work with file names

, 26 Dec 2003
Rate this:
Please Sign up or sign in to vote.
Working with filenames
<!-- Download Links --> <!-- Add the rest of your HTML here -->


As programmers we often find ourselves having to deal with filenames and to perform various operations on the filenames. Some years ago I found myself repeatedly writing code that received a filename and needed to perform some simple transformation such as changing the extension. After about the fifteenth function that contained code such as the following <pre lang=c++> TCHAR tszDrive[_MAX_DRIVE], tszPath[_MAX_PATH], tszFilename[_MAX_FNAME], tszExtension[_MAX_EXT]; CString csNewPath; _tsplitpath(tszFile, tszDrive, tszPath, tszFilename, tszExtension); . . . csNewPath = tszDrive; csNewPath += tszPath + tszFilename; csNewPath += someotherextension; I realised that much of the work could be encapsulated in a class.


You may recognise this class. It was first published on CodeGuru[^] but it's been extended since those days (August 1998) and a few bugs have been fixed. The class now understands Unicode.

The class encapsulates the basic idea of a filename whilst catering for UNC formatted filenames. It can also convert a filename specified, using drive letters on the local machine, into UNC format. How it does this depends on whether the drive portion of the filename refers to a local drive or a mapped network drive (I discuss this later in this article).

A filename has at least two forms. The first form is (for example)

You can break this down into four pieces.
  • c: is the hard drive name.
  • \seti\01\ is the directory path leading to the file.
  • seti is the file name and
  • .exe is the file extension.
CFileSpec understands this format.

The other form that this class understands is a UNC filename of the form

  • \\Rob\C is the server name and the share name.
  • \seti\01\ is the directory path leading to the file.
  • seti is the file name and
  • .exe is the file extension.
You can see that apart from the server/share name or hard drive name, the rest of the filename spec is the same. The class leverages that equivalence by treating a drive name or a server/share name as interchangeable. But more on this equiavalence a little later...

The class interface

The class definition looks like this. <pre lang=c++> class CFileSpec { public: enum FS_BUILTINS { FS_EMPTY, // Nothing FS_APP, // Full application path and name FS_APPDIR, // Application folder FS_WINDIR, // Windows folder FS_SYSDIR, // System folder FS_TMPDIR, // Temporary folder FS_DESKTOP, // Desktop folder FS_FAVOURITES, // Favourites folder FS_MEDIA, // Default media folder FS_CURRDIR, // Current folder FS_TEMPNAME // Create a temporary name }; CFileSpec(FS_BUILTINS eSpec = FS_EMPTY); CFileSpec(FS_BUILTINS eSpec, LPCTSTR szFileame); CFileSpec(LPCTSTR szSpec, LPCTSTR szFilename); CFileSpec(LPCTSTR szFilename); // Operations BOOL Exists() const; BOOL IsUNCPath() const; BOOL LoadArchive(CObject *pObj) const; BOOL SaveArchive(CObject *pObj) const; // Access functions CString& Drive() { return m_csDrive; } CString& Path() { return m_csPath; } CString& FileName() { return m_csFilename; } CString& Extension() { return m_csExtension; } const CString FullPathNoExtension() const; const CString GetFolder() const; const CString GetFullSpec() const; const CString GetFileName() const; const CString ConvertToUNCPath() const; void SetFullSpec(LPCTSTR szSpec); void SetFullSpec(FS_BUILTINS eSpec = FS_EMPTY); void SetFileName(LPCTSTR szSpec); void Initialise(FS_BUILTINS eSpec); private: BOOL IsUNCPath(LPCTSTR szPath) const; void WriteAble() const; void ReadOnly() const; void GetShellFolder(int iFolder); CString m_csDrive, m_csPath, m_csFilename, m_csExtension; }; Let's ignore the FS_BUILTINS enum for now (and all constructors using that enum) and look at the constructors that take a string or two.

The simplest constructor takes a single string which is a filename and path. The constructor simply deconstructs the string into it's constituent parts via a call to SetFullSpec which in turn checks if the string is a filename in UNC format or not. If not it uses _tsplitpath to populate the CStrings. If it is a UNC filename it copies the server/sharename part of the string (if present) to the m_csDrive member and then uses _tsplitpath to deconstruct the remainder of the filename.

You can then use the member functions to retrieve the drive, the path, the filename, the extension, or the entire path. You can also change any of those constituent parts and expect the class to do reasonable things after the change. For example <pre lang=c++> CFileSpec fs(_T("c:\seti\01\seti.exe")); fs.Extension() = _T("dat"); printf(_T("%s\n"), fs.GetFullSpec()); would print

showing that the extension was changed.

Note well that I said '(if present)' earlier. The class is perfectly capable of handing filenames that don't start at the top of the filename namespace and it doesn't care if it's a UNC path or not. (Though if it's a UNC path by definition it will be rooted at the top of the filename namespace).

So now we've got a filename parsed into it's component parts. Big deal. The power of the class comes from what you can do once you've got an instance of the class.

Suppose your application has an initialisation file stored in the same directory as the exe? How to get at it? You can use GetModuleFileName() to get the name of your exe and then do some massaging of the returned value to isolate the path portion of the returned filename and then append the initialisation filename to the result to get the final file. It might look somewhat like this. <pre lang=c++> TCHAR tszDrive[_MAX_DRIVE], tszPath[_MAX_PATH], tszFile[_MAX_PATH]; CString csNewPath; GetModuleFileName(NULL, tszFile, _MAX_PATH); _tsplitpath(tszFile, tszDrive, tszPath, NULL, NULL); csNewPath = tszDrive + tszPath; csNewPath += _T("InitialisationFile.ext");

Or you could do this. <pre lang=c++> CFileSpec fs(CFileSpec::FS_APPDIR); fs.SetFileNameEx(_T("InitialisationFile.ext"); LoadMyInitFile(fs); or even this <pre lang=c++> CFileSpec fs(CFileSpec::FS_APPDIR, _T("InitialisationFile.ext")); LoadMyInitFile(fs); The same work is performed but you have to write rather less code.

Which leads into a discussion of the FS_BUILTINS that we ignored. These, via the constructors or the SetFullSpec() overload, let us initialise a CFileSpec object with the kinds of paths I thought were commonly used when I wrote the class. It's easy to extend the list if you think I missed something. (Thats a hint that if you want something added just add it, don't write a comment on this articles message board asking me to add it, write it yourself). That said, I'd be interested to know what things were added to FS_BUILTINS and would certainly consider adding them to the download.

If you examine the source code posted on CodeGuru 5 years ago you'll see that I used the Windows Registry to obtain the paths for the users desktop and favourites folders. Well it turns out that according to Raymond Chen[^], this is evil, so I changed the class to use the recommended method, SHGetSpecialFolderLocation. (I suspect Mike Dunn might also have railed at me if I'd continued using the registry :))

So far it's a pretty uninteresting though possibly useful class. Where it becomes interesting is in two features.

UNC Conversion

Elaine is working with an app that refers to data through drive F:. She doesn't know (and doesn't care) that her machine doesn't have a physical drive F:. Somehow, through the magic of drive mapping, a folder on the server called \\Roger\CPData is mapped to drive F: on her machine. Everything works as it should and, apart from network throughput issues, the world is a shining place. When Elaine closes her app it saves a reference to the drive it was using.

Somewhat later, Ian logs on to the same machine. He opens the same application and it, using the saved path, attempts to access the same files that Elaine was using. If Ian and Elaine have the same mapped drives it all works. But woe if Elaine mas mapped drive F: to \\Roger\CPData and Ian has mapped drive F: to \\Stan\Rants. This is where UNC conversion comes in. If Elaine's app had converted her reference to F:/CPData to a UNC path then, when Ian runs the same app under his ID it would refer to the same place, not caring if Elaine and Ian had the same drive mappings.

You call the member function ConvertToUNCPath() to perform the conversion. The function looks like this. <pre lang=c++> const CString CFileSpec::ConvertToUNCPath() const { USES_CONVERSION; CString csPath = GetFullSpec(); if (IsUNCPath(csPath)) return csPath; if (csPath[1] == ':') { // Fully qualified pathname including a drive letter, check if it's a // mapped drive UINT uiDriveType = GetDriveType(m_csDrive); if (uiDriveType & DRIVE_REMOTE) { // Yup - it's mapped so convert to a UNC path... TCHAR tszTemp[_MAX_PATH]; UNIVERSAL_NAME_INFO *uncName = (UNIVERSAL_NAME_INFO *) tszTemp; DWORD dwSize = _MAX_PATH; DWORD dwRet = WNetGetUniversalName(m_csDrive, REMOTE_NAME_INFO_LEVEL, uncName, &dwSize); CString csDBShare; if (dwRet == NO_ERROR) return uncName->lpUniversalName + m_csPath + m_csFilename + m_csExtension; } else { // It's a local drive so search for a share to it... NET_API_STATUS res; PSHARE_INFO_502 BufPtr, p; DWORD er = 0, tr = 0, resume = 0, i; int iBestMatch = 0; CString csTemp, csTempDrive, csBestMatch; do { res = NetShareEnum(NULL, 502, (LPBYTE *) &BufPtr, DWORD(-1), &er, &tr, &resume); // // If the call succeeds, // if (res == ERROR_SUCCESS || res == ERROR_MORE_DATA) { csTempDrive = GetFolder(); csTempDrive.MakeLower(); p = BufPtr; // // Loop through the entries; // for (i = 1; i <= er; i++) { if (p->shi502_type == STYPE_DISKTREE) { csTemp = W2A((LPWSTR) p->shi502_path); csTemp.MakeLower(); if (csTempDrive.Find(csTemp) == 0) { // We found a match if (iBestMatch < csTemp.GetLength()) { iBestMatch = csTemp.GetLength(); csBestMatch = W2A((LPWSTR) p->shi502_netname); } } } p++; } // // Free the allocated buffer. // NetApiBufferFree(BufPtr); if (iBestMatch) { TCHAR tszComputerName[MAX_COMPUTERNAME_LENGTH + 1]; DWORD dwBufLen = countof(tszComputerName); csTemp = GetFolder(); csTemp = csTemp.Right(csTemp.GetLength() - iBestMatch + 1); GetComputerName(tszComputerName, &dwBufLen); csPath.Format(_T("\\\\%s\\%s%s%s%s"), tszComputerName, csBestMatch, csTemp, m_csFilename, m_csExtension); } } else TRACE(_T("Error: %ld\n"), res); // Continue to call NetShareEnum while // there are more entries. // } while (res == ERROR_MORE_DATA); // end do } } return csPath; } The function is const meaning that it doesn't modify the underlying CFileSpec object. It merely converts the path represented by that object into a UNC path (if possible). Either way it returns a CString.

The function first checks if the object already contains a UNC path. If so it returns the path.

If not, the function then checks if the object contains a path rooted to the file system namespace. Ie, does it contain a drive value. That's done by the line <pre lang=c++>if (csPath[1] == ':')If this test fails we simply return the path the object represents (no conversion possible).

Assuming the test passes we then have two possibilities. The path may be on a share or it may be on local storage. The simpler case is when the path is on a share on another machine (ie, it's on a mapped drive). We get the drive type using the GetDriveType() API and branch to one of two paths. If it's a remote drive (the path is on a share) we execute this code. <pre lang=c++> // Yup - it's mapped so convert to a UNC path... TCHAR tszTemp[_MAX_PATH]; UNIVERSAL_NAME_INFO *uncName = (UNIVERSAL_NAME_INFO *) tszTemp; DWORD dwSize = _MAX_PATH; DWORD dwRet = WNetGetUniversalName(m_csDrive, REMOTE_NAME_INFO_LEVEL, uncName, &dwSize); CString csDBShare; if (dwRet == NO_ERROR) return uncName->lpUniversalName + m_csPath + m_csFilename + m_csExtension; This is pretty simple. We call the WNetGetUniversalName() API passing the drive name (c:, d: etc) and, if the API succeeds, we get back a server/share name to which we append the remainder of the path. If the API fails (for whatever reason), the UNC conversion method returns the untranslated path.

Where it gets interesting is when the path refers to a drive on the local machine. In this case we want to search for a share on this machine that will be reachable from another machine.

To do this we need to enumerate shares. Naturally it's not quite that simple. Shares can be print queues, disk drives or IPC channels. Our code needs to ignore any share except drive shares. We do this by checking the share type for each share that's enumerated. Pretty simple. We set up the enum using the NetShareEnum API and then go into a loop examing each share returned. For each share that's a disk share we examine the path p->shi502_path associated with the share.

Now things get complicated. There may be more than one share on your machine that can reach the path. For example, you may have a share for c: and another share for c:\seti. Without considerations of permissions (and this code does no consideration) there's no necessarily best path. The code I wrote looks for the 'best match' where I define the 'best match' to be the longest server/share name. In other words, given a choice between c: and c:\seti and I'm looking for a match to c:\seti\somefile the code will choose the longest match. Of course, having chosen the longest match it has to delete some parts of the path.

This is done in this code <pre lang=c++> if (iBestMatch) { TCHAR tszComputerName[MAX_COMPUTERNAME_LENGTH + 1]; DWORD dwBufLen = countof(tszComputerName); csTemp = GetFolder(); csTemp = csTemp.Right(csTemp.GetLength() - iBestMatch); GetComputerName(tszComputerName, &dwBufLen); csPath.Format(_T("\\\\%s\\%s%s%s%s"), tszComputerName, csBestMatch, csTemp, m_csFilename, m_csExtension); } We delete that part of the path that is contained in the share. Eg, if the share on machine Rob is called seti and represents c:\seti on that machine, and the path is c:\seti\setisrv.exe then the returned UNC path will be \\Rob\seti\setisrv.exe.

A gotcha

If you've examined the code carefully you might be puzzled by this line. <pre lang=c++> csTemp = W2A((LPWSTR) p->shi502_path); You'll have noticed that the function uses the USES_CONVERSION macro. This macro sets up some local variables to allow for Unicode to MBCS/ANSI conversion within the function (and vice versa). If you know about USES_CONVERSION then you probably also know about the W2A macro. This macro does a conversion from Wide to MBCS/ANSI, ie, from Unicode to MBCS/ANSI. The macro, if compiled within an ANSI/MBCS application, expects to see a Unicode input string (LPWSTR). Hmmm, so the example project included isn't Unicode. Why then the cast to LPWSTR? Well it turns out that in the version of the PSDK I have on this machine, the SHARE_INFO_502 structure defines it's member variables as LPSTR's if it's an ANSI/MBCS build and as LPWSTR's if it's a Unicode build. Sounds good except for one thing. Regardless of how the build is done the OS returns Unicode strings! (Win2k SP4). If you've done an MBCS/ANSI build the code will return the wrong path unless you do the cast and use the W2A macro. So even though the compiler imagines that in an ANSI/MBCS build those members are ANSI/MBCS, they're really Unicode. But the W2A macro complains if you pass it a non LPWSTR argument. Hence the casts. This cost me a few minutes of head scratching.

Serialisation support

Lots of people don't like MFC's serialisation support. I've yet to see a convincing argument against it so this class contains support for it. This support wasn't a part of the original class. I just noticed that I was writing a lot of similar code so I added it and lost the need to write pretty much the same code for every new application I write.
open a file
create an archive object
attach the file to the archive
serialise the object through the archive
detach the file from the archive
close the archive
close the file
It seemed to me that adding MFC archive support to this class was a no brainer - and it was. You've seen the functions defined above in the class header. Here's the code. <pre lang=c++> BOOL CFilename::LoadArchive(CObject *pObj) const { CFile file; BOOL bStatus = FALSE; ASSERT(pObj); ASSERT_VALID(pObj); ASSERT_KINDOF(CObject, pObj); ASSERT(pObj->IsSerializable()); if (Exists()) { try { if (file.Open(GetFullSpec(), CFile::modeRead | CFile::typeBinary | CFile::shareExclusive)) { CArchive ar(&file, CArchive::load); pObj->Serialize(ar); ar.Close(); file.Close(); bStatus = TRUE; } } catch(CException *e) { e->Delete(); } } return bStatus; } Simple code. Check if the file exists. If it does, open it, attach an archive object to the file object and then serialize it into the object pointer passed. Naturally the object pointed to must support serialisation, hence the debug checks. It's almost exactly the same code for saving an archive.

The Demo Application

The demo application does some simple conversions of paths to translated paths. It also shows the member variables of the underlying CFileSpec object. The examples shown here demonstrate how the class converts from a reference to my wifes machine (f:\seti\setispy.ini) where f: is mapped to \\Suzy\C.

The demo application

The second example shows how the class converts a UNC input path.

The demo application


The sample project uses a class, CFileEditCtrl, written by P J Arends. An excellent class found here.


December 24 2003, Initial CodeProject version.

December 24 2003, Fixed a bug noted by PJ Arends.

December 27 2003, Fixed a bug noted by Mike Dunn. <!------------------------------- That's it! --------------------------->


This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


About the Author

Rob Manderson
United States United States
I've been programming for 35 years - started in machine language on the National Semiconductor SC/MP chip, moved via the 8080 to the Z80 - graduated through HP Rocky Mountain Basic and HPL - then to C and C++ and now C#.

I used (30 or so years ago when I worked for Hewlett Packard) to repair HP Oscilloscopes and Spectrum Analysers - for a while there I was the one repairing DC to daylight SpecAns in the Asia Pacific area.

Afterward I was the fourth team member added to the Australia Post EPOS project at Unisys Australia. We grew to become an A$400 million project. I wrote a few device drivers for the project under Microsoft OS/2 v 1.3 - did hardware qualification and was part of the rollout team dealing directly with the customer.

Born and bred in Melbourne Australia, now living in Scottsdale Arizona USA, became a US Citizen on September 29th, 2006.

I work for a medical insurance broker, learning how to create ASP.NET websites in VB.Net and C#. It's all good.

Oh, I'm also a Kentucky Colonel.

You may also be interested in...


Comments and Discussions

GeneralConvertToUNCPath Bug Pin
Garth Watkins19-Apr-06 1:35
memberGarth Watkins19-Apr-06 1:35 
GeneralRe: ConvertToUNCPath Bug Pin
Jay Ernst22-Sep-16 12:13
memberJay Ernst22-Sep-16 12:13 
GeneralNice Class Pin
Monty224-Aug-04 22:16
memberMonty224-Aug-04 22:16 
Generaldifference between drive and directory Pin
Willem_G9-Jun-04 3:21
memberWillem_G9-Jun-04 3:21 
GeneralSmall problem (not really a bug :) Pin
Phil J Pearson1-Apr-04 5:18
memberPhil J Pearson1-Apr-04 5:18 
GeneralCool Pin
Ian Darling8-Jan-04 0:28
memberIan Darling8-Jan-04 0:28 
GeneralRe: Cool Pin
Rob Manderson8-Jan-04 0:31
protectorRob Manderson8-Jan-04 0:31 
Thank you Smile | :)

Rob Manderson

Paul Watson wrote:What sense would you most dislike loosing?

Ian Darling replied.


Then I'd no longer be able to find out everyones dirty little secrets The Lounge, December 4 2003

QuestionWhat OS's will this work on Pin
Dan Baker31-Dec-03 6:15
memberDan Baker31-Dec-03 6:15 
AnswerRe: What OS's will this work on Pin
Rob Manderson31-Dec-03 8:41
protectorRob Manderson31-Dec-03 8:41 
GeneralRe: What OS's will this work on Pin
CarstenSp3-Oct-04 9:09
memberCarstenSp3-Oct-04 9:09 
GeneralCPathSplit Pin
Kochise27-Dec-03 1:56
memberKochise27-Dec-03 1:56 
GeneralBug in adding a backslash Pin
Michael Dunn26-Dec-03 20:45
sitebuilderMichael Dunn26-Dec-03 20:45 
GeneralRe: Bug in adding a backslash Pin
Rob Manderson26-Dec-03 23:10
protectorRob Manderson26-Dec-03 23:10 
GeneralRe: Bug in adding a backslash Pin
David Crow12-Mar-04 6:04
memberDavid Crow12-Mar-04 6:04 
GeneralAuthor of CFileEditCtrl class Pin
PJ Arends24-Dec-03 6:32
professionalPJ Arends24-Dec-03 6:32 
GeneralRe: Author of CFileEditCtrl class Pin
Rob Manderson24-Dec-03 11:00
protectorRob Manderson24-Dec-03 11:00 
GeneralRe: Author of CFileEditCtrl class Pin
PJ Arends24-Dec-03 13:27
professionalPJ Arends24-Dec-03 13:27 
GeneralRe: Author of CFileEditCtrl class Pin
Rob Manderson24-Dec-03 13:57
protectorRob Manderson24-Dec-03 13:57 
QuestionBoost.Filesystem? Pin
Hartmut Kaiser24-Dec-03 2:11
memberHartmut Kaiser24-Dec-03 2:11 
AnswerRe: Boost.Filesystem? Pin
Frank De prins24-Dec-03 4:15
memberFrank De prins24-Dec-03 4:15 
GeneralRe: Boost.Filesystem? Pin
Vadim Tabakman24-Dec-03 5:57
memberVadim Tabakman24-Dec-03 5:57 
GeneralRe: Boost.Filesystem? Pin
Hartmut Kaiser26-Dec-03 0:30
memberHartmut Kaiser26-Dec-03 0:30 
GeneralRe: Boost.Filesystem? Pin
Rob Manderson26-Dec-03 0:47
protectorRob Manderson26-Dec-03 0:47 
GeneralRe: Boost.Filesystem? Pin
Adam Watkins17-May-04 9:52
memberAdam Watkins17-May-04 9:52 
GeneralRe: Boost.Filesystem? Pin
Frank De prins26-Dec-03 1:14
memberFrank De prins26-Dec-03 1:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01 | 2.8.181218.1 | Last Updated 27 Dec 2003
Article Copyright 2003 by Rob Manderson
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid