相変わらず、騷しいう~さん
シクラメン

「旦那、また何かやったそうですね?」
『そやねん、気になっていたことをチョコットな』

「長いパス」対応の組み込みコマンド (nanoShell Tools)

1. 指定されたフォルダまたはファイルを複写する
 int copy( <folder[/wildcard]>, <folder> [," [sync] [progress]"])
 int copy( <file>, <file | folder> )
・名前 / サイズ / 日時 が異なるファイルのみ差分コピーする
・"sync" オプションを指定すると更に出力側にのみ存在するファイルを削除する(同期コピー)
   このメッセージは標準エラーに出力する (本体のメッセージは標準出力)

・ワイルドカード判定は、*、?、| を含む場合、
   または最後が ’.’ (ファイル名規約違反のため識別子として利用) の場合にワイルドカードと判断する
・ワイルドカードは、*→.*、?→.、.→\. に変換し、正規表現マッチする (大小文字を区別しない)
   こんな指定も可能です "[baz]*.txt|*.log"

・出力が (新規)フォルダの場合は、"foo/" と指定する

2. 指定されたフォルダまたはファイルを移動する
 int move( <folder[/wildcard]>, <folder>) [," [progress]"])
 int move( <file>, <file | folder> )
・"move"は入力側空フォルダ(Time0以外)を削除します

3. 指定されたフォルダまたはファイルを削除する
 int remove( <folder [/wildcard]> [," [root] [progress]"])
 int remove( <file>)
・ルートフォルダを指定する場合は "root" オプションが必要
・"remove"は空フォルダ(Time0以外)を削除します

4. 指定されたフォルダをコンパクトなツリー表示する
 int tree( <folder[/wildcard]> [," [file]"])
・"file" を指定するとファイル名を表示する

5. ファイルを一覧表示する (listSegment)
 int ls( <folder[/wildcard]> [," [path] | [0 | , | k] [260 | no260]"])
・ファイル長、0: 編集なし、, : カンマ編集、K: 単位変換 (属性を表示するためにはいずれかを指定する)
・path: ファイル長の代わりに パス長を sort キーとし使用するための編集("%, 7d")をして表示する
・260: 長いパスを表示、no260: 短いパス、省略: 全て

・ls はシェル連携をを考慮してタブ区切りで出力しています (日時、長さ、パス)

6. フォルダをクリーンアップする
 int clean( [," [clean] [noTime]"])
・"clean" オプションは空のフォルダを削除する
・"noTime"、標準ではフォルダの更新日時を子要素の日時に設定する(空フォルダは Time0(日時 0))

7. シェル変数を設定する
 int set( [," [var=val] …"])
・MAX_PATH=260 パスの最大長を指定する (tree、ls: 260、no260 オプションで使用)
・DISPLAY_WIDTH=40 パスを画面に表示する幅
・オプションを省略すると一覧表示します

8. その他、共通オプションなど
 プログレス: progress
・"copy", "move", "remove" で使用する
 ・リダイレクトと共存可能

 リダイレクト: [12]>{1,2} path
・リダイレクトは ①後続オプションが存在する場合は、パスをシングルクオートで囲む仕様です >'path'
   これは後続の一般オプションを食ってしまうためで、②オプションの最後に指定または、
   ③リダイレクト達のみの単独指定でも回避できます
   すなわち "/*オプション*/ 1>foo 2>bar /*ここにオプションは書けない*/"

