Come creare un nome file di Windows valido da una stringa arbitraria?


97

Ho una stringa come "Foo: Bar" che voglio usare come nome di file, ma su Windows il carattere ":" non è consentito in un nome di file.

C'è un metodo che trasformerà "Foo: Bar" in qualcosa come "Foo Bar"?


1
Oggi ho fatto la stessa cosa. Non ho controllato SO per qualche motivo, ma ho trovato comunque la risposta.
Aaron Smith

Risposte:


153

Prova qualcosa di simile:

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

Modificare:

Poiché GetInvalidFileNameChars()restituirà 10 o 15 caratteri, è meglio usare a StringBuilderinvece di una semplice stringa; la versione originale richiederà più tempo e consumerà più memoria.


1
Puoi usare uno StringBuilder se lo desideri, ma se i nomi sono brevi e immagino che non ne valga la pena. Puoi anche creare il tuo metodo per creare un carattere [] e sostituire tutti i caratteri sbagliati in un'unica iterazione. È sempre meglio mantenerlo semplice a meno che non funzioni, potresti avere colli di bottiglia peggiori
Diego Jancic

2
InvalidFileNameChars = new char [] {'"', '<', '>', '|', '\ 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à di avere 2 o più caratteri non validi diversi nella stringa è così piccola che preoccuparsi delle prestazioni di string.Replace () è inutile.
Serge Wautier

1
Ottima soluzione, a parte interessante, resharper ha suggerito questa versione di Linq: fileName = System.IO.Path.GetInvalidFileNameChars (). Aggregate (fileName, (current, c) => current.Replace (c, '_')); Mi chiedo se ci siano possibili miglioramenti delle prestazioni lì. Ho mantenuto l'originale per motivi di leggibilità poiché le prestazioni non sono la mia più grande preoccupazione. Ma se qualcuno è interessato, potrebbe valere la pena fare un benchmarking
chrispepper1989

1
@ AndyM Non è necessario. file.name.txt.pdfè un pdf valido. Windows legge solo l'ultimo .per l'estensione.
Diego Jancic

33
fileName = fileName.Replace(":", "-") 

Tuttavia ":" non è l'unico carattere illegale per Windows. Dovrai anche gestire:

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

Questi sono contenuti in System.IO.Path.GetInvalidFileNameChars ();

Inoltre (su Windows), "." non può essere l'unico carattere nel nome del file (entrambi ".", "..", "..." e così via non sono validi). Fare attenzione quando si denominano i file con ".", Ad esempio:

echo "test" > .test.

Genererà un file denominato ".test"

Infine, se vuoi davvero fare le cose correttamente, ci sono alcuni nomi di file speciali che devi cercare. Su Windows non puoi creare file denominati:

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.

3
Non ho mai saputo dei nomi riservati. Ha senso però
Greg Dean

4
Inoltre, per quel che vale, non puoi creare un nome di file che inizi con uno di questi nomi riservati, seguito da un decimale. cioè con.air.avi
John Conrad

".foo" è un nome file valido. Non conoscevo il nome del file "CON": a cosa serve?
configuratore

Grattalo. CON è per console.
configuratore

Grazie configuratore; Ho aggiornato la risposta, hai ragione ".foo" è valido; tuttavia ".foo." porta a risultati possibili e indesiderati. Aggiornato.
Phil Price

13

Non è più efficiente, ma è più divertente :)

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

12

Nel caso in cui qualcuno desideri una versione ottimizzata basata su StringBuilder, usa questo. Include il trucco di rkagerer come opzione.

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;
}

+1 per un codice piacevole e leggibile. Rende molto facile leggere e notare i bug: P .. Questa funzione dovrebbe restituire sempre la stringa originale poiché le modifiche non saranno mai vere.
Erti-Chris Eelmaa

Grazie, penso sia meglio ora. Sai cosa dicono dell'open source, "molti occhi rendono tutti i bug superficiali quindi non devo scrivere test unitari" ...
Qwertie

8

