web-dev-qa-db-fra.com

Comment créer un nom de fichier Windows valide à partir d'une chaîne arbitraire?

J'ai une chaîne comme "Foo: Bar" que je veux utiliser comme nom de fichier, mais sous Windows, le caractère ":" n'est pas autorisé dans un nom de fichier.

Existe-t-il une méthode qui transformera "Foo: Bar" en quelque chose comme "Foo-Bar"?

83
Ken

Essayez quelque chose comme ceci:

string fileName = "something";
foreach (char c in System.IO.Path.GetInvalidFileNameChars())
{
   fileName = fileName.Replace(c, '_');
}

Modifier:

Puisque GetInvalidFileNameChars() renverra 10 ou 15 caractères, il est préférable d'utiliser un StringBuilder au lieu d'une simple chaîne; la version originale prendra plus de temps et consommera plus de mémoire.

139
Diego Jancic
fileName = fileName.Replace(":", "-") 

Cependant ":" n'est pas le seul caractère illégal pour Windows. Vous devrez également gérer:

/, \, :, *, ?, ", <, > and |

Ceux-ci sont contenus dans System.IO.Path.GetInvalidFileNameChars ();

Aussi (sous Windows), "." ne peut pas être le seul caractère du nom de fichier (les deux ".", "..", "...", etc. ne sont pas valides). Soyez prudent lorsque vous nommez des fichiers avec ".", Par exemple:

echo "test" > .test.

Générera un fichier nommé ".test"

Enfin, si vous voulez vraiment faire les choses correctement, il y a quelques noms de fichiers spéciaux que vous devez rechercher. Sous Windows vous ne pouvez pas créer de fichiers nommés:

CON, PRN, AUX, CLOCK$, NUL
COM0, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9
LPT0, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, and LPT9.
31
Phil Price

Ce n'est pas plus efficace, mais c'est plus amusant :)

    var fileName = "foo:bar";
    var invalidChars = System.IO.Path.GetInvalidFileNameChars();
    var cleanFileName = new string(fileName.Where(m => !invalidChars.Contains(m)).ToArray<char>());
12
Joseph Gabriel

Si quelqu'un souhaite une version optimisée basée sur StringBuilder, utilisez-la. Inclut l'astuce de rkagerer en option.

static char[] _invalids;

/// <summary>Replaces characters in <c>text</c> that are not allowed in 
/// file names with the specified replacement character.</summary>
/// <param name="text">Text to make into a valid filename. The same string is returned if it is valid already.</param>
/// <param name="replacement">Replacement character, or null to simply remove bad characters.</param>
/// <param name="fancy">Whether to replace quotes and slashes with the non-ASCII characters ” and ⁄.</param>
/// <returns>A string that can be used as a filename. If the output string would otherwise be empty, returns "_".</returns>
public static string MakeValidFileName(string text, char? replacement = '_', bool fancy = true)
{
    StringBuilder sb = new StringBuilder(text.Length);
    var invalids = _invalids ?? (_invalids = Path.GetInvalidFileNameChars());
    bool changed = false;
    for (int i = 0; i < text.Length; i++) {
        char c = text[i];
        if (invalids.Contains(c)) {
            changed = true;
            var repl = replacement ?? '\0';
            if (fancy) {
                if (c == '"')       repl = '”'; // U+201D right double quotation mark
                else if (c == '\'') repl = '’'; // U+2019 right single quotation mark
                else if (c == '/')  repl = '⁄'; // U+2044 fraction slash
            }
            if (repl != '\0')
                sb.Append(repl);
        } else
            sb.Append(c);
    }
    if (sb.Length == 0)
        return "_";
    return changed ? sb.ToString() : text;
}
11
Qwertie

Diego a la bonne solution, mais il y a une très petite erreur. La version de string.Replace utilisée doit être string.Replace (char, char), il n'y a pas de string.Replace (char, string)

Je ne peux pas modifier la réponse ou j'aurais juste apporté la modification mineure.

Il devrait donc être:

string fileName = "something";
foreach (char c in System.IO.Path.GetInvalidFileNameChars())
{
   fileName = fileName.Replace(c, '_');
}
6
leggetter

