Mac
Windows
matlab
NTFS
ファイル名

Macに保存していたデータをWindowsマシンへ移行する時の苦労話

More than 1 year has passed since last update.

元々 Windows で使用していた実験データを、日本に帰国した際に、Macへ移行しました。Macのほうが断然デザイン性が高いことと、英語ならまだ良かったのですが日本語環境に関しては Windows で生きていくのはさぞかし辛いだろうと思ったからです。基本的に MS明朝フォント、MSゴシックフォントは、ヒラギノの表現力には遠くおよばない上に、スクリーン上でのフォントの表示に関して、初代OS Xが10年以上前に標準でアンチエイリアスを導入したのに対して、Windows 7ではやっと導入した ClearTypeという技術も水平方向の見えしか改善しないため、依然としてスクリーン上の日本語はギザギザです。一般企業や官公庁を含めて、恐ろしい数の日本人が Windowsを使用していることを考えると、この状況はちょっと信じがたいのですが。自分では使ったことがないので分かりませんが Windows 8や10では改善されていることを願っています。

話が脱線しましたが、WindowsからMacへの移行時は大して苦労はありませんでした。何かを特にやったという記憶がありません。そして、そのデータにはMac上で VMware Fusion上のWindows 7からしょっちゅうアクセスしていたので、再びイギリスへ戻って、同じデータをWindowsに戻す事になった時にまさか、こんなに面倒くさいことになっているとは夢にも思っていませんでした。

基本的な状況

Windows 7の標準的なファイルシステムは NTFSであるが、MacはこのNTFSを使用しているドライブを読むことはできるが(WindowsからMacへの移行時はこれで充分だった)、書き込むことができない。またNTFSでは case-sensitive で大文字小文字が区別される。/ ? < > \ : * | "などの文字は使用できない

これに対してMacの標準的なファイルシステムは HFS+というもので、Windows側からはこれを見ることすらできない。標準では case-insensitiveつまり大文字小文字が区別されない。ファイル名に使用できない文字はWindowsよりも少なく、:のみである。

対処法 1: exFATを使用する

すぐに思いつく方法は、外付けのハードディスクを exFAT ファイルシステムに再フォーマットして、Mac OS X、Windows の両方から読み書き可能にすることです。当初の予定ではこれだけですんなりと解決するはずでしたが(多くの人は大丈夫なはず)、、、、私の場合、外付けのハードディスクが壊れかけていたことも後で判明しましたが、ハードディスクがそもそもマウントできなかったり、コピー作業の途中で停止するなどして結局うまくいきませんでした。

対処法 2: HFSExplorer

HFSExplorerを使えば、MacのHFS+にフォーマットされているデータを読み込むことができるようになるそうです。ただし書き込みはできません。それでは不自由ということで結局採用せず。

対処法 3: NTFS-3G + OS X Fusion

WindowsのNTFSをMacで読み書き可能にする方法があるとわかりました。

有料のソフトもあるようです。

ができるだけ無料で済ませたいところ。無料の手段が二つ紹介されています。

  • OS X付属の fstab というファイルを編集する
  • FUSE for OS X と NTFS-3G を組み合わせて使う

私は後者を試してようやくのことコピー作業を成し遂げたので、その詳細を紹介しておく。大まか流れは以下の通り。

  1. OS Xをリカバリーモードで再起動し、csrutil を一時的に無効化する
  2. OS X Fusionをインストールする
  3. NTFS-3Gをインストールする
  4. csrutil を有効化する

1. csrutilの無効化

OS X Fusionや NTFS-3Gをインストールするためには、Mac OS Xのセキュリティの一部である System Integrity Protecton を一時的に無効化する必要があるそうです。

1: Macを再起動させるときに ⌘ + R の二つのキーを押し続けるとリカバリーモードで再起動する
2: ユーティリティの中から Terminal を起動する
3: コマンドラインに、以下のように入力して Enter

csrutil disable

4: Macを再起動する

2. OS X Fusionのインストール

正直 OS X Fusionが何をしているか理解していないが手順だけ紹介しておく。また類似のプロジェクトなどが複数ありどのソフトをインストールすればよいのか、道標なしではかなり迷うところである。

