Como fazer um nome de arquivo do Windows válido a partir de uma string arbitrária?

97

Eu tenho uma string como "Foo: Bar" que quero usar como nome de arquivo, mas no Windows o caractere ":" não é permitido em um nome de arquivo.

Existe um método que transformará "Foo: Bar" em algo como "Foo- Bar"?

Ken
fonte
1
Eu fiz a mesma coisa hoje. Não marquei SO por algum motivo, mas encontrei a resposta mesmo assim.
Aaron Smith

Respostas:

153

Experimente algo assim:

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

Editar:

Como GetInvalidFileNameChars()retornará 10 ou 15 caracteres, é melhor usar um em StringBuildervez de uma string simples; a versão original vai demorar mais e consumir mais memória.

Diego jancic
fonte
1
Você poderia usar um StringBuilder se desejar, mas se os nomes forem curtos e eu acho que não vale a pena. Você também pode criar seu próprio método para criar um char [] e substituir todos os caracteres errados em uma iteração. É sempre melhor manter as coisas simples, a menos que não funcione, você pode ter gargalos de garrafa piores
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
A probabilidade de ter 2+ caracteres inválidos diferentes na string é tão pequena que se preocupar com o desempenho da string.Replace () é inútil.
Serge Wautier,
1
Ótima solução, interessante à parte, resharper sugeriu esta versão do Linq: fileName = System.IO.Path.GetInvalidFileNameChars (). Aggregate (fileName, (current, c) => current.Replace (c, '_')); Eu me pergunto se há alguma melhoria de desempenho possível lá. Eu mantive o original para fins de legibilidade, pois o desempenho não é minha maior preocupação. Mas se alguém estiver interessado, pode valer a pena fazer um benchmarking
chrispepper1989
1
@AndyM Não precisa. file.name.txt.pdfé um pdf válido. O Windows lê apenas o último .da extensão.
Diego Jancic
33
fileName = fileName.Replace(":", "-") 

No entanto, ":" não é o único caractere ilegal para Windows. Você também terá que lidar com:

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

Eles estão contidos em System.IO.Path.GetInvalidFileNameChars ();

Também (no Windows), "." não pode ser o único caractere no nome do arquivo (ambos ".", "..", "..." e assim por diante são inválidos). Tenha cuidado ao nomear arquivos com ".", Por exemplo:

echo "test" > .test.

Irá gerar um arquivo chamado ".test"

Por último, se você realmente deseja fazer as coisas corretamente, existem alguns nomes de arquivos especiais que você precisa observar. No Windows, você não pode criar arquivos chamados:

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
fonte
3
Nunca soube dos nomes reservados. Mas faz sentido
Greg Dean
4
Além disso, você não pode criar um nome de arquivo começando com um desses nomes reservados, seguido por um decimal. ou seja, con.air.avi
John Conrad
".foo" é um nome de arquivo válido. Não sabia sobre o nome do arquivo "CON" - para que ele serve?
configurador
Risca isso. CON é para console.
configurador
Obrigado configurador; Eu atualizei a resposta, você está correto ".foo" é válido; no entanto, ".foo." leva a resultados possíveis e indesejados. Atualizada.
Phil Price,
13

Isso não é mais eficiente, mas é mais divertido :)

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

Caso alguém queira uma versão otimizada com base no StringBuilder, use-o. Inclui o truque do rkagerer como opção.

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
fonte
1 para código bom e legível. Facilita a leitura e a observação dos bugs: P .. Esta função deve retornar sempre a string original, pois a alteração nunca será verdadeira.
Erti-Chris Eelmaa
Obrigado, acho que está melhor agora. Você sabe o que dizem sobre o código aberto, "muitos olhos tornam todos os bugs superficiais, então não tenho que escrever testes de unidade" ...
Qwertie
8

Esta é uma versão da resposta aceita Linqque usa Enumerable.Aggregate:

string fileName = "something";

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

Diego tem a solução correta, mas há um pequeno erro aí. A versão da string.Replace sendo usado deve ser string.Replace (char, char), não há uma string.Replace (char, string)

Não consigo editar a resposta ou teria apenas feito uma pequena alteração.

Portanto, deve ser:

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

Aqui está uma pequena mudança na resposta de Diego.

Se você não tem medo do Unicode, pode manter um pouco mais de fidelidade substituindo os caracteres inválidos por símbolos Unicode válidos que se assemelham a eles. Aqui está o código que usei em um projeto recente envolvendo listas de corte de madeira:

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

Isso produz nomes de arquivos como em 1⁄2” spruce.txtvez de1_2_ spruce.txt

Sim, realmente funciona:

Amostra do explorador

Caveat Emptor

Eu sabia que esse truque funcionaria em NTFS, mas fiquei surpreso ao descobrir que também funciona em partições FAT e FAT32. Isso ocorre porque nomes longos de arquivos são armazenados em Unicode , mesmo desde o Windows 95 / NT. Eu testei no Win7, XP e até mesmo em um roteador baseado em Linux e eles mostraram-se OK. Não posso dizer o mesmo para dentro de um DOSBox.

Dito isso, antes de enlouquecer com isso, considere se você realmente precisa de fidelidade extra. As semelhanças com o Unicode podem confundir as pessoas ou programas antigos, por exemplo, sistemas operacionais mais antigos que dependem de páginas de código .

rkagerer
fonte
5

Aqui está uma versão que usa StringBuildere IndexOfAnycom acréscimo em massa para eficiência total. Ele também retorna a string original em vez de criar uma string duplicada.

Por último, mas não menos importante, ele tem uma instrução switch que retorna caracteres parecidos que você pode personalizar da maneira que desejar. Confira a pesquisa de confundíveis do Unicode.org para ver quais opções você pode ter, dependendo da fonte.

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

Ele não verifica ., ..ou nomes reservados, como CONporque não está claro o que a substituição deve ser.

jnm2
fonte
3

Limpando um pouco meu código e fazendo uma pequena refatoração ... Criei uma extensão para tipo de 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));
}

Agora é mais fácil de usar com:

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

Se você deseja substituir por um caractere diferente de "_", você pode usar:

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

E você pode adicionar caracteres para substituir .. por exemplo, você não quer espaços ou vírgulas:

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

Espero que ajude...

Felicidades

Joan Vilariño
fonte
3

Outra solução simples:

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
fonte
3

Um código simples de uma linha:

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

Você pode envolvê-lo em um método de extensão se quiser reutilizá-lo.

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

Eu precisava de um sistema que não pudesse criar colisões, então não poderia mapear vários personagens para um. Acabei com:

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
fonte
0

Eu precisava fazer isso hoje ... no meu caso, precisava concatenar o nome de um cliente com a data e a hora para um arquivo .kmz final. Minha solução final foi 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));

Você pode até mesmo fazer com que ele substitua os espaços se adicionar o caractere de espaço ao array inválido.

Talvez não seja o mais rápido, mas como o desempenho não era um problema, achei-o elegante e compreensível.

Felicidades!

Joan Vilariño
fonte
-2

Você pode fazer isso com um sedcomando:

 sed -e "
 s/[?()\[\]=+<>:;©®”,*|]/_/g
 s/"$'\t'"/ /g
 s/–/-/g
 s/\"/_/g
 s/[[:cntrl:]]/_/g"
DW
fonte
veja também uma pergunta mais complicada, mas relacionada em: stackoverflow.com/questions/4413427/…
DW
Por que isso precisa ser feito em C # em vez de Bash? Agora vejo uma tag de C # na pergunta original, mas por quê?
DW de
1
Eu sei, certo, por que não apenas pagar do aplicativo C # para o Bash que pode não estar instalado para fazer isso?
Peter Ritchie