kiyo_hikoのブログ

メモ+日記?

複雑なフォルダー階層から異なるファイルだけを見つける

WinMergeとかでやるのもしんどくなるぐらい大量のフォルダー階層AとBを比較したい。
かつ、AとB全体で異なるファイルが数個とか数十個とか、大した数無い。
…といった場合に比較が楽になりそうな方法を考えた。

次の方針でAとBからテキストを吐き出して、テキスト比較すればできるはず。

  • ファイルの一覧をすべて引いてくる(これはgci Recurseでよさげ)
  • 取得したファイル名の横にハッシュ値を表示する(Get-FileHashくっつける)
$result = "C:/AforDiff.txt"; # これは例。BならBforDiff.txtなどやっぱりそれっぽい名前に
NullFile $result; # 前のブログエントリー参照

cd A # B分吐くならcd B
foreach ($f in gci . -File -Recurse) {
  $relPath = rvpa -Relative $f.FullName;
  $relPath + "`t" + (Get-FileHash $f.FullName).Hash >> $result;
}

これをAとBでやり、吐かれたテキストを比較します。
変更や追加/削除があればハッシュ部が違ったり、行の増分/減分が発生し、わかる。

PowerShellで空のファイルを作る(bashの":>filename"相当)

作業中のフォルダーに、指定したファイル名でとにかく空のファイルがほしい場合。

  • ファイルが無いときはNew-Item (エイリアス:ni)で空のファイル作ればOKなはず。
  • ファイルが有るときはClear-Contents(エイリアス:clc)でファイルの中身を空にすればOKなはず。

くっつけて関数にしておく。