1: osxfuse-2.8.3-2.dmg を入手
2: FUSE For OS X をインストールする。
3: Installation Typeの選択画面では MacFUSE Compatibility Layerを含めておく。
1074948559-fuse-osx.png

3. NTFS-3Gのインストール

これがどうやらNTFSへの書き込みを可能にしてくれるものらしい。現在開発が続いているのは Texture NTFSというもののようだが、上に紹介したようにこれは有償である。無償のものは2010.10.2に公開されたものが最後のようだ。

1: ntfs-3g-2010.10.2-macosx.dmgを入手する
2: インストーラーの指示に従ってインストールする
3: (注意点)Caching modeの選択画面では、図のように No cachingを選ぶとインストールに失敗してしまった。
3532609176-ntfs-3g-install.png
再度挑戦して分かったが、Installtion Typeの画面で Customizeを選び、MacFuseの選択を外すと、問題なくインストール出来ることが分かった。
488892429-screenshot.png
4104343938-screenshot-2.png

4: 私の参考にした記事にはこうある。"Once the installation is complete, reboot your computer. You might find you get a lot of on-screen warnings when your desktop loads back onto the screen, but you can safely ignore them – they are caused by the fact the NTFS-3G software has not been updated by its developers in a long time. Finally, you need to install fuse-wait. This is the part of the process that will remove those annoying pop-up error messages."「インストールが完了したらコンピュータを再起動せよ。デスクトップが再び表示される時大量のエラーメッセージが表示されるがこれらは無視しても大丈夫。NTFS-3Gは開発者が更新を止めてからかなり時間が経っていることが原因だ。最後に fuse-waitをインストールする必要がある。これはこのエラーメッセージを防ぐためのものである。」しかし、私の場合、特にエラーメッセージは表示されなかったので、fuse-waitのインストールは不要と判断した。

4. csrutilの有効化

1: 上記と同様の手順でリカバリーモードで再起動。Macを再起動させるときに ⌘ + R の二つのキーを押し続けるとリカバリーモードで再起動する
2: ユーティリティの中から Terminal を起動する
3: コマンドラインに、以下のように入力して Enter

csrutil enable

4: Macを再起動する

不正文字の問題

新しい外付けのハードディスクを買い、上記の手順でめでたくNTFSの外付けハードディスクにMacの中のデータを書き込んで、それを今度はWindowsにコピーして全て完了、のはずだった。しかし、もう一つ問題が浮上してきた。上にも少し触れたが、不正文字の問題である。Mac OS Xに比べて Windowsではファイル名やフォルダ名に使用できない不正文字が多数ある。すなわち / ? < > \ : * | "である。

Macから外付けのハードディスクへのコピーには Terminalでの以下のコマンドを使用した。

ditto -v sourcefolder destinationfolder

この際には Windowsの不正文字は問題なくコピーされてしまった。

次に外付けのハードディスクからWindowsマシンへとコピーする際に、不正文字を使用しているファイルまたはフォルダのところでコピーが止まってしまうか、止まらなくてもそのファイルやフォルダは結局コピーされないまま放置されてしまった。

これらのファイルの多くは大して重要でもないような、過去の遺物が多く、一部はMac OS 9時代のファイルだったのだが、ここまで来た以上、すべての実験データをできうる限り一箇所に保存したいと思い、もう少し努力してみることにした。

フォルダー差分

コピーが途中で終了したりすると、何がコピーされて何がコピーされていないのかを知ることが重要になる。この時に、フォルダー差分を取ってくれるソフトが便利である。

WindowsではWinMergeという優れた無償ソフトウェアが有る。Mac では無償のものはあまり良い物がなかったが、有料のDeltaWalkerはMacでもWindowsでも使え、ファイル差分もフォルダー差分も申し分なく、おすすめできる。

WinMergeを使って、コピーに失敗したファイルやフォルダを同定するところまではできるが、結局Windows上なので、これら不正文字を使用しているファイル名やフォルダ名を変更すること自体がそもそもできない。

Windows不正文字を含むファイル名を自動変更するMATLAB関数 correctWinIncompatibleFileNames