Voici une légère torsion sur la réponse de Diego.

Si vous n'avez pas peur d'Unicode, vous pouvez conserver un peu plus de fidélité en remplaçant les caractères invalides par des symboles Unicode valides qui leur ressemblent. Voici le code que j'ai utilisé dans un projet récent impliquant des listes de coupe de bois:

static string MakeValidFilename(string text) {
  text = text.Replace('\'', '’'); // U+2019 right single quotation mark
  text = text.Replace('"',  '”'); // U+201D right double quotation mark
  text = text.Replace('/', '⁄');  // U+2044 fraction slash
  foreach (char c in System.IO.Path.GetInvalidFileNameChars()) {
    text = text.Replace(c, '_');
  }
  return text;
}

Cela produit des noms de fichiers comme 1⁄2” spruce.txt au lieu de 1_2_ spruce.txt

Oui, ça marche vraiment:

Explorer sample

Caveat Emptor

Je savais que cette astuce fonctionnerait sur NTFS mais j'ai été surpris de constater qu'elle fonctionne également sur les partitions FAT et FAT32. C'est parce que noms de fichiers longs sont stockés dans Unicode , même aussi loin que Windows 95/NT. J'ai testé sur Win7, XP et même un routeur basé sur Linux et ils se sont révélés OK. Je ne peux pas en dire autant de l'intérieur d'une DOSBox.

Cela dit, avant de devenir fou avec cela, demandez-vous si vous avez vraiment besoin de la fidélité supplémentaire. Les sosies Unicode peuvent confondre les gens ou les anciens programmes, par exemple systèmes d'exploitation plus anciens s'appuyant sur pages de codes .

6
rkagerer

Voici une version de la réponse acceptée utilisant Linq qui utilise Enumerable.Aggregate :

string fileName = "something";

Path.GetInvalidFileNameChars()
    .Aggregate(fileName, (current, c) => current.Replace(c, '_'));
6
DavidG

Voici une version qui utilise StringBuilder et IndexOfAny avec ajout en vrac pour une efficacité totale. Il renvoie également la chaîne d'origine plutôt que de créer une chaîne en double.

Enfin, il a une instruction switch qui renvoie des caractères similaires que vous pouvez personnaliser comme vous le souhaitez. Consultez recherche de confusables d'Unicode.org pour voir quelles options vous pourriez avoir, selon la police.

public static string GetSafeFilename(string arbitraryString)
{
    var invalidChars = System.IO.Path.GetInvalidFileNameChars();
    var replaceIndex = arbitraryString.IndexOfAny(invalidChars, 0);
    if (replaceIndex == -1) return arbitraryString;

    var r = new StringBuilder();
    var i = 0;

    do
    {
        r.Append(arbitraryString, i, replaceIndex - i);

        switch (arbitraryString[replaceIndex])
        {
            case '"':
                r.Append("''");
                break;
            case '<':
                r.Append('\u02c2'); // '˂' (modifier letter left arrowhead)
                break;
            case '>':
                r.Append('\u02c3'); // '˃' (modifier letter right arrowhead)
                break;
            case '|':
                r.Append('\u2223'); // '∣' (divides)
                break;
            case ':':
                r.Append('-');
                break;
            case '*':
                r.Append('\u2217'); // '∗' (asterisk operator)
                break;
            case '\\':
            case '/':
                r.Append('\u2044'); // '⁄' (fraction slash)
                break;
            case '\0':
            case '\f':
            case '?':
                break;
            case '\t':
            case '\n':
            case '\r':
            case '\v':
                r.Append(' ');
                break;
            default:
                r.Append('_');
                break;
        }

        i = replaceIndex + 1;
        replaceIndex = arbitraryString.IndexOfAny(invalidChars, i);
    } while (replaceIndex != -1);

    r.Append(arbitraryString, i, arbitraryString.Length - i);

    return r.ToString();
}

Il ne vérifie pas ., .., ou des noms réservés comme CON, car le remplacement ne doit pas être clair.

3
jnm2

