File checker by ai

2025-09-10 ruby /posts/2025/2025-09-10-an-unique-keyboard.jpg

昔はこういうのは、テキストエディタを立ち上げて、自分たちで書いていたんだよ!という年寄りの自慢話を若い人にする日は近い(というか今度来るインターンに自慢しよう)。

一覧表(テキストファイル)と実際のフォルダのファイルの内容を突合する仕組みを作って、とお願いして出てきたのが以下のスクリプト。内省(自己改善)という技を身につけたのが、大きいようで、最初はいきなりファイルを削除しようとしていたのが、表示だけのモードを追加したり、実際に削除する前に確認ができるようにしたりと、より安全に使えるような工夫を勝手に入れてくれます。

いやー便利。

写真は、あまり関係なくて、初めて生で見る分割キーボード。かっこいい…!音声入力も進化してるし、人間がタイプする量ってこれから減る方向にしか進まないかもしれないですが、そういうのと関係なくこういうのは拘りたいですよね(笑)と言いつつ、HHKB Liteで満足しちゃってます。体がキー配置を覚えてしまってるので、拘りというより固着と言うべきか(^^

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

require 'pathname'
require 'optparse'

class FileChecker
  def initialize(files_list_path = 'files.txt', target_dir = '.', options = {})
    @files_list_path = files_list_path
    @target_dir = target_dir
    @options = options
  end

  def run
    puts "=" * 60
    puts "ファイル突合チェック"
    puts "=" * 60
    puts "対象ディレクトリ: #{File.expand_path(@target_dir)}"
    puts "ファイル一覧: #{@files_list_path}"
    puts "モード: #{@options[:dry_run] ? 'ドライラン(削除は実行されません)' : '通常'}"
    puts

    # files.txtから期待されるファイル一覧を読み込み
    expected_files = read_expected_files

    # 実際に存在するファイル一覧を取得
    actual_files = get_actual_files

    # 突合結果を出力
    check_missing_files(expected_files, actual_files)
    check_extra_files(expected_files, actual_files)

    # 自動削除モードの場合
    if @options[:auto_delete] && !@options[:dry_run]
      auto_delete_extra_files(expected_files, actual_files)
    end
  end

  private

  def read_expected_files
    unless File.exist?(@files_list_path)
      puts "エラー: #{@files_list_path} が見つかりません"
      exit 1
    end

    files = []
    File.readlines(@files_list_path, chomp: true).each do |line|
      line = line.strip
      next if line.empty? || line.start_with?('#')
      files << line
    end

    puts "期待されるファイル数: #{files.size}"
    files
  end

  def get_actual_files
    files = []

    # 現在のディレクトリ内のファイルを取得(ディレクトリは除外)
    Dir.glob(File.join(@target_dir, '*')).each do |path|
      if File.file?(path)
        files << File.basename(path)
      end
    end

    # サブディレクトリ内のファイルも取得
    Dir.glob(File.join(@target_dir, '**/*')).each do |path|
      if File.file?(path)
        relative_path = Pathname.new(path).relative_path_from(Pathname.new(@target_dir)).to_s
        files << relative_path unless files.include?(relative_path)
      end
    end

    puts "実際に存在するファイル数: #{files.size}"
    puts
    files
  end

  def check_missing_files(expected_files, actual_files)
    missing_files = expected_files - actual_files

    puts "【1. files.txtにあるが実際には存在しないファイル】"
    puts "-" * 50

    if missing_files.empty?
      puts "✅ なし(すべてのファイルが存在します)"
    else
      puts "⚠️  見つからないファイル数: #{missing_files.size}"
      missing_files.sort.each do |file|
        puts "  ❌ #{file}"
      end
    end
    puts
  end

  def check_extra_files(expected_files, actual_files)
    extra_files = actual_files - expected_files

    # 自分自身とfiles.txtは除外
    extra_files = extra_files.reject { |f| f == File.basename($0) || f == @files_list_path }

    puts "【2. 実際に存在するがfiles.txtにないファイル】"
    puts "-" * 50

    if extra_files.empty?
      puts "✅ なし(余分なファイルはありません)"
    else
      puts "⚠️  余分なファイル数: #{extra_files.size}"
      puts

      if @options[:show_commands]
        puts "📝 削除コマンド案:"
        extra_files.sort.each do |file|
          puts "  rm '#{file}'"
        end
        puts

        puts "📦 一括削除スクリプト:"
        puts "cat << 'EOF' > delete_extra_files.sh"
        puts "#!/bin/bash"
        extra_files.sort.each do |file|
          puts "rm '#{file}'"
        end
        puts "echo '削除完了'"
        puts "EOF"
        puts "chmod +x delete_extra_files.sh"
        puts "# 実行前に内容を確認: cat delete_extra_files.sh"
        puts "# 実行: ./delete_extra_files.sh"
      else
        puts "📝 余分なファイル一覧:"
        extra_files.sort.each do |file|
          puts "  🗂️  #{file}"
        end
        puts
        puts "💡 削除コマンドを表示するには --show-commands オプションを使用してください"
      end
    end
    puts
  end

  def auto_delete_extra_files(expected_files, actual_files)
    extra_files = actual_files - expected_files
    extra_files = extra_files.reject { |f| f == File.basename($0) || f == @files_list_path }

    return if extra_files.empty?

    puts "🗑️  自動削除モードを実行中..."

    deleted_count = 0
    error_count = 0

    extra_files.sort.each do |file|
      file_path = File.join(@target_dir, file)
      begin
        File.delete(file_path)
        puts "  ✅ 削除: #{file}"
        deleted_count += 1
      rescue => e
        puts "  ❌ 削除失敗: #{file} (#{e.message})"
        error_count += 1
      end
    end

    puts
    puts "📊 削除結果:"
    puts "  ✅ 削除成功: #{deleted_count}ファイル"
    puts "  ❌ 削除失敗: #{error_count}ファイル" if error_count > 0
  end
end

# オプション解析
options = {}
OptionParser.new do |opts|
  opts.banner = "使用法: #{$0} [オプション] [files.txt] [対象ディレクトリ]"

  opts.on("-c", "--show-commands", "削除コマンドを表示") do
    options[:show_commands] = true
  end

  opts.on("-d", "--dry-run", "ドライラン(実際の削除は行わない)") do
    options[:dry_run] = true
  end

  opts.on("-a", "--auto-delete", "余分なファイルを自動削除(危険)") do
    options[:auto_delete] = true
  end

  opts.on("-h", "--help", "このヘルプを表示") do
    puts opts
    exit
  end
end.parse!

# メイン処理
if __FILE__ == $0
  files_list = ARGV[0] || 'files.txt'
  target_dir = ARGV[1] || '.'

  if options[:auto_delete] && !options[:dry_run]
    puts "⚠️  警告: 自動削除モードが有効です。"
    print "本当に余分なファイルを削除しますか? (yes/no): "
    response = gets.chomp.downcase
    unless response == 'yes'
      puts "キャンセルしました。"
      exit 0
    end
  end

  checker = FileChecker.new(files_list, target_dir, options)
  checker.run
end
益荒雄の今昔 自分史