6

On most Linux filesystems, NUL (\0) is the only invalid character in paths (and / is reserved as the path separator). Windows has a complicated set of rules for valid paths. Rather than fixing paths automagically (dangerous, could result in a file overwriting another), how can I find all paths within a directory not compatible with Windows?

The original issue was that my Google Drive folder was on a drive mounted using ext2fs, but the official Gdrive client told me thousands of files could not be synced. I could find no error messages, and when I asked it to show me the files it would simply hang indefinitely. Restarting the client or OS did not help, but I had a hunch fixing any non-Windows-compatible paths would unstick Gdrive. It seems to have worked...

  • In what context would you want that? To see if you can export over samba, to see if you can copy without loss of information onto a NTFS fs? You'll have to consider charsets. Unix doesn't care about charsets, while I believe Windows uses/assumes some form of UCS-2 or UTF16, so in any case, samba or mount.ntfs will have to do the conversion and assume a from charset when you create a file there. – Stéphane Chazelas May 27 '14 at 9:50
3

Paths with reserved names/characters:

LC_ALL=C find . -name '*[[:cntrl:]<>:"\\|?*]*' \
             -o -iname 'CON' \
             -o -iname 'PRN' \
             -o -iname 'AUX' \
             -o -iname 'NUL' \
             -o -iname 'COM[1-9]' \
             -o -iname 'LPT[1-9]' \
             -o -name '* ' \
             -o -name '?*.'

Test:

$ cd -- "$(mktemp --directory)"
$ touch foo \\ LPT9 'space ' 'dot.'
$ LC_ALL=C find . -name '*[[:cntrl:]<>:"\\|?*]*' -o -iname 'CON' -o -iname 'PRN' -o -iname 'AUX' -o -iname 'NUL' -o -iname 'COM[1-9]' -o -iname 'LPT[1-9]' -o -name '* ' -o -name '?*.'
./dot.
./space 
./LPT9
./\

Paths which are identical when ignoring case (does not work with paths containing newlines):

find . | sort | LC_ALL=C tr '[:upper:]' '[:lower:]' | uniq -c | grep -v '^      1 ' | cut -c '9-'

Test:

$ cd -- "$(mktemp --directory)"
$ touch foo bar BAR
$ find . | sort | LC_ALL=C tr '[:upper:]' '[:lower:]' | LC_ALL=C uniq -c | grep -v '^      1 ' | cut -c '9-'
./bar

Paths longer than MAX_PATH (260 characters):

find "$PWD" | while IFS= read -r path
do
    if [ "${#path}" -gt 260 ]
    then
        printf '%s\n' "$path"
    fi
done

Test:

$ cd -- "$(mktemp --directory)"
$ touch foo 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345
$ find "$PWD" | while IFS= read -r path
> do
>     if [ "${#path}" -gt 260 ]
>     then
>         printf '%s\n' "$path"
>     fi
> done
/tmp/tmp.HEANyAI8Hy/123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345
  • I thought Windows was able to deal with files of different cases. The rules of which language does it use to decide two characters are the same with different case? Your answer probably doesn't deal properly with non-ASCII characters. For instance sort | tr | uniq only does what you think it does in the C locale. – Stéphane Chazelas May 24 '14 at 21:48
  • You can handle the newline thing portably by beginning your path search with a path that will resolve correctly but not occur in find's results. Like with .//// or something. You could even do find ... -exec sh -c 'printf "\\//// %s \\////\n" "$@"' -- \{\} + to completely surround each result in a fence of //// characters. – mikeserv May 24 '14 at 23:37
  • 1
    You're missing some files: it's not just CON that doesn't work, but also con, cOn, CON.foo, etc. – Gilles May 24 '14 at 23:48
  • @Gilles Thanks; fixed using -iname – l0b0 May 31 '14 at 14:16
-1

For a Perl solution, you could use the Win32::Filenames module for this.

http://search.cpan.org/~bch/Win32-Filenames-0.01/lib/Win32/Filenames.pm

A simple program using this module:

#!/usr/bin/env perl

use strict;
use warnings;

use Win32::Filenames qw(validate sanitize);

while ( my $filename = shift ) {
    printf( "checking filename %s\t", $filename );

    if ( validate($filename) ) {
        print("ok.\n");
    }
    else {
        printf( "failed, try this instead: %s\n", sanitize($filename) );
    }
}

You invoke it with one or several file names on the command line:

$ ./check.pl '<a>' 'hi?' '"right"' 'pipe|me'
checking filename <a>   failed, try this instead: -a-
checking filename hi?   failed, try this instead: hi-
checking filename "right"       failed, try this instead: -right-
checking filename pipe|me       failed, try this instead: pipe-me

For a whole file hierarchy:

$ find . -exec basename {} \; | xargs ./check.pl

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.