correctWinIncompatibleFileNames() はMac上で走らせてWindows不正文字を使用しているファイル名を自動で修正するMATLAB functionである。これを使うことで不正文字を使用したファイル名の変更のほとんどを行うことができ、残る一部はMac上でフォルダ差分情報に基づいて手動で修正し、ほとんどすべてのファイルをWindows側へコピーできた(と思う)。

  • [report,failures] = correctWinIncompatibleFileNames(parentFolder)と指定すると、parentFolderの下位フォルダを再帰的に移動しながら不正文字を使用しているファイルまたはフォルダの情報を収集し、修正を試みる。reportfailurestable型で不正文字を使用しているファイル情報を含む。reportは修正が成功したものについて、failuresは失敗したものについてerror messageを含んでいる。
  • [report,failures] = correctWinIncompatibleFileNames(parentFolder,false)とすると、ファイル名の変更は行わず、情報収集だけを行う。
  • folder名の変更についてはサポートしていない。
  • Windows上では変更作業はできないが、不正文字を使用しているファイルまたはフォルダの一覧を得ることは出来る
  • 変更ルールは以下の通り。用途に応じて該当箇所のコードを書き換えれば良い。
* ===> x
: ===> -
? ===> _
" ===> ' 
< ===> [
> ===> ]
| ===> _
  • 通常、MATLABでのファイル名の変更は movefile()関数を用いるが、今回の用途では、変更対象に wildcardとして使用される*アステリスクが入っているのが問題らしく、\*のようにエスケープしても正常に動作しなかった。
  • そこで unixコマンドを介して Terminal command mv -f name1 name2 を使用している。
  • 変更対象に二重引用符が含まれているのも事態を面倒にした。
function [report,failures] = correctWinIncompatibleFileNames(parentFolder,varargin)
% [report,failures] = correctWinIncompatibleFileNames(parentFolder)
% [report,failures] = correctWinIncompatibleFileNames(parentFolder,dorename)
%
% correctWinIncompatibleFileNames runs recursively across folder hierarchy
% to correct invalid file names in Windows OS. You can use this only on Mac
% platform though.
%
% * ===> x
% : ===> -
% ? ===> _
% " ===> ' 
% < ===> [
% > ===> ]
% | ===> _
%
% INPUT ARGUMENTS
% parentFolder  A valid foder path
%
% dorename      true (default) | false
%               If you choose false, correctWinIncompatibleFileNames does
%               not run Terminal's mv command, but only returns report
%               table for you to simulate the outcome.
%
% OUTPUT ARGUMENT
% report        A table output
%               Variables
%                   Folder
%                   OriginalNames
%                   NewNames
%                   Illegals
%                   Bytes
%                   Date
%
% failures      Similar to report but for failures 
%
%
%% Filter for WinMerge
%
% ## This is a directory/file filter template for WinMerge
% name: Ignore Asterisk and Colon
% desc: Ignore files whose names include asterisk (*) and Colon (:)
%
% ## Select if filter is inclusive or exclusive
% ## Inclusive (loose) filter lets through all items not matching rules
% ## Exclusive filter lets through only items that match to rule
% ## include or exclude
% def: include
%
% ## Filters for filenames begin with f:
% ## Filters for directories begin with d:
% ## (Inline comments begin with " ##" and extend to the end of the line)
%
% f: \w+\*\w*\.{0,1}\w* ## Filter for filename
% f: \:
% f: \?
%
% ##d: \\subdir$ ## Filter for directory
%
%
%
% See also 
% unix, movefile, dir, preallocatestruct, regexptranslate, 
% correctWinIncompatibleFileNames_test,
% correctWinIncompatibleFileNames_fixture
%
% https://msdn.microsoft.com/en-us/library/aa365247


p = inputParser;
p.addRequired('parentFolder',@(x) isdir(x));
p.addOptional('dorename',true,@(x) isscalar(x) && x == 0 || x == 1);
p.parse(parentFolder,varargin{:});

dorename = p.Results.dorename;