Ecco una versione della risposta accettata utilizzando Linqquale utilizza Enumerable.Aggregate:

string fileName = "something";

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

7

Diego ha la soluzione corretta ma c'è un piccolo errore lì dentro. La versione di string.Replace utilizzata dovrebbe essere string.Replace (char, char), non c'è una stringa.Replace (char, string)

Non riesco a modificare la risposta o avrei solo apportato una piccola modifica.

Quindi dovrebbe essere:

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

7

Ecco una leggera svolta nella risposta di Diego.

Se non hai paura di Unicode, puoi mantenere un po 'più di fedeltà sostituendo i caratteri non validi con simboli Unicode validi che li assomigliano. Ecco il codice che ho usato in un recente progetto che coinvolgeva cutlist di legname:

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;
}

Questo produce nomi di file come 1⁄2” spruce.txtinvece di1_2_ spruce.txt

Sì, funziona davvero:

Esempio di Explorer

Caveat Emptor

Sapevo che questo trucco avrebbe funzionato su NTFS, ma sono rimasto sorpreso di scoprire che funziona anche su partizioni FAT e FAT32. Questo perché i nomi di file lunghi vengono memorizzati in Unicode , anche nel passato a Windows 95 / NT. Ho provato su Win7, XP e persino un router basato su Linux e sono risultati OK. Non posso dire lo stesso per all'interno di un DOSBox.

Detto questo, prima di impazzire con questo, considera se hai davvero bisogno della fedeltà extra. I sosia di Unicode potrebbero confondere le persone oi vecchi programmi, ad esempio i vecchi sistemi operativi che si affidano alle tabelle codici .


5

Ecco una versione che utilizza StringBuildereIndexOfAny con aggiunta di massa per la piena efficienza. Restituisce anche la stringa originale anziché creare una stringa duplicata.

Ultimo ma non meno importante, ha un'istruzione switch che restituisce caratteri simili che puoi personalizzare in qualsiasi modo desideri. Controlla la ricerca di elementi confusibili di Unicode.org per vedere quali opzioni potresti avere, a seconda del carattere.

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();
}

Esso non controlla ., ..o nomi riservati come CON, perché non è chiaro quale sia la sostituzione dovrebbe essere.


3

Pulendo un po 'il mio codice e facendo un po' di refactoring ... ho creato un'estensione per il tipo di stringa:

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));
}

Ora è più facile da usare con:

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

Se vuoi sostituire con un carattere diverso da "_" puoi usare:

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

E puoi aggiungere caratteri da sostituire .. per esempio non vuoi spazi o virgole:

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

Spero che sia d'aiuto...

Saluti


3

Un'altra semplice soluzione:

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

Un semplice codice di una riga:

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

Puoi avvolgerlo in un metodo di estensione se desideri riutilizzarlo.

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

1

Avevo bisogno di un sistema che non potesse creare collisioni, quindi non potevo mappare più personaggi su uno. Ho finito con:

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

Avevo bisogno di farlo oggi ... nel mio caso, avevo bisogno di concatenare il nome di un cliente con la data e l'ora per un file .kmz finale. La mia soluzione finale è stata questa:

 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));

Puoi anche fare in modo che sostituisca gli spazi se aggiungi lo spazio char all'array non valido.

Forse non è il più veloce, ma poiché le prestazioni non erano un problema, l'ho trovato elegante e comprensibile.

Saluti!


-2

Puoi farlo con un sedcomando:

 sed -e "
 s/[?()\[\]=+<>:;©®”,*|]/_/g
 s/"$'\t'"/ /g
 s/–/-/g
 s/\"/_/g
 s/[[:cntrl:]]/_/g"

vedi anche una domanda più complicata ma correlata su: stackoverflow.com/questions/4413427/…
DW

Perché è necessario farlo in C # anziché in Bash? Ora vedo un tag di C # sulla domanda originale, ma perché?
DW

1
Lo so, giusto, perché non sborsare dall'applicazione C # a Bash che potrebbe non essere installata per farlo?
Peter Ritchie
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.