Jak utworzyć prawidłową nazwę pliku systemu Windows z dowolnego ciągu?

97

Mam ciąg, taki jak „Foo: Bar”, którego chcę użyć jako nazwy pliku, ale w systemie Windows znak „:” nie jest dozwolony w nazwie pliku.

Czy istnieje metoda, która zamieni „Foo: Bar” w coś takiego jak „Foo-Bar”?

Rozpoznać
źródło
1
Zrobiłem to samo dzisiaj. Nie sprawdziłem SO z jakiegoś powodu, ale i tak znalazłem odpowiedź.
Aaron Smith,

Odpowiedzi:

154

Spróbuj czegoś takiego:

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

Edytować:

Ponieważ GetInvalidFileNameChars()zwróci 10 lub 15 znaków, lepiej jest użyć a StringBuilderzamiast prostego ciągu; oryginalna wersja potrwa dłużej i zużyje więcej pamięci.

Diego Jancic
źródło
1
Jeśli chcesz, możesz użyć StringBuilder, ale jeśli nazwy są krótkie i myślę, że nie warto. Możesz także stworzyć własną metodę tworzenia znaku char [] i zastępowania wszystkich złych znaków w jednej iteracji. Zawsze lepiej jest zachować prostotę, chyba że to nie działa, możesz mieć gorsze szyjki butelek
Diego Jancic
2
InvalidFileNameChars = nowy znak [] {'"', '<', '>', '|', '\ 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
Prawdopodobieństwo posiadania ponad 2 różnych nieprawidłowych znaków w ciągu jest tak małe, że dbanie o wydajność string.Replace () jest bezcelowe.
Serge Wautier
1
Świetne rozwiązanie, poza tym ciekawe, resharper zasugerował tę wersję Linq: fileName = System.IO.Path.GetInvalidFileNameChars (). Aggregate (fileName, (current, c) => current.Replace (c, '_')); Zastanawiam się, czy są tam możliwe ulepszenia wydajności. Zachowałem oryginał ze względu na czytelność, ponieważ wydajność nie jest moim największym zmartwieniem. Ale jeśli ktoś jest zainteresowany, może warto go
porównać
1
@AndyM Nie ma takiej potrzeby. file.name.txt.pdfjest prawidłowym plikiem PDF. Windows odczytuje tylko ostatnie .rozszerzenie.
Diego Jancic
33
fileName = fileName.Replace(":", "-") 

Jednak „:” nie jest jedynym niedozwolonym znakiem w systemie Windows. Będziesz musiał również poradzić sobie z:

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

Są one zawarte w System.IO.Path.GetInvalidFileNameChars ();

Również (w systemie Windows) „.” nie może być jedynym znakiem w nazwie pliku (oba „.”, „..”, „...” itd. są nieprawidłowe). Zachowaj ostrożność podczas nazywania plików za pomocą „.”, Na przykład:

echo "test" > .test.

Wygeneruje plik o nazwie „.test”

Na koniec, jeśli naprawdę chcesz zrobić coś poprawnie, istnieje kilka specjalnych nazw plików, na które musisz zwrócić uwagę. W systemie Windows nie można tworzyć plików o nazwach:

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
źródło
3
Nigdy nie wiedziałem o zastrzeżonych nazwach. Ma to jednak sens
Greg Dean
4
Poza tym, co jest warte, nie możesz utworzyć nazwy pliku zaczynającej się od jednej z tych zastrzeżonych nazw, po której następuje ułamek dziesiętny. tj. con.air.avi
John Conrad
„.foo” to poprawna nazwa pliku. Nie wiedziałeś o nazwie pliku „CON” - do czego służy?
konfigurator
Podrap to. CON jest dla konsoli.
konfigurator
Dzięki konfiguratorowi; Zaktualizowałem odpowiedź, masz rację „.foo” jest poprawne; jednak „.foo”. prowadzi do możliwych, niepożądanych rezultatów. Zaktualizowano.
Phil Price,
13

To nie jest bardziej wydajne, ale jest fajniejsze :)

var fileName = "foo:bar";
var invalidChars = System.IO.Path.GetInvalidFileNameChars();
var cleanFileName = new string(fileName.Where(m => !invalidChars.Contains(m)).ToArray<char>());
Joseph Gabriel
źródło
12

Jeśli ktoś chce mieć zoptymalizowaną wersję StringBuilder, użyj tego. Zawiera sztuczkę rkagerera jako opcję.

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
źródło
+1 za ładny i czytelny kod. Sprawia, że ​​jest bardzo łatwy do odczytania i zauważenia błędów: P .. Ta funkcja powinna zawsze zwracać oryginalny ciąg, ponieważ zmieniony nigdy nie będzie prawdziwy.
Erti-Chris Eelmaa
Dzięki, myślę, że teraz jest lepiej. Wiesz, co mówią o open source, „wiele oczu sprawia, że ​​wszystkie błędy są płytkie, więc nie muszę pisać testów jednostkowych”…
Qwertie
8

Oto wersja zaakceptowanej odpowiedzi, Linqktóra używa Enumerable.Aggregate:

