├── README.md └── inplace.c /README.md: -------------------------------------------------------------------------------- 1 | STREAM EDITOR ADAPTOR: EDIT FILES IN-PLACE 2 | ========================================== 3 | 4 | A command-line tool for in-place stream editing of files with stream 5 | editors that don't know how to do that themselves. 6 | 7 | $ inplace somefile sed -e 's/foo/bar/g' 8 | 9 | This is similar to the GNU sponge(1) command, which is used like this: 10 | 11 | $ sed -e 's/foo/bar/g' somefile | sponge somefile 12 | 13 | The sponge usage is also supported: 14 | 15 | $ sed -e 's/foo/bar/g' | inplace somefile 16 | 17 | inplace can also be used with xargs. The example here is looking for json files with a field called foo, and setting that field to an empty list. 18 | 19 | $ find -name '*.json' | xargs grep '"foo":' | xargs -i@ -- inplace @ jq 'foo|=[]' 20 | 21 | The file edited in-place will be renamed into place unless the `-w` option is 22 | used, then the file will be re-written and will keep its identity. 23 | 24 | $ inplace 25 | Usage: ./inplace [options] FILE [COMMAND [ARGUMENTS]] 26 | 27 | This program runs the COMMAND, if given, with stdin from FILE, 28 | and saves the output of COMMAND to FILE when the COMMAND 29 | completes. Any ARGUMENTS are passed to the COMMAND. 30 | 31 | If a COMMAND is not given then the stdin will be saved to FILE. 32 | 33 | Options: 34 | -h, 35 | --help -> this message 36 | -b SUFFIX, 37 | --backup SUFFIX -> keep a backup named $FILE$SUFFIX 38 | -w, 39 | --write -> re-write FILE to keep file identity 40 | the same, do not rename into place 41 | 42 | By default ./inplace renames the new FILE into place; use the 43 | -w option to have ./inplace rewrite the FILE.. 44 | 45 | BUILD INSTRUCTIONS 46 | ================== 47 | 48 | $ cc -o inplace inplace.c 49 | 50 | There's no build system yet as this program is so simple. 51 | 52 | TODO 53 | ==== 54 | 55 | - Add a build system (cc -o inplace inplace.c) 56 | - Add manpage 57 | - Add examples with jq and other stream editors 58 | 59 | -------------------------------------------------------------------------------- /inplace.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #ifdef HAVE_POSIX_SPAWN 16 | #include 17 | #endif 18 | 19 | static const char *prog = "inplace"; 20 | 21 | static 22 | void 23 | usage(const char *progname, int code) 24 | { 25 | printf("Usage:\t%1$s [options] FILE [COMMAND [ARGUMENTS]]\n\n" 26 | "\tThis program runs the COMMAND, if given, with stdin from FILE,\n" 27 | "\tand saves the output of COMMAND to FILE when the COMMAND\n" 28 | "\tcompletes. Any ARGUMENTS are passed to the COMMAND.\n\n" 29 | "\tIf a COMMAND is not given then the stdin will be saved to FILE.\n\n" 30 | "\tOptions:\n" 31 | "\t -h\n" 32 | "\t --help -> this message;\n" 33 | "\t -b SUFFIX,\n" 34 | "\t --backup SUFFIX -> keep a backup named $FILE$SUFFIX;\n" 35 | "\t -w,\n" 36 | "\t --write -> re-write FILE to keep file identity\n" 37 | "\t the same, do not rename into place.\n\n" 38 | "\tBy default %1$s renames the new FILE into place; use the\n" 39 | "\t-w option to have %1$s rewrite the FILE..\n", progname); 40 | exit(code); 41 | } 42 | 43 | static 44 | int 45 | copy_file(int from_fd, int to_fd) 46 | { 47 | char buf[4096]; 48 | ssize_t bytes, wbytes; 49 | 50 | while ((bytes = read(from_fd, buf, sizeof(buf))) != 0) { 51 | if (bytes == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) 52 | return 0; 53 | if (bytes == -1 && errno != EINTR) 54 | return 0; 55 | while (bytes > 0 && (wbytes = write(to_fd, buf, bytes)) != bytes) { 56 | if (wbytes == -1 && errno != EINTR) 57 | return 0; 58 | bytes -= wbytes; 59 | } 60 | } 61 | return 1; 62 | } 63 | 64 | static 65 | int 66 | run_cmd(const char **what, int argc, char **argv) 67 | { 68 | /* XXX Add int pip_fds[2] for use in detecting pre-exec child failure */ 69 | pid_t pid, tmp_fd; 70 | int status; 71 | /* XXX Call pipe()/pipe2() and write side of pipe_fds[] to be O_CLOEXEC */ 72 | 73 | #ifdef HAVE_POSIX_SPAWN 74 | if (argv[0][0] == '/') { 75 | *what = "posix_spawn"; 76 | errno = posix_spawn(&pid, argv[0], &actions, NULL, &argv[0], NULL); 77 | if (errno != 0) 78 | return -1; 79 | } else { 80 | *what = "posix_spawnp"; 81 | errno = posix_spawnp(&pid, argv[0], &actions, NULL, &argv[0], NULL); 82 | if (errno != 0) 83 | return -1; 84 | } 85 | #else /* HAVE_POSIX_SPAWN */ 86 | *what = "fork"; 87 | pid = fork(); 88 | if (pid == -1) 89 | return -1; 90 | if (pid == 0) { 91 | /* child */ 92 | if (argv[0][1] == '/') { 93 | (void) execv(argv[0], &argv[0]); 94 | perror("execv"); 95 | } else { 96 | (void) execvp(argv[0], &argv[0]); 97 | perror("execvp"); 98 | } 99 | /* XXX Write to pipe */ 100 | exit(125); 101 | } 102 | #endif /* HAVE_POSIX_SPAWN */ 103 | 104 | /* parent */ 105 | while (wait(&status) != pid) 106 | ; 107 | 108 | return status; 109 | } 110 | 111 | static 112 | int 113 | fix_it(const char *dst_fname, const char *src_fname, int rename_into_place) 114 | { 115 | if (src_fname == NULL) 116 | return 1; 117 | 118 | if (rename_into_place) { 119 | if (rename(src_fname, dst_fname) == -1) { 120 | fprintf(stderr, 121 | "%s: Error: could not restore file %s after " 122 | "failed stream edit: %s\n", prog, dst_fname, strerror(errno)); 123 | return 0; 124 | } 125 | } else { 126 | int src, dst; 127 | 128 | if ((src = open(src_fname, O_RDONLY)) == -1 || 129 | (dst = open(dst_fname, O_WRONLY | O_TRUNC)) == -1 || 130 | !copy_file(src, dst)) { 131 | fprintf(stderr, 132 | "%s: Error: could not restore file %s after " 133 | "failed stream edit: %s\n", prog, dst_fname, strerror(errno)); 134 | if (src != -1) 135 | (void) close(src); 136 | if (dst != -1) 137 | (void) close(dst); 138 | return 0; 139 | } 140 | } 141 | 142 | return 1; 143 | } 144 | 145 | int 146 | main(int argc, char **argv) 147 | { 148 | const char *what; 149 | const char *fname; 150 | const char *bkp = NULL; 151 | char *tmp_fname = NULL; 152 | char *bkp_fname = NULL; 153 | int tmp_fd = -1; 154 | int bkp_fd = -1; 155 | int rename_into_place = 1; 156 | 157 | assert(argc > 0); 158 | prog = argv[0]; 159 | 160 | /* This program is too simple for getopt and friends */ 161 | for (argc--, argv++; argc && argv[0][0] == '-'; argc--, argv++) { 162 | if (argv[0][1] == '-') { 163 | if (strcmp(argv[0], "--help") == 0) 164 | usage(prog, 0); 165 | if (strcmp(argv[0], "--write") == 0) { 166 | rename_into_place = 0; 167 | } else if (strcmp(argv[0], "--backup") == 0) { 168 | bkp = argv[1]; 169 | argc--; 170 | argv++; 171 | } else if (strcmp(argv[0], "--") == 0) { 172 | argc--; 173 | argv++; 174 | break; 175 | } else { 176 | usage(prog, 1); 177 | } 178 | } else { 179 | char *p; 180 | for (p = argv[0] + 1; *p != '\0'; p++) { 181 | if (p[0] == 'h') 182 | usage(prog, 0); 183 | if (p[0] == 'b') { 184 | bkp = argv[1]; 185 | argc--; 186 | argv++; 187 | } else if (p[0] == 'w') { 188 | rename_into_place = 0; 189 | } else if (p[0] == 'h') { 190 | usage(prog, 0); 191 | } else { 192 | usage(prog, 1); 193 | } 194 | } 195 | } 196 | } 197 | if (argc < 1) 198 | usage(prog, 0); 199 | 200 | fname = argv[0]; 201 | argc--; 202 | argv++; 203 | 204 | /* Prep temp file or bkp file */ 205 | what = "asprintf"; 206 | if (asprintf(&tmp_fname, "%s-XXXXXX", fname) == -1) 207 | goto fail; 208 | 209 | what = "mkstemp"; 210 | tmp_fd = mkstemp(tmp_fname); 211 | if (tmp_fd == -1) 212 | goto fail; 213 | 214 | if (bkp != NULL) { 215 | /* Make backup */ 216 | what = "asprintf"; 217 | if (asprintf(&bkp_fname, "%s%s", fname, bkp) == -1) 218 | goto fail; 219 | 220 | if (rename_into_place) { 221 | what = "link"; 222 | (void) unlink(bkp_fname); 223 | if (link(fname, bkp_fname) == -1) { 224 | if (errno == EEXIST) { 225 | fprintf(stderr, "%s: Error: racing with another %s to " 226 | "update %s? %s\n", prog, prog, fname, 227 | strerror(errno)); 228 | exit(2); 229 | } 230 | goto fail; 231 | } 232 | } else { 233 | int fd; 234 | 235 | what = "open (bkp file)"; 236 | (void) unlink(bkp_fname); 237 | bkp_fd = open(bkp_fname, O_CREAT | O_EXCL | O_WRONLY, 0600); 238 | if (bkp_fd == -1) { 239 | if (errno == EEXIST) { 240 | fprintf(stderr, "%s: Error: racing with another %s to " 241 | "update %s? %s\n", prog, prog, fname, 242 | strerror(errno)); 243 | exit(2); 244 | } 245 | goto fail; 246 | } 247 | 248 | what = "open (target file)"; 249 | fd = open(fname, O_RDONLY); 250 | if (fd == -1) 251 | goto fail; 252 | 253 | if (!copy_file(fd, bkp_fd) || 254 | close(bkp_fd) == -1) { 255 | (void) close(fd); 256 | fprintf(stderr, "%s: I/O error creating backup\n", prog); 257 | exit(2); 258 | } 259 | (void) close(fd); 260 | } 261 | } 262 | 263 | /* Redirect stdout to go to tmp file */ 264 | what = "dup2"; 265 | if (dup2(tmp_fd, STDOUT_FILENO) == -1) 266 | goto fail; 267 | (void) close(tmp_fd); 268 | tmp_fd = -1; 269 | 270 | if (argc == 0) { 271 | /* stdin has the input we want; write it to stdout */ 272 | // XXX Better info in copy_file for error handling? 273 | if (!copy_file(STDIN_FILENO, STDOUT_FILENO)) { 274 | fprintf(stderr, "%s: Error: I/O error copying stdin\n", prog); 275 | (void) unlink(tmp_fname); 276 | (void) fix_it(fname, bkp_fname, rename_into_place); 277 | exit(2); 278 | } 279 | } else { 280 | int fd, status; 281 | 282 | /* Redirect stdin to be from fname */ 283 | what = "open"; 284 | fd = open(fname, O_RDONLY); 285 | if (fd == -1) 286 | goto fail; 287 | what = "dup2"; 288 | if (dup2(fd, STDIN_FILENO) == -1) 289 | goto fail; 290 | (void) close(fd); 291 | 292 | status = run_cmd(&what, argc, argv); 293 | 294 | if (status == -1) 295 | goto fail; 296 | 297 | if (WIFSIGNALED(status)) { 298 | (void) unlink(tmp_fname); 299 | (void) fix_it(fname, bkp_fname, rename_into_place); 300 | (void) kill(getpid(), WTERMSIG(status)); /* exit with same signal */ 301 | /* NOTREACHED */ 302 | exit(2); 303 | } 304 | if (!WIFEXITED(status)) { 305 | (void) unlink(tmp_fname); 306 | (void) fix_it(fname, bkp_fname, rename_into_place); 307 | exit(2); 308 | } 309 | 310 | status = WEXITSTATUS(status); 311 | if (status != 0) { 312 | (void) unlink(tmp_fname); 313 | (void) fix_it(fname, bkp_fname, rename_into_place); 314 | exit(status); 315 | } 316 | } 317 | 318 | /* Rename or write into place */ 319 | if (!fix_it(fname, tmp_fname, rename_into_place)) { 320 | (void) fix_it(fname, bkp_fname, rename_into_place); 321 | exit(2); 322 | } 323 | 324 | if (!rename_into_place) 325 | (void) unlink(tmp_fname); 326 | 327 | /* Leave backup file around */ 328 | 329 | return 0; 330 | 331 | fail: 332 | perror(what); 333 | 334 | if (tmp_fname != NULL) { 335 | (void) unlink(tmp_fname); 336 | free(tmp_fname); 337 | } 338 | } 339 | --------------------------------------------------------------------------------