assert(isdir(parentFolder),...
    'correctFileNamesOnWindows:parentFolder:invalid',...
    'folder %s is invalid for a folder',parentFolder);

cs = repmat({''},1e5,1); % up to 100000 illegal files at one time
cc = cell(1e5,1);
nan = NaN(1e5,1);

report = table(cs,cs,cs,cc,nan,cs);
report.Properties.VariableNames = {'Folder','OriginalNames','NewNames','Illegals','Bytes','Date'};

failures = table(cs,cs,cs,cc,nan,cs,cs);
failures.Properties.VariableNames = [report.Properties.VariableNames,{'Cmdout'}];
clear cs c

row = 0;
row2 = 0; % for failures
[report,row,failures,row2] = doOneFolder(parentFolder,report,row,dorename,failures,row2);
report(row+1:end,:) = [];
failures(row2+1:end,:) = [];

if ~dorename
    warning('correctWinIncompatibleFileNames:dorename:false',...
        'No file name has been changed by correctWinIncompatibleFileNames.')
end


end

%--------------------------------------------------------------------------

function [report,row,failures,row2] = doOneFolder(thisFolder,report,row,dorename,failures,row2)
% report is a table


list = dir(thisFolder);
names = {list(:).name}';

% if there is a folder

n = length(names);