string fileName = "something";

Path.GetInvalidFileNameChars()
    .Aggregate(fileName, (current, c) => current.Replace(c, '_'));
DavidG
źródło
7

Diego ma właściwe rozwiązanie, ale jest tam jeden bardzo mały błąd. Używana wersja string.Replace powinna być string.Replace (char, char), nie ma łańcucha.Replace (char, string)

Nie mogę edytować odpowiedzi lub właśnie wprowadziłbym drobną zmianę.

Więc powinno być:

string fileName = "something";
foreach (char c in System.IO.Path.GetInvalidFileNameChars())
{
   fileName = fileName.Replace(c, '_');
}
leggetter
źródło
7

Oto drobny zwrot w odpowiedzi Diego.

Jeśli nie boisz się Unicode, możesz zachować nieco większą wierność, zastępując nieprawidłowe znaki prawidłowymi symbolami Unicode, które je przypominają. Oto kod, którego użyłem w niedawnym projekcie dotyczącym list krojenia drewna:

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

Tworzy nazwy plików takie jak 1⁄2” spruce.txtzamiast1_2_ spruce.txt

Tak, to naprawdę działa:

Próbka Explorer

Caveat Emptor

Wiedziałem, że ta sztuczka zadziała na NTFS, ale byłem zaskoczony, że działa również na partycjach FAT i FAT32. To dlatego, że długie nazwy plikówprzechowywane w Unicode , nawet tak daleko wstecz jak Windows 95 / NT. Testowałem na Win7, XP, a nawet na routerze opartym na Linuksie i pokazały się OK. Nie mogę powiedzieć tego samego o wnętrzu DOSBox.

To powiedziawszy, zanim zwariujesz z tym, zastanów się, czy naprawdę potrzebujesz dodatkowej wierności. Podobieństwa do Unicode mogą zmylić ludzi lub stare programy, np. Starsze systemy operacyjne polegające na stronach kodowych .

rkagerer
źródło
5

Oto wersja, która używa StringBuilderi IndexOfAnyz dołączaniem zbiorczym dla pełnej wydajności. Zwraca również oryginalny ciąg zamiast tworzyć zduplikowany ciąg.

Wreszcie, zawiera instrukcję przełącznika, która zwraca wyglądające znaki, które można dostosować w dowolny sposób. Zapoznaj się z wyszukiwaniem elementów zagmatwanych na Unicode.org, aby zobaczyć, jakie opcje mogą być dostępne w zależności od czcionki.

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

To nie sprawdza ., ..czy zarezerwowane nazwy takie jak CON, ponieważ nie jest jasne, co powinno być zastąpienie.

jnm2
źródło
3

Trochę wyczyszczę mój kod i zrobię trochę refaktoryzacji ... Stworzyłem rozszerzenie dla typu 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));
}

Teraz jest łatwiejszy w użyciu z:

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

Jeśli chcesz zamienić na inny znak niż „_”, możesz użyć:

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

I możesz dodać znaki, aby zastąpić ... na przykład nie chcesz spacji ani przecinków:

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

Mam nadzieję, że to pomoże...

Twoje zdrowie

Joan Vilariño
źródło
3

Kolejne proste rozwiązanie:

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
źródło
3

Prosty kod jednowierszowy:

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

Możesz zawinąć go w metodę rozszerzenia, jeśli chcesz go ponownie użyć.

public static string ToValidFileName(this string fileName) => Path.GetInvalidFileNameChars().Aggregate(fileName, (f, c) => f.Replace(c, '_'));
Moch Yusup
źródło
1

Potrzebowałem systemu, który nie mógł tworzyć kolizji, więc nie mogłem odwzorować wielu znaków na jeden. Skończyło się na:

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
źródło
0

Musiałem to zrobić dzisiaj ... w moim przypadku musiałem połączyć nazwę klienta z datą i godziną dla końcowego pliku .kmz. Moje ostateczne rozwiązanie było takie:

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

Możesz nawet zastąpić spacje, jeśli dodasz znak spacji do nieprawidłowej tablicy.

Może nie jest najszybszy, ale ponieważ wydajność nie była problemem, uznałem to za eleganckie i zrozumiałe.

Twoje zdrowie!

Joan Vilariño
źródło
-2

Możesz to zrobić za pomocą sedpolecenia:

 sed -e "
 s/[?()\[\]=+<>:;©®”,*|]/_/g
 s/"$'\t'"/ /g
 s/–/-/g
 s/\"/_/g
 s/[[:cntrl:]]/_/g"
DW
źródło
zobacz także bardziej skomplikowane, ale powiązane pytanie na: stackoverflow.com/questions/4413427/ ...
DW,
Dlaczego trzeba to zrobić w C #, a nie w Bash? Widzę teraz tag C # w oryginalnym pytaniu, ale dlaczego?
DW
1
Wiem, prawda, dlaczego po prostu nie wyskoczyć z aplikacji C # do Bash, która może nie zostać zainstalowana, aby to osiągnąć?
Peter Ritchie