function NullFile ($f) {
  if (Test-Path $f) {
    clc $f;
  } else {
    ni -ItemType file $f;
}

こんな感じでいけます。

> NullFile C:/test.txt

Perl:配列を奇数番目要素と偶数番目要素に分ける

あと一歩シンプルな書き方が思いつかなくて、とりあえずメモっときます。
割とどうでもいい話です。

やりたいこと

@data = qw/able baker charlie dog easy fox/;
# これを次の形にする
@even_index_data = qw/able charlie easy/;
@odd__index_data = qw/baker dog fox/;

逆zip的な事がやりたくて。

答え

@even_index_data = @data[grep { $_ % 2 == 0 } 0..$#data];
@odd__index_data = @data[grep { $_ % 2 == 1 } 0..$#data];

Perlではこれが一番シンプルかな。パフォーマンスは考えてません。

世間では

stackoverflow.com - 本質的には多分上と同じ。

www.perlmonks.org - 配列にpushする人とか。

タッチパネルWindows PCで誤ってファイルコピーしたときに取り消す方法

なにを言ってるのかイミフなタイトルなので補足

状況が伝わりにくいかもしれません。こういうことがありました。
そのとき対処した内容のメモです。

  1. タッチパネルPCで外付けドライブに一部のファイルを退避しようと思い立った
  2. ファイルが飛び飛びなのでCtrlを押しながらタップして選択まくった
  3. 100個ぐらい選んだところで手が滑ってスワイプ(ドラッグ)した

→ Ctrlマウスドラッグでファイルコピーしたときと同じ操作になったぞい。

  • 選択の労力はパーになるわ、「なになに - コピー.*」なファイルが大量に作られるわで怒髪天

コピーを無理くり取り消す

  • なんか右クリックでundoできませんでした。何故。

仕方ないので「コピー」の名を持ってるファイルを検索してあぶり出し、全部けします。
しかしWindowsの検索窓は気まぐれなので、よくわからん検索結果を提示してきてあてになりません。

  • Windows2000ぐらいまでの厳密文字列の比較とか正直選ばせてほしい

そこでPowerShellです。「コピー」の名を持つファイルをリストアップして消します。

  • どうせPowerShell使うならと、コピー元のハッシュと照合して安全に消します。

組んだPowerShellスクリプトは下段にあります。

一応注意喚起(免責)

ファイルコピーの取り消し(=ファイル削除)を伴うPC操作の話なので取り扱いは事前テストしたうえで慎重に。
不注意で事故っても私は何の責任も持ちません。

作ったもの(説明はあと)

これを走らせます。
自分はPowerShell ISEで即席で書いたのでそのままISEで走らせてps1に保存してました。

# 対象フォルダーは逐次指定するなりカレントにするなりお好きに
cd C:\Users\kyhk\Downloads

# 重複したやつをリストアップ
$dups = gci -Filter "* - コピー*"

foreach ($dup in $dups) {
    # 元ファイル名はたぶんこんなだろう
    $orig = $dup -replace " - コピー", ""

    # 元ファイル名でファイル見つけた
    if (Test-Path $orig) {
        $hash_orig = Get-FileHash $orig
        $hash_dup = Get-FileHash $dup

        # ハッシュも一致した
        if ($hash_orig.Hash -eq $hash_dup.Hash) {
            $dup
            Write-Host "ハッシュ値が同じ元ファイルを検出しました。コピー側を自動で削除します。"
            del $dup # 消す
        }
    }
}

説明

極めて大雑把に

  • gci:dirコマンドみたいなものです。Filterでワイルドカード付けられます
  • Test-Path:ファイルの存在確認できます
  • Get-FileHash:ファイルからハッシュオブジェクトを作ります。別のハッシュ値と比べるときはHashプロパティ経由で値を取りませう

これで解決しました

超めんどくさかった

プロジェクトのステップ数をカウントする(Windows)

もらったプロジェクトの大きさを大雑把に把握したいときとかに即席で。
コードに出てくるLengthを知ってれば何のことはないですが、備忘録的にメモ。
PowerShellでファイル名と個別のステップ数を表示しつつ全ステップ数出します。

2手順かかります

  1. プロジェクトフォルダーに移動してアドレスバーで「powershell」と打ってEnter

  2. 起動したPowerShellで以下のような感じでgciからgc lengthをループするコマンドを打つ。

PS> $acc = 0; foreach ($f in gci . -Filter "ワイルドカードでファイル名") { Write-Host $f.FullName; $len = (gc $f.FullName).Length; $acc = $acc + $len; $len }; $acc

出力は大体こんな感じになります。

C:/foo/bar/baz/Able.cpp
111
C:/foo/bar/baz/Baker.cpp
222
C:/foo/bar/baz/Charlie.cpp
333
666

zip由来のエラー「マルチボリューム セットの最後のディスクを挿入してください」対策(おまけでzip数が多いときの原因特定方法)

Windows10使っていて、タイトルにあるエラーが頻発してとても鬱陶しかったので対策しました。

どんなエラー?

Windows起動中に画像のようなダイアログが頻繁に出現するようになります。
特に何も操作してない時でもバンバン出ます。
邪魔すぎます。

f:id:kiyo_hiko:20190930000739p:plain
マルチボリューム セットの最後のディスクを挿入してください

原因とか症状は?

ぶっ壊れたzipファイルが「どこか」に居るとこの症状が出るっぽいです。
例えば「ダウンロード」フォルダーに壊れたzipが居ると、別に「ダウンロード」開いてないのにさっきのダイアログがバンバンダイアログが出ます。
Windowsのシステムサービスが裏で検索インデックスらへんを作ってるせいかな?

修復方法

壊れたzipがどこかにあるはずです。それを特定します。
見つけたらzipを消すか(要らなければ)、WinRARとかで修復を試みます(大事なファイルなら。修復できなければ諦めて消す)

以上。

おまけ:zipがたくさんあって、特定が大変な場合でも壊れたzipを検出する

拙者のケースがこれなのですが、掲示板のログとか収集してて数千個とかzipがあります。
壊れたzipとか手動で探せる気がしません。

なので、PowerShellで壊れたzipを探しました。
PowerShellの起動/操作手順とコードを掲示します。ある程度知っている人を想定して、基本的な操作は省きます。

手順1. PowerShellを起動する

最近のWindows10では、「Winキー+x」→「i」でPowerShellが起動します。または他の方法で起動します。

手順2. コードを編集する

次のようなコードをエディターで書きます。

Add-Type -AssemblyName system.io.compression.filesystem

$workdir = "C:\Users\" # ここを探したいフォルダーのパスに変えましょう
$zips = gci $workdir -Recurse -Filter "*.zip"

foreach ($zip in $zips) {
    Write-Host $zip.FullName
    $files = [io.compression.zipfile]::OpenRead($zip.FullName).Entries
}

意味は以下:

  • Add-Type:zip操作用ライブラリを読み込む(多分)
  • $workdir:探したいパスを変数に代入
  • $zips = 以下:gciコマンドでファイルをリスト化、その時に子フォルダーも検索して「*.zip」でフィルターする
  • foreach:$zipsにフィルターしたファイル一覧が入ってるのでループするよと言いたい
  • Write-Host:画面にファイル名を書き出す
  • $files:zip内のファイルを一覧で取得する

$filesに一覧を拾っておこうとすると壊れたzipから検出できない→ここでエラーが出ます。

手順3. コードをPowerShellに貼り付けて実行

コード全部コピーします。 その後、PowerShellのウィンドウをクリック→「Alt+Space」メニューから→「編集」→「貼り付け」するか、右クリックで貼り付けます。 - すぐ実行されない場合、PowerShellウィンドウにした状態でEnterキーを2回ぐらい叩きましょう。

壊れたzipを無事見つけると、以下のような表示になります。

C:\Users\kiyohiiko\Downloads\BoardsLog20190925.zip
"1" 個の引数を指定して "OpenRead" を呼び出し中に例外が発生しました: "中央ディレクトリの終わりレコードが見つかりません。"
+     $files = [io.compression.zipfile]::OpenRead($zip.FullName).Entrie ...
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : InvalidDataException

Write-Hostで画面にファイル名を書き出しているので、エラーの直前の行に書かれているzip名(上の例ではBoardsLog20190925.zip)が壊れたzipだとわかります。 このノリで1件または複数件のzipが炙り出されるので、それぞれに消すなり修復して対策すればOKです。

もっと高速かつ楽な方法があれば少し知りたい

以上です。

Markdownに目次を作る(Perlで)

最近は高級なエディターソフトが使えない環境でMarkdownを書いていて、Markdowの目次をツールで生成することができない。

Perlなら使えるのでMarkdown本文を渡して実行すると目次を作成するPerlを5分以内で書いた。
(近年稀に見る雑さ)

# 使い方:__DATA__以降にMarkdown本文貼り付けてperlスクリプト実行→ToCをターミナルに吐く
use strict;
use warnings;

sub md2toc {
  my @cont = <::DATA>;

  my $inQuote = 0; # 引用文取り除くために今引用内の行なのか判別する

  for my $line (@cont) {
    # 引用検出
    if ($line =~ m/^[`]/) {
      $inQuote = ! $inQuote;
      next;
    }
    $inQuote and next; # 引用文に興味ない
    ($line =~ m/^[#]/) or next; # アンカー以外興味ない
    $line =~ s/#\s/- /;
    $line =~ s/#/  /g;
    $line =~ s/\s{2}-\s/- [/;
    chomp $line;
    $line .= "]\n";
    print $line;
  }
}

::md2toc;

__DATA__
ここにMarkdownはる