標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

Rustでゴミ箱コマンドを作ったり

nyagos には、ファイルをゴミ箱へ移動させる trash というコマンドがあります。

実装は Lua で、こんな感じで COM オブジェクトで実現してました。

nyagos.d/trash.lua :

if not nyagos then
    print("This is a script for nyagos not lua.exe")
    os.exit()
end

nyagos.alias.trash = function(args)
    if #args <= 0 then
        nyagos.writerr("Move files or directories to Windows Trashbox\n")
        nyagos.writerr("Usage: trash file(s)...\n")
        return
    end
    local fsObj = nyagos.create_object("Scripting.FileSystemObject")
    local shellApp = nyagos.create_object("Shell.Application")
    local trashBox = shellApp:NameSpace(nyagos.to_ole_integer(10))
    args = nyagos.glob((table.unpack or unpack)(args))
    for i=1,#args do
        if fsObj:FileExists(args[i]) or fsObj:FolderExists(args[i]) then
            trashBox:MoveHere(fsObj:GetAbsolutePathName(args[i]))
        else
            nyagos.writerr(args[i]..": such a file or directory not found.\n")
        end
    end
    trashBox:_release()
    shellApp:_release()
    fsObj:_release()
end

この trash、削除したファイルを復元できるので、まちがったとき便利なのですが、nyagos 内蔵コマンドなので、バッチファイルなどからは使えません。

これを JScript化すると、こんな感じになります。

trash.js

var shellApp = new ActiveXObject("Shell.Application");
var trashBox = shellApp.NameSpace(10);
var fsObj = new ActiveXObject("Scripting.FileSystemObject");
for(var i=0 ; i <WScript.Arguments.length ; i++ ){
    var arg1 = WScript.Arguments(i);
    var path1 = fsObj.GetAbsolutePathName(arg1);
    trashBox.MoveHere(path1);
}

これをそのまま使ってもよいのですが、 VBScript が今後 Windows ではオプション機能になるという発表もあったため、JScript も今後継続して使えるか分かりません。

ということで、勉強がてら Rust で作ってみました。

当初は Lua/JScript 版と同様に COM を使うつもりだったのですが、Rust の場合、Go の go-ole ほどお手軽に使えるライブラリがなく、予想以上に難度が高かったので、API を直たたきするアプローチに切り替えました。

によると、SHFileOperationW という API で実現可能のようです。幸い Rust には既にそれにアクセスするためのパッケージも用意されていました。

ここまでくると、もう構造体 SHFILEOPSTRUCTW のフィールドに設定する値をうまいことでっちあげるだけです。Rust で NUL終端なUTF16テキストでパスのリストを作るのが少々めんどくさかったのですが、意外とさっくりと出来ました ( COM でなんとかしようと右往左往していた時間の方が圧倒的に長かった )

extern crate glob;

use core::ffi::c_void;
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{BOOL, HWND};
use windows::Win32::UI::Shell::{
    SHFileOperationW, FOF_ALLOWUNDO, FOF_NOCONFIRMATION, FO_DELETE, SHFILEOPSTRUCTW,
};

fn append_fname(buffer: &mut Vec<u16>, fname: &str) {
    println!("{}", fname);
    let mut fname_vec: Vec<u16> = fname.encode_utf16().collect();
    buffer.append(&mut fname_vec);
    buffer.push(0)
}

fn trash() -> Result<i32, Box<dyn std::error::Error>> {
    let mut source: Vec<u16> = Vec::new();
    for fname in std::env::args().skip(1) {
        let mut glob_ok = false;
        for filename in glob::glob(&fname)? {
            if let Some(filename) = filename?.to_str() {
                append_fname(&mut source, &filename);
                glob_ok = true;
            }
        }
        if !glob_ok {
            append_fname(&mut source, &fname);
        }
    }
    if source.len() <= 0 {
        return Ok(0)
    }
    source.push(0);
    let mut sh_file_op_struct = SHFILEOPSTRUCTW {
        hwnd: HWND(0),
        wFunc: FO_DELETE,
        pFrom: PCWSTR(source.as_mut_ptr()),
        pTo: w!(""),
        fFlags: (FOF_ALLOWUNDO | FOF_NOCONFIRMATION) as u16,
        fAnyOperationsAborted: BOOL(0),
        hNameMappings: 0 as *mut c_void,
        lpszProgressTitle: w!("to trash"),
    };
    unsafe {
        Ok(SHFileOperationW(&mut sh_file_op_struct))
    }
}

fn main() {
    match trash(){
        Err(err) => {
            eprintln!("{}", err);
            std::process::exit(1)
        }
        Ok(n) => std::process::exit(n),
    }
}