Une autre solution simple:

private string MakeValidFileName(string original, char replacementChar = '_')
{
  var invalidChars = new HashSet<char>(Path.GetInvalidFileNameChars());
  return new string(original.Select(c => invalidChars.Contains(c) ? replacementChar : c).ToArray());
}
3
GDemartini

Nettoyer un peu mon code et faire un peu de refactoring ... J'ai créé une extension pour le type chaîne:

public static string ToValidFileName(this string s, char replaceChar = '_', char[] includeChars = null)
{
  var invalid = Path.GetInvalidFileNameChars();
  if (includeChars != null) invalid = invalid.Union(includeChars).ToArray();
  return string.Join(string.Empty, s.ToCharArray().Select(o => o.In(invalid) ? replaceChar : o));
}

Maintenant, il est plus facile à utiliser avec:

var name = "Any string you want using ? / \ or even +.Zip";
var validFileName = name.ToValidFileName();

Si vous souhaitez remplacer par un caractère différent de "_", vous pouvez utiliser:

var validFileName = name.ToValidFileName(replaceChar:'#');

Et vous pouvez ajouter des caractères à remplacer. Par exemple, vous ne voulez pas d'espaces ni de virgules:

var validFileName = name.ToValidFileName(includeChars: new [] { ' ', ',' });

J'espère que cela aide...

À votre santé

2
Joan Vilariño

Je devais le faire aujourd'hui ... dans mon cas, j'avais besoin de concaténer un nom de client avec la date et l'heure pour un fichier .kmz final. Ma dernière solution était la suivante:

 string name = "Whatever name with valid/invalid chars";
 char[] invalid = System.IO.Path.GetInvalidFileNameChars();
 string validFileName = string.Join(string.Empty,
                            string.Format("{0}.{1:G}.kmz", name, DateTime.Now)
                            .ToCharArray().Select(o => o.In(invalid) ? '_' : o));

Vous pouvez même lui faire remplacer des espaces si vous ajoutez le caractère d'espace au tableau non valide.

Ce n'est peut-être pas le plus rapide, mais comme les performances n'étaient pas un problème, je les ai trouvées élégantes et compréhensibles.

À votre santé!

0
Joan Vilariño

J'avais besoin d'un système qui ne pouvait pas créer de collisions, donc je ne pouvais pas mapper plusieurs personnages en un seul. Je me suis retrouvé avec:

public static class Extension
{
    /// <summary>
    /// Characters allowed in a file name. Note that curly braces don't show up here
    /// becausee they are used for escaping invalid characters.
    /// </summary>
    private static readonly HashSet<char> CleanFileNameChars = new HashSet<char>
    {
        ' ', '!', '#', '$', '%', '&', '\'', '(', ')', '+', ',', '-', '.',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '=', '@',
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
        'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
        '[', ']', '^', '_', '`',
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
        'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
    };

    /// <summary>
    /// Creates a clean file name from one that may contain invalid characters in 
    /// a way that will not collide.
    /// </summary>
    /// <param name="dirtyFileName">
    /// The file name that may contain invalid filename characters.
    /// </param>
    /// <returns>
    /// A file name that does not contain invalid filename characters.
    /// </returns>
    /// <remarks>
    /// <para>
    /// Escapes invalid characters by converting their ASCII values to hexadecimal
    /// and wrapping that value in curly braces. Curly braces are escaped by doubling
    /// them, for example '{' => "{{".
    /// </para>
    /// <para>
    /// Note that although NTFS allows unicode characters in file names, this
    /// method does not.
    /// </para>
    /// </remarks>
    public static string CleanFileName(this string dirtyFileName)
    {
        string EscapeHexString(char c) =>
            "{" + (c > 255 ? $"{(uint)c:X4}" : $"{(uint)c:X2}") + "}";

        return string.Join(string.Empty,
                           dirtyFileName.Select(
                               c =>
                                   c == '{' ? "{{" :
                                   c == '}' ? "}}" :
                                   CleanFileNameChars.Contains(c) ? $"{c}" :
                                   EscapeHexString(c)));
    }
}
0
mheyman