・後続オプション問題を回避する 3+1つの方法
   ①パスをシングルクオートで囲む (エスケープは、<\ '>、<' '>)
   ②(空白を含む)パスと(空白を含まない)拡張子を指定する
   ③(空白を含まない)拡張子無しパスを指定する
   ④リダイレクト達のみの ”単独オプション” を指定する
   ①で失敗( ' 抜け 2>'foo)すると→②→③とアルゴリズムを切り替えます

・パスは、DISPLAY_WIDTH 桁に切り詰めますが、リダイレクトの場合はそのまま出力する

 リカーシブ: noRecursive
・オプションは大小文字を区別しないで前方一致で比較する (コマンド内でユニークなら良い)
   "foo", "bar" では煩雑になるため "foo bar" 形式を推奨する
   否定オプション noRecursive はマイナスオプション -r[ecursive] に短縮可能です

 ネットワークパス: UNCパス【Universal Naming Convention】
・パスの形式: //コンピュータ名/共有名/フォルダ・ファイル名
   共有の設定: ドライブまたはフォルダで右ボタン→プロパティ→共有→詳細な共有→アクセス許可
   共有名の最後の文字を”$”にすると、エクスプローラーの共有一覧に名前が表示されなくなる(隠し共有)
・ベンチマークは、"move" 1:20秒と遅い("copy"より速い)

 0: 正常、1: インフォーメーション、2: ワーニング(マゼンタ)、3: エラー(赤)
・重大エラーは、throw するためあまり意味を持た持たないため、警告に使用しています

10. 外部コマンド呼び出し
 int system("<command> [<args...>] [>redirect]")

11. PowerShell 呼び出し
 int system("PowerShell <command> [<args...>] [>redirect]")

12. コマンドプロンプト呼び出し
 int system("CMD /C <command> [<args...>] [>redirect]")

※ ソースコード: plus/io/NanoTools.java

CHANGELOG

  • 2022-12-28 sort コマンドを Android nanoShell から持って来た
  • 2022-12-28 CSV ツールにカラム削除(del)機能を追加した
  • 2023-01-01 ls コマンドに path オプションを追加した
  • 2023-01-05 set (シェル変数の設定)コマンドを追加した
  • 2023-01-09 コマンドラインのパスを切り詰めて表示するようにした
  • 2023-01-12 コマンドの実行時間を表示するようにした
  • 2023-01-19 clean コマンドを追加した
  • 2023-01-24 progress 機能を実装した
  • 2023-01-26 リファクタリング開始
  • 2023-01-28 async I/O を実装した
  • 2023-02-04 clean コマンドの誤植を修正
  • 2023-02-05 move コマンドに ATOMIC_MOVE 機能を実装
  • 2023-02-10 clean コマンドの不具合を修正
  • 2023-02-18 リダイレクトの仕様を見直した
            (正規表現が妙な動作をするため諦めてましたが前方参照で解決しました)
  • 2023-02-20 ネットワークパスを実装 (UNC: Universal Naming Convention)

ベンチマーク (バックアップ)

ベンチマーク環境

  • ソース: ミュージックデータ、1,773楽曲、180フォルダ、65.5GB
    楽曲".flac"(可逆圧縮)はサイズが大きいため I/O効率が良い
  • 入力: 内蔵 SSD
  • 出力: 外付け(USB3.0) SSD
※ スクリプト
## nanoAsyncC.awk -v DISPLAY_WIDTH=n
#
var DISPLAY_WIDTH = 35
BEGIN {
   DD = "D:/"
   MM = "M:/"
   INPUT = DD "ミュージック/"
   OUTPUT = MM "ミュージック/"
   print "\e[92m# copy コマンドでバックアップする\e[m"
   set("DISPLAY_WIDTH="DISPLAY_WIDTH)
   print "\e[92m# 前回のバックアップを削除\e[m"
   remove(OUTPUT, "pro")
   print "\e[92m# バックアップ\e[m"
   copy(INPUT, OUTPUT, "pro")
}

※ make ファイル
nanoAC :
 $(PLUS) sample/nanoAsyncC.awk -v DISPLAY_WIDTH=35

スクリーンショット

path3

※ 結果は 4:09秒です
ファイルI/O は nio.Files#copy/move で実装しておりコードは 1step です
"Files" のバッファサイズが気になったためファイルを一気にI/Oしてみました

※ 結果は 8:19秒です
丁度、倍遅いです。ということは非同期 I/O(スレッド)を使っているということです! ^^);

Robocopy (ファイルサーバー用大量・高速コピーツールで、Windowsにしては良くできています)

※ バッチファイル
 robocopy.exe D:\ミュージック M:\ミュージック /E /MIR /DCOPY:DAT /R:1 /NP /NFL /NDL /LOG:.\robocopy.log

※ 結果は 3:53秒で、16秒差、射程距離内ですかね?

FastCopy (フリーソフト)

path4

※ 結果は 3:14秒(最速)です
設定画面を開くと小さなファイルはキャッシュする、非同期I/O=8 といった項目が並んでいます
Robocopy と比べて欠点はフォルダ・ファイルへの属性設定が限られていることです

中締め

『締めはこんなもんやろ?』
『リファクタリング前のベンチマークは鉄則やからな!何事もやらな判らんで』
「今後の展開が楽しみ~」

「敵を知り、己を知れば、百戦して殆(あや)うからず」(孫子)

世界の頂点へ - To the top of the world

