ファイルをゴミ箱に送るコンソールプログラムをささっと作る

# はじめに
Windows でもコマンドプロンプトを愛用しているが，ファイルを削除するときについ勢いで `DEL *` と打ち込んでしまって後悔することが多々ある。かつてはファイルをゴミ箱に送る WSH スクリプトを自作して愛用していたが，ワイルドカードの処理に納得できず悩んでいた。

そうだ，C 言語で作ろう！シェル API を呼び出すだけなのでささっと作れるはずだ！

# 基本方針
Windows のシェル API である SHFileOperation を使う。詳しい仕様は下記参照されたい。

[SHFileOperationW関数(shellapi.h) - learn.microsoft.com](https://learn.microsoft.com/ja-jp/windows/win32/api/shellapi/nf-shellapi-shfileoperationw)

# シェル API の仕様

ファイルをゴミ箱に送るためには `sfo.wFunc = FO_DELETE` とした上で `sfo.fFlags` に `FOF_ALLOWUNDO` 属性を付加しなくてはならない。コンソールプログラムなので余計な進行状況ダイアログの表示を抑制したい人はさらに `FOF_SILENT` 属性を追加しても良いだろう。あるいは `FOF_NO_UI` 属性でも良いかもしれない。

`sfo.pFrom` にはファイル名またはフォルダ名のリストを格納したバッファのポインタを指定する。注意事項として以下の点に留意しなくてはならない。
- フルパス名であること。
- ワイルドカードが使える，ただしファイル名の位置のみで使える。
- 複数のファイル名またはフォルダ名を指定できる。区切り文字はヌル文字(\0)とする。
- バッファの末尾にはヌル文字(\0)を二つ連続する。


```C:シェル API を使ってファイル・フォルダをゴミ箱に送る
SHFILEOPSTRUCT	sfo = { 0 };
sfo.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION;
sfo.wFunc = FO_DELETE;
sfo.pFrom = buf;
SHFileOperation( &sfo );
```

# ファイル名・フォルダ名の一覧作成
コマンドライン引数，すなわち `argv[]` にファイル名・フォルダ名が格納されているとする。存在しないファイル・フォルダが含まれているとシェル API の実行そのものが失敗してしまうようだ。存在するファイルやフォルダのみが削除されるという訳ではない。ファイル名やフォルダ名の指定にはワイルドカードが含まれている可能性があるので `_findfirst` を用いて存在チェックを行う。同時にフルパス名に変換した場合の文字列の長さを得る。ヌル文字(\0)の分も漏れずにカウントする必要がある。

```C:ファイル・フォルダの存在チェック＆バッファサイズを得る
struct _wfinddata_t	info;
wchar_t	path[MAX_PATH];
int		len = 0;
for( int i = 1; i < argc; i++ ) {
	intptr_t	handle = _wfindfirst( argv[i], &info );
	if( handle == -1 ) {
		fwprintf( stderr, L"%s は存在しません!!\n", argv[i] );
		return (wchar_t*)NULL;
	}
	_findclose( handle );
	_wfullpath( path, argv[i], MAX_PATH );
	len += wcslen( path );
	len++;
}
len++;
```

 
```C:バッファを確保する
wchar_t	*buf = (wchar_t*)malloc( sizeof(wchar_t) * len );
if( buf == (wchar_t*)NULL ) {
	fputws( L"メモリの確保に失敗しました!!\n", stderr );
	return (wchar_t*)NULL;
}
```

```C:ファイル・フォルダ名一覧をフルパスに変換して作成する
wchar_t	*p = buf;
for( int i = 1; i < argc; i++ ) {
	_wfullpath( path, argv[i], MAX_PATH );
	p = wcscpy2( p, path );
	*p++ = L'\0';
}
*p++ = L'\0';
```

文字列のコピーには愛用している自作のコピー関数を使用する。この関数はコピー先の文字列の末尾のヌル文字(\0)を示すポインタを返すので，連続して使用することで文字列連結するのに適しているのだ。

```C:自作の文字列コピー関数
wchar_t	*wcscpy2( wchar_t *s1, wchar_t *s2 ) {
	while( ( *s1 = *s2 ) != L'\0' ) {
		s1++;
		s2++;
	}
	return( s1 );
}
```

# 実装コード

実装コードを以下に示す。なお，本プログラムはVisual Studio 2022 Community Edition にてコンパイルした。

```C:REMOVE.C
#define	UNICODE
#include <windows.h>
#include <shellapi.h>
#include <stdio.h>
#include <string.h>
#include <locale.h>
#include <io.h>
#pragma comment( lib, "shell32" )
//------------------------------------------------------------------------------
// 文字列 s2 を文字列 s1 にコピーする
// 文字列 s1 の末尾（ヌル文字）を示すポインタを返す
//------------------------------------------------------------------------------
static	wchar_t	*wcscpy2( wchar_t *s1, wchar_t *s2 ) {
	while( ( *s1 = *s2 ) != L'\0' ) {
		s1++;
		s2++;
	}
	return( s1 );
}
//------------------------------------------------------------------------------
// ファイル・フォルダ一覧を作成する
// ・ファイル・フォルダが存在しない場合はヌルポインタを返す
// ・ファイル・フォルダ名をフルパスに変換する
// ・区切り文字はヌル文字(\0)とし，最後はヌル文字(\0)を二連続とする
//------------------------------------------------------------------------------
static	wchar_t	*create_list( int argc, wchar_t *argv[] ) {
	//--------------------------------------------------------------------------
	// ファイル・フォルダの存在チェック＆バッファサイズを得る
	//--------------------------------------------------------------------------
	struct _wfinddata_t	info;
	wchar_t	path[MAX_PATH];
	int		len = 0;
	for( int i = 1; i < argc; i++ ) {
		//----------------------------------------------------------------------
		// ワイルドカードを含むパス名の存在チェック
		//----------------------------------------------------------------------
		intptr_t	handle = _wfindfirst( argv[i], &info );
		if( handle == -1 ) {
			fwprintf( stderr, L"%s は存在しません!!\n", argv[i] );
			return (wchar_t*)NULL;
		}
		_findclose( handle );
		//----------------------------------------------------------------------
		// フルパス名の長さを得る
		//----------------------------------------------------------------------
		_wfullpath( path, argv[i], MAX_PATH );
		len += wcslen( path );
		len++;
	}
	len++;
	//--------------------------------------------------------------------------
	// バッファを確保する
	//--------------------------------------------------------------------------
	wchar_t	*buf = (wchar_t*)malloc( sizeof(wchar_t) * len );
	if( buf == (wchar_t*)NULL ) {
		fputws( L"メモリの確保に失敗しました!!\n", stderr );
		return (wchar_t*)NULL;
	}
	//--------------------------------------------------------------------------
	// ファイル・フォルダ名一覧をフルパスに変換して作成する
	//--------------------------------------------------------------------------
	wchar_t	*p = buf;
	for( int i = 1; i < argc; i++ ) {
		_wfullpath( path, argv[i], MAX_PATH );
		p = wcscpy2( p, path );
		*p++ = L'\0';
	}
	*p++ = L'\0';
	return( buf );
}
//------------------------------------------------------------------------------
// ファイル・フォルダをごみ箱に移動する
//------------------------------------------------------------------------------
static	int		remove_list( wchar_t *buf ) {
	SHFILEOPSTRUCT	sfo = { 0 };
	sfo.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION;
	sfo.wFunc = FO_DELETE;
	sfo.pFrom = buf;
	return SHFileOperation( &sfo );
}
//------------------------------------------------------------------------------
// メイン関数
//------------------------------------------------------------------------------
int		wmain( int argc, wchar_t *argv[] ) {
	//--------------------------------------------------------------------------
	// ロケールの初期化
	//--------------------------------------------------------------------------
	setlocale( LC_ALL, "" );
	//--------------------------------------------------------------------------
	// ヘルプメッセージ
	//--------------------------------------------------------------------------
	if( argc < 2 ) {
		fputws( L"ファイル（複数可）をごみ箱に移動します。\n", stderr );
		fputws( L"\n", stderr );
		fputws( L"REMOVE(.EXE) [ファイル名] ...\n", stderr );
		fputws( L"\n", stderr );
		fputws( L"ファイルまたはフォルダ（複数可）の一覧を指定します。\n", stderr );
		fputws( L"ファイルまたはフォルダの指定にはワイルドカードを使用できます。\n", stderr );
		fputws( L"フォルダを指定すると、フォルダ内の全てのファイルを削除します。\n", stderr );
		return( -1 );
	}
	//--------------------------------------------------------------------------
	// ファイル・フォルダ一覧の作成
	//--------------------------------------------------------------------------
	wchar_t	*buf = create_list( argc, argv );
	//--------------------------------------------------------------------------
	// ファイル・フォルダをごみ箱に移動する
	//--------------------------------------------------------------------------
	int		ret = remove_list( buf );
	if( ret != 0 )
		fputws( L"ファイル・フォルダの削除に失敗しました!!\n", stderr );
	//--------------------------------------------------------------------------
	// バッファの解放
	//--------------------------------------------------------------------------
	free( buf );
	return( ret );
}
```

# 実行画面

引数なしで実行すると下記のようなヘルプメッセージが表示される。

```
ファイル（複数可）をごみ箱に移動します。

REMOVE(.EXE) [ファイル名] ...

ファイルまたはフォルダ（複数可）の一覧を指定します。
ファイルまたはフォルダの指定にはワイルドカードを使用できます。
フォルダを指定すると、フォルダ内の全てのファイルを削除します。
```

# 参考文献

[Windowsで，簡単にファイルを「ごみ箱」に送るバッチのサンプルコード。削除処理に「シェル名前空間」を使う仕組みの解説 - はてなブログ](https://language-and-engineering.hatenablog.jp/entry/20150528/WindowsBatSendFileToRecycleBin)