¿Cómo hacer un nombre de archivo de Windows válido a partir de una cadena arbitraria?

97

Tengo una cadena como "Foo: Bar" que quiero usar como nombre de archivo, pero en Windows el carácter ":" no está permitido en un nombre de archivo.

¿Existe algún método que convierta "Foo: Bar" en algo como "Foo-Bar"?

Conocido
fuente
1
Hice lo mismo hoy. No marqué SO por alguna razón, pero encontré la respuesta de todos modos.
Aaron Smith

Respuestas:

153

Intente algo como esto:

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

Editar:

Dado GetInvalidFileNameChars()que devolverá 10 o 15 caracteres, es mejor usar a en StringBuilderlugar de una cadena simple; la versión original tardará más y consumirá más memoria.

Diego Jancic
fuente
1
Podría usar un StringBuilder si lo desea, pero si los nombres son cortos y supongo que no vale la pena. También puede crear su propio método para crear un carácter [] y reemplazar todos los caracteres incorrectos en una iteración. Siempre es mejor mantenerlo simple a menos que no funcione, es posible que tenga peores cuellos de botella
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 probabilidad de tener 2+ caracteres inválidos diferentes en la cadena es tan pequeña que preocuparse por el rendimiento de string.Replace () no tiene sentido.
Serge Wautier
1
Gran solución, aparte de interesante, resharper sugirió esta versión de Linq: fileName = System.IO.Path.GetInvalidFileNameChars (). Aggregate (fileName, (current, c) => current.Replace (c, '_')); Me pregunto si hay posibles mejoras de rendimiento allí. He conservado el original por motivos de legibilidad, ya que el rendimiento no es mi mayor preocupación. Pero si alguien está interesado, podría valer la pena hacer una evaluación comparativa
chrispepper1989
1
@AndyM No es necesario. file.name.txt.pdfes un pdf válido. Windows lee solo lo último .de la extensión.
Diego Jancic
33
fileName = fileName.Replace(":", "-") 

Sin embargo, ":" no es el único carácter ilegal para Windows. También tendrás que manejar:

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

Estos están contenidos en System.IO.Path.GetInvalidFileNameChars ();

También (en Windows), "." no puede ser el único carácter en el nombre de archivo (tanto ".", "..", "...", etc. no son válidos). Tenga cuidado al nombrar archivos con ".", Por ejemplo:

echo "test" > .test.

Generará un archivo llamado ".test"

Por último, si realmente desea hacer las cosas correctamente, hay algunos nombres de archivo especiales que debe tener en cuenta. En Windows no puede crear archivos con el nombre:

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.
Precio de Phil
fuente
3
Nunca supe de los nombres reservados. Sin embargo
Greg Dean
4
Además, por si sirve de algo, no puede crear un nombre de archivo que comience con uno de estos nombres reservados, seguido de un decimal. ie con.air.avi
John Conrad
".foo" es un nombre de archivo válido. No conocía el nombre de archivo "CON", ¿para qué sirve?
configurador
Rasca eso. CON es para consola.
configurador
Gracias configurador; He actualizado la respuesta, estás en lo correcto ".foo" es válido; sin embargo ".foo". conduce a posibles resultados no deseados. Actualizado.
Phil Price
13

Esto no es más eficiente, pero es más divertido :)

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

En caso de que alguien quiera una versión optimizada basada en StringBuilder, use esto. Incluye el truco de rkagerer como opción.

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
fuente
+1 para código agradable y legible. Hace muy fácil de leer y notar los errores: P .. Esta función debe devolver siempre la cadena original ya que cambiada nunca será verdadera.
Erti-Chris Eelmaa
Gracias, creo que ahora está mejor. Ya sabes lo que dicen sobre el código abierto, "muchos ojos hacen que todos los errores sean superficiales, así que no tengo que escribir pruebas unitarias" ...
Qwertie
8

Aquí hay una versión de la respuesta aceptada usando Linqwhich uses Enumerable.Aggregate:

string fileName = "something";

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

Diego tiene la solución correcta, pero hay un pequeño error ahí. La versión de string.Replace que se está utilizando debe ser string.Replace (char, char), no hay una cadena.Replace (char, string)

No puedo editar la respuesta o simplemente habría hecho un pequeño cambio.

Entonces debería ser:

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

Aquí hay un pequeño giro en la respuesta de Diego.

Si no le teme a Unicode, puede conservar un poco más de fidelidad reemplazando los caracteres no válidos con símbolos Unicode válidos que se parezcan a ellos. Aquí está el código que utilicé en un proyecto reciente que involucraba listas de corte de madera:

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

Esto produce nombres de archivo como en 1⁄2” spruce.txtlugar de1_2_ spruce.txt

Sí, realmente funciona:

Muestra de explorador

Caveat Emptor

Sabía que este truco funcionaría en NTFS, pero me sorprendió descubrir que también funciona en particiones FAT y FAT32. Eso es porque los nombres de archivo largos se almacenan en Unicode , incluso desde hace mucho tiempo. Windows 95 / NT. Probé en Win7, XP e incluso en un enrutador basado en Linux y salieron bien. No puedo decir lo mismo dentro de un DOSBox.

Dicho esto, antes de volverse loco con esto, considere si realmente necesita la fidelidad adicional. Los imitaciones de Unicode podrían confundir a las personas o los programas antiguos, por ejemplo, los sistemas operativos más antiguos que se basan en páginas de códigos .

rkagerer
fuente
5

Aquí hay una versión que usa StringBuildery IndexOfAnycon agregación masiva para una eficiencia total. También devuelve la cadena original en lugar de crear una cadena duplicada.

Por último, pero no menos importante, tiene una declaración de cambio que devuelve personajes similares que puede personalizar de la forma que desee. Consulte la búsqueda de confusables de Unicode.org para ver qué opciones puede tener, según la fuente.

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

No busca ., ..ni nombres reservados CONporque no está claro cuál debería ser el reemplazo.

jnm2
fuente
3

Limpiando un poco mi código y haciendo una pequeña refactorización ... Creé una extensión para el tipo de cadena:

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

Ahora es más fácil de usar con:

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

Si desea reemplazar con un carácter diferente a "_", puede usar:

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

Y puede agregar caracteres para reemplazar ... por ejemplo, no desea espacios ni comas:

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

Espero eso ayude...

Salud

Joan Vilariño
fuente
3

Otra solución 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
fuente
3

Un código simple de una línea:

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

Puede envolverlo en un método de extensión si desea reutilizarlo.

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

Necesitaba un sistema que no pudiera crear colisiones, por lo que no podía asignar varios caracteres a uno. Terminé 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)));
    }
}
mheyman
fuente
0

Necesitaba hacer esto hoy ... en mi caso, necesitaba concatenar el nombre de un cliente con la fecha y la hora para un archivo .kmz final. Mi solución final fue esta:

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

Incluso puede hacer que reemplace espacios si agrega el carácter de espacio a la matriz no válida.

Quizás no sea el más rápido, pero como el rendimiento no fue un problema, lo encontré elegante y comprensible.

¡Salud!

Joan Vilariño
fuente
-2

Puedes hacer esto con un sedcomando:

 sed -e "
 s/[?()\[\]=+<>:;©®”,*|]/_/g
 s/"$'\t'"/ /g
 s/–/-/g
 s/\"/_/g
 s/[[:cntrl:]]/_/g"
DW
fuente
también vea una pregunta más complicada pero relacionada en: stackoverflow.com/questions/4413427/…
DW
¿Por qué es necesario hacer esto en C # en lugar de Bash? Ahora veo una etiqueta de C # en la pregunta original, pero ¿por qué?
DW
1
Lo sé, ¿por qué no pagar desde la aplicación C # a Bash que podría no estar instalado para lograr esto?
Peter Ritchie