Is there any class in the .NET framework that can read/write standard .ini files:
[Section]
<keyname>=<value>
...
Delphi has the TIniFile component and I want to know if there is anything similar for C#?
Preface
Firstly, read this MSDN blog post on the limitations of INI files. If it suits your needs, read on.
This is a concise implementation I wrote, utilising the original Windows P/Invoke, so it is supported by all versions of Windows with .NET installed, (i.e. Windows 98 - Windows 11). I hereby release it into the public domain - you're free to use it commercially without attribution.
The tiny class
Add a new class called IniFile.cs to your project:
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
// Change this to match your program's normal namespace
namespace MyProg
{
class IniFile // revision 11
{
string Path;
string EXE = Assembly.GetExecutingAssembly().GetName().Name;
[DllImport("kernel32", CharSet = CharSet.Unicode)]
static extern long WritePrivateProfileString(string Section, string Key, string Value, string FilePath);
[DllImport("kernel32", CharSet = CharSet.Unicode)]
static extern int GetPrivateProfileString(string Section, string Key, string Default, StringBuilder RetVal, int Size, string FilePath);
public IniFile(string IniPath = null)
{
Path = new FileInfo(IniPath ?? EXE + ".ini").FullName;
}
public string Read(string Key, string Section = null)
{
var RetVal = new StringBuilder(255);
GetPrivateProfileString(Section ?? EXE, Key, "", RetVal, 255, Path);
return RetVal.ToString();
}
public void Write(string Key, string Value, string Section = null)
{
WritePrivateProfileString(Section ?? EXE, Key, Value, Path);
}
public void DeleteKey(string Key, string Section = null)
{
Write(Key, null, Section ?? EXE);
}
public void DeleteSection(string Section = null)
{
Write(null, null, Section ?? EXE);
}
public bool KeyExists(string Key, string Section = null)
{
return Read(Key, Section).Length > 0;
}
}
}
How to use it
Open the INI file in one of the 3 following ways:
// Creates or loads an INI file in the same directory as your executable
// named EXE.ini (where EXE is the name of your executable)
var MyIni = new IniFile();
// Or specify a specific name in the current dir
var MyIni = new IniFile("Settings.ini");
// Or specify a specific name in a specific dir
var MyIni = new IniFile(#"C:\Settings.ini");
You can write some values like so:
MyIni.Write("DefaultVolume", "100");
MyIni.Write("HomePage", "http://www.google.com");
To create a file like this:
[MyProg]
DefaultVolume=100
HomePage=http://www.google.com
To read the values out of the INI file:
var DefaultVolume = MyIni.Read("DefaultVolume");
var HomePage = MyIni.Read("HomePage");
Optionally, you can set [Section]'s:
MyIni.Write("DefaultVolume", "100", "Audio");
MyIni.Write("HomePage", "http://www.google.com", "Web");
To create a file like this:
[Audio]
DefaultVolume=100
[Web]
HomePage=http://www.google.com
You can also check for the existence of a key like so:
if(!MyIni.KeyExists("DefaultVolume", "Audio"))
{
MyIni.Write("DefaultVolume", "100", "Audio");
}
You can delete a key like so:
MyIni.DeleteKey("DefaultVolume", "Audio");
You can also delete a whole section (including all keys) like so:
MyIni.DeleteSection("Web");
Please feel free to comment with any improvements!
The creators of the .NET framework want you to use XML-based config files, rather than INI files. So no, there is no built-in mechanism for reading them.
There are third party solutions available, though.
INI handlers can be obtained as NuGet packages, such as INI Parser.
You can write your own INI handler, which is the old-school, laborious way. It gives you more control over the implementation, which you can use for bad or good. See e.g. an INI file handling class using C#, P/Invoke and Win32.
This article on CodeProject "An INI file handling class using C#" should help.
The author created a C# class "Ini" which exposes two functions from KERNEL32.dll. These functions are: WritePrivateProfileString and GetPrivateProfileString. You will need two namespaces: System.Runtime.InteropServices and System.Text.
Steps to use the Ini class
In your project namespace definition add
using INI;
Create a INIFile like this
INIFile ini = new INIFile("C:\\test.ini");
Use IniWriteValue to write a new value to a specific key in a section or use IniReadValue to read a value FROM a key in a specific Section.
Note: if you're beginning from scratch, you could read this MSDN article: How to: Add Application Configuration Files to C# Projects. It's a better way for configuring your application.
I found this simple implementation:
http://bytes.com/topic/net/insights/797169-reading-parsing-ini-file-c
Works well for what I need.
Here is how you use it:
public class TestParser
{
public static void Main()
{
IniParser parser = new IniParser(#"C:\test.ini");
String newMessage;
newMessage = parser.GetSetting("appsettings", "msgpart1");
newMessage += parser.GetSetting("appsettings", "msgpart2");
newMessage += parser.GetSetting("punctuation", "ex");
//Returns "Hello World!"
Console.WriteLine(newMessage);
Console.ReadLine();
}
}
Here is the code:
using System;
using System.IO;
using System.Collections;
public class IniParser
{
private Hashtable keyPairs = new Hashtable();
private String iniFilePath;
private struct SectionPair
{
public String Section;
public String Key;
}
/// <summary>
/// Opens the INI file at the given path and enumerates the values in the IniParser.
/// </summary>
/// <param name="iniPath">Full path to INI file.</param>
public IniParser(String iniPath)
{
TextReader iniFile = null;
String strLine = null;
String currentRoot = null;
String[] keyPair = null;
iniFilePath = iniPath;
if (File.Exists(iniPath))
{
try
{
iniFile = new StreamReader(iniPath);
strLine = iniFile.ReadLine();
while (strLine != null)
{
strLine = strLine.Trim().ToUpper();
if (strLine != "")
{
if (strLine.StartsWith("[") && strLine.EndsWith("]"))
{
currentRoot = strLine.Substring(1, strLine.Length - 2);
}
else
{
keyPair = strLine.Split(new char[] { '=' }, 2);
SectionPair sectionPair;
String value = null;
if (currentRoot == null)
currentRoot = "ROOT";
sectionPair.Section = currentRoot;
sectionPair.Key = keyPair[0];
if (keyPair.Length > 1)
value = keyPair[1];
keyPairs.Add(sectionPair, value);
}
}
strLine = iniFile.ReadLine();
}
}
catch (Exception ex)
{
throw ex;
}
finally
{
if (iniFile != null)
iniFile.Close();
}
}
else
throw new FileNotFoundException("Unable to locate " + iniPath);
}
/// <summary>
/// Returns the value for the given section, key pair.
/// </summary>
/// <param name="sectionName">Section name.</param>
/// <param name="settingName">Key name.</param>
public String GetSetting(String sectionName, String settingName)
{
SectionPair sectionPair;
sectionPair.Section = sectionName.ToUpper();
sectionPair.Key = settingName.ToUpper();
return (String)keyPairs[sectionPair];
}
/// <summary>
/// Enumerates all lines for given section.
/// </summary>
/// <param name="sectionName">Section to enum.</param>
public String[] EnumSection(String sectionName)
{
ArrayList tmpArray = new ArrayList();
foreach (SectionPair pair in keyPairs.Keys)
{
if (pair.Section == sectionName.ToUpper())
tmpArray.Add(pair.Key);
}
return (String[])tmpArray.ToArray(typeof(String));
}
/// <summary>
/// Adds or replaces a setting to the table to be saved.
/// </summary>
/// <param name="sectionName">Section to add under.</param>
/// <param name="settingName">Key name to add.</param>
/// <param name="settingValue">Value of key.</param>
public void AddSetting(String sectionName, String settingName, String settingValue)
{
SectionPair sectionPair;
sectionPair.Section = sectionName.ToUpper();
sectionPair.Key = settingName.ToUpper();
if (keyPairs.ContainsKey(sectionPair))
keyPairs.Remove(sectionPair);
keyPairs.Add(sectionPair, settingValue);
}
/// <summary>
/// Adds or replaces a setting to the table to be saved with a null value.
/// </summary>
/// <param name="sectionName">Section to add under.</param>
/// <param name="settingName">Key name to add.</param>
public void AddSetting(String sectionName, String settingName)
{
AddSetting(sectionName, settingName, null);
}
/// <summary>
/// Remove a setting.
/// </summary>
/// <param name="sectionName">Section to add under.</param>
/// <param name="settingName">Key name to add.</param>
public void DeleteSetting(String sectionName, String settingName)
{
SectionPair sectionPair;
sectionPair.Section = sectionName.ToUpper();
sectionPair.Key = settingName.ToUpper();
if (keyPairs.ContainsKey(sectionPair))
keyPairs.Remove(sectionPair);
}
/// <summary>
/// Save settings to new file.
/// </summary>
/// <param name="newFilePath">New file path.</param>
public void SaveSettings(String newFilePath)
{
ArrayList sections = new ArrayList();
String tmpValue = "";
String strToSave = "";
foreach (SectionPair sectionPair in keyPairs.Keys)
{
if (!sections.Contains(sectionPair.Section))
sections.Add(sectionPair.Section);
}
foreach (String section in sections)
{
strToSave += ("[" + section + "]\r\n");
foreach (SectionPair sectionPair in keyPairs.Keys)
{
if (sectionPair.Section == section)
{
tmpValue = (String)keyPairs[sectionPair];
if (tmpValue != null)
tmpValue = "=" + tmpValue;
strToSave += (sectionPair.Key + tmpValue + "\r\n");
}
}
strToSave += "\r\n";
}
try
{
TextWriter tw = new StreamWriter(newFilePath);
tw.Write(strToSave);
tw.Close();
}
catch (Exception ex)
{
throw ex;
}
}
/// <summary>
/// Save settings back to ini file.
/// </summary>
public void SaveSettings()
{
SaveSettings(iniFilePath);
}
}
The code in joerage's answer is inspiring.
Unfortunately, it changes the character casing of the keys and does not handle comments. So I wrote something that should be robust enough to read (only) very dirty INI files and allows to retrieve keys as they are.
It uses some LINQ, a nested case insensitive string dictionary to store sections, keys and values, and read the file in one go.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
class IniReader
{
Dictionary<string, Dictionary<string, string>> ini = new Dictionary<string, Dictionary<string, string>>(StringComparer.InvariantCultureIgnoreCase);
public IniReader(string file)
{
var txt = File.ReadAllText(file);
Dictionary<string, string> currentSection = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
ini[""] = currentSection;
foreach(var line in txt.Split(new[]{"\n"}, StringSplitOptions.RemoveEmptyEntries)
.Where(t => !string.IsNullOrWhiteSpace(t))
.Select(t => t.Trim()))
{
if (line.StartsWith(";"))
continue;
if (line.StartsWith("[") && line.EndsWith("]"))
{
currentSection = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
ini[line.Substring(1, line.LastIndexOf("]") - 1)] = currentSection;
continue;
}
var idx = line.IndexOf("=");
if (idx == -1)
currentSection[line] = "";
else
currentSection[line.Substring(0, idx)] = line.Substring(idx + 1);
}
}
public string GetValue(string key)
{
return GetValue(key, "", "");
}
public string GetValue(string key, string section)
{
return GetValue(key, section, "");
}
public string GetValue(string key, string section, string #default)
{
if (!ini.ContainsKey(section))
return #default;
if (!ini[section].ContainsKey(key))
return #default;
return ini[section][key];
}
public string[] GetKeys(string section)
{
if (!ini.ContainsKey(section))
return new string[0];
return ini[section].Keys.ToArray();
}
public string[] GetSections()
{
return ini.Keys.Where(t => t != "").ToArray();
}
}
I want to introduce an IniParser library I've created completely in c#, so it contains no dependencies in any OS, which makes it Mono compatible. Open Source with MIT license -so it can be used in any code.
You can check out the source in GitHub, and it is also available as a NuGet package
It's heavily configurable, and really simple to use.
Sorry for the shameless plug but I hope it can be of help of anyone revisiting this answer.
If you only need read access and not write access and you are using the Microsoft.Extensions.Confiuration (comes bundled in by default with ASP.NET Core but works with regular programs too) you can use the NuGet package Microsoft.Extensions.Configuration.Ini to import ini files in to your configuration settings.
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddIniFile("SomeConfig.ini", optional: false);
Configuration = builder.Build();
}
PeanutButter.INI is a Nuget-packaged class for INI files manipulation. It supports read/write, including comments – your comments are preserved on write. It appears to be reasonably popular, is tested and easy to use. It's also totally free and open-source.
Disclaimer: I am the author of PeanutButter.INI.
Usually, when you create applications using C# and the .NET framework, you will not use INI files. It is more common to store settings in an XML-based configuration file or in the registry.
However, if your software shares settings with a legacy application it may be easier to use its configuration file, rather than duplicating the information elsewhere.
The .NET framework does not support the use of INI files directly. However, you can use Windows API functions with Platform Invocation Services (P/Invoke) to write to and read from the files. In this link we create a class that represents INI files and uses Windows API functions to manipulate them.
Please go through the following link.
Reading and Writing INI Files
If you want just a simple reader without sections and any other dlls here is simple solution:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Tool
{
public class Config
{
Dictionary <string, string> values;
public Config (string path)
{
values = File.ReadLines(path)
.Where(line => (!String.IsNullOrWhiteSpace(line) && !line.StartsWith("#")))
.Select(line => line.Split(new char[] { '=' }, 2, 0))
.ToDictionary(parts => parts[0].Trim(), parts => parts.Length>1?parts[1].Trim():null);
}
public string Value (string name, string value=null)
{
if (values!=null && values.ContainsKey(name))
{
return values[name];
}
return value;
}
}
}
Usage sample:
file = new Tool.Config (Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + "\\config.ini");
command = file.Value ("command");
action = file.Value ("action");
string value;
//second parameter is default value if no key found with this name
value = file.Value("debug","true");
this.debug = (value.ToLower()=="true" || value== "1");
value = file.Value("plain", "false");
this.plain = (value.ToLower() == "true" || value == "1");
Config file content meanwhile (as you see supports # symbol for line comment):
#command to run
command = php
#default script
action = index.php
#debug mode
#debug = true
#plain text mode
#plain = false
#icon = favico.ico
Try this method:
public static Dictionary<string, string> ParseIniDataWithSections(string[] iniData)
{
var dict = new Dictionary<string, string>();
var rows = iniData.Where(t =>
!String.IsNullOrEmpty(t.Trim()) && !t.StartsWith(";") && (t.Contains('[') || t.Contains('=')));
if (rows == null || rows.Count() == 0) return dict;
string section = "";
foreach (string row in rows)
{
string rw = row.TrimStart();
if (rw.StartsWith("["))
section = rw.TrimStart('[').TrimEnd(']');
else
{
int index = rw.IndexOf('=');
dict[section + "-" + rw.Substring(0, index).Trim()] = rw.Substring(index+1).Trim().Trim('"');
}
}
return dict;
}
It creates the dictionary where the key is "-". You can load it like this:
var dict = ParseIniDataWithSections(File.ReadAllLines(fileName));
I'm late to join the party, but I had the same issue today and I've written the following implementation:
using System.Text.RegularExpressions;
static bool match(this string str, string pat, out Match m) =>
(m = Regex.Match(str, pat, RegexOptions.IgnoreCase)).Success;
static void Main()
{
Dictionary<string, Dictionary<string, string>> ini = new Dictionary<string, Dictionary<string, string>>();
string section = "";
foreach (string line in File.ReadAllLines(.........)) // read from file
{
string ln = (line.Contains('#') ? line.Remove(line.IndexOf('#')) : line).Trim();
if (ln.match(#"^[ \t]*\[(?<sec>[\w\-]+)\]", out Match m))
section = m.Groups["sec"].ToString();
else if (ln.match(#"^[ \t]*(?<prop>[\w\-]+)\=(?<val>.*)", out m))
{
if (!ini.ContainsKey(section))
ini[section] = new Dictionary<string, string>();
ini[section][m.Groups["prop"].ToString()] = m.Groups["val"].ToString();
}
}
// access the ini file as follows:
string content = ini["section"]["property"];
}
It must be noted, that this implementation does not handle sections or properties which are not found.
To achieve this, you should extend the Dictionary<,>-class to handle unfound keys.
To serialize an instance of Dictionary<string, Dictionary<string, string>> to an .ini-file, I use the following code:
string targetpath = .........;
Dictionary<string, Dictionary<string, string>> ini = ........;
StringBuilder sb = new StringBuilder();
foreach (string section in ini.Keys)
{
sb.AppendLine($"[{section}]");
foreach (string property in ini[section].Keys)
sb.AppendLine($"{property}={ini[section][property]");
}
File.WriteAllText(targetpath, sb.ToString());
There is an Ini Parser available in CommonLibrary.NET
This has various very convenient overloads for getting sections/values and is very light weight.
Here is my own version, using regular expressions. This code assumes that each section name is unique - if however this is not true - it makes sense to replace Dictionary with List. This function supports .ini file commenting, starting from ';' character. Section starts normally [section], and key value pairs also comes normally "key = value". Same assumption as for sections - key name is unique.
/// <summary>
/// Loads .ini file into dictionary.
/// </summary>
public static Dictionary<String, Dictionary<String, String>> loadIni(String file)
{
Dictionary<String, Dictionary<String, String>> d = new Dictionary<string, Dictionary<string, string>>();
String ini = File.ReadAllText(file);
// Remove comments, preserve linefeeds, if end-user needs to count line number.
ini = Regex.Replace(ini, #"^\s*;.*$", "", RegexOptions.Multiline);
// Pick up all lines from first section to another section
foreach (Match m in Regex.Matches(ini, "(^|[\r\n])\\[([^\r\n]*)\\][\r\n]+(.*?)(\\[([^\r\n]*)\\][\r\n]+|$)", RegexOptions.Singleline))
{
String sectionName = m.Groups[2].Value;
Dictionary<String, String> lines = new Dictionary<String, String>();
// Pick up "key = value" kind of syntax.
foreach (Match l in Regex.Matches(ini, #"^\s*(.*?)\s*=\s*(.*?)\s*$", RegexOptions.Multiline))
{
String key = l.Groups[1].Value;
String value = l.Groups[2].Value;
// Open up quotation if any.
value = Regex.Replace(value, "^\"(.*)\"$", "$1");
if (!lines.ContainsKey(key))
lines[key] = value;
}
if (!d.ContainsKey(sectionName))
d[sectionName] = lines;
}
return d;
}
If you don't need bells and whistles (ie sections) here's a one liner:
List<(string, string)> ini = File.ReadLines(filename)
.Select(s => {
var spl = s.Split('=', 2);
return spl.Length == 2 ? (spl[0], spl[1]) : (s, "");
})
.Select(vt => (vt.Item1.Trim(), vt.Item2.Trim()))
.Where(vt => vt.Item1 != "")
.ToList();
To write:
File.WriteAllLines(filename, ini.Select(vt => $"{vt.Item1}={vt.Item2}"));
(if you don't care about duplicates use .ToDictionary() instead of .ToList() for easier access)
Here is my class, works like a charm :
public static class IniFileManager
{
[DllImport("kernel32")]
private static extern long WritePrivateProfileString(string section,
string key, string val, string filePath);
[DllImport("kernel32")]
private static extern int GetPrivateProfileString(string section,
string key, string def, StringBuilder retVal,
int size, string filePath);
[DllImport("kernel32.dll")]
private static extern int GetPrivateProfileSection(string lpAppName,
byte[] lpszReturnBuffer, int nSize, string lpFileName);
/// <summary>
/// Write Data to the INI File
/// </summary>
/// <PARAM name="Section"></PARAM>
/// Section name
/// <PARAM name="Key"></PARAM>
/// Key Name
/// <PARAM name="Value"></PARAM>
/// Value Name
public static void IniWriteValue(string sPath,string Section, string Key, string Value)
{
WritePrivateProfileString(Section, Key, Value, sPath);
}
/// <summary>
/// Read Data Value From the Ini File
/// </summary>
/// <PARAM name="Section"></PARAM>
/// <PARAM name="Key"></PARAM>
/// <PARAM name="Path"></PARAM>
/// <returns></returns>
public static string IniReadValue(string sPath,string Section, string Key)
{
StringBuilder temp = new StringBuilder(255);
int i = GetPrivateProfileString(Section, Key, "", temp,
255, sPath);
return temp.ToString();
}
}
The use is obviouse since its a static class, just call IniFileManager.IniWriteValue for readsing a section or IniFileManager.IniReadValue for reading a section.
You should read and write data from xml files since you can save a whole object to xml and also you can populate a object from a saved xml. It is better an easy to manipulate objects.
Here is how to do it:
Write Object Data to an XML File: https://msdn.microsoft.com/en-us/library/ms172873.aspx
Read Object Data from an XML File: https://msdn.microsoft.com/en-us/library/ms172872.aspx
Related
I would like to scan a directory ("C:/test") and get all files .pdf recursively
I create a provider like this :
IFileProvider provider = new PhysicalFileProvider("C:/test"); // using config in my code and also tried with "C:/test/"
I placed some pdf in directories and subdirectories
There's a file with this path : C:/test/pdf59.pdf
Another with C:/test/testComplexe/pdf59.pdf
Where I try these lines, they all return "NotFoundDirectoryException" :
provider.getDirectoryContents(#"**")
provider.getDirectoryContents(#"*")
provider.getDirectoryContents(#"*.*")
provider.getDirectoryContents(#"**.*")
provider.getDirectoryContents(#"pdf59.pdf")
provider.getDirectoryContents(#"*.pdf")
Exception this line :
provider.getDirectoryContents(#"testComplexe")
How could i query these recursive directories and files ? Thank you
You can write your own recursive function.
var files = new List<IFileInfo>();
GetFiles("C:/Tests", files);
private void GetFiles(string path, ICollection<IFileInfo> files)
{
IFileProvider provider = new PhysicalFileProvider(path);
var contents = provider.GetDirectoryContents("");
foreach (var content in contents)
{
if (!content.IsDirectory && content.Name.ToLower().EndsWith(".pdf"))
{
files.Add(content);
}
else
{
GetFiles(content.PhysicalPath, files);
}
}
}
For my future self (and others) to copy-paste...
public static class Demo
{
public static void FindPdfs()
{
var provider = new PhysicalFileProvider("c:\\temp");
var pdfs = new List<IFileInfo>();
provider.FindFiles(
directory: "/",
match: file => file.Name.EndsWith(".pdf"),
process: pdfs.Add,
recursive: true);
foreach (var pdf in pdfs)
{
using (var stream = pdf.CreateReadStream())
{
// etc...
}
}
}
}
public static class FileProviderExtensions
{
/// <summary>
/// Searches for files matching some <paramref name="match"/>, and invokes <paramref name="process"/> on them.
/// </summary>
/// <param name="provider">File provider</param>
/// <param name="directory">parent directory for the search, a relative path, leading slashes are ignored,
/// use "" or "/" for starting at the root of <paramref name="provider"/></param>
/// <param name="match">the match predicate, if this returns true the file is passed to <paramref name="process"/></param>
/// <param name="process">this action is invoked on <paramref name="match"/>ing files </param>
/// <param name="recursive">if true directories a</param>
/// <returns>the number of files <paramref name="match"/>ed and <paramref name="process"/>ed</returns>
public static int FindFiles(this IFileProvider provider, string directory, Predicate<IFileInfo> match, Action<IFileInfo> process, bool recursive = false)
{
var dirsToSearch = new Stack<string>();
dirsToSearch.Push(directory);
var count = 0;
while (dirsToSearch.Count > 0)
{
var dir = dirsToSearch.Pop();
foreach (var file in provider.GetDirectoryContents(dir))
{
if (file.IsDirectory)
{
if (!recursive)
continue;
var relPath = Path.Join(dir, file.Name);
dirsToSearch.Push(relPath);
}
else
{
if (!match(file))
continue;
process(file);
count++;
}
}
}
return count;
}
}
This is the simplest version if you just want a list of IFileInfo objects. It uses an extension method. Assumes you already have a IFileProvider, such as one that has been injected.
var files = FileProvider.GetRecursiveFiles("/images").ToArray();
using Microsoft.Extensions.FileProviders;
using System.Collections.Generic;
using System.IO;
public static class FileProviderExtensions
{
public static IEnumerable<IFileInfo> GetRecursiveFiles(this IFileProvider fileProvider, string path, bool includeDirectories = true)
{
var directoryContents = fileProvider.GetDirectoryContents(path);
foreach (var file in directoryContents)
{
if (file.IsDirectory)
{
if (includeDirectories)
{
yield return file;
}
// recursively call GetFiles and return each one
// file.Name is the directory name alone (eg. images)
foreach (var f in fileProvider.GetRecursiveFiles(Path.Combine(path, file.Name), includeDirectories))
{
yield return f;
}
}
else
{
// return file
yield return file;
}
}
}
}
To get the content of your root folder ("C:/test/"), use the provider this way :
var contents = provider.getDirectoryContents("")
Then, you have to enumerate results in contents, and do what ever you want with each one.
Documentation : https://learn.microsoft.com/en-us/aspnet/core/fundamentals/file-providers?view=aspnetcore-2.1
Is there any class in the .NET framework that can read/write standard .ini files:
[Section]
<keyname>=<value>
...
Delphi has the TIniFile component and I want to know if there is anything similar for C#?
Preface
Firstly, read this MSDN blog post on the limitations of INI files. If it suits your needs, read on.
This is a concise implementation I wrote, utilising the original Windows P/Invoke, so it is supported by all versions of Windows with .NET installed, (i.e. Windows 98 - Windows 11). I hereby release it into the public domain - you're free to use it commercially without attribution.
The tiny class
Add a new class called IniFile.cs to your project:
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
// Change this to match your program's normal namespace
namespace MyProg
{
class IniFile // revision 11
{
string Path;
string EXE = Assembly.GetExecutingAssembly().GetName().Name;
[DllImport("kernel32", CharSet = CharSet.Unicode)]
static extern long WritePrivateProfileString(string Section, string Key, string Value, string FilePath);
[DllImport("kernel32", CharSet = CharSet.Unicode)]
static extern int GetPrivateProfileString(string Section, string Key, string Default, StringBuilder RetVal, int Size, string FilePath);
public IniFile(string IniPath = null)
{
Path = new FileInfo(IniPath ?? EXE + ".ini").FullName;
}
public string Read(string Key, string Section = null)
{
var RetVal = new StringBuilder(255);
GetPrivateProfileString(Section ?? EXE, Key, "", RetVal, 255, Path);
return RetVal.ToString();
}
public void Write(string Key, string Value, string Section = null)
{
WritePrivateProfileString(Section ?? EXE, Key, Value, Path);
}
public void DeleteKey(string Key, string Section = null)
{
Write(Key, null, Section ?? EXE);
}
public void DeleteSection(string Section = null)
{
Write(null, null, Section ?? EXE);
}
public bool KeyExists(string Key, string Section = null)
{
return Read(Key, Section).Length > 0;
}
}
}
How to use it
Open the INI file in one of the 3 following ways:
// Creates or loads an INI file in the same directory as your executable
// named EXE.ini (where EXE is the name of your executable)
var MyIni = new IniFile();
// Or specify a specific name in the current dir
var MyIni = new IniFile("Settings.ini");
// Or specify a specific name in a specific dir
var MyIni = new IniFile(#"C:\Settings.ini");
You can write some values like so:
MyIni.Write("DefaultVolume", "100");
MyIni.Write("HomePage", "http://www.google.com");
To create a file like this:
[MyProg]
DefaultVolume=100
HomePage=http://www.google.com
To read the values out of the INI file:
var DefaultVolume = MyIni.Read("DefaultVolume");
var HomePage = MyIni.Read("HomePage");
Optionally, you can set [Section]'s:
MyIni.Write("DefaultVolume", "100", "Audio");
MyIni.Write("HomePage", "http://www.google.com", "Web");
To create a file like this:
[Audio]
DefaultVolume=100
[Web]
HomePage=http://www.google.com
You can also check for the existence of a key like so:
if(!MyIni.KeyExists("DefaultVolume", "Audio"))
{
MyIni.Write("DefaultVolume", "100", "Audio");
}
You can delete a key like so:
MyIni.DeleteKey("DefaultVolume", "Audio");
You can also delete a whole section (including all keys) like so:
MyIni.DeleteSection("Web");
Please feel free to comment with any improvements!
The creators of the .NET framework want you to use XML-based config files, rather than INI files. So no, there is no built-in mechanism for reading them.
There are third party solutions available, though.
INI handlers can be obtained as NuGet packages, such as INI Parser.
You can write your own INI handler, which is the old-school, laborious way. It gives you more control over the implementation, which you can use for bad or good. See e.g. an INI file handling class using C#, P/Invoke and Win32.
This article on CodeProject "An INI file handling class using C#" should help.
The author created a C# class "Ini" which exposes two functions from KERNEL32.dll. These functions are: WritePrivateProfileString and GetPrivateProfileString. You will need two namespaces: System.Runtime.InteropServices and System.Text.
Steps to use the Ini class
In your project namespace definition add
using INI;
Create a INIFile like this
INIFile ini = new INIFile("C:\\test.ini");
Use IniWriteValue to write a new value to a specific key in a section or use IniReadValue to read a value FROM a key in a specific Section.
Note: if you're beginning from scratch, you could read this MSDN article: How to: Add Application Configuration Files to C# Projects. It's a better way for configuring your application.
I found this simple implementation:
http://bytes.com/topic/net/insights/797169-reading-parsing-ini-file-c
Works well for what I need.
Here is how you use it:
public class TestParser
{
public static void Main()
{
IniParser parser = new IniParser(#"C:\test.ini");
String newMessage;
newMessage = parser.GetSetting("appsettings", "msgpart1");
newMessage += parser.GetSetting("appsettings", "msgpart2");
newMessage += parser.GetSetting("punctuation", "ex");
//Returns "Hello World!"
Console.WriteLine(newMessage);
Console.ReadLine();
}
}
Here is the code:
using System;
using System.IO;
using System.Collections;
public class IniParser
{
private Hashtable keyPairs = new Hashtable();
private String iniFilePath;
private struct SectionPair
{
public String Section;
public String Key;
}
/// <summary>
/// Opens the INI file at the given path and enumerates the values in the IniParser.
/// </summary>
/// <param name="iniPath">Full path to INI file.</param>
public IniParser(String iniPath)
{
TextReader iniFile = null;
String strLine = null;
String currentRoot = null;
String[] keyPair = null;
iniFilePath = iniPath;
if (File.Exists(iniPath))
{
try
{
iniFile = new StreamReader(iniPath);
strLine = iniFile.ReadLine();
while (strLine != null)
{
strLine = strLine.Trim().ToUpper();
if (strLine != "")
{
if (strLine.StartsWith("[") && strLine.EndsWith("]"))
{
currentRoot = strLine.Substring(1, strLine.Length - 2);
}
else
{
keyPair = strLine.Split(new char[] { '=' }, 2);
SectionPair sectionPair;
String value = null;
if (currentRoot == null)
currentRoot = "ROOT";
sectionPair.Section = currentRoot;
sectionPair.Key = keyPair[0];
if (keyPair.Length > 1)
value = keyPair[1];
keyPairs.Add(sectionPair, value);
}
}
strLine = iniFile.ReadLine();
}
}
catch (Exception ex)
{
throw ex;
}
finally
{
if (iniFile != null)
iniFile.Close();
}
}
else
throw new FileNotFoundException("Unable to locate " + iniPath);
}
/// <summary>
/// Returns the value for the given section, key pair.
/// </summary>
/// <param name="sectionName">Section name.</param>
/// <param name="settingName">Key name.</param>
public String GetSetting(String sectionName, String settingName)
{
SectionPair sectionPair;
sectionPair.Section = sectionName.ToUpper();
sectionPair.Key = settingName.ToUpper();
return (String)keyPairs[sectionPair];
}
/// <summary>
/// Enumerates all lines for given section.
/// </summary>
/// <param name="sectionName">Section to enum.</param>
public String[] EnumSection(String sectionName)
{
ArrayList tmpArray = new ArrayList();
foreach (SectionPair pair in keyPairs.Keys)
{
if (pair.Section == sectionName.ToUpper())
tmpArray.Add(pair.Key);
}
return (String[])tmpArray.ToArray(typeof(String));
}
/// <summary>
/// Adds or replaces a setting to the table to be saved.
/// </summary>
/// <param name="sectionName">Section to add under.</param>
/// <param name="settingName">Key name to add.</param>
/// <param name="settingValue">Value of key.</param>
public void AddSetting(String sectionName, String settingName, String settingValue)
{
SectionPair sectionPair;
sectionPair.Section = sectionName.ToUpper();
sectionPair.Key = settingName.ToUpper();
if (keyPairs.ContainsKey(sectionPair))
keyPairs.Remove(sectionPair);
keyPairs.Add(sectionPair, settingValue);
}
/// <summary>
/// Adds or replaces a setting to the table to be saved with a null value.
/// </summary>
/// <param name="sectionName">Section to add under.</param>
/// <param name="settingName">Key name to add.</param>
public void AddSetting(String sectionName, String settingName)
{
AddSetting(sectionName, settingName, null);
}
/// <summary>
/// Remove a setting.
/// </summary>
/// <param name="sectionName">Section to add under.</param>
/// <param name="settingName">Key name to add.</param>
public void DeleteSetting(String sectionName, String settingName)
{
SectionPair sectionPair;
sectionPair.Section = sectionName.ToUpper();
sectionPair.Key = settingName.ToUpper();
if (keyPairs.ContainsKey(sectionPair))
keyPairs.Remove(sectionPair);
}
/// <summary>
/// Save settings to new file.
/// </summary>
/// <param name="newFilePath">New file path.</param>
public void SaveSettings(String newFilePath)
{
ArrayList sections = new ArrayList();
String tmpValue = "";
String strToSave = "";
foreach (SectionPair sectionPair in keyPairs.Keys)
{
if (!sections.Contains(sectionPair.Section))
sections.Add(sectionPair.Section);
}
foreach (String section in sections)
{
strToSave += ("[" + section + "]\r\n");
foreach (SectionPair sectionPair in keyPairs.Keys)
{
if (sectionPair.Section == section)
{
tmpValue = (String)keyPairs[sectionPair];
if (tmpValue != null)
tmpValue = "=" + tmpValue;
strToSave += (sectionPair.Key + tmpValue + "\r\n");
}
}
strToSave += "\r\n";
}
try
{
TextWriter tw = new StreamWriter(newFilePath);
tw.Write(strToSave);
tw.Close();
}
catch (Exception ex)
{
throw ex;
}
}
/// <summary>
/// Save settings back to ini file.
/// </summary>
public void SaveSettings()
{
SaveSettings(iniFilePath);
}
}
The code in joerage's answer is inspiring.
Unfortunately, it changes the character casing of the keys and does not handle comments. So I wrote something that should be robust enough to read (only) very dirty INI files and allows to retrieve keys as they are.
It uses some LINQ, a nested case insensitive string dictionary to store sections, keys and values, and read the file in one go.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
class IniReader
{
Dictionary<string, Dictionary<string, string>> ini = new Dictionary<string, Dictionary<string, string>>(StringComparer.InvariantCultureIgnoreCase);
public IniReader(string file)
{
var txt = File.ReadAllText(file);
Dictionary<string, string> currentSection = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
ini[""] = currentSection;
foreach(var line in txt.Split(new[]{"\n"}, StringSplitOptions.RemoveEmptyEntries)
.Where(t => !string.IsNullOrWhiteSpace(t))
.Select(t => t.Trim()))
{
if (line.StartsWith(";"))
continue;
if (line.StartsWith("[") && line.EndsWith("]"))
{
currentSection = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
ini[line.Substring(1, line.LastIndexOf("]") - 1)] = currentSection;
continue;
}
var idx = line.IndexOf("=");
if (idx == -1)
currentSection[line] = "";
else
currentSection[line.Substring(0, idx)] = line.Substring(idx + 1);
}
}
public string GetValue(string key)
{
return GetValue(key, "", "");
}
public string GetValue(string key, string section)
{
return GetValue(key, section, "");
}
public string GetValue(string key, string section, string #default)
{
if (!ini.ContainsKey(section))
return #default;
if (!ini[section].ContainsKey(key))
return #default;
return ini[section][key];
}
public string[] GetKeys(string section)
{
if (!ini.ContainsKey(section))
return new string[0];
return ini[section].Keys.ToArray();
}
public string[] GetSections()
{
return ini.Keys.Where(t => t != "").ToArray();
}
}
I want to introduce an IniParser library I've created completely in c#, so it contains no dependencies in any OS, which makes it Mono compatible. Open Source with MIT license -so it can be used in any code.
You can check out the source in GitHub, and it is also available as a NuGet package
It's heavily configurable, and really simple to use.
Sorry for the shameless plug but I hope it can be of help of anyone revisiting this answer.
If you only need read access and not write access and you are using the Microsoft.Extensions.Confiuration (comes bundled in by default with ASP.NET Core but works with regular programs too) you can use the NuGet package Microsoft.Extensions.Configuration.Ini to import ini files in to your configuration settings.
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddIniFile("SomeConfig.ini", optional: false);
Configuration = builder.Build();
}
PeanutButter.INI is a Nuget-packaged class for INI files manipulation. It supports read/write, including comments – your comments are preserved on write. It appears to be reasonably popular, is tested and easy to use. It's also totally free and open-source.
Disclaimer: I am the author of PeanutButter.INI.
Usually, when you create applications using C# and the .NET framework, you will not use INI files. It is more common to store settings in an XML-based configuration file or in the registry.
However, if your software shares settings with a legacy application it may be easier to use its configuration file, rather than duplicating the information elsewhere.
The .NET framework does not support the use of INI files directly. However, you can use Windows API functions with Platform Invocation Services (P/Invoke) to write to and read from the files. In this link we create a class that represents INI files and uses Windows API functions to manipulate them.
Please go through the following link.
Reading and Writing INI Files
If you want just a simple reader without sections and any other dlls here is simple solution:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Tool
{
public class Config
{
Dictionary <string, string> values;
public Config (string path)
{
values = File.ReadLines(path)
.Where(line => (!String.IsNullOrWhiteSpace(line) && !line.StartsWith("#")))
.Select(line => line.Split(new char[] { '=' }, 2, 0))
.ToDictionary(parts => parts[0].Trim(), parts => parts.Length>1?parts[1].Trim():null);
}
public string Value (string name, string value=null)
{
if (values!=null && values.ContainsKey(name))
{
return values[name];
}
return value;
}
}
}
Usage sample:
file = new Tool.Config (Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + "\\config.ini");
command = file.Value ("command");
action = file.Value ("action");
string value;
//second parameter is default value if no key found with this name
value = file.Value("debug","true");
this.debug = (value.ToLower()=="true" || value== "1");
value = file.Value("plain", "false");
this.plain = (value.ToLower() == "true" || value == "1");
Config file content meanwhile (as you see supports # symbol for line comment):
#command to run
command = php
#default script
action = index.php
#debug mode
#debug = true
#plain text mode
#plain = false
#icon = favico.ico
Try this method:
public static Dictionary<string, string> ParseIniDataWithSections(string[] iniData)
{
var dict = new Dictionary<string, string>();
var rows = iniData.Where(t =>
!String.IsNullOrEmpty(t.Trim()) && !t.StartsWith(";") && (t.Contains('[') || t.Contains('=')));
if (rows == null || rows.Count() == 0) return dict;
string section = "";
foreach (string row in rows)
{
string rw = row.TrimStart();
if (rw.StartsWith("["))
section = rw.TrimStart('[').TrimEnd(']');
else
{
int index = rw.IndexOf('=');
dict[section + "-" + rw.Substring(0, index).Trim()] = rw.Substring(index+1).Trim().Trim('"');
}
}
return dict;
}
It creates the dictionary where the key is "-". You can load it like this:
var dict = ParseIniDataWithSections(File.ReadAllLines(fileName));
I'm late to join the party, but I had the same issue today and I've written the following implementation:
using System.Text.RegularExpressions;
static bool match(this string str, string pat, out Match m) =>
(m = Regex.Match(str, pat, RegexOptions.IgnoreCase)).Success;
static void Main()
{
Dictionary<string, Dictionary<string, string>> ini = new Dictionary<string, Dictionary<string, string>>();
string section = "";
foreach (string line in File.ReadAllLines(.........)) // read from file
{
string ln = (line.Contains('#') ? line.Remove(line.IndexOf('#')) : line).Trim();
if (ln.match(#"^[ \t]*\[(?<sec>[\w\-]+)\]", out Match m))
section = m.Groups["sec"].ToString();
else if (ln.match(#"^[ \t]*(?<prop>[\w\-]+)\=(?<val>.*)", out m))
{
if (!ini.ContainsKey(section))
ini[section] = new Dictionary<string, string>();
ini[section][m.Groups["prop"].ToString()] = m.Groups["val"].ToString();
}
}
// access the ini file as follows:
string content = ini["section"]["property"];
}
It must be noted, that this implementation does not handle sections or properties which are not found.
To achieve this, you should extend the Dictionary<,>-class to handle unfound keys.
To serialize an instance of Dictionary<string, Dictionary<string, string>> to an .ini-file, I use the following code:
string targetpath = .........;
Dictionary<string, Dictionary<string, string>> ini = ........;
StringBuilder sb = new StringBuilder();
foreach (string section in ini.Keys)
{
sb.AppendLine($"[{section}]");
foreach (string property in ini[section].Keys)
sb.AppendLine($"{property}={ini[section][property]");
}
File.WriteAllText(targetpath, sb.ToString());
There is an Ini Parser available in CommonLibrary.NET
This has various very convenient overloads for getting sections/values and is very light weight.
Here is my own version, using regular expressions. This code assumes that each section name is unique - if however this is not true - it makes sense to replace Dictionary with List. This function supports .ini file commenting, starting from ';' character. Section starts normally [section], and key value pairs also comes normally "key = value". Same assumption as for sections - key name is unique.
/// <summary>
/// Loads .ini file into dictionary.
/// </summary>
public static Dictionary<String, Dictionary<String, String>> loadIni(String file)
{
Dictionary<String, Dictionary<String, String>> d = new Dictionary<string, Dictionary<string, string>>();
String ini = File.ReadAllText(file);
// Remove comments, preserve linefeeds, if end-user needs to count line number.
ini = Regex.Replace(ini, #"^\s*;.*$", "", RegexOptions.Multiline);
// Pick up all lines from first section to another section
foreach (Match m in Regex.Matches(ini, "(^|[\r\n])\\[([^\r\n]*)\\][\r\n]+(.*?)(\\[([^\r\n]*)\\][\r\n]+|$)", RegexOptions.Singleline))
{
String sectionName = m.Groups[2].Value;
Dictionary<String, String> lines = new Dictionary<String, String>();
// Pick up "key = value" kind of syntax.
foreach (Match l in Regex.Matches(ini, #"^\s*(.*?)\s*=\s*(.*?)\s*$", RegexOptions.Multiline))
{
String key = l.Groups[1].Value;
String value = l.Groups[2].Value;
// Open up quotation if any.
value = Regex.Replace(value, "^\"(.*)\"$", "$1");
if (!lines.ContainsKey(key))
lines[key] = value;
}
if (!d.ContainsKey(sectionName))
d[sectionName] = lines;
}
return d;
}
If you don't need bells and whistles (ie sections) here's a one liner:
List<(string, string)> ini = File.ReadLines(filename)
.Select(s => {
var spl = s.Split('=', 2);
return spl.Length == 2 ? (spl[0], spl[1]) : (s, "");
})
.Select(vt => (vt.Item1.Trim(), vt.Item2.Trim()))
.Where(vt => vt.Item1 != "")
.ToList();
To write:
File.WriteAllLines(filename, ini.Select(vt => $"{vt.Item1}={vt.Item2}"));
(if you don't care about duplicates use .ToDictionary() instead of .ToList() for easier access)
Here is my class, works like a charm :
public static class IniFileManager
{
[DllImport("kernel32")]
private static extern long WritePrivateProfileString(string section,
string key, string val, string filePath);
[DllImport("kernel32")]
private static extern int GetPrivateProfileString(string section,
string key, string def, StringBuilder retVal,
int size, string filePath);
[DllImport("kernel32.dll")]
private static extern int GetPrivateProfileSection(string lpAppName,
byte[] lpszReturnBuffer, int nSize, string lpFileName);
/// <summary>
/// Write Data to the INI File
/// </summary>
/// <PARAM name="Section"></PARAM>
/// Section name
/// <PARAM name="Key"></PARAM>
/// Key Name
/// <PARAM name="Value"></PARAM>
/// Value Name
public static void IniWriteValue(string sPath,string Section, string Key, string Value)
{
WritePrivateProfileString(Section, Key, Value, sPath);
}
/// <summary>
/// Read Data Value From the Ini File
/// </summary>
/// <PARAM name="Section"></PARAM>
/// <PARAM name="Key"></PARAM>
/// <PARAM name="Path"></PARAM>
/// <returns></returns>
public static string IniReadValue(string sPath,string Section, string Key)
{
StringBuilder temp = new StringBuilder(255);
int i = GetPrivateProfileString(Section, Key, "", temp,
255, sPath);
return temp.ToString();
}
}
The use is obviouse since its a static class, just call IniFileManager.IniWriteValue for readsing a section or IniFileManager.IniReadValue for reading a section.
You should read and write data from xml files since you can save a whole object to xml and also you can populate a object from a saved xml. It is better an easy to manipulate objects.
Here is how to do it:
Write Object Data to an XML File: https://msdn.microsoft.com/en-us/library/ms172873.aspx
Read Object Data from an XML File: https://msdn.microsoft.com/en-us/library/ms172872.aspx
My path is \\server\folderName1\another name\something\another folder\
How do I extract each folder name into a string if I don't know how many folders there are in the path and I don't know the folder names?
Many thanks
string mypath = #"..\folder1\folder2\folder2";
string[] directories = mypath.Split(Path.DirectorySeparatorChar);
Edit:
This returns each individual folder in the directories array. You can get the number of folders returned like this:
int folderCount = directories.Length;
This is good in the general case:
yourPath.Split(#"\/", StringSplitOptions.RemoveEmptyEntries)
There is no empty element in the returned array if the path itself ends in a (back)slash (e.g. "\foo\bar\"). However, you will have to be sure that yourPath is really a directory and not a file. You can find out what it is and compensate if it is a file like this:
if(Directory.Exists(yourPath)) {
var entries = yourPath.Split(#"\/", StringSplitOptions.RemoveEmptyEntries);
}
else if(File.Exists(yourPath)) {
var entries = Path.GetDirectoryName(yourPath).Split(
#"\/", StringSplitOptions.RemoveEmptyEntries);
}
else {
// error handling
}
I believe this covers all bases without being too pedantic. It will return a string[] that you can iterate over with foreach to get each directory in turn.
If you want to use constants instead of the #"\/" magic string, you need to use
var separators = new char[] {
Path.DirectorySeparatorChar,
Path.AltDirectorySeparatorChar
};
and then use separators instead of #"\/" in the code above. Personally, I find this too verbose and would most likely not do it.
I see your method Wolf5370 and raise you.
internal static List<DirectoryInfo> Split(this DirectoryInfo path)
{
if(path == null) throw new ArgumentNullException("path");
var ret = new List<DirectoryInfo>();
if (path.Parent != null) ret.AddRange(Split(path.Parent));
ret.Add(path);
return ret;
}
On the path c:\folder1\folder2\folder3 this returns
c:\
c:\folder1
c:\folder1\folder2
c:\folder1\folder2\folder3
In that order
OR
internal static List<string> Split(this DirectoryInfo path)
{
if(path == null) throw new ArgumentNullException("path");
var ret = new List<string>();
if (path.Parent != null) ret.AddRange(Split(path.Parent));
ret.Add(path.Name);
return ret;
}
will return
c:\
folder1
folder2
folder3
Realise this is an old post, but I came across it looking - in the end I decided apon the below function as it sorted what I was doing at the time better than any of the above:
private static List<DirectoryInfo> SplitDirectory(DirectoryInfo parent)
{
if (parent == null) return null;
var rtn = new List<DirectoryInfo>();
var di = parent;
while (di.Name != di.Root.Name)
{
rtn.Add(di);
di = di.Parent;
}
rtn.Add(di.Root);
rtn.Reverse();
return rtn;
}
There are a few ways that a file path can be represented. You should use the System.IO.Path class to get the separators for the OS, since it can vary between UNIX and Windows. Also, most (or all if I'm not mistaken) .NET libraries accept either a '\' or a '/' as a path separator, regardless of OS. For this reason, I'd use the Path class to split your paths. Try something like the following:
string originalPath = "\\server\\folderName1\\another\ name\\something\\another folder\\";
string[] filesArray = originalPath.Split(Path.AltDirectorySeparatorChar,
Path.DirectorySeparatorChar);
This should work regardless of the number of folders or the names.
public static IEnumerable<string> Split(this DirectoryInfo path)
{
if (path == null)
throw new ArgumentNullException("path");
if (path.Parent != null)
foreach(var d in Split(path.Parent))
yield return d;
yield return path.Name;
}
Inspired by the earlier answers, but simpler, and without recursion. Also, it does not care what the separation symbol is, as Dir.Parent covers this:
/// <summary>
/// Split a directory in its components.
/// Input e.g: a/b/c/d.
/// Output: d, c, b, a.
/// </summary>
/// <param name="Dir"></param>
/// <returns></returns>
public static IEnumerable<string> DirectorySplit(this DirectoryInfo Dir)
{
while (Dir != null)
{
yield return Dir.Name;
Dir = Dir.Parent;
}
}
Either stick this in a static class to create a nice extension method, or just leave out the this (and static).
Usage example (as an extension method) to access the path parts by number:
/// <summary>
/// Return one part of the directory path.
/// Path e.g.: a/b/c/d. PartNr=0 is a, Nr 2 = c.
/// </summary>
/// <param name="Dir"></param>
/// <param name="PartNr"></param>
/// <returns></returns>
public static string DirectoryPart(this DirectoryInfo Dir, int PartNr)
{
string[] Parts = Dir.DirectorySplit().ToArray();
int L = Parts.Length;
return PartNr >= 0 && PartNr < L ? Parts[L - 1 - PartNr] : "";
}
Both above methods are now in my personal library, hence the xml comments. Usage example:
DirectoryInfo DI_Data = new DirectoryInfo(#"D:\Hunter\Data\2019\w38\abc\000.d");
label_Year.Text = DI_Data.DirectoryPart(3); // --> 2019
label_Entry.Text = DI_Data.DirectoryPart(6);// --> 000.d
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
/// <summary>
/// Use to emulate the C lib function _splitpath()
/// </summary>
/// <param name="path">The path to split</param>
/// <param name="rootpath">optional root if a relative path</param>
/// <returns>the folders in the path.
/// Item 0 is drive letter with ':'
/// If path is UNC path then item 0 is "\\"
/// </returns>
/// <example>
/// string p1 = #"c:\p1\p2\p3\p4";
/// string[] ap1 = p1.SplitPath();
/// // ap1 = {"c:", "p1", "p2", "p3", "p4"}
/// string p2 = #"\\server\p2\p3\p4";
/// string[] ap2 = p2.SplitPath();
/// // ap2 = {#"\\", "server", "p2", "p3", "p4"}
/// string p3 = #"..\p3\p4";
/// string root3 = #"c:\p1\p2\";
/// string[] ap3 = p1.SplitPath(root3);
/// // ap3 = {"c:", "p1", "p3", "p4"}
/// </example>
public static string[] SplitPath(this string path, string rootpath = "")
{
string drive;
string[] astr;
path = Path.GetFullPath(Path.Combine(rootpath, path));
if (path[1] == ':')
{
drive = path.Substring(0, 2);
string newpath = path.Substring(2);
astr = newpath.Split(new[] { Path.DirectorySeparatorChar }
, StringSplitOptions.RemoveEmptyEntries);
}
else
{
drive = #"\\";
astr = path.Split(new[] { Path.DirectorySeparatorChar }
, StringSplitOptions.RemoveEmptyEntries);
}
string[] splitPath = new string[astr.Length + 1];
splitPath[0] = drive;
astr.CopyTo(splitPath, 1);
return splitPath;
}
Maybe call Directory.GetParent in a loop? That's if you want the full path to each directory and not just the directory names.
The quick answer is to use the .Split('\\') method.
I use this for looping folder ftp server
public List<string> CreateMultiDirectory(string remoteFile)
var separators = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
string[] directory = Path.GetDirectoryName(remoteFile).Split(separators);
var path = new List<string>();
var folder = string.Empty;
foreach (var item in directory)
{
folder += $#"{item}\";
path.Add(folder);
}
return path;
Or, if you need to do something with each folder, have a look at the System.IO.DirectoryInfo class. It also has a Parent property that allows you to navigate to the parent directory.
I wrote the following method which works for me.
protected bool isDirectoryFound(string path, string pattern)
{
bool success = false;
DirectoryInfo directories = new DirectoryInfo(#path);
DirectoryInfo[] folderList = directories.GetDirectories();
Regex rx = new Regex(pattern);
foreach (DirectoryInfo di in folderList)
{
if (rx.IsMatch(di.Name))
{
success = true;
break;
}
}
return success;
}
The lines most pertinent to your question being:
DirectoryInfo directories = new DirectoryInfo(#path);
DirectoryInfo[] folderList = directories.GetDirectories();
DirectoryInfo objDir = new DirectoryInfo(direcotryPath);
DirectoryInfo [] directoryNames = objDir.GetDirectories("*.*", SearchOption.AllDirectories);
This will give you all the directories and subdirectories.
I am adding to Matt Brunell's answer.
string[] directories = myStringWithLotsOfFolders.Split(Path.DirectorySeparatorChar);
string previousEntry = string.Empty;
if (null != directories)
{
foreach (string direc in directories)
{
string newEntry = previousEntry + Path.DirectorySeparatorChar + direc;
if (!string.IsNullOrEmpty(newEntry))
{
if (!newEntry.Equals(Convert.ToString(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(newEntry);
previousEntry = newEntry;
}
}
}
}
This should give you:
"\server"
"\server\folderName1"
"\server\folderName1\another name"
"\server\folderName1\another name\something"
"\server\folderName1\another name\something\another folder\"
(or sort your resulting collection by the string.Length of each value.
Here's a modification of Wolf's answer that leaves out the root and fixes what seemed to be a couple of bugs. I used it to generate a breadcrumbs and I didn't want the root showing.
this is an extension of the DirectoryInfo type.
public static List<DirectoryInfo> PathParts(this DirectoryInfo source, string rootPath)
{
if (source == null) return null;
DirectoryInfo root = new DirectoryInfo(rootPath);
var pathParts = new List<DirectoryInfo>();
var di = source;
while (di != null && di.FullName != root.FullName)
{
pathParts.Add(di);
di = di.Parent;
}
pathParts.Reverse();
return pathParts;
}
I just coded this since I didn't find any already built in in C#.
/// <summary>
/// get the directory path segments.
/// </summary>
/// <param name="directoryPath">the directory path.</param>
/// <returns>a IEnumerable<string> containing the get directory path segments.</returns>
public IEnumerable<string> GetDirectoryPathSegments(string directoryPath)
{
if (string.IsNullOrEmpty(directoryPath))
{ throw new Exception($"Invalid Directory: {directoryPath ?? "null"}"); }
var currentNode = new System.IO.DirectoryInfo(directoryPath);
var targetRootNode = currentNode.Root;
if (targetRootNode == null) return new string[] { currentNode.Name };
var directorySegments = new List<string>();
while (string.Compare(targetRootNode.FullName, currentNode.FullName, StringComparison.InvariantCultureIgnoreCase) != 0)
{
directorySegments.Insert(0, currentNode.Name);
currentNode = currentNode.Parent;
}
directorySegments.Insert(0, currentNode.Name);
return directorySegments;
}
I'd like to contribute using this options (without split method)
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace SampleConsoleApp
{
class Program
{
static void Main(string[] args)
{
var filePaths = new[]
{
"C:/a/b/c/d/files-samples/formdata.bmp",
#"\\127.0.0.1\c$\a\b\c\d\formdata.bmp",
"/usr/home/john/a/b/c/d/formdata.bmp"
};
foreach (var filePath in filePaths)
{
var directorySegments = GetDirectorySegments(filePath);
Console.WriteLine(filePath);
Console.WriteLine(string.Join(Environment.NewLine,
directorySegments.Select((e, i) => $"\t Segment#={i + 1} Text={e}")));
}
}
private static IList<string> GetDirectorySegments(string filePath)
{
var directorySegments = new List<string>();
if (string.IsNullOrEmpty(filePath))
return directorySegments;
var fileInfo = new FileInfo(filePath);
if (fileInfo.Directory == null)
return directorySegments;
for (var currentDirectory = fileInfo.Directory;
currentDirectory != null;
currentDirectory = currentDirectory.Parent)
directorySegments.Insert(0, currentDirectory.Name);
return directorySegments;
}
}
}
if everything goes well, an output will be like:
C:/a/b/c/d/files-samples/formdata.bmp
Segment#=1 Text=C:\
Segment#=2 Text=a
Segment#=3 Text=b
Segment#=4 Text=c
Segment#=5 Text=d
Segment#=6 Text=files-samples
\\127.0.0.1\c$\a\b\c\d\formdata.bmp
Segment#=1 Text=\\127.0.0.1\c$
Segment#=2 Text=a
Segment#=3 Text=b
Segment#=4 Text=c
Segment#=5 Text=d
/usr/home/john/a/b/c/d/formdata.bmp
Segment#=1 Text=C:\
Segment#=2 Text=usr
Segment#=3 Text=home
Segment#=4 Text=john
Segment#=5 Text=a
Segment#=6 Text=b
Segment#=7 Text=c
Segment#=8 Text=d
You can still perform additional filters to GetDirectorySegments (since you have an instance of DirectoryInfo you can check atributes or use the Exist property)
I am currently trying to generate a CSV using nhibernate. This error does not occur on my development enviroment but it does on the live site that it's being used on. I have tried fiddling with time out's but this does not seem to have any effect as it's timing out way before it should. The timing is completely random, sometimes it'll be 3 seconds before it times out the next it will be 10 seconds. There doesn't seem to be any real consistancy in the timing.
Stack Trace:
System.Web.HttpException: The remote host closed the connection. The error code is 0x800703E3.
at System.Web.Hosting.IIS7WorkerRequest.RaiseCommunicationError(Int32 result, Boolean throwOnDisconnect)
at System.Web.Hosting.IIS7WorkerRequest.ExplicitFlush()
at System.Web.HttpResponse.Flush(Boolean finalFlush)
at Reports.CustomCSVWriter.WritetoHttpStream(String filename, Boolean header)
The code is as follows:
public class ProductSpreadSheetDownload : CustomCSVWriter
{
protected override string[] GetCollection()
{
Page.Server.ScriptTimeout = 300;
IList<Product> products = new List<Product>();
IStockScheme stockScheme = Fabric.ObjectProvider.Get<IStockScheme>();
ICriteria criteria = CoreHttpModule.Session.CreateCriteria(typeof(Product))
.Add(NHibernate.Expression.Expression.IsNotNull(Product.STOCK_CODE))
.Add(NHibernate.Expression.Expression.Eq(Product.IS_VISIBLE_ON_WEBSITE, true))
.Add(NHibernate.Expression.Expression.Eq(Product.STOCK_TYPE, StockType.StockItem))
.Add(NHibernate.Expression.Expression.Not(NHibernate.Expression.Expression.Like(Product.NAME, "%*%")));
AddCustomCriteria(criteria);
products = criteria.List<Product>();
products = Product.RemoveOrphanedAndUnrooted((List<Product>)products);
Product[] productArray = new Product[products.Count];
products.CopyTo(productArray, 0);
double?[] levels = stockScheme.GetStock(productArray, false);
List<string> productStringList = new List<string>();
IProductMapper mapper = Fabric.ObjectProvider.Get<IProductMapper>();
var rootUrl = Fabric.SettingsProvider.ReadSetting<string>("RootUrl", string.Empty);
string showOutOfStock = Page.Request.QueryString["ShowOutOfStock"];
int minStockLevel = int.MinValue;
if (showOutOfStock == "False")
minStockLevel = 0;
for (int i = 0; i < productArray.Length; i++)
{
if (levels[i] > minStockLevel && levels[i] != null && productArray[i].Parent != null && productArray[i].Parent.IsVisibleOnWebsite)
{
StringBuilder productStringBuilder = new StringBuilder();
productStringBuilder.AppendFormat("{0}, ", CleanString(productArray[i].Name));
productStringBuilder.AppendFormat("{0}, ", CleanString(productArray[i].StockCode));
productStringBuilder.AppendFormat("{0}, ", levels[i]);
productStringBuilder.AppendFormat("{0}, ", mapper.GetUrl(productArray[i]) );
productStringBuilder.AppendFormat("{0}, ", CleanString(productArray[i].Category));
productStringBuilder.AppendFormat("{0}, ", CleanString(productArray[i].SubCategory));
productStringBuilder.AppendFormat("{0}, ", CleanString(mapper.GetText(productArray[i], "Description")));
productStringBuilder.AppendFormat("{0}, ", mapper.GetImageUrl(productArray[i], "Main"));
AddCustomFields(productStringBuilder, mapper);
productStringList.Add(productStringBuilder.ToString().Trim().TrimEnd(','));
}
}
string[] productstrings = new string[productStringList.Count];
productStringList.CopyTo(productstrings, 0);
return productstrings;
}
/// <summary>
/// Override this method to add custom criteria to the feed
/// </summary>
/// <example>
/// criteria.Add(NHibernate.Expression.Expression.Eq(Product.IS_VISIBLE_ON_WEBSITE, true));
/// </example>
protected virtual void AddCustomCriteria(ICriteria criteria) {}
/// <summary>
/// Override this method to add custom fields to the CSV output
/// </summary>
/// <example>
/// productStringBuilder.AppendFormat("{0}, ", mapper.GetImageUrl(productArray[i], "Main"));
/// </example>
protected virtual void AddCustomFields(StringBuilder productStringBuilder, IProductMapper mapper) { }
protected override string Headers()
{
string headers = "Name, Stockcode, Stock_Level, URL, Category, SubCategory, Description, Main Image URL";
return headers;
}
/// <summary>
/// Removes characters that are not safe in a CSV file.
/// </summary>
protected static string CleanString(string stringToClean)
{
return string.IsNullOrEmpty(stringToClean) ? string.Empty : stringToClean.Replace("\n", " ").Replace(',', ' ');
}
}
}
I recently have been moving a bunch of MP3s from various locations into a repository. I had been constructing the new file names using the ID3 tags (thanks, TagLib-Sharp!), and I noticed that I was getting a System.NotSupportedException:
"The given path's format is not supported."
This was generated by either File.Copy() or Directory.CreateDirectory().
It didn't take long to realize that my file names needed to be sanitized. So I did the obvious thing:
public static string SanitizePath_(string path, char replaceChar)
{
string dir = Path.GetDirectoryName(path);
foreach (char c in Path.GetInvalidPathChars())
dir = dir.Replace(c, replaceChar);
string name = Path.GetFileName(path);
foreach (char c in Path.GetInvalidFileNameChars())
name = name.Replace(c, replaceChar);
return dir + name;
}
To my surprise, I continued to get exceptions. It turned out that ':' is not in the set of Path.GetInvalidPathChars(), because it is valid in a path root. I suppose that makes sense - but this has to be a pretty common problem. Does anyone have some short code that sanitizes a path? The most thorough I've come up with this, but it feels like it is probably overkill.
// replaces invalid characters with replaceChar
public static string SanitizePath(string path, char replaceChar)
{
// construct a list of characters that can't show up in filenames.
// need to do this because ":" is not in InvalidPathChars
if (_BadChars == null)
{
_BadChars = new List<char>(Path.GetInvalidFileNameChars());
_BadChars.AddRange(Path.GetInvalidPathChars());
_BadChars = Utility.GetUnique<char>(_BadChars);
}
// remove root
string root = Path.GetPathRoot(path);
path = path.Remove(0, root.Length);
// split on the directory separator character. Need to do this
// because the separator is not valid in a filename.
List<string> parts = new List<string>(path.Split(new char[]{Path.DirectorySeparatorChar}));
// check each part to make sure it is valid.
for (int i = 0; i < parts.Count; i++)
{
string part = parts[i];
foreach (char c in _BadChars)
{
part = part.Replace(c, replaceChar);
}
parts[i] = part;
}
return root + Utility.Join(parts, Path.DirectorySeparatorChar.ToString());
}
Any improvements to make this function faster and less baroque would be much appreciated.
To clean up a file name you could do this
private static string MakeValidFileName( string name )
{
string invalidChars = System.Text.RegularExpressions.Regex.Escape( new string( System.IO.Path.GetInvalidFileNameChars() ) );
string invalidRegStr = string.Format( #"([{0}]*\.+$)|([{0}]+)", invalidChars );
return System.Text.RegularExpressions.Regex.Replace( name, invalidRegStr, "_" );
}
A shorter solution:
var invalids = System.IO.Path.GetInvalidFileNameChars();
var newName = String.Join("_", origFileName.Split(invalids, StringSplitOptions.RemoveEmptyEntries) ).TrimEnd('.');
Based on Andre's excellent answer but taking into account Spud's comment on reserved words, I made this version:
/// <summary>
/// Strip illegal chars and reserved words from a candidate filename (should not include the directory path)
/// </summary>
/// <remarks>
/// http://stackoverflow.com/questions/309485/c-sharp-sanitize-file-name
/// </remarks>
public static string CoerceValidFileName(string filename)
{
var invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars()));
var invalidReStr = string.Format(#"[{0}]+", invalidChars);
var reservedWords = new []
{
"CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4",
"COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4",
"LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
};
var sanitisedNamePart = Regex.Replace(filename, invalidReStr, "_");
foreach (var reservedWord in reservedWords)
{
var reservedWordPattern = string.Format("^{0}\\.", reservedWord);
sanitisedNamePart = Regex.Replace(sanitisedNamePart, reservedWordPattern, "_reservedWord_.", RegexOptions.IgnoreCase);
}
return sanitisedNamePart;
}
And these are my unit tests
[Test]
public void CoerceValidFileName_SimpleValid()
{
var filename = #"thisIsValid.txt";
var result = PathHelper.CoerceValidFileName(filename);
Assert.AreEqual(filename, result);
}
[Test]
public void CoerceValidFileName_SimpleInvalid()
{
var filename = #"thisIsNotValid\3\\_3.txt";
var result = PathHelper.CoerceValidFileName(filename);
Assert.AreEqual("thisIsNotValid_3__3.txt", result);
}
[Test]
public void CoerceValidFileName_InvalidExtension()
{
var filename = #"thisIsNotValid.t\xt";
var result = PathHelper.CoerceValidFileName(filename);
Assert.AreEqual("thisIsNotValid.t_xt", result);
}
[Test]
public void CoerceValidFileName_KeywordInvalid()
{
var filename = "aUx.txt";
var result = PathHelper.CoerceValidFileName(filename);
Assert.AreEqual("_reservedWord_.txt", result);
}
[Test]
public void CoerceValidFileName_KeywordValid()
{
var filename = "auxillary.txt";
var result = PathHelper.CoerceValidFileName(filename);
Assert.AreEqual("auxillary.txt", result);
}
string clean = String.Concat(dirty.Split(Path.GetInvalidFileNameChars()));
there are a lot of working solutions here. just for the sake of completeness, here's an approach that doesn't use regex, but uses LINQ:
var invalids = Path.GetInvalidFileNameChars();
filename = invalids.Aggregate(filename, (current, c) => current.Replace(c, '_'));
Also, it's a very short solution ;)
I'm using the System.IO.Path.GetInvalidFileNameChars() method to check invalid characters and I've got no problems.
I'm using the following code:
foreach( char invalidchar in System.IO.Path.GetInvalidFileNameChars())
{
filename = filename.Replace(invalidchar, '_');
}
I wanted to retain the characters in some way, not just simply replace the character with an underscore.
One way I thought was to replace the characters with similar looking characters which are (in my situation), unlikely to be used as regular characters. So I took the list of invalid characters and found look-a-likes.
The following are functions to encode and decode with the look-a-likes.
This code does not include a complete listing for all System.IO.Path.GetInvalidFileNameChars() characters. So it is up to you to extend or utilize the underscore replacement for any remaining characters.
private static Dictionary<string, string> EncodeMapping()
{
//-- Following characters are invalid for windows file and folder names.
//-- \/:*?"<>|
Dictionary<string, string> dic = new Dictionary<string, string>();
dic.Add(#"\", "Ì"); // U+OOCC
dic.Add("/", "Í"); // U+OOCD
dic.Add(":", "¦"); // U+00A6
dic.Add("*", "¤"); // U+00A4
dic.Add("?", "¿"); // U+00BF
dic.Add(#"""", "ˮ"); // U+02EE
dic.Add("<", "«"); // U+00AB
dic.Add(">", "»"); // U+00BB
dic.Add("|", "│"); // U+2502
return dic;
}
public static string Escape(string name)
{
foreach (KeyValuePair<string, string> replace in EncodeMapping())
{
name = name.Replace(replace.Key, replace.Value);
}
//-- handle dot at the end
if (name.EndsWith(".")) name = name.CropRight(1) + "°";
return name;
}
public static string UnEscape(string name)
{
foreach (KeyValuePair<string, string> replace in EncodeMapping())
{
name = name.Replace(replace.Value, replace.Key);
}
//-- handle dot at the end
if (name.EndsWith("°")) name = name.CropRight(1) + ".";
return name;
}
You can select your own look-a-likes. I used the Character Map app in windows to select mine %windir%\system32\charmap.exe
As I make adjustments through discovery, I will update this code.
I think the problem is that you first call Path.GetDirectoryName on the bad string. If this has non-filename characters in it, .Net can't tell which parts of the string are directories and throws. You have to do string comparisons.
Assuming it's only the filename that is bad, not the entire path, try this:
public static string SanitizePath(string path, char replaceChar)
{
int filenamePos = path.LastIndexOf(Path.DirectorySeparatorChar) + 1;
var sb = new System.Text.StringBuilder();
sb.Append(path.Substring(0, filenamePos));
for (int i = filenamePos; i < path.Length; i++)
{
char filenameChar = path[i];
foreach (char c in Path.GetInvalidFileNameChars())
if (filenameChar.Equals(c))
{
filenameChar = replaceChar;
break;
}
sb.Append(filenameChar);
}
return sb.ToString();
}
I have had success with this in the past.
Nice, short and static :-)
public static string returnSafeString(string s)
{
foreach (char character in Path.GetInvalidFileNameChars())
{
s = s.Replace(character.ToString(),string.Empty);
}
foreach (char character in Path.GetInvalidPathChars())
{
s = s.Replace(character.ToString(), string.Empty);
}
return (s);
}
Here's an efficient lazy loading extension method based on Andre's code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LT
{
public static class Utility
{
static string invalidRegStr;
public static string MakeValidFileName(this string name)
{
if (invalidRegStr == null)
{
var invalidChars = System.Text.RegularExpressions.Regex.Escape(new string(System.IO.Path.GetInvalidFileNameChars()));
invalidRegStr = string.Format(#"([{0}]*\.+$)|([{0}]+)", invalidChars);
}
return System.Text.RegularExpressions.Regex.Replace(name, invalidRegStr, "_");
}
}
}
Your code would be cleaner if you appended the directory and filename together and sanitized that rather than sanitizing them independently. As for sanitizing away the :, just take the 2nd character in the string. If it is equal to "replacechar", replace it with a colon. Since this app is for your own use, such a solution should be perfectly sufficient.
using System;
using System.IO;
using System.Linq;
using System.Text;
public class Program
{
public static void Main()
{
try
{
var badString = "ABC\\DEF/GHI<JKL>MNO:PQR\"STU\tVWX|YZA*BCD?EFG";
Console.WriteLine(badString);
Console.WriteLine(SanitizeFileName(badString, '.'));
Console.WriteLine(SanitizeFileName(badString));
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
private static string SanitizeFileName(string fileName, char? replacement = null)
{
if (fileName == null) { return null; }
if (fileName.Length == 0) { return ""; }
var sb = new StringBuilder();
var badChars = Path.GetInvalidFileNameChars().ToList();
foreach (var #char in fileName)
{
if (badChars.Contains(#char))
{
if (replacement.HasValue)
{
sb.Append(replacement.Value);
}
continue;
}
sb.Append(#char);
}
return sb.ToString();
}
}
Based #fiat's and #Andre's approach, I'd like to share my solution too.
Main difference:
its an extension method
regex is compiled at first use to save some time with a lot executions
reserved words are preserved
public static class StringPathExtensions
{
private static Regex _invalidPathPartsRegex;
static StringPathExtensions()
{
var invalidReg = System.Text.RegularExpressions.Regex.Escape(new string(Path.GetInvalidFileNameChars()));
_invalidPathPartsRegex = new Regex($"(?<reserved>^(CON|PRN|AUX|CLOCK\\$|NUL|COM0|COM1|COM2|COM3|COM4|COM5|COM6|COM7|COM8|COM9|LPT0|LPT1|LPT2|LPT3|LPT4|LPT5|LPT6|LPT7|LPT8|LPT9))|(?<invalid>[{invalidReg}:]+|\\.$)", RegexOptions.Compiled);
}
public static string SanitizeFileName(this string path)
{
return _invalidPathPartsRegex.Replace(path, m =>
{
if (!string.IsNullOrWhiteSpace(m.Groups["reserved"].Value))
return string.Concat("_", m.Groups["reserved"].Value);
return "_";
});
}
}