I ran into a few minor nuisances with sed's -i mode, which are mostly compatible with gnu sed, but I reckon we should address.
The problem is sed works by writing the output to a second file in the same directory as the original and after completion renaming the file to the original. This has two disadvantages: 1) The inode number changes, resulting in loss of carefully crafted hardlinks. 2) We require to have write permission in the same directory as the original file, even if we don't want to have a backup file. Diff below tries to solve this by doing the following. Copy the file to the backup location (/tmp/sedXXXXXXXXXX if no extension) and use the backup as the infile and the original as the outfile. Furthermore I changed the lstat to fstat, so we can edit symlinks (gsed supports symlinks by replacing the symlink by a new real file, which is also fun), and I extended the warning messages in process to show the backup file if we crash during operation, so people will always know where to recover the file in case of disaster. Because process also error(FATAL, ...)s during process and we always have a backup file I don't think the warning in sed.1 is worth keeping. The only downside to this new approach (that I can think of) is that we now temporarily have a file that is in an inconsistent state, but that's not much different to writing a file with any other editor. $ echo test > /usr/obj/test && dd if=/dev/zero of=/usr/obj/bloat /usr/obj: write failed, file system is full dd: /usr/obj/bloat: No space left on device 614113+0 records in 614112+0 records out 314425344 bytes transferred in 2.206 secs (142470196 bytes/sec) $ ./obj/sed -i -f /tmp/test.sed /usr/obj/test /usr/obj: write failed, file system is full sed: /usr/obj/test: No space left on device (backup at /tmp/sedgRPSLImG9N) $ cat /tmp/test.sed s/test/aaa<repeat to infinity>/ $ cat /tmp/sedgRPSLImG9N test $ ls -i /tmp/test 104 /tmp/test $ sed -i s/test/foo/ /tmp/test $ ls -i /tmp/test 104 /tmp/test $ doas touch /etc/test $ doas chown martijn /etc/test $ echo test > /etc/test $ sed -i s/test/foo/ /etc/test $ cat /etc/test foo The diff does change quite a few mechanics, so some scrutiny is welcome. martijn@ Index: main.c =================================================================== RCS file: /cvs/src/usr.bin/sed/main.c,v retrieving revision 1.39 diff -u -p -r1.39 main.c --- main.c 6 Dec 2018 20:16:04 -0000 1.39 +++ main.c 7 Dec 2018 07:31:54 -0000 @@ -35,6 +35,7 @@ #include <sys/types.h> #include <sys/ioctl.h> +#include <sys/param.h> #include <sys/stat.h> #include <ctype.h> @@ -94,13 +95,13 @@ static int rval; /* Exit status */ */ const char *fname; /* File name. */ const char *outfname; /* Output file name */ -static char oldfname[PATH_MAX]; /* Old file name (for in-place editing) */ -static char tmpfname[PATH_MAX]; /* Temporary file name (for in-place editing) */ +char oldfname[PATH_MAX]; /* Old file name (for in-place editing) */ char *inplace; /* Inplace edit file extension */ u_long linenum; static void add_compunit(enum e_cut, char *); static void add_file(char *); +int copy(FILE *, FILE *); static int next_files_have_lines(void); int termwidth; @@ -310,26 +311,46 @@ again: return (NULL); } +int +copy(FILE *src, FILE *dst) +{ + unsigned char buf[MAXBSIZE]; + size_t r, w, tw; + + while(1) { + if ((r = fread(buf, sizeof(*buf), sizeof(buf), src)) == 0) { + if (feof(src)) { + if (fflush(dst) == EOF) + return 0; + return 1; + } + if (errno != EINTR) + return 0; + continue; + } + tw = 0; + while(tw < r) { + w = fwrite(buf + tw, sizeof(*buf), r - tw, dst); + if (w == 0) { + if (errno != EINTR) + return 0; + continue; + } + tw += w; + } + } +} + void finish_file(void) { if (infile != NULL) { fclose(infile); - if (*oldfname != '\0') { - if (rename(fname, oldfname) != 0) { - warning("rename()"); - unlink(tmpfname); - exit(1); - } + if (inplace != NULL) { + if (*oldfname == '\0') + unlink(oldfname); *oldfname = '\0'; } - if (*tmpfname != '\0') { - if (outfile != NULL && outfile != stdout) - fclose(outfile); - outfile = NULL; - rename(tmpfname, fname); - *tmpfname = '\0'; - } outfname = NULL; } } @@ -341,7 +362,7 @@ finish_file(void) int mf_fgets(SPACE *sp, enum e_spflag spflag) { - struct stat sb; + struct stat sb, isb; size_t len; char *p; int c, fd; @@ -369,10 +390,18 @@ mf_fgets(SPACE *sp, enum e_spflag spflag outfname = "stdout"; break; } + infile = fopen(fname, inplace == NULL ? "r" : "r+"); + if (infile == NULL) { + warning("%s", strerror(errno)); + rval = 1; + continue; + } if (inplace != NULL) { - if (lstat(fname, &sb) != 0) + /* inplace reads from copy */ + outfile = infile; + if (fstat(fileno(outfile), &sb) != 0) error(FATAL, "%s: %s", fname, - strerror(errno ? errno : EIO)); + strerror(errno)); if (!S_ISREG(sb.st_mode)) error(FATAL, "%s: %s %s", fname, "in-place editing only", @@ -382,32 +411,48 @@ mf_fgets(SPACE *sp, enum e_spflag spflag sizeof(oldfname)); len = strlcat(oldfname, inplace, sizeof(oldfname)); - if (len > sizeof(oldfname)) - error(FATAL, "%s: name too long", fname); - } - len = snprintf(tmpfname, sizeof(tmpfname), "%s/sedXXXXXXXXXX", - dirname(fname)); - if (len >= sizeof(tmpfname)) - error(FATAL, "%s: name too long", fname); - if ((fd = mkstemp(tmpfname)) == -1) - error(FATAL, "%s: %s", fname, strerror(errno)); - if ((outfile = fdopen(fd, "w")) == NULL) { - unlink(tmpfname); - error(FATAL, "%s", fname); + if (len >= sizeof(oldfname)) + error(FATAL, "backup %s: name too long", + fname); + (void)unlink(oldfname); + if ((infile = fopen(oldfname, "w+")) == NULL) + error(FATAL, "can't create backup " + "file: %s", oldfname); + if (fstat(fileno(infile), &isb) != 0) + error(FATAL, "%s: %s", oldfname, + strerror(errno)); + if (!S_ISREG(isb.st_mode)) + error(FATAL, "backup file %s not a " + "regular file and can't be " + "replaced", oldfname); + fchown(fileno(infile), sb.st_uid, sb.st_gid); + fchmod(fileno(infile), sb.st_mode & ALLPERMS); + } else { + (void)strlcpy(oldfname, "/tmp/sedXXXXXXXXXX", + sizeof(oldfname)); + if ((fd = mkstemp(oldfname)) == -1) + error(FATAL, "%s: %s", fname, + strerror(errno)); + if ((infile = fdopen(fd, "a+")) == NULL) { + unlink(oldfname); + error(FATAL, "%s", fname); + } } - fchown(fileno(outfile), sb.st_uid, sb.st_gid); - fchmod(fileno(outfile), sb.st_mode & ALLPERMS); - outfname = tmpfname; + outfname = fname; linenum = 0; resetstate(); + if (!copy(outfile, infile)) { + unlink(oldfname); + error(FATAL, "%s: failed to create backup copy " + "to %s: %s", fname, oldfname, + strerror(errno)); + } + ftruncate(fileno(outfile), 0); + rewind(outfile); + rewind(infile); } else { outfile = stdout; outfname = "stdout"; - } - if ((infile = fopen(fname, "r")) == NULL) { - warning("%s", strerror(errno)); - rval = 1; - continue; } } Index: process.c =================================================================== RCS file: /cvs/src/usr.bin/sed/process.c,v retrieving revision 1.34 diff -u -p -r1.34 process.c --- process.c 14 Nov 2018 10:59:33 -0000 1.34 +++ process.c 7 Dec 2018 07:31:54 -0000 @@ -75,6 +75,7 @@ static int sdone; /* If any substitutes /* Iov structure for 'w' commands. */ static regex_t *defpreg; size_t maxnsub; +extern char oldfname[PATH_MAX]; regmatch_t *match; #define OUT() do {\ @@ -473,7 +474,11 @@ flush_appends(void) break; } if (ferror(outfile)) - error(FATAL, "%s: %s", outfname, strerror(errno ? errno : EIO)); + error(FATAL, "%s: %s%s%s%s", outfname, + strerror(errno ? errno : EIO), + *oldfname == '\0' ? "" : " (backup at ", + *oldfname == '\0' ? "" : oldfname, + *oldfname == '\0' ? "" : ")"); appendx = sdone = 0; } @@ -513,7 +518,11 @@ lputs(char *s) (void)fputc('$', outfile); (void)fputc('\n', outfile); if (ferror(outfile)) - error(FATAL, "%s: %s", outfname, strerror(errno ? errno : EIO)); + error(FATAL, "%s: %s%s%s%s", outfname, + strerror(errno ? errno : EIO), + *oldfname == '\0' ? "" : " (backup at ", + *oldfname == '\0' ? "" : oldfname, + *oldfname == '\0' ? "" : ")"); } static inline int Index: sed.1 =================================================================== RCS file: /cvs/src/usr.bin/sed/sed.1,v retrieving revision 1.57 diff -u -p -r1.57 sed.1 --- sed.1 14 Nov 2018 10:59:33 -0000 1.57 +++ sed.1 7 Dec 2018 07:31:54 -0000 @@ -102,10 +102,6 @@ Edit files in place, saving backups with If a zero length .Ar extension is given, no backup will be saved. -It is not recommended to give a zero length -.Ar extension -when in place editing files, as it risks corruption or partial content -in situations where disk space is exhausted, etc. In .Fl i mode, the hold space, line numbers, and ranges are reset between files.