copy
  • スレドプールを実装した
  • 結果は copy(3:00秒→ 2:54秒) move(3:01秒)です (robocopy 3:53秒、FastCopy 3:14秒)
  • 出力側 Disk能力を100% 使い切っており、これ以上の性能向上は望めません
  • 舌の根も乾かぬうちに 3分を切りましたが、
    これはズルしておりSSD初期化後は数秒速い、おそらくSSDのTrimのせい? ^^);
  • スレドプールAPIは、Executors.newWorkStealingPool を使用しており、
    「すべての使用可能なプロセッサをターゲット並列性レベルとして使用」します
    (DiskとCPUを限界まで使い切るAPIでう~さんの大好物です)
  • copy/move I/Oエラー発生時は 7回(wait 1sec.)リトライします
task

世界の果てへ - To the ends of the world

moveX
  • 結果は copy(3:00秒→ 2:54秒) move(3:01秒 → 1秒)です (robocopy 3:53秒、FastCopy 3:14秒)
  • 気になっていた move をアルゴリズムチューニングし ATOMIC_MOVEオプションに対応した
  • 一言でいうと同一ドライブ内の移動をフォルダの付け替えで処理し高速化を図ったと言う事です
  • しかし、これには落とし穴があり下記のドキュメントが読み解けないと実装できません

Java Doc (抜粋)

 nio.file.Files#move: ファイルをターゲット・ファイルに移動するか、そのファイル名を変更します。
デフォルトでは、このメソッドはターゲット・ファイルへのファイルの移動を試み、 ソースとターゲットが同じファイルである場合を除き(その場合、このメソッドは何の効果もない)、 ターゲット・ファイルが存在する場合は失敗します。

 ATOMIC_MOVE オプション:
この移動は原子的なファイル・システム操作として実行され、他のオプションはすべて無視されます。 ターゲット・ファイルが存在する場合、その既存ファイルが置換されるか、 またはこのメソッドがIOExceptionをスローして失敗するかは実装に固有です。 移動を原子的なファイル・システム操作として実行できない場合は、 AtomicMoveNotSupportedExceptionがスローされます。 これは、ターゲットの場所が異なるFileStore上にあるときにファイルをコピーする必要があったり、 ターゲットの場所がこのオブジェクトとは異なるプロバイダに関連付けられていたりする場合などに発生することがあります。

 難しい言葉が並んでいますが、まずは同一ドライブなら OK らしいと言う事でコードを書きました
Windows ならドライブの一致、Linuxなら同一ユーザ空間内で ATOMIC_MOVEモードになります
これが、3分 → 1秒を叩き出したコードの判定ルーチンです

// Files#move の ATOMIC_MOVE オプションの利用可否を返す
private boolean isAtomic(Path input, Path output) {
   String d1 = getDriveInfo(input);
   String d2 = getDriveInfo(output);
   return d1.equals(d2);
}

private static final Pattern EXTRACT_DRIVE_LETTER = Pattern
           .compile("^(.:)"); // ドライブレターを抽出 (Windows)

// @return ドライブレター: NAME(e.g.ボリュームラベル): TYPE(e.g. NTFS)
private String getDriveInfo(Path path) {
   try {
     Path abs = path.toAbsolutePath();
     Matcher m = EXTRACT_DRIVE_LETTER.matcher(abs.toString());
     String name = m.find() ? // ドライブレター (Linux は空)
           getValue(m.group(1)).toUpperCase() : "";
     FileStore fs = Files.getFileStore(path);
     name += fs.name() + ':' + fs.type(); // 実装依存 (Linux)
     return name;
   } catch (IOException e) {
     throw new RuntimeException(exception("getDriveInfo", e));
   }
}

まとめ

「ところで旦那、今回の勝因は何ですかね?」
『そやな、諦めの悪さと閃きの良さかな』「あはは、普段のまんまということですか?」
『それや、平常心ということやな』

「世界一になる理由は何があるんでしょうか?2位じゃダメなんでしょうか?」

『そや、星の王子様に登場してもらお』
「完璧とは、付け加えるべきものがないということではなく、取り去るべきものがないということのようだ」 (アントワーヌ・ド・サン=テグジュペリ、飛行機設計技術者)

オープンソース

検証環境

  • Windows 10
  • Windows Terminal (オープンソース)、Windows PowerShell (オープンソース)
  • GNU Make (オープンソース)
  • Java 19 (オープンソース)
  • AWK˜plus for Java (オープンソース)

インストール

  1. Java をダウンロード(環境を汚さない .zip 版を推奨、複数の Javaもインストールできます)「Java Downloads」
  2. AWK~plus をダウンロード「AWK~plus for Java」 (コマンドを添付しています)
  3. AWK~plus フォルダ中の makefile の JAVAHOME 変数に Javaホームパスを設定する。

実行

ターミナルを開き、AWK~plus フォルダをカレントディレクトリにして、と入力する。

「Table of contents」 2023.02.20