for i = 1:n
    if strcmp(list(i).name,'.')
        continue
    elseif strcmp(list(i).name,'..')
        continue
    end

    if list(i).isdir
        [report,row,failures,row2] = doOneFolder(fullfile(thisFolder,list(i).name),...
            report,row,dorename,failures,row2);
    else

        origName = list(i).name;
        NewNames = origName;

        illegal_count = 0;
        illegals = preallocatestruct({'char','newchar','startIndex'},[64,1]);

        % correct illegal file names
        % https://msdn.microsoft.com/en-us/library/aa365247

        [illegals,illegal_count,NewNames] = findillegals('*','x',...
            NewNames,illegals,illegal_count);

        [illegals,illegal_count,NewNames] = findillegals(':','-',...
            NewNames,illegals,illegal_count);

        [illegals,illegal_count,NewNames] = findillegals('?','_',...
            NewNames,illegals,illegal_count);

        [illegals,illegal_count,NewNames] = findillegals('"','\''',...
            NewNames,illegals,illegal_count);

        [illegals,illegal_count,NewNames] = findillegals('<','[',...
            NewNames,illegals,illegal_count);

        [illegals,illegal_count,NewNames] = findillegals('>',']',...
            NewNames,illegals,illegal_count);

        [illegals,illegal_count,NewNames] = findillegals('|','_',...
            NewNames,illegals,illegal_count);

        %% KEEP THESE EXAMPLES
        %
        % movefile('abc"de.rtf','abc''de.rtf'); % works on Mac
        %
        % unix('mv abc\"de\.rtf abcde.rtf'); % works on Mac
        %
        % unix('mv abc\"de.rtf abcde.rtf'); % works on Mac
        %
        % unix('mv abc"de.rtf abcde.rtf'); % does not work on Mac
        % % /bin/bash: -c: line 0: unexpected EOF while looking for matching `"'
        % % /bin/bash: -c: line 1: syntax error: unexpected end of file
        %
        % unix('mv "abc\"de.rtf" abcde.rtf'); % works on Mac
        %
        %
        %
        % unix('mv abc\"de.rtf "abc''de.rtf"'); % works on Mac
        %
        % unix('mv abc\"de.rtf abc\''de.rtf'); % works on Mac
        %
        %
        %
        % movefile('abc\*de.rtf','abcxde.rtf'); % does not work on Mac
        %
        % unix('mv abc\*de\.rtf abcxde\.rtf'); % works on Mac
        %




        illegals(illegal_count+1:end) = []; % up to 64 illegals

        if illegal_count > 0            
            % sort illegals
            startInd = [illegals(:).startIndex];

            [~,ind] = sort(startInd);

            illegals = illegals(ind);

            if ismac 
                if dorename               
%                     escape2 = @(x) regexprep(x,'"','\\"'); % need to escape "
%                     escape = @(x) regexprep(regexptranslate('escape',fullfile(thisFolder,x)),'\s','\\ ');
%                     %NOTE need to escape a space with \ even after regexptranslate
%                     
%                     src = escape2(escape(origName));
%                     dst = escape2(escape(NewNames));

                    src = fullfile(thisFolder,origName);
                    dst = fullfile(thisFolder,NewNames);

                    src = findQinpath(src);
                    dst = findQinpath(dst);

                    cmd = sprintf('mv -f %s %s',src,dst);
                    [status,cmdout] = unix(cmd,'-echo');

                    %NOTE movefile won't work properly with \*
                    % movefile(src,dst,'f'); % rename the file
                    % movefile(fullfile(thisFolder,origName),fullfile(thisFolder,NewNames),'f')

                    if status == 0 % success
                        row = row + 1;
                        %                 fprintf('Renamed %s to %s \t\tin %s\n',origName,NewNames,thisFolder);
                    else
                        row2 = row2 + 1;

                        disp(cmdout)
                        warning('Failed to rename %s to %s in %s\n',origName,NewNames,thisFolder);

                        thisfailure = table({thisFolder},{origName},{NewNames},{illegals},...
                            list(i).bytes,{datestr(list(i).datenum)},{cmdout});

                        thisfailure.Properties.VariableNames = failures.Properties.VariableNames;

                        failures(row2,:) = thisfailure;
                        continue

                    end
                else
                    row = row + 1;
                end
            else
                if dorename
                    error('correctFileNamesOnWindows:OS:notmac',...
                        'This function is for Mac OS X only if dorename option is true');
                else
                   row = row + 1;
                end
            end


            thisrep = table({thisFolder},{origName},{NewNames},{illegals},...
                list(i).bytes,{datestr(list(i).datenum)});

            thisrep.Properties.VariableNames = report.Properties.VariableNames;

            report(row,:) = thisrep;
        end

    end


end

end

%--------------------------------------------------------------------------

function [illegals,illegal_count,NewNames] = findillegals(target,replacement,...
    NewNames,illegals,illegal_count)

targetreg = regexptranslate('escape',target);

if isregexpmatched(NewNames,targetreg)
    startIndex = regexp(NewNames,targetreg);
    NewNames = regexprep(NewNames,targetreg,replacement);
    for k = 1:length(startIndex)
        illegals(illegal_count + k).char = target;
        illegals(illegal_count + k).newchar = replacement;
        illegals(illegal_count + k).startIndex = startIndex(k); 
    end
    illegal_count = illegal_count + length(startIndex);
end
end

%--------------------------------------------------------------------------

function pathout = findQinpath(path)
%
% pathout = findQinpath(path)
%
% findQinpath searches for ' or "
%
% If only 's or "s are found, then encompass path with the other quotation
% mark.
%
% If both ' and " are found, make sure they are escaped by \?
%
%
%NOTE
% unix() takes bash command. By default, you don't need to use
% quatations for file names, but that makes you escape all the
% space and other special characters in file path. And the code
% gets difficult to read.
%
% You can use double " or single ' quotation marks for string. So
%
% unixt('mv "filepath1" "filepath2")
%
% may be generally a good choice, because you don't need to escpae special
% characters within quatation marks and " does not interfere with MATLAB's
% use of '.
%
% When a quotation mark is used in a file path, then you need to be
% careful. If only single quotation mark (') is used, then you can use
% doble quotation marks for the entire file path string, and vice versa.
%
% In case the file path uses both single and double quotation
% marks, then you can only escape with \.

singlefound = isregexpmatched(path,'''');
doublefound = isregexpmatched(path,'"');

if singlefound && ~doublefound
    pathout = ['"',path,'"'];
elseif ~singlefound && doublefound
    pathout = ['''',path,''''];
elseif ~singlefound && ~doublefound
    pathout = ['"',path,'"'];    
elseif singlefound && doublefound
   path1 = regexptranslate('escape',path);
   path2 = regexprep(path1,'\s','\\ ');
   path3 = regexprep(path2,'"','\\"'); % need to escape "
   pathout = regexprep(path3,'''','\\'''); % need to escape '   
end

end