標準入出力を使って単一ファイルを書き換える


標準入出力を使うツールで、入力と出力を同じファイルにリダイレクトしたい場合、しばしばあると思います。
しかし、これを行うと、ファイルは破壊されてしまいます(最近は入力と出力が同じファイルの場合警告されるようになりましたね)。
一方で、`sed -i`のように、書き換えモードが存在するツールもあります。
この書き換えモードを汎用的に作れないか？
ネットを検索しましたが見つけられませんでした。
ベタではありますが、ファイルを全部読み込むことで実現してみました(現状LinuxやBSD系でしか使えませんが…)。

一応、異常終了や中止した時にファイルを復元する機能も付けてあります(が、Usageを表示して正常終了するツールには無力なのでご注意を)。

- 巨大なファイルには使ってはいけません。
- 対象ツールの使い方を熟知した上で使わないとファイル破損のリスクが確実に上がります。

## 使用例

```sh
$ cat 000
abc
de
ghikl
cme
bde
$ stdreplace 000 sed -e 's/b//' # this has the same meaning as `sed -i -e 's/b//'`
$ cat 000
ac
de
ghikl
cme
de
```

## コード

- https://github.com/cielavenir/7razf/blob/master/compat.h をお使い下さい

```c:stdreplace.c
#include "../compat.h"

#ifdef STANDALONE
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
unsigned char buf[BUFLEN];

#if defined(_WIN32) || (!defined(__GNUC__) && !defined(__clang__))
#else
//constant phrase
long long filelengthi64(int fd){
	struct stat st;
	fstat(fd,&st);
	return st.st_size;
}
int filelength(int fd){return filelengthi64(fd);}
#endif
#else
#include "../cielbox.h"
#endif

#if defined(_WIN32) || (!defined(__GNUC__) && !defined(__clang__)) || defined(FEOS)
#ifdef STANDALONE
int main(const int argc, const char **argv){
	initstdio();
#else
int stdreplace(const int argc, const char **argv){
#endif
	fprintf(stderr,"Sorry: currently stdreplace is supported only on POSIX.\n");
	return -1;
}
#else

#include <signal.h>
static int g_pid;
static void sighan(int sig){
	kill(g_pid,sig);
}

static int run(const char* file, int pid, FILE *fin, FILE *fout){
	int threshold=2048; //must be FIFO size or less.

	setbuf(fin,0);
	setbuf(fout,0);
	FILE *f=fopen(file,"r+b");
	if(!f){
		fprintf(stderr,"Failed to open %s\n",file);
		return 1;
	}
	long long flen=filelengthi64(fileno(f)),off=0;
	unsigned char *ibuf=malloc(flen);
	if(!ibuf){
		fprintf(stderr,"Failed to allocate buffer\n");
		fclose(f);
		return 1;
	}
	fread(ibuf,1,flen,f);

	g_pid=pid;
	signal(SIGINT,sighan);
	signal(SIGTERM,sighan);
	signal(SIGHUP,sighan); // POSIX

	rewind(f);
	ftruncate(fileno(f),0);
	for(;off+=fwrite(ibuf+off,1,threshold<flen-off?threshold:flen-off,fout);){
		if(off==flen){fclose(fout);break;}
		int len=fread(buf,1,threshold,fin);
		if(len<0)break;
		fwrite(buf,1,len,f);
	}
	for(;;){
		int len=fread(buf,1,threshold,fin);
		if(len<0)break;
		fwrite(buf,1,len,f);
		if(feof(fin))break;
	}
	fclose(fin);
	int st;
	waitpid(pid,&st,WUNTRACED);
	if(!WIFEXITED(st)||WEXITSTATUS(st)!=0){
		fprintf(stderr,"Execution failed (%d). Recovering backup... ",!WIFEXITED(st)?255:WEXITSTATUS(st));
		rewind(f);
		ftruncate(fileno(f),0);
		fwrite(ibuf,1,flen,f);
		fprintf(stderr,"Done.\n");
	}
	free(ibuf);
	fclose(f);
	return 0;
}

#ifdef STANDALONE
int main(const int argc, const char **argv){
	initstdio();
#else
int stdreplace(const int argc, const char **argv){
#endif
	if(argc<3){
		fprintf(stderr,"stdreplace file program/interpreter...\n");
		return 0;
	}
	const char *cmd=argv[2];

	//Initialize
	int fd_in[2],fd_out[2];
	pipe(fd_in);
	pipe(fd_out);
	fcntl(fd_out[0],F_SETFL,O_NONBLOCK);
	fcntl(fd_in[1],F_SETFL,O_NONBLOCK);

	int pid=fork();
	if(pid==-1){
		fprintf(stderr,"Fork error\n");
		return 1;
	}
	if(pid==0){//child
		close(0);
		dup2(fd_in[0],0);
		close(1);
		dup2(fd_out[1],1);
		close(fd_in[0]);
		close(fd_in[1]);
		close(fd_out[0]);
		close(fd_out[1]);
		char **args=(char**)malloc(sizeof(char*)*(argc-1));
		for(int i=2;i<argc;i++)args[i-2]=argv[i];
		args[argc-2]=NULL;
		execvp(cmd,args); //execvp is required; cmd might be interpreter name
		free(args);
		return 0;
	}
	//parent
	close(fd_in[0]);
	close(fd_out[1]);
	FILE *fin=fdopen(fd_out[0],"r");
	FILE *fout=fdopen(fd_in[1],"w");
	run(argv[1],pid,fin,fout);
}
#endif
```

## 終わりに

- non-blocking IOは初めて使ったので使い方が間違っているかもしれません。よろしければご指摘下さい。