Как сделать действительное имя файла Windows из произвольной строки?

97

У меня есть строка типа «Foo: Bar», которую я хочу использовать в качестве имени файла, но в Windows символ «:» не допускается в имени файла.

Есть ли метод, который превратит "Foo: Bar" в нечто вроде "Foo-Bar"?

Кен
источник
1
Я сделал то же самое сегодня. Я почему-то не проверял ТАК, но все равно нашел ответ.
Аарон Смит,

Ответы:

154

Попробуйте что-то вроде этого:

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

Редактировать:

Так GetInvalidFileNameChars()как вернет 10 или 15 символов, лучше использовать StringBuilderвместо простой строки; исходная версия займет больше времени и потребляет больше памяти.

Диего Янчич
источник
1
Вы можете использовать StringBuilder, если хотите, но если имена короткие, и я думаю, это того не стоит. Вы также можете создать свой собственный метод для создания char [] и замены всех неправильных символов за одну итерацию. Всегда лучше делать это простым, если это не работает, у вас могут быть более узкие горлышки
Диего Янчич
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 ',': ',' * ','? ',' \\ ', '/'};
Диего Янчич,
9
Вероятность иметь 2+ разных недопустимых символа в строке настолько мала, что заботиться о производительности string.Replace () бессмысленно.
Серж Вотье,
1
Отличное решение, помимо интересного, resharper предложил эту версию Linq: fileName = System.IO.Path.GetInvalidFileNameChars (). Aggregate (fileName, (current, c) => current.Replace (c, '_')); Интересно, есть ли там возможные улучшения производительности. Я сохранил оригинал для удобства чтения, так как производительность не является моей самой большой проблемой. Но если кому-то интересно, может быть, стоит провести сравнительный анализ
chrispepper1989,
1
@AndyM В этом нет необходимости. file.name.txt.pdfэто действительный PDF-файл. Windows читает только последнее .для расширения.
Диего Янчич
33
fileName = fileName.Replace(":", "-") 

Однако «:» - не единственный недопустимый символ для Windows. Вам также придется обрабатывать:

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

Они содержатся в System.IO.Path.GetInvalidFileNameChars ();

Также (в Windows) "." не может быть единственным символом в имени файла (оба символа «.», «..», «...» и т. д. недопустимы). Будьте осторожны, называя файлы с помощью ".", Например:

echo "test" > .test.

Сгенерирует файл с именем ".test"

Наконец, если вы действительно хотите делать что-то правильно, есть несколько специальных имен файлов, на которые нужно обратить внимание. В Windows вы не можете создавать файлы с именами:

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
Я никогда не знал о зарезервированных именах. Хотя это имеет смысл
Грег Дин
4
Кроме того, как бы то ни было, вы не можете создать имя файла, начинающееся с одного из этих зарезервированных имен, за которым следует десятичная дробь. т.е. con.air.avi
Джон Конрад
".foo" - допустимое имя файла. Не знал о имени файла "CON" - для чего оно?
конфигуратор
Сотрите это. CON для консоли.
конфигуратор
Спасибо конфигуратору; Я обновил ответ, вы правы. ".Foo" действительно; однако ".foo." приводит к возможным нежелательным результатам. Обновлено.
Фил Прайс,
13

Это не эффективнее, но веселее :)

var fileName = "foo:bar";
var invalidChars = System.IO.Path.GetInvalidFileNameChars();
var cleanFileName = new string(fileName.Where(m => !invalidChars.Contains(m)).ToArray<char>());
Джозеф Габриэль
источник
12

Если кому-то нужна оптимизированная версия на основе StringBuilder, используйте это. В качестве опции включает трюк rkagerer .

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
источник
+1 за красивый и читаемый код. Облегчает чтение и обнаружение ошибок: P .. Эта функция всегда должна возвращать исходную строку, поскольку измененная строка никогда не будет истинной.
Эрти-Крис Элмаа
Спасибо, думаю, сейчас лучше. Вы знаете, что они говорят об открытом исходном коде: «Многие глаза делают все ошибки поверхностными, поэтому мне не нужно писать модульные тесты» ...
Qwertie
8

Вот небольшой поворот в ответе Диего.

