diff options
-rw-r--r-- | editors/patch.c | 825 | ||||
-rw-r--r-- | editors/patch_bbox.c | 306 | ||||
-rw-r--r-- | editors/patch_toybox.c | 164 | ||||
-rwxr-xr-x | testsuite/patch.tests | 13 | ||||
-rw-r--r-- | testsuite/testing.sh | 3 |
5 files changed, 969 insertions, 342 deletions
diff --git a/editors/patch.c b/editors/patch.c index 62477af16..7f3234e66 100644 --- a/editors/patch.c +++ b/editors/patch.c @@ -1,306 +1,591 @@ -/* vi: set sw=4 ts=4: */ -/* - * busybox patch applet to handle the unified diff format. - * Copyright (C) 2003 Glenn McGrath +/* Adapted from toybox's patch. */ + +/* vi: set sw=4 ts=4: + * + * patch.c - Apply a "universal" diff. * - * Licensed under the GPL v2 or later, see the file LICENSE in this tarball. + * Copyright 2007 Rob Landley <rob@landley.net> * - * This applet is written to work with patches generated by GNU diff, - * where there is equivalent functionality busybox patch shall behave - * as per GNU patch. + * see http://www.opengroup.org/onlinepubs/009695399/utilities/patch.html + * (But only does -u, because who still cares about "ed"?) * - * There is a SUSv3 specification for patch, however it looks to be - * incomplete, it doesnt even mention unified diff format. - * http://www.opengroup.org/onlinepubs/007904975/utilities/patch.html + * TODO: + * -b backup + * -l treat all whitespace as a single space + * -N ignore already applied + * -d chdir first + * -D define wrap #ifdef and #ifndef around changes + * -o outfile output here instead of in place + * -r rejectfile write rejected hunks to this file * - * Issues - * - Non-interactive - * - Patches must apply cleanly or patch (not just one hunk) will fail. - * - Reject file isnt saved - */ + * -E remove empty files --remove-empty-files + * -f force (no questions asked) + * -F fuzz (number, default 2) + * [file] which file to patch + +USE_PATCH(NEWTOY(patch, USE_TOYBOX_DEBUG("x")"up#i:R", TOYFLAG_USR|TOYFLAG_BIN)) + +config PATCH + bool "patch" + default y + help + usage: patch [-i file] [-p depth] [-Ru] + + Apply a unified diff to one or more files. + -i Input file (defaults=stdin) + -p number of '/' to strip from start of file paths (default=all) + -R Reverse patch. + -u Ignored (only handles "unified" diffs) + + This version of patch only handles unified diffs, and only modifies + a file when all all hunks to that file apply. Patch prints failed + hunks to stderr, and exits with nonzero status if any hunks fail. + + A file compared against /dev/null (or with a date <= the epoch) is + created/deleted as appropriate. +*/ #include "libbb.h" -static unsigned copy_lines(FILE *src_stream, FILE *dst_stream, unsigned lines_count) +struct double_list { + struct double_list *next; + struct double_list *prev; + char *data; +}; + +// Return the first item from the list, advancing the list (which must be called +// as &list) +static +void *TOY_llist_pop(void *list) { - while (src_stream && lines_count) { - char *line; - line = xmalloc_fgets(src_stream); - if (line == NULL) { - break; - } - if (fputs(line, dst_stream) == EOF) { - bb_perror_msg_and_die("error writing to new file"); + // I'd use a void ** for the argument, and even accept the typecast in all + // callers as documentation you need the &, except the stupid compiler + // would then scream about type-punned pointers. Screw it. + void **llist = (void **)list; + void **next = (void **)*llist; + *llist = *next; + + return (void *)next; +} + +// Free all the elements of a linked list +// if freeit!=NULL call freeit() on each element before freeing it. +static +void TOY_llist_free(void *list, void (*freeit)(void *data)) +{ + while (list) { + void *pop = TOY_llist_pop(&list); + if (freeit) freeit(pop); + else free(pop); + + // End doubly linked list too. + if (list==pop) break; + } +} + +// Add an entry to the end off a doubly linked list +static +struct double_list *dlist_add(struct double_list **list, char *data) +{ + struct double_list *line = xmalloc(sizeof(struct double_list)); + + line->data = data; + if (*list) { + line->next = *list; + line->prev = (*list)->prev; + (*list)->prev->next = line; + (*list)->prev = line; + } else *list = line->next = line->prev = line; + + return line; +} + +// Ensure entire path exists. +// If mode != -1 set permissions on newly created dirs. +// Requires that path string be writable (for temporary null terminators). +static +void xmkpath(char *path, int mode) +{ + char *p, old; + mode_t mask; + int rc; + struct stat st; + + for (p = path; ; p++) { + if (!*p || *p == '/') { + old = *p; + *p = rc = 0; + if (stat(path, &st) || !S_ISDIR(st.st_mode)) { + if (mode != -1) { + mask = umask(0); + rc = mkdir(path, mode); + umask(mask); + } else rc = mkdir(path, 0777); + } + *p = old; + if(rc) bb_perror_msg_and_die("mkpath '%s'", path); } - free(line); - lines_count--; + if (!*p) break; + } +} + +// Slow, but small. +static +char *get_rawline(int fd, long *plen, char end) +{ + char c, *buf = NULL; + long len = 0; + + for (;;) { + if (1>read(fd, &c, 1)) break; + if (!(len & 63)) buf=xrealloc(buf, len+65); + if ((buf[len++]=c) == end) break; + } + if (buf) buf[len]=0; + if (plen) *plen = len; + + return buf; +} + +static +char *get_line(int fd) +{ + long len; + char *buf = get_rawline(fd, &len, '\n'); + + if (buf && buf[--len]=='\n') buf[len]=0; + + return buf; +} + +// Copy the rest of in to out and close both files. +static +void xsendfile(int in, int out) +{ + long len; + char buf[4096]; + + if (in<0) return; + for (;;) { + len = safe_read(in, buf, 4096); + if (len<1) break; + xwrite(out, buf, len); + } +} + +// Copy the rest of the data and replace the original with the copy. +static +void replace_tempfile(int fdin, int fdout, char **tempname) +{ + char *temp = xstrdup(*tempname); + + temp[strlen(temp)-6]=0; + if (fdin != -1) { + xsendfile(fdin, fdout); + xclose(fdin); } - return lines_count; + xclose(fdout); + rename(*tempname, temp); + free(*tempname); + free(temp); + *tempname = NULL; +} + +// Open a temporary file to copy an existing file into. +static +int copy_tempfile(int fdin, char *name, char **tempname) +{ + struct stat statbuf; + int fd; + + *tempname = xasprintf("%sXXXXXX", name); + fd = mkstemp(*tempname); + if(-1 == fd) bb_perror_msg_and_die("no temp file"); + + // Set permissions of output file + fstat(fdin, &statbuf); + fchmod(fd, statbuf.st_mode); + + return fd; +} + +// Abort the copy and delete the temporary file. +static +void delete_tempfile(int fdin, int fdout, char **tempname) +{ + close(fdin); + close(fdout); + unlink(*tempname); + free(*tempname); + *tempname = NULL; +} + + + +struct globals { + char *infile; + long prefix; + + struct double_list *current_hunk; + long oldline, oldlen, newline, newlen, linenum; + int context, state, filein, fileout, filepatch, hunknum; + char *tempname; + + // was toys.foo: + int exitval; +}; +#define TT (*ptr_to_globals) +#define INIT_TT() do { \ + SET_PTR_TO_GLOBALS(xzalloc(sizeof(TT))); \ +} while (0) + + +//bbox had: "p:i:RN" +#define FLAG_STR "Rup:i:x" +/* FLAG_REVERSE must be == 1! Code uses this fact. */ +#define FLAG_REVERSE (1 << 0) +#define FLAG_u (1 << 1) +#define FLAG_PATHLEN (1 << 2) +#define FLAG_INPUT (1 << 3) +//non-standard: +#define FLAG_DEBUG (1 << 4) + +// Dispose of a line of input, either by writing it out or discarding it. + +// state < 2: just free +// state = 2: write whole line to stderr +// state = 3: write whole line to fileout +// state > 3: write line+1 to fileout when *line != state + +#define PATCH_DEBUG (option_mask32 & FLAG_DEBUG) + +static void do_line(void *data) +{ + struct double_list *dlist = (struct double_list *)data; + + if (TT.state>1 && *dlist->data != TT.state) + fdprintf(TT.state == 2 ? 2 : TT.fileout, + "%s\n", dlist->data+(TT.state>3 ? 1 : 0)); + + if (PATCH_DEBUG) fdprintf(2, "DO %d: %s\n", TT.state, dlist->data); + + free(dlist->data); + free(data); +} + +static void finish_oldfile(void) +{ + if (TT.tempname) replace_tempfile(TT.filein, TT.fileout, &TT.tempname); + TT.fileout = TT.filein = -1; } -/* If patch_level is -1 it will remove all directory names - * char *line must be greater than 4 chars - * returns NULL if the file doesnt exist or error - * returns malloc'ed filename - * NB: frees 1st argument! - */ -static char *extract_filename(char *line, int patch_level, const char *pat) +static void fail_hunk(void) { - char *temp = NULL, *filename_start_ptr = line + 4; - - if (strncmp(line, pat, 4) == 0) { - /* Terminate string at end of source filename */ - line[strcspn(line, "\t\n\r")] = '\0'; - - /* Skip over (patch_level) number of leading directories */ - while (patch_level--) { - temp = strchr(filename_start_ptr, '/'); - if (!temp) - break; - filename_start_ptr = temp + 1; + if (!TT.current_hunk) return; + TT.current_hunk->prev->next = 0; + + fdprintf(2, "Hunk %d FAILED %ld/%ld.\n", TT.hunknum, TT.oldline, TT.newline); + TT.exitval = 1; + + // If we got to this point, we've seeked to the end. Discard changes to + // this file and advance to next file. + + TT.state = 2; + TOY_llist_free(TT.current_hunk, do_line); + TT.current_hunk = NULL; + delete_tempfile(TT.filein, TT.fileout, &TT.tempname); + TT.state = 0; +} + +// Given a hunk of a unified diff, make the appropriate change to the file. +// This does not use the location information, but instead treats a hunk +// as a sort of regex. Copies data from input to output until it finds +// the change to be made, then outputs the changed data and returns. +// (Finding EOF first is an error.) This is a single pass operation, so +// multiple hunks must occur in order in the file. + +static int apply_one_hunk(void) +{ + struct double_list *plist, *buf = NULL, *check; + int matcheof = 0, reverse = option_mask32 & FLAG_REVERSE, backwarn = 0; + + // Break doubly linked list so we can use singly linked traversal function. + TT.current_hunk->prev->next = NULL; + + // Match EOF if there aren't as many ending context lines as beginning + for (plist = TT.current_hunk; plist; plist = plist->next) { + if (plist->data[0]==' ') matcheof++; + else matcheof = 0; + if (PATCH_DEBUG) fdprintf(2, "HUNK:%s\n", plist->data); + } + matcheof = matcheof < TT.context; + + if (PATCH_DEBUG) fdprintf(2,"MATCHEOF=%c\n", matcheof ? 'Y' : 'N'); + + // Loop through input data searching for this hunk. Match all context + // lines and all lines to be removed until we've found the end of a + // complete hunk. + plist = TT.current_hunk; + buf = NULL; + if (TT.context) for (;;) { + char *data = get_line(TT.filein); + + TT.linenum++; + + // Figure out which line of hunk to compare with next. (Skip lines + // of the hunk we'd be adding.) + while (plist && *plist->data == "+-"[reverse]) { + if (data && !strcmp(data, plist->data+1)) { + if (!backwarn) { + fdprintf(2,"Possibly reversed hunk %d at %ld\n", + TT.hunknum, TT.linenum); + backwarn++; + } + } + plist = plist->next; } - temp = xstrdup(filename_start_ptr); + + // Is this EOF? + if (!data) { + if (PATCH_DEBUG) fdprintf(2, "INEOF\n"); + + // Does this hunk need to match EOF? + if (!plist && matcheof) break; + + // File ended before we found a place for this hunk. + fail_hunk(); + goto done; + } else if (PATCH_DEBUG) fdprintf(2, "IN: %s\n", data); + check = dlist_add(&buf, data); + + // Compare this line with next expected line of hunk. + // todo: teach the strcmp() to ignore whitespace. + + // A match can fail because the next line doesn't match, or because + // we hit the end of a hunk that needed EOF, and this isn't EOF. + + // If match failed, flush first line of buffered data and + // recheck buffered data for a new match until we find one or run + // out of buffer. + + for (;;) { + if (!plist || strcmp(check->data, plist->data+1)) { + // Match failed. Write out first line of buffered data and + // recheck remaining buffered data for a new match. + + if (PATCH_DEBUG) + fdprintf(2, "NOT: %s\n", plist->data); + + TT.state = 3; + check = TOY_llist_pop(&buf); + check->prev->next = buf; + buf->prev = check->prev; + do_line(check); + plist = TT.current_hunk; + + // If we've reached the end of the buffer without confirming a + // match, read more lines. + if (check==buf) { + buf = 0; + break; + } + check = buf; + } else { + if (PATCH_DEBUG) + fdprintf(2, "MAYBE: %s\n", plist->data); + // This line matches. Advance plist, detect successful match. + plist = plist->next; + if (!plist && !matcheof) goto out; + check = check->next; + if (check == buf) break; + } + } + } +out: + // We have a match. Emit changed data. + TT.state = "-+"[reverse]; + TOY_llist_free(TT.current_hunk, do_line); + TT.current_hunk = NULL; + TT.state = 1; +done: + if (buf) { + buf->prev->next = NULL; + TOY_llist_free(buf, do_line); } - free(line); - return temp; + + return TT.state; } +// Read a patch file and find hunks, opening/creating/deleting files. +// Call apply_one_hunk() on each hunk. + +// state 0: Not in a hunk, look for +++. +// state 1: Found +++ file indicator, look for @@ +// state 2: In hunk: counting initial context lines +// state 3: In hunk: getting body + int patch_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; int patch_main(int argc UNUSED_PARAM, char **argv) { - struct stat saved_stat; - char *patch_line; - FILE *patch_file; - int patch_level; - int ret = 0; - char plus = '+'; - unsigned opt; - enum { - OPT_R = (1 << 2), - OPT_N = (1 << 3), - /*OPT_f = (1 << 4), ignored */ - /*OPT_E = (1 << 5), ignored, this is the default */ - /*OPT_g = (1 << 6), ignored */ - OPT_dry_run = (1 << 7) * ENABLE_LONG_OPTS, - }; - - xfunc_error_retval = 2; - { - const char *p = "-1"; - const char *i = "-"; /* compat */ -#if ENABLE_LONG_OPTS - static const char patch_longopts[] ALIGN1 = - "strip\0" Required_argument "p" - "input\0" Required_argument "i" - "reverse\0" No_argument "R" - "forward\0" No_argument "N" - /* "Assume user knows what [s]he is doing, do not ask any questions": */ - "force\0" No_argument "f" /*ignored*/ -# if ENABLE_DESKTOP - "remove-empty-files\0" No_argument "E" /*ignored*/ - /* "Controls actions when a file is under RCS or SCCS control, - * and does not exist or is read-only and matches the default version, - * or when a file is under ClearCase control and does not exist..." - * IOW: rather obscure option. - * But Gentoo's portage does use -g0 */ - "get\0" Required_argument "g" /*ignored*/ -# endif - "dry-run\0" No_argument "\xfd" -# if ENABLE_DESKTOP - "backup-if-mismatch\0" No_argument "\xfe" /*ignored*/ - "no-backup-if-mismatch\0" No_argument "\xff" /*ignored*/ -# endif - ; - applet_long_options = patch_longopts; -#endif - /* -f,-E,-g are ignored */ - opt = getopt32(argv, "p:i:RN""fEg:", &p, &i, NULL); - if (opt & OPT_R) - plus = '-'; - patch_level = xatoi(p); /* can be negative! */ - patch_file = xfopen_stdin(i); - } + int opts; + int reverse, state = 0; + char *oldname = NULL, *newname = NULL; + char *opt_p, *opt_i; + + INIT_TT(); + + opts = getopt32(argv, FLAG_STR, &opt_p, &opt_i); + reverse = opts & FLAG_REVERSE; + TT.prefix = (opts & FLAG_PATHLEN) ? xatoi(opt_p) : 0; // can be negative! + if (opts & FLAG_INPUT) TT.filepatch = xopen(opt_i, O_RDONLY); + TT.filein = TT.fileout = -1; - patch_line = xmalloc_fgetline(patch_file); - while (patch_line) { - FILE *src_stream; - FILE *dst_stream; - //char *old_filename; - char *new_filename; - char *backup_filename = NULL; - unsigned src_cur_line = 1; - unsigned dst_cur_line = 0; - unsigned dst_beg_line; - unsigned bad_hunk_count = 0; - unsigned hunk_count = 0; - smallint copy_trailing_lines_flag = 0; - - /* Skip everything upto the "---" marker - * No need to parse the lines "Only in <dir>", and "diff <args>" - */ - do { - /* Extract the filename used before the patch was generated */ - new_filename = extract_filename(patch_line, patch_level, "--- "); - // was old_filename above - patch_line = xmalloc_fgetline(patch_file); - if (!patch_line) goto quit; - } while (!new_filename); - free(new_filename); // "source" filename is irrelevant - - new_filename = extract_filename(patch_line, patch_level, "+++ "); - if (!new_filename) { - bb_error_msg_and_die("invalid patch"); + // Loop through the lines in the patch + for(;;) { + char *patchline; + + patchline = get_line(TT.filepatch); + if (!patchline) break; + + // Other versions of patch accept damaged patches, + // so we need to also. + if (!*patchline) { + free(patchline); + patchline = xstrdup(" "); } - /* Get access rights from the file to be patched */ - if (stat(new_filename, &saved_stat) != 0) { - char *slash = strrchr(new_filename, '/'); - if (slash) { - /* Create leading directories */ - *slash = '\0'; - bb_make_directory(new_filename, -1, FILEUTILS_RECUR); - *slash = '/'; + // Are we assembling a hunk? + if (state >= 2) { + if (*patchline==' ' || *patchline=='+' || *patchline=='-') { + dlist_add(&TT.current_hunk, patchline); + + if (*patchline != '+') TT.oldlen--; + if (*patchline != '-') TT.newlen--; + + // Context line? + if (*patchline==' ' && state==2) TT.context++; + else state=3; + + // If we've consumed all expected hunk lines, apply the hunk. + + if (!TT.oldlen && !TT.newlen) state = apply_one_hunk(); + continue; } - src_stream = NULL; - saved_stat.st_mode = 0644; - } else if (!(opt & OPT_dry_run)) { - backup_filename = xasprintf("%s.orig", new_filename); - xrename(new_filename, backup_filename); - src_stream = xfopen_for_read(backup_filename); - } else - src_stream = xfopen_for_read(new_filename); - - if (opt & OPT_dry_run) { - dst_stream = xfopen_for_write("/dev/null"); - } else { - dst_stream = xfopen_for_write(new_filename); - fchmod(fileno(dst_stream), saved_stat.st_mode); + fail_hunk(); + state = 0; + continue; } - printf("patching file %s\n", new_filename); - - /* Handle all hunks for this file */ - patch_line = xmalloc_fgets(patch_file); - while (patch_line) { - unsigned count; - unsigned src_beg_line; - unsigned hunk_offset_start; - unsigned src_last_line = 1; - unsigned dst_last_line = 1; - - if ((sscanf(patch_line, "@@ -%d,%d +%d,%d", &src_beg_line, &src_last_line, &dst_beg_line, &dst_last_line) < 3) - && (sscanf(patch_line, "@@ -%d +%d,%d", &src_beg_line, &dst_beg_line, &dst_last_line) < 2) - ) { - /* No more hunks for this file */ - break; - } - if (plus != '+') { - /* reverse patch */ - unsigned tmp = src_last_line; - src_last_line = dst_last_line; - dst_last_line = tmp; - tmp = src_beg_line; - src_beg_line = dst_beg_line; - dst_beg_line = tmp; + // Open a new file? + if (!strncmp("--- ", patchline, 4) || !strncmp("+++ ", patchline, 4)) { + char *s, **name = reverse ? &newname : &oldname; + int i; + + if (*patchline == '+') { + name = reverse ? &oldname : &newname; + state = 1; } - hunk_count++; - - if (src_beg_line && dst_beg_line) { - /* Copy unmodified lines upto start of hunk */ - /* src_beg_line will be 0 if it's a new file */ - count = src_beg_line - src_cur_line; - if (copy_lines(src_stream, dst_stream, count)) { - bb_error_msg_and_die("bad src file"); - } - src_cur_line += count; - dst_cur_line += count; - copy_trailing_lines_flag = 1; + + free(*name); + finish_oldfile(); + + // Trim date from end of filename (if any). We don't care. + for (s = patchline+4; *s && *s!='\t'; s++) + if (*s=='\\' && s[1]) s++; + i = atoi(s); + if (i>1900 && i<=1970) + *name = xstrdup("/dev/null"); + else { + *s = 0; + *name = xstrdup(patchline+4); } - src_last_line += hunk_offset_start = src_cur_line; - dst_last_line += dst_cur_line; - - while (1) { - free(patch_line); - patch_line = xmalloc_fgets(patch_file); - if (patch_line == NULL) - break; /* EOF */ - if (!*patch_line) { - /* whitespace-damaged patch with "" lines */ - free(patch_line); - patch_line = xstrdup(" "); + + // We defer actually opening the file because svn produces broken + // patches that don't signal they want to create a new file the + // way the patch man page says, so you have to read the first hunk + // and _guess_. + + // Start a new hunk? + } else if (state == 1 && !strncmp("@@ -", patchline, 4)) { + int i; + + i = sscanf(patchline+4, "%ld,%ld +%ld,%ld", &TT.oldline, + &TT.oldlen, &TT.newline, &TT.newlen); + if (i != 4) + bb_error_msg_and_die("corrupt hunk %d at %ld", TT.hunknum, TT.linenum); + + TT.context = 0; + state = 2; + + // If this is the first hunk, open the file. + if (TT.filein == -1) { + int oldsum, newsum, del = 0; + char *s, *name; + + oldsum = TT.oldline + TT.oldlen; + newsum = TT.newline + TT.newlen; + + name = reverse ? oldname : newname; + + // We're deleting oldname if new file is /dev/null (before -p) + // or if new hunk is empty (zero context) after patching + if (!strcmp(name, "/dev/null") || !(reverse ? oldsum : newsum)) + { + name = reverse ? newname : oldname; + del++; } - if ((*patch_line != '-') && (*patch_line != '+') - && (*patch_line != ' ') - ) { - break; /* End of hunk */ + + // handle -p path truncation. + for (i=0, s = name; *s;) { + if ((option_mask32 & FLAG_PATHLEN) && TT.prefix == i) break; + if (*(s++)=='/') { + name = s; + i++; + } } - if (*patch_line != plus) { /* '-' or ' ' */ - char *src_line = NULL; - if (src_cur_line == src_last_line) - break; - if (src_stream) { - src_line = xmalloc_fgets(src_stream); - if (src_line) { - int diff = strcmp(src_line, patch_line + 1); - src_cur_line++; - free(src_line); - if (diff) - src_line = NULL; + + if (del) { + printf("removing %s\n", name); + xunlink(name); + state = 0; + // If we've got a file to open, do so. + } else if (!(option_mask32 & FLAG_PATHLEN) || i <= TT.prefix) { + // If the old file was null, we're creating a new one. + if (!strcmp(oldname, "/dev/null") || !oldsum) { + printf("creating %s\n", name); + s = strrchr(name, '/'); + if (s) { + *s = 0; + xmkpath(name, -1); + *s = '/'; } + TT.filein = xopen3(name, O_CREAT|O_EXCL|O_RDWR, 0666); + } else { + printf("patching file %s\n", name); + TT.filein = xopen(name, O_RDWR); } - /* Do not patch an already patched hunk with -N */ - if (src_line == 0 && (opt & OPT_N)) { - continue; - } - if (!src_line) { - bb_error_msg("hunk #%u FAILED at %u", hunk_count, hunk_offset_start); - bad_hunk_count++; - break; - } - if (*patch_line != ' ') { /* '-' */ - continue; - } + TT.fileout = copy_tempfile(TT.filein, name, &TT.tempname); + TT.linenum = 0; + TT.hunknum = 0; } - if (dst_cur_line == dst_last_line) - break; - fputs(patch_line + 1, dst_stream); - dst_cur_line++; - } /* end of while loop handling one hunk */ - } /* end of while loop handling one file */ - - /* Cleanup last patched file */ - if (copy_trailing_lines_flag) { - copy_lines(src_stream, dst_stream, (unsigned)(-1)); - } - if (src_stream) { - fclose(src_stream); - } - fclose(dst_stream); - if (bad_hunk_count) { - ret = 1; - bb_error_msg("%u out of %u hunk FAILED", bad_hunk_count, hunk_count); - } else { - /* It worked, we can remove the backup */ - if (backup_filename) { - unlink(backup_filename); - } - if (!(opt & OPT_dry_run) - && ((dst_cur_line == 0) || (dst_beg_line == 0)) - ) { - /* The new patched file is empty, remove it */ - xunlink(new_filename); - // /* old_filename and new_filename may be the same file */ - // unlink(old_filename); } + + TT.hunknum++; + + continue; } - free(backup_filename); - //free(old_filename); - free(new_filename); - } /* end of "while there are patch lines" */ - quit: - /* 0 = SUCCESS - * 1 = Some hunks failed - * 2 = More serious problems (exited earlier) - */ - return ret; + + // If we didn't continue above, discard this line. + free(patchline); + } + + finish_oldfile(); + + if (ENABLE_FEATURE_CLEAN_UP) { + close(TT.filepatch); + free(oldname); + free(newname); + } + + return TT.exitval; } diff --git a/editors/patch_bbox.c b/editors/patch_bbox.c new file mode 100644 index 000000000..62477af16 --- /dev/null +++ b/editors/patch_bbox.c @@ -0,0 +1,306 @@ +/* vi: set sw=4 ts=4: */ +/* + * busybox patch applet to handle the unified diff format. + * Copyright (C) 2003 Glenn McGrath + * + * Licensed under the GPL v2 or later, see the file LICENSE in this tarball. + * + * This applet is written to work with patches generated by GNU diff, + * where there is equivalent functionality busybox patch shall behave + * as per GNU patch. + * + * There is a SUSv3 specification for patch, however it looks to be + * incomplete, it doesnt even mention unified diff format. + * http://www.opengroup.org/onlinepubs/007904975/utilities/patch.html + * + * Issues + * - Non-interactive + * - Patches must apply cleanly or patch (not just one hunk) will fail. + * - Reject file isnt saved + */ + +#include "libbb.h" + +static unsigned copy_lines(FILE *src_stream, FILE *dst_stream, unsigned lines_count) +{ + while (src_stream && lines_count) { + char *line; + line = xmalloc_fgets(src_stream); + if (line == NULL) { + break; + } + if (fputs(line, dst_stream) == EOF) { + bb_perror_msg_and_die("error writing to new file"); + } + free(line); + lines_count--; + } + return lines_count; +} + +/* If patch_level is -1 it will remove all directory names + * char *line must be greater than 4 chars + * returns NULL if the file doesnt exist or error + * returns malloc'ed filename + * NB: frees 1st argument! + */ +static char *extract_filename(char *line, int patch_level, const char *pat) +{ + char *temp = NULL, *filename_start_ptr = line + 4; + + if (strncmp(line, pat, 4) == 0) { + /* Terminate string at end of source filename */ + line[strcspn(line, "\t\n\r")] = '\0'; + + /* Skip over (patch_level) number of leading directories */ + while (patch_level--) { + temp = strchr(filename_start_ptr, '/'); + if (!temp) + break; + filename_start_ptr = temp + 1; + } + temp = xstrdup(filename_start_ptr); + } + free(line); + return temp; +} + +int patch_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; +int patch_main(int argc UNUSED_PARAM, char **argv) +{ + struct stat saved_stat; + char *patch_line; + FILE *patch_file; + int patch_level; + int ret = 0; + char plus = '+'; + unsigned opt; + enum { + OPT_R = (1 << 2), + OPT_N = (1 << 3), + /*OPT_f = (1 << 4), ignored */ + /*OPT_E = (1 << 5), ignored, this is the default */ + /*OPT_g = (1 << 6), ignored */ + OPT_dry_run = (1 << 7) * ENABLE_LONG_OPTS, + }; + + xfunc_error_retval = 2; + { + const char *p = "-1"; + const char *i = "-"; /* compat */ +#if ENABLE_LONG_OPTS + static const char patch_longopts[] ALIGN1 = + "strip\0" Required_argument "p" + "input\0" Required_argument "i" + "reverse\0" No_argument "R" + "forward\0" No_argument "N" + /* "Assume user knows what [s]he is doing, do not ask any questions": */ + "force\0" No_argument "f" /*ignored*/ +# if ENABLE_DESKTOP + "remove-empty-files\0" No_argument "E" /*ignored*/ + /* "Controls actions when a file is under RCS or SCCS control, + * and does not exist or is read-only and matches the default version, + * or when a file is under ClearCase control and does not exist..." + * IOW: rather obscure option. + * But Gentoo's portage does use -g0 */ + "get\0" Required_argument "g" /*ignored*/ +# endif + "dry-run\0" No_argument "\xfd" +# if ENABLE_DESKTOP + "backup-if-mismatch\0" No_argument "\xfe" /*ignored*/ + "no-backup-if-mismatch\0" No_argument "\xff" /*ignored*/ +# endif + ; + applet_long_options = patch_longopts; +#endif + /* -f,-E,-g are ignored */ + opt = getopt32(argv, "p:i:RN""fEg:", &p, &i, NULL); + if (opt & OPT_R) + plus = '-'; + patch_level = xatoi(p); /* can be negative! */ + patch_file = xfopen_stdin(i); + } + + patch_line = xmalloc_fgetline(patch_file); + while (patch_line) { + FILE *src_stream; + FILE *dst_stream; + //char *old_filename; + char *new_filename; + char *backup_filename = NULL; + unsigned src_cur_line = 1; + unsigned dst_cur_line = 0; + unsigned dst_beg_line; + unsigned bad_hunk_count = 0; + unsigned hunk_count = 0; + smallint copy_trailing_lines_flag = 0; + + /* Skip everything upto the "---" marker + * No need to parse the lines "Only in <dir>", and "diff <args>" + */ + do { + /* Extract the filename used before the patch was generated */ + new_filename = extract_filename(patch_line, patch_level, "--- "); + // was old_filename above + patch_line = xmalloc_fgetline(patch_file); + if (!patch_line) goto quit; + } while (!new_filename); + free(new_filename); // "source" filename is irrelevant + + new_filename = extract_filename(patch_line, patch_level, "+++ "); + if (!new_filename) { + bb_error_msg_and_die("invalid patch"); + } + + /* Get access rights from the file to be patched */ + if (stat(new_filename, &saved_stat) != 0) { + char *slash = strrchr(new_filename, '/'); + if (slash) { + /* Create leading directories */ + *slash = '\0'; + bb_make_directory(new_filename, -1, FILEUTILS_RECUR); + *slash = '/'; + } + src_stream = NULL; + saved_stat.st_mode = 0644; + } else if (!(opt & OPT_dry_run)) { + backup_filename = xasprintf("%s.orig", new_filename); + xrename(new_filename, backup_filename); + src_stream = xfopen_for_read(backup_filename); + } else + src_stream = xfopen_for_read(new_filename); + + if (opt & OPT_dry_run) { + dst_stream = xfopen_for_write("/dev/null"); + } else { + dst_stream = xfopen_for_write(new_filename); + fchmod(fileno(dst_stream), saved_stat.st_mode); + } + + printf("patching file %s\n", new_filename); + + /* Handle all hunks for this file */ + patch_line = xmalloc_fgets(patch_file); + while (patch_line) { + unsigned count; + unsigned src_beg_line; + unsigned hunk_offset_start; + unsigned src_last_line = 1; + unsigned dst_last_line = 1; + + if ((sscanf(patch_line, "@@ -%d,%d +%d,%d", &src_beg_line, &src_last_line, &dst_beg_line, &dst_last_line) < 3) + && (sscanf(patch_line, "@@ -%d +%d,%d", &src_beg_line, &dst_beg_line, &dst_last_line) < 2) + ) { + /* No more hunks for this file */ + break; + } + if (plus != '+') { + /* reverse patch */ + unsigned tmp = src_last_line; + src_last_line = dst_last_line; + dst_last_line = tmp; + tmp = src_beg_line; + src_beg_line = dst_beg_line; + dst_beg_line = tmp; + } + hunk_count++; + + if (src_beg_line && dst_beg_line) { + /* Copy unmodified lines upto start of hunk */ + /* src_beg_line will be 0 if it's a new file */ + count = src_beg_line - src_cur_line; + if (copy_lines(src_stream, dst_stream, count)) { + bb_error_msg_and_die("bad src file"); + } + src_cur_line += count; + dst_cur_line += count; + copy_trailing_lines_flag = 1; + } + src_last_line += hunk_offset_start = src_cur_line; + dst_last_line += dst_cur_line; + + while (1) { + free(patch_line); + patch_line = xmalloc_fgets(patch_file); + if (patch_line == NULL) + break; /* EOF */ + if (!*patch_line) { + /* whitespace-damaged patch with "" lines */ + free(patch_line); + patch_line = xstrdup(" "); + } + if ((*patch_line != '-') && (*patch_line != '+') + && (*patch_line != ' ') + ) { + break; /* End of hunk */ + } + if (*patch_line != plus) { /* '-' or ' ' */ + char *src_line = NULL; + if (src_cur_line == src_last_line) + break; + if (src_stream) { + src_line = xmalloc_fgets(src_stream); + if (src_line) { + int diff = strcmp(src_line, patch_line + 1); + src_cur_line++; + free(src_line); + if (diff) + src_line = NULL; + } + } + /* Do not patch an already patched hunk with -N */ + if (src_line == 0 && (opt & OPT_N)) { + continue; + } + if (!src_line) { + bb_error_msg("hunk #%u FAILED at %u", hunk_count, hunk_offset_start); + bad_hunk_count++; + break; + } + if (*patch_line != ' ') { /* '-' */ + continue; + } + } + if (dst_cur_line == dst_last_line) + break; + fputs(patch_line + 1, dst_stream); + dst_cur_line++; + } /* end of while loop handling one hunk */ + } /* end of while loop handling one file */ + + /* Cleanup last patched file */ + if (copy_trailing_lines_flag) { + copy_lines(src_stream, dst_stream, (unsigned)(-1)); + } + if (src_stream) { + fclose(src_stream); + } + fclose(dst_stream); + if (bad_hunk_count) { + ret = 1; + bb_error_msg("%u out of %u hunk FAILED", bad_hunk_count, hunk_count); + } else { + /* It worked, we can remove the backup */ + if (backup_filename) { + unlink(backup_filename); + } + if (!(opt & OPT_dry_run) + && ((dst_cur_line == 0) || (dst_beg_line == 0)) + ) { + /* The new patched file is empty, remove it */ + xunlink(new_filename); + // /* old_filename and new_filename may be the same file */ + // unlink(old_filename); + } + } + free(backup_filename); + //free(old_filename); + free(new_filename); + } /* end of "while there are patch lines" */ + quit: + /* 0 = SUCCESS + * 1 = Some hunks failed + * 2 = More serious problems (exited earlier) + */ + return ret; +} diff --git a/editors/patch_toybox.c b/editors/patch_toybox.c index 0e5c070e2..7f3234e66 100644 --- a/editors/patch_toybox.c +++ b/editors/patch_toybox.c @@ -1,4 +1,4 @@ -/* Adapted from toybox's patch. Currently unused */ +/* Adapted from toybox's patch. */ /* vi: set sw=4 ts=4: * @@ -23,7 +23,7 @@ * -F fuzz (number, default 2) * [file] which file to patch -USE_PATCH(NEWTOY(patch, "up#i:R", TOYFLAG_USR|TOYFLAG_BIN)) +USE_PATCH(NEWTOY(patch, USE_TOYBOX_DEBUG("x")"up#i:R", TOYFLAG_USR|TOYFLAG_BIN)) config PATCH bool "patch" @@ -223,15 +223,16 @@ void delete_tempfile(int fdin, int fdout, char **tempname) struct globals { - struct double_list *plines; - long linenum; - int context; - int hunknum; - int filein; - int fileout; - int state; + char *infile; + long prefix; + + struct double_list *current_hunk; + long oldline, oldlen, newline, newlen, linenum; + int context, state, filein, fileout, filepatch, hunknum; char *tempname; - smallint exitval; + + // was toys.foo: + int exitval; }; #define TT (*ptr_to_globals) #define INIT_TT() do { \ @@ -240,12 +241,14 @@ struct globals { //bbox had: "p:i:RN" -#define FLAG_STR "Rup:i:" +#define FLAG_STR "Rup:i:x" /* FLAG_REVERSE must be == 1! Code uses this fact. */ #define FLAG_REVERSE (1 << 0) #define FLAG_u (1 << 1) #define FLAG_PATHLEN (1 << 2) #define FLAG_INPUT (1 << 3) +//non-standard: +#define FLAG_DEBUG (1 << 4) // Dispose of a line of input, either by writing it out or discarding it. @@ -254,6 +257,8 @@ struct globals { // state = 3: write whole line to fileout // state > 3: write line+1 to fileout when *line != state +#define PATCH_DEBUG (option_mask32 & FLAG_DEBUG) + static void do_line(void *data) { struct double_list *dlist = (struct double_list *)data; @@ -262,6 +267,8 @@ static void do_line(void *data) fdprintf(TT.state == 2 ? 2 : TT.fileout, "%s\n", dlist->data+(TT.state>3 ? 1 : 0)); + if (PATCH_DEBUG) fdprintf(2, "DO %d: %s\n", TT.state, dlist->data); + free(dlist->data); free(data); } @@ -274,94 +281,118 @@ static void finish_oldfile(void) static void fail_hunk(void) { - if (!TT.plines) return; - TT.plines->prev->next = 0; + if (!TT.current_hunk) return; + TT.current_hunk->prev->next = 0; - fdprintf(2, "Hunk %d FAILED.\n", TT.hunknum); + fdprintf(2, "Hunk %d FAILED %ld/%ld.\n", TT.hunknum, TT.oldline, TT.newline); TT.exitval = 1; // If we got to this point, we've seeked to the end. Discard changes to // this file and advance to next file. TT.state = 2; - TOY_llist_free(TT.plines, do_line); - TT.plines = NULL; + TOY_llist_free(TT.current_hunk, do_line); + TT.current_hunk = NULL; delete_tempfile(TT.filein, TT.fileout, &TT.tempname); TT.state = 0; } -static int apply_hunk(void) +// Given a hunk of a unified diff, make the appropriate change to the file. +// This does not use the location information, but instead treats a hunk +// as a sort of regex. Copies data from input to output until it finds +// the change to be made, then outputs the changed data and returns. +// (Finding EOF first is an error.) This is a single pass operation, so +// multiple hunks must occur in order in the file. + +static int apply_one_hunk(void) { struct double_list *plist, *buf = NULL, *check; - int i = 0, backwards = 0, matcheof = 0, - reverse = option_mask32 & FLAG_REVERSE; + int matcheof = 0, reverse = option_mask32 & FLAG_REVERSE, backwarn = 0; // Break doubly linked list so we can use singly linked traversal function. - TT.plines->prev->next = NULL; + TT.current_hunk->prev->next = NULL; // Match EOF if there aren't as many ending context lines as beginning - for (plist = TT.plines; plist; plist = plist->next) { - if (plist->data[0]==' ') i++; - else i = 0; + for (plist = TT.current_hunk; plist; plist = plist->next) { + if (plist->data[0]==' ') matcheof++; + else matcheof = 0; + if (PATCH_DEBUG) fdprintf(2, "HUNK:%s\n", plist->data); } - if (i < TT.context) matcheof++; + matcheof = matcheof < TT.context; - // Search for a place to apply this hunk. Match all context lines and - // lines to be removed. - plist = TT.plines; - buf = NULL; - i = 0; + if (PATCH_DEBUG) fdprintf(2,"MATCHEOF=%c\n", matcheof ? 'Y' : 'N'); - // Start of for loop + // Loop through input data searching for this hunk. Match all context + // lines and all lines to be removed until we've found the end of a + // complete hunk. + plist = TT.current_hunk; + buf = NULL; if (TT.context) for (;;) { char *data = get_line(TT.filein); TT.linenum++; - // Skip lines of the hunk we'd be adding. + // Figure out which line of hunk to compare with next. (Skip lines + // of the hunk we'd be adding.) while (plist && *plist->data == "+-"[reverse]) { if (data && !strcmp(data, plist->data+1)) { - if (++backwards == TT.context) + if (!backwarn) { fdprintf(2,"Possibly reversed hunk %d at %ld\n", TT.hunknum, TT.linenum); - } else backwards=0; + backwarn++; + } + } plist = plist->next; } // Is this EOF? if (!data) { + if (PATCH_DEBUG) fdprintf(2, "INEOF\n"); + // Does this hunk need to match EOF? if (!plist && matcheof) break; // File ended before we found a place for this hunk. fail_hunk(); goto done; - } + } else if (PATCH_DEBUG) fdprintf(2, "IN: %s\n", data); check = dlist_add(&buf, data); + // Compare this line with next expected line of hunk. // todo: teach the strcmp() to ignore whitespace. - for (;;) { - // If we hit the end of a hunk that needed EOF and this isn't EOF, - // or next line doesn't match, flush first line of buffered data and - // recheck match until we find a new match or run out of buffer. + // A match can fail because the next line doesn't match, or because + // we hit the end of a hunk that needed EOF, and this isn't EOF. + + // If match failed, flush first line of buffered data and + // recheck buffered data for a new match until we find one or run + // out of buffer. + for (;;) { if (!plist || strcmp(check->data, plist->data+1)) { - // First line isn't a match, write it out. + // Match failed. Write out first line of buffered data and + // recheck remaining buffered data for a new match. + + if (PATCH_DEBUG) + fdprintf(2, "NOT: %s\n", plist->data); + TT.state = 3; check = TOY_llist_pop(&buf); check->prev->next = buf; buf->prev = check->prev; do_line(check); - plist = TT.plines; + plist = TT.current_hunk; - // Out of buffered lines? + // If we've reached the end of the buffer without confirming a + // match, read more lines. if (check==buf) { buf = 0; break; } check = buf; } else { + if (PATCH_DEBUG) + fdprintf(2, "MAYBE: %s\n", plist->data); // This line matches. Advance plist, detect successful match. plist = plist->next; if (!plist && !matcheof) goto out; @@ -371,10 +402,10 @@ static int apply_hunk(void) } } out: - // Got it. Emit changed data. + // We have a match. Emit changed data. TT.state = "-+"[reverse]; - TOY_llist_free(TT.plines, do_line); - TT.plines = NULL; + TOY_llist_free(TT.current_hunk, do_line); + TT.current_hunk = NULL; TT.state = 1; done: if (buf) { @@ -385,6 +416,9 @@ done: return TT.state; } +// Read a patch file and find hunks, opening/creating/deleting files. +// Call apply_one_hunk() on each hunk. + // state 0: Not in a hunk, look for +++. // state 1: Found +++ file indicator, look for @@ // state 2: In hunk: counting initial context lines @@ -397,24 +431,20 @@ int patch_main(int argc UNUSED_PARAM, char **argv) int reverse, state = 0; char *oldname = NULL, *newname = NULL; char *opt_p, *opt_i; - int prefix; - - long oldline = 0, oldlen = 0, newline = 0, newlen = 0; INIT_TT(); opts = getopt32(argv, FLAG_STR, &opt_p, &opt_i); reverse = opts & FLAG_REVERSE; - - if (opts & FLAG_INPUT) xmove_fd(xopen(opt_i, O_RDONLY), STDIN_FILENO); - prefix = (opts & FLAG_PATHLEN) ? xatoi(opt_p) : 0; // can be negative! + TT.prefix = (opts & FLAG_PATHLEN) ? xatoi(opt_p) : 0; // can be negative! + if (opts & FLAG_INPUT) TT.filepatch = xopen(opt_i, O_RDONLY); TT.filein = TT.fileout = -1; // Loop through the lines in the patch for(;;) { char *patchline; - patchline = get_line(STDIN_FILENO); + patchline = get_line(TT.filepatch); if (!patchline) break; // Other versions of patch accept damaged patches, @@ -427,17 +457,18 @@ int patch_main(int argc UNUSED_PARAM, char **argv) // Are we assembling a hunk? if (state >= 2) { if (*patchline==' ' || *patchline=='+' || *patchline=='-') { - dlist_add(&TT.plines, patchline); + dlist_add(&TT.current_hunk, patchline); - if (*patchline != '+') oldlen--; - if (*patchline != '-') newlen--; + if (*patchline != '+') TT.oldlen--; + if (*patchline != '-') TT.newlen--; // Context line? if (*patchline==' ' && state==2) TT.context++; else state=3; // If we've consumed all expected hunk lines, apply the hunk. - if (!oldlen && !newlen) state = apply_hunk(); + + if (!TT.oldlen && !TT.newlen) state = apply_one_hunk(); continue; } fail_hunk(); @@ -447,11 +478,11 @@ int patch_main(int argc UNUSED_PARAM, char **argv) // Open a new file? if (!strncmp("--- ", patchline, 4) || !strncmp("+++ ", patchline, 4)) { - char *s, **name = &oldname; + char *s, **name = reverse ? &newname : &oldname; int i; if (*patchline == '+') { - name = &newname; + name = reverse ? &oldname : &newname; state = 1; } @@ -462,7 +493,7 @@ int patch_main(int argc UNUSED_PARAM, char **argv) for (s = patchline+4; *s && *s!='\t'; s++) if (*s=='\\' && s[1]) s++; i = atoi(s); - if (i && i<=1970) + if (i>1900 && i<=1970) *name = xstrdup("/dev/null"); else { *s = 0; @@ -478,8 +509,8 @@ int patch_main(int argc UNUSED_PARAM, char **argv) } else if (state == 1 && !strncmp("@@ -", patchline, 4)) { int i; - i = sscanf(patchline+4, "%ld,%ld +%ld,%ld", - &oldline, &oldlen, &newline, &newlen); + i = sscanf(patchline+4, "%ld,%ld +%ld,%ld", &TT.oldline, + &TT.oldlen, &TT.newline, &TT.newlen); if (i != 4) bb_error_msg_and_die("corrupt hunk %d at %ld", TT.hunknum, TT.linenum); @@ -491,22 +522,22 @@ int patch_main(int argc UNUSED_PARAM, char **argv) int oldsum, newsum, del = 0; char *s, *name; - oldsum = oldline + oldlen; - newsum = newline + newlen; + oldsum = TT.oldline + TT.oldlen; + newsum = TT.newline + TT.newlen; name = reverse ? oldname : newname; // We're deleting oldname if new file is /dev/null (before -p) // or if new hunk is empty (zero context) after patching - if (!strcmp(name, "/dev/null") || !(reverse ? oldsum : newsum)) { + if (!strcmp(name, "/dev/null") || !(reverse ? oldsum : newsum)) + { name = reverse ? newname : oldname; del++; } // handle -p path truncation. for (i=0, s = name; *s;) { - if ((option_mask32 & FLAG_PATHLEN) && prefix == i) - break; + if ((option_mask32 & FLAG_PATHLEN) && TT.prefix == i) break; if (*(s++)=='/') { name = s; i++; @@ -518,7 +549,7 @@ int patch_main(int argc UNUSED_PARAM, char **argv) xunlink(name); state = 0; // If we've got a file to open, do so. - } else if (!(option_mask32 & FLAG_PATHLEN) || i <= prefix) { + } else if (!(option_mask32 & FLAG_PATHLEN) || i <= TT.prefix) { // If the old file was null, we're creating a new one. if (!strcmp(oldname, "/dev/null") || !oldsum) { printf("creating %s\n", name); @@ -551,6 +582,7 @@ int patch_main(int argc UNUSED_PARAM, char **argv) finish_oldfile(); if (ENABLE_FEATURE_CLEAN_UP) { + close(TT.filepatch); free(oldname); free(newname); } diff --git a/testsuite/patch.tests b/testsuite/patch.tests index 749d936ea..6ee795dba 100755 --- a/testsuite/patch.tests +++ b/testsuite/patch.tests @@ -7,7 +7,7 @@ # testing "test name" "options" "expected result" "file input" "stdin" testing "patch with old_file == new_file" \ - 'patch; echo $?; cat input' \ + 'patch 2>&1; echo $?; cat input' \ "\ patching file input 0 @@ -29,7 +29,7 @@ zxc " \ testing "patch with nonexistent old_file" \ - 'patch; echo $?; cat input' \ + 'patch 2>&1; echo $?; cat input' \ "\ patching file input 0 @@ -51,7 +51,7 @@ zxc " \ testing "patch -R with nonexistent old_file" \ - 'patch -R; echo $?; cat input' \ + 'patch -R 2>&1; echo $?; cat input' \ "\ patching file input 0 @@ -75,9 +75,12 @@ zxc testing "patch detects already applied hunk" \ 'patch 2>&1; echo $?; cat input' \ "\ +Possibly reversed hunk 1 at 2 +Hunk 1 FAILED 1/1. + abc ++def + 123 patching file input -patch: hunk #1 FAILED at 1 -patch: 1 out of 1 hunk FAILED 1 abc def diff --git a/testsuite/testing.sh b/testsuite/testing.sh index f907deade..c7c9ca6af 100644 --- a/testsuite/testing.sh +++ b/testsuite/testing.sh @@ -1,4 +1,4 @@ -# Simple test harness infrastructurei for BusyBox +# Simple test harness infrastructure for BusyBox # # Copyright 2005 by Rob Landley # @@ -87,6 +87,7 @@ testing() $ECHO -ne "$3" > expected $ECHO -ne "$4" > input + [ -z "$VERBOSE" ] || echo "echo -ne '$4' >input" [ -z "$VERBOSE" ] || echo "echo -ne '$5' | $2" $ECHO -ne "$5" | eval "$2" > actual RETVAL=$? |