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

97

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 transforme "Foo: Bar" en quelque chose comme "Foo-Bar"?

Ken
la source
1
J'ai fait la même chose aujourd'hui. Je n'ai pas vérifié SO pour une raison quelconque, mais j'ai quand même trouvé la réponse.
Aaron Smith

Réponses:

153

Essayez quelque chose comme ceci:

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

Éditer:

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

Diego Jancic
la source
1
Vous pouvez utiliser un StringBuilder si vous le souhaitez, mais si les noms sont courts et je suppose que cela ne vaut pas la peine. Vous pouvez également créer votre propre méthode pour créer un char [] et remplacer tous les mauvais caractères en une seule itération. Il vaut toujours mieux garder les choses simples à moins que cela ne fonctionne pas, vous pourriez avoir des
goulots d'étranglement
2
InvalidFileNameChars = nouveau caractère [] {'"', '<', '>', '|', '\ 0', '\ x0001', '\ x0002', '\ x0003', '\ x0004', '\ x0005 ',' \ x0006 ',' \ a ',' \ b ',' \ t ',' \ n ',' \ v ',' \ f ',' \ r ',' \ x000e ',' \ x000f ',' \ x0010 ',' \ x0011 ',' \ x0012 ',' \ x0013 ',' \ x0014 ',' \ x0015 ',' \ x0016 ',' \ x0017 ',' \ x0018 ',' \ x0019 ',' \ x001a ',' \ x001b ',' \ x001c ',' \ x001d ',' \ x001e ',' \ x001f ',': ',' * ','? ',' \\ ', '/'};
Diego Jancic
9
La probabilité d'avoir plus de 2 caractères invalides différents dans la chaîne est si faible que se soucier des performances de string.Replace () est inutile.
Serge Wautier
1
Excellente solution, intéressante mise à part, resharper a suggéré cette version Linq: fileName = System.IO.Path.GetInvalidFileNameChars (). Aggregate (fileName, (current, c) => current.Replace (c, '_')); Je me demande s'il y a des améliorations de performances possibles. J'ai conservé l'original à des fins de lisibilité car les performances ne sont pas ma plus grande préoccupation. Mais si quelqu'un est intéressé, cela pourrait valoir la peine d'être comparé
chrispepper1989
1
@AndyM Pas besoin de le faire. file.name.txt.pdfest un pdf valide. Windows ne lit que le dernier .pour l'extension.
Diego Jancic
33
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. sont invalides). 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, vous devez rechercher certains noms de fichiers spéciaux . 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.
Phil Price
la source
3
Je n'ai jamais connu les noms réservés. Cela a du sens
Greg Dean
4
De plus, pour ce que ça vaut, vous ne pouvez pas créer un nom de fichier commençant par l'un de ces noms réservés, suivi d'une décimale. ie con.air.avi
John Conrad
".foo" est un nom de fichier valide. Je ne connaissais pas le nom de fichier "CON" - à quoi ça sert?
configurateur
Grattez ça. CON est pour la console.
configurateur
Merci configurateur; J'ai mis à jour la réponse, vous avez raison ".foo" est valide; cependant ".foo." conduit à des résultats possibles et indésirables. Actualisé.
Phil Price
13

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>());
Joseph Gabriel
la source
12

Si quelqu'un veut une version optimisée basée sur StringBuilder, utilisez ceci. Inclut le truc 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;
}
Qwertie
la source
+1 pour un code agréable et lisible. Rend très facile à lire et à remarquer les bogues: P .. Cette fonction doit toujours renvoyer la chaîne d'origine car modifiée ne sera jamais vraie.
Erti-Chris Eelmaa
Merci, je pense que c'est mieux maintenant. Vous savez ce qu'ils disent à propos de l'open source, "beaucoup d'yeux rendent tous les bugs superficiels donc je n'ai pas à écrire de tests unitaires" ...
Qwertie
8

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

string fileName = "something";

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

Diego a la bonne solution, mais il y a une très petite erreur là-dedans. 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 fait le changement mineur.

Donc ça devrait être:

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

Voici une légère torsion de 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 non valides 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.txtau lieu de1_2_ spruce.txt

Oui, ça marche vraiment:

Exemple d'explorateur

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 les noms de fichiers longs sont stockés en 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 montrés OK. Je ne peux pas dire la même chose à 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 pourraient semer la confusion chez les personnes ou les anciens programmes, par exemple les anciens OS reposant sur des pages de codes .

rkagerer
la source
5

Voici une version qui utilise StringBuilderet IndexOfAnyavec un ajout en masse pour une efficacité totale. Il renvoie également la chaîne d'origine plutôt que de créer une chaîne en double.

Dernier point mais non le moindre, il dispose d'une instruction switch qui renvoie des caractères similaires que vous pouvez personnaliser comme vous le souhaitez. Consultez la recherche confusables d'Unicode.org pour voir quelles options vous pourriez avoir, en fonction de 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 CONparce qu'il est pas clair que le remplacement doit être.

jnm2
la source
3

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

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, c'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 ça aide...

À votre santé

Joan Vilariño
la source
3

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());
}
GDemartini
la source
3

Un simple code en une ligne:

var validFileName = Path.GetInvalidFileNameChars().Aggregate(fileName, (f, c) => f.Replace(c, '_'));

Vous pouvez l'envelopper dans une méthode d'extension si vous souhaitez le réutiliser.

public static string ToValidFileName(this string fileName) => Path.GetInvalidFileNameChars().Aggregate(fileName, (f, c) => f.Replace(c, '_'));
Moch Yusup
la source
1

J'avais besoin d'un système qui ne pouvait pas créer de collisions, donc je ne pouvais pas mapper plusieurs personnages sur un seul. J'ai fini 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)));
    }
}
mheyman
la source
0

J'avais besoin de 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 solution finale é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 le faire remplacer des espaces si vous ajoutez le caractère d'espace au tableau invalide.

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

À votre santé!

Joan Vilariño
la source
-2

Vous pouvez le faire avec une sedcommande:

 sed -e "
 s/[?()\[\]=+<>:;©®”,*|]/_/g
 s/"$'\t'"/ /g
 s/–/-/g
 s/\"/_/g
 s/[[:cntrl:]]/_/g"
DW
la source
voir également une question plus compliquée mais connexe à: stackoverflow.com/questions/4413427/…
DW
Pourquoi cela doit-il être fait en C # plutôt qu'en Bash? Je vois maintenant une balise C # sur la question d'origine, mais pourquoi?
DW
1
Je sais, non, pourquoi ne pas simplement passer de l'application C # à Bash qui pourrait ne pas être installée pour accomplir cela?
Peter Ritchie