Если вы не боитесь Unicode, вы можете сохранить немного большую точность, заменив недопустимые символы на действительные символы Unicode, которые на них похожи. Вот код, который я использовал в недавнем проекте по спискам срезов пиломатериалов:

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

Это создает имена файлов, например, 1⁄2” spruce.txtвместо1_2_ spruce.txt

Да, действительно работает:

Образец проводника

Пусть покупатель будет бдителен

Я знал, что этот трюк будет работать с NTFS, но был удивлен, обнаружив, что он также работает с разделами FAT и FAT32. Это потому , что длинные имена файлов будут сохранены в Unicode , даже еще в Windows 95 / NT. Я тестировал Win7, XP и даже маршрутизатор на базе Linux, и они показали себя нормально. Не могу сказать то же самое для DOSBox.

Тем не менее, прежде чем вы сходите с ума от этого, подумайте, действительно ли вам нужна дополнительная точность. Сходства Unicode могут сбивать с толку людей или старые программы, например, старые ОС, полагающиеся на кодовые страницы .

rkagerer
источник
8

Вот версия принятого ответа, в Linqкоторой используются Enumerable.Aggregate:

string fileName = "something";

Path.GetInvalidFileNameChars()
    .Aggregate(fileName, (current, c) => current.Replace(c, '_'));
DavidG
источник
7

У Диего действительно есть правильное решение, но есть одна очень маленькая ошибка. Используемая версия string.Replace должна быть string.Replace (char, char), строки нет.Replace (char, string)

Я не могу отредактировать ответ, иначе я бы просто внес незначительное изменение.

Так и должно быть:

string fileName = "something";
foreach (char c in System.IO.Path.GetInvalidFileNameChars())
{
   fileName = fileName.Replace(c, '_');
}
леггеттер
источник
5

Вот версия, которая использует StringBuilderи IndexOfAnyс массовым добавлением для полной эффективности. Он также возвращает исходную строку, а не создает повторяющуюся строку.

И последнее, но не менее важное: в нем есть оператор switch, который возвращает похожие символы, которые вы можете настроить по своему усмотрению. Ознакомьтесь с поиском confusables на Unicode.org, чтобы узнать, какие варианты у вас могут быть в зависимости от шрифта.

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

Он не проверяет ., ..или зарезервированные имена , как , CONпотому что не ясно , что замена должна быть.

jnm2
источник
3

Немного почистив свой код и сделав небольшой рефакторинг ... Я создал расширение для строкового типа:

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

Теперь стало проще использовать:

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

Если вы хотите заменить символом, отличным от "_", вы можете использовать:

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

И вы можете добавить символы для замены .. например, вам не нужны пробелы или запятые:

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

Надеюсь, поможет...

Ура

Хоан Вилариньо
источник
3

Еще одно простое решение:

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
источник
3

Простой однострочный код:

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

Вы можете обернуть его в метод расширения, если хотите использовать его повторно.

public static string ToValidFileName(this string fileName) => Path.GetInvalidFileNameChars().Aggregate(fileName, (f, c) => f.Replace(c, '_'));
Мох Юсуп
источник
1

Мне нужна была система, которая не могла создавать коллизии, чтобы я не мог сопоставить несколько символов одному. Я получил:

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
источник
0

Мне нужно было сделать это сегодня ... в моем случае мне нужно было объединить имя клиента с датой и временем для окончательного файла .kmz. Мое окончательное решение было таким:

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

Вы даже можете заставить его заменить пробелы, если вы добавите пробел в неверный массив.

Возможно, он не самый быстрый, но, поскольку производительность не была проблемой, я нашел его элегантным и понятным.

Ура!

Хоан Вилариньо
источник
-2

Сделать это можно с помощью sedкоманды:

 sed -e "
 s/[?()\[\]=+<>:;©®”,*|]/_/g
 s/"$'\t'"/ /g
 s/–/-/g
 s/\"/_/g
 s/[[:cntrl:]]/_/g"
DW
источник
также см. более сложный, но связанный вопрос по адресу: stackoverflow.com/questions/4413427/…
DW
Почему это нужно делать на C #, а не на Bash? Теперь я вижу тег C # в исходном вопросе, но почему?
DW
1
Я знаю, верно, почему бы просто не передать оболочку из приложения C # в Bash, который может не быть установлен для этого?
Питер Ричи