Added the --info=FLAGS an --debug=FLAGS options, which allows
fine-grained output control (in addition to the coarse -v).
diff --git a/NEWS b/NEWS
index 924e788..7be38f2 100644
--- a/NEWS
+++ b/NEWS
@@ -7,11 +7,18 @@
     - Changed the way --progress overwrites its prior output in order to make
       it nearly impossible for the progress to get overwritten by an error.
 
+    - Improved the keep-alive in-loop check in the generator to work properly
+      in incremental recursion mode.
+
   ENHANCEMENTS:
 
     - Added the --remote-option=OPT (-M OPT) command-line option that is useful
       for things like sending a remote --log-file=FILE or --fake-super option.
 
+    - Added the --info=FLAGS and --debug=FLAGS options to allow finer-grained
+      control over what is output.  Added an extra type of --progress output
+      using --info=progress2.
+
     - Rsync will not send an -e option to the server if the user specifies the
       --protocol=29 option.  This lets rsync3 use an overly-restrictive server.
 
diff --git a/acls.c b/acls.c
index d9023e2..3e829ad 100644
--- a/acls.c
+++ b/acls.c
@@ -1081,7 +1081,7 @@
 	/* Apply the permission-bit entries of the default ACL, if any. */
 	if (racl.user_obj != NO_ENTRY) {
 		perms = rsync_acl_get_perms(&racl);
-		if (verbose > 2)
+		if (DEBUG_GTE(ACL, 1))
 			rprintf(FINFO, "got ACL-based default perms %o for directory %s\n", perms, dir);
 	}
 
diff --git a/backup.c b/backup.c
index 934f838..60c85b1 100644
--- a/backup.c
+++ b/backup.c
@@ -20,7 +20,6 @@
 
 #include "rsync.h"
 
-extern int verbose;
 extern int am_root;
 extern int preserve_acls;
 extern int preserve_xattrs;
@@ -62,7 +61,7 @@
 
 	while (1) {
 		if (do_rename(fname, fnamebak) == 0) {
-			if (verbose > 1) {
+			if (INFO_GTE(BACKUP, 1)) {
 				rprintf(FINFO, "backed up %s to %s\n",
 					fname, fnamebak);
 			}
@@ -260,7 +259,7 @@
 			}
 		} else
 			save_errno = 0;
-		if (verbose > 2 && save_errno == 0) {
+		if (DEBUG_GTE(BACKUP, 1) && save_errno == 0) {
 			rprintf(FINFO, "make_backup: DEVICE %s successful.\n",
 				fname);
 		}
@@ -285,7 +284,7 @@
 		}
 
 		ret_code = do_rmdir(fname);
-		if (verbose > 2) {
+		if (DEBUG_GTE(BACKUP, 1)) {
 			rprintf(FINFO, "make_backup: RMDIR %s returns %i\n",
 				full_fname(fname), ret_code);
 		}
@@ -296,7 +295,7 @@
 	if (!kept && preserve_links && S_ISLNK(file->mode)) {
 		const char *sl = F_SYMLINK(file);
 		if (safe_symlinks && unsafe_symlink(sl, buf)) {
-			if (verbose) {
+			if (INFO_GTE(SYMSAFE, 1)) {
 				rprintf(FINFO, "ignoring unsafe symlink %s -> %s\n",
 					full_fname(buf), sl);
 			}
@@ -345,7 +344,7 @@
 	preserve_xattrs = save_preserve_xattrs;
 	unmake_file(file);
 
-	if (verbose > 1) {
+	if (INFO_GTE(BACKUP, 1)) {
 		rprintf(FINFO, "backed up %s to %s\n",
 			fname, buf);
 	}
diff --git a/batch.c b/batch.c
index 033368a..ac89583 100644
--- a/batch.c
+++ b/batch.c
@@ -135,7 +135,7 @@
 					set ? "Please" : "Do not");
 				exit_cleanup(RERR_SYNTAX);
 			}
-			if (verbose) {
+			if (INFO_GTE(MISC, 1)) {
 				rprintf(FINFO,
 					"%sing the %s option to match the batchfile.\n",
 					set ? "Sett" : "Clear", flag_name[i]);
diff --git a/cleanup.c b/cleanup.c
index 279b532..9146782 100644
--- a/cleanup.c
+++ b/cleanup.c
@@ -27,7 +27,7 @@
 extern int io_error;
 extern int keep_partial;
 extern int got_xfer_error;
-extern int progress_is_active;
+extern int output_needs_newline;
 extern char *partial_dir;
 extern char *logfile_name;
 
@@ -116,12 +116,12 @@
 
 		exit_code = unmodified_code = code;
 
-		if (progress_is_active) {
+		if (output_needs_newline) {
 			fputc('\n', stdout);
-			progress_is_active = 0;
+			output_needs_newline = 0;
 		}
 
-		if (verbose > 3) {
+		if (DEBUG_GTE(EXIT, 2)) {
 			rprintf(FINFO,
 				"_exit_cleanup(code=%d, file=%s, line=%d): entered\n",
 				code, file, line);
@@ -184,13 +184,13 @@
 				code = exit_code = RERR_PARTIAL;
 		}
 
-		if (code || am_daemon || (logfile_name && (am_server || !verbose)))
+		if (code || am_daemon || (logfile_name && (am_server || !INFO_GTE(STATS, 1))))
 			log_exit(code, file, line);
 
 		/* FALLTHROUGH */
 #include "case_N.h"
 
-		if (verbose > 2) {
+		if (DEBUG_GTE(EXIT, 1)) {
 			rprintf(FINFO,
 				"_exit_cleanup(code=%d, file=%s, line=%d): "
 				"about to call exit(%d)\n",
diff --git a/clientserver.c b/clientserver.c
index f78c61c..2ba7dbd 100644
--- a/clientserver.c
+++ b/clientserver.c
@@ -23,7 +23,6 @@
 #include "ifuncs.h"
 
 extern int quiet;
-extern int verbose;
 extern int dry_run;
 extern int output_motd;
 extern int list_only;
@@ -267,7 +266,7 @@
 
 	sargs[sargc] = NULL;
 
-	if (verbose > 1)
+	if (DEBUG_GTE(CMD, 1))
 		print_child_argv("sending daemon args:", sargs);
 
 	io_printf(f_out, "%.*s\n", modlen, modname);
@@ -747,7 +746,7 @@
 	read_args(f_in, name, line, sizeof line, rl_nulls, &argv, &argc, &request);
 	orig_argv = argv;
 
-	verbose = 0; /* future verbosity is controlled by client options */
+	reset_output_levels(); /* future verbosity is controlled by client options */
 	ret = parse_arguments(&argc, (const char ***) &argv);
 	if (protect_args && ret) {
 		orig_early_argv = orig_argv;
@@ -798,8 +797,7 @@
 
 #ifndef DEBUG
 	/* don't allow the logs to be flooded too fast */
-	if (verbose > lp_max_verbosity(i))
-		verbose = lp_max_verbosity(i);
+	limit_output_verbosity(lp_max_verbosity(i));
 #endif
 
 	if (protocol_version < 23
diff --git a/compat.c b/compat.c
index ef220e2..811f6ec 100644
--- a/compat.c
+++ b/compat.c
@@ -25,7 +25,6 @@
 int file_extra_cnt = 0; /* count of file-list extras that everyone gets */
 int inc_recurse = 0;
 
-extern int verbose;
 extern int am_server;
 extern int am_sender;
 extern int local_server;
@@ -157,7 +156,7 @@
 		exit_cleanup(RERR_PROTOCOL);
 	}
 
-	if (verbose > 3) {
+	if (DEBUG_GTE(PROTO, 1)) {
 		rprintf(FINFO, "(%s) Protocol versions: remote=%d, negotiated=%d\n",
 			am_server? "Server" : "Client", remote_protocol, protocol_version);
 	}
diff --git a/exclude.c b/exclude.c
index 085d264..080aa49 100644
--- a/exclude.c
+++ b/exclude.c
@@ -22,7 +22,6 @@
 
 #include "rsync.h"
 
-extern int verbose;
 extern int am_server;
 extern int am_sender;
 extern int eol_nulls;
@@ -123,7 +122,7 @@
 	const char *cp;
 	unsigned int pre_len, suf_len, slash_cnt = 0;
 
-	if (verbose > 2) {
+	if (DEBUG_GTE(FILTER, 2)) {
 		rprintf(FINFO, "[%s] add_rule(%s%.*s%s)%s\n",
 			who_am_i(), get_rule_prefix(mflags, pat, 0, NULL),
 			(int)pat_len, pat,
@@ -455,7 +454,7 @@
 		struct filter_struct *ex = mergelist_parents[i];
 		struct filter_list_struct *lp = ex->u.mergelist;
 
-		if (verbose > 2) {
+		if (DEBUG_GTE(FILTER, 2)) {
 			rprintf(FINFO, "[%s] pushing filter list%s\n",
 				who_am_i(), lp->debug_type);
 		}
@@ -495,7 +494,7 @@
 		struct filter_struct *ex = mergelist_parents[i];
 		struct filter_list_struct *lp = ex->u.mergelist;
 
-		if (verbose > 2) {
+		if (DEBUG_GTE(FILTER, 2)) {
 			rprintf(FINFO, "[%s] popping filter list%s\n",
 				who_am_i(), lp->debug_type);
 		}
@@ -629,7 +628,7 @@
 	 * then it is stripped out by add_rule().  So as a special
 	 * case we add it back in here. */
 
-	if (verbose >= 2) {
+	if (DEBUG_GTE(FILTER, 1)) {
 		static char *actions[2][2]
 		    = { {"show", "hid"}, {"risk", "protect"} };
 		const char *w = who_am_i();
@@ -973,7 +972,7 @@
 		}
 
 		if (new_mflags & MATCHFLG_CLEAR_LIST) {
-			if (verbose > 2) {
+			if (DEBUG_GTE(FILTER, 2)) {
 				rprintf(FINFO,
 					"[%s] clearing filter list%s\n",
 					who_am_i(), listp->debug_type);
@@ -1047,7 +1046,7 @@
 	} else
 		fp = stdin;
 
-	if (verbose > 2) {
+	if (DEBUG_GTE(FILTER, 2)) {
 		rprintf(FINFO, "[%s] parse_filter_file(%s,%x,%x)%s\n",
 			who_am_i(), fname, mflags, xflags,
 			fp ? "" : " [not found]");
diff --git a/flist.c b/flist.c
index 18b239e..e0cad79 100644
--- a/flist.c
+++ b/flist.c
@@ -25,14 +25,12 @@
 #include "rounding.h"
 #include "io.h"
 
-extern int verbose;
 extern int am_root;
 extern int am_server;
 extern int am_daemon;
 extern int am_sender;
 extern int am_generator;
 extern int inc_recurse;
-extern int do_progress;
 extern int always_checksum;
 extern int module_id;
 extern int ignore_errors;
@@ -67,6 +65,7 @@
 extern int sanitize_paths;
 extern int munge_symlinks;
 extern int need_unsorted_flist;
+extern int output_needs_newline;
 extern int unsort_ndx;
 extern struct stats stats;
 extern char *filesfrom_host;
@@ -129,7 +128,7 @@
 
 void init_flist(void)
 {
-	if (verbose > 4) {
+	if (DEBUG_GTE(FLIST, 4)) {
 		rprintf(FINFO, "FILE_STRUCT_LEN=%d, EXTRA_LEN=%d\n",
 			(int)FILE_STRUCT_LEN, (int)EXTRA_LEN);
 	}
@@ -140,14 +139,13 @@
 
 static int show_filelist_p(void)
 {
-	return verbose && xfer_dirs && !am_server && !inc_recurse;
+	return INFO_GTE(FLIST, 1) && xfer_dirs && !am_server && !inc_recurse;
 }
 
 static void start_filelist_progress(char *kind)
 {
 	rprintf(FCLIENT, "%s ... ", kind);
-	if (verbose > 1 || do_progress)
-		rprintf(FCLIENT, "\n");
+	output_needs_newline = 1;
 	rflush(FINFO);
 }
 
@@ -158,18 +156,20 @@
 
 static void maybe_emit_filelist_progress(int count)
 {
-	if (do_progress && show_filelist_p() && (count % 100) == 0)
+	if (INFO_GTE(FLIST, 2) && show_filelist_p() && (count % 100) == 0)
 		emit_filelist_progress(count);
 }
 
 static void finish_filelist_progress(const struct file_list *flist)
 {
-	if (do_progress) {
+	if (INFO_GTE(FLIST, 2)) {
 		/* This overwrites the progress line */
 		rprintf(FINFO, "%d file%sto consider\n",
 			flist->used, flist->used == 1 ? " " : "s ");
-	} else
+	} else {
+		output_needs_newline = 0;
 		rprintf(FINFO, "done\n");
+	}
 }
 
 void show_flist_stats(void)
@@ -196,7 +196,7 @@
 			return -1;
 		linkbuf[llen] = '\0';
 		if (copy_unsafe_links && unsafe_symlink(linkbuf, path)) {
-			if (verbose > 1) {
+			if (INFO_GTE(SYMSAFE, 1)) {
 				rprintf(FINFO,"copying unsafe symlink \"%s\" -> \"%s\"\n",
 					path, linkbuf);
 			}
@@ -316,7 +316,7 @@
 	new_ptr = realloc_array(flist->files, struct file_struct *,
 				flist->malloced);
 
-	if (verbose >= 2 && flist->malloced != FLIST_START) {
+	if (DEBUG_GTE(FLIST, 1) && flist->malloced != FLIST_START) {
 		rprintf(FCLIENT, "[%s] expand file_list pointer array to %.0f bytes, did%s move\n",
 		    who_am_i(),
 		    (double)sizeof flist->files[0] * flist->malloced,
@@ -1110,7 +1110,7 @@
 		if (one_file_system && st.st_dev != filesystem_dev
 		 && BITS_SETnUNSET(flags, FLAG_CONTENT_DIR, FLAG_TOP_DIR)) {
 			if (one_file_system > 1) {
-				if (verbose > 1) {
+				if (INFO_GTE(MOUNT, 1)) {
 					rprintf(FINFO,
 					    "[%s] skipping mount-point dir %s\n",
 					    who_am_i(), thisname);
@@ -1159,7 +1159,7 @@
 		pool = NULL;
 	}
 
-	if (verbose > 2) {
+	if (DEBUG_GTE(FLIST, 2)) {
 		rprintf(FINFO, "[%s] make_file(%s,*,%d)\n",
 			who_am_i(), thisname, filter_level);
 	}
@@ -1829,7 +1829,7 @@
 		file_total += flist->used;
 		stats.flist_size += stats.total_written - start_write;
 		stats.num_files += flist->used;
-		if (verbose > 3)
+		if (DEBUG_GTE(FLIST, 3))
 			output_flist(flist);
 
 		if (DIR_FIRST_CHILD(dp) >= 0) {
@@ -1880,7 +1880,7 @@
 	rprintf(FLOG, "building file list\n");
 	if (show_filelist_p())
 		start_filelist_progress("building file list");
-	else if (inc_recurse && verbose && !am_server)
+	else if (inc_recurse && INFO_GTE(FLIST, 1) && !am_server)
 		rprintf(FCLIENT, "sending incremental file list\n");
 
 	start_write = stats.total_written;
@@ -2155,10 +2155,10 @@
 	stats.flist_size = stats.total_written - start_write;
 	stats.num_files = flist->used;
 
-	if (verbose > 3)
+	if (DEBUG_GTE(FLIST, 3))
 		output_flist(flist);
 
-	if (verbose > 2)
+	if (DEBUG_GTE(FLIST, 2))
 		rprintf(FINFO, "send_file_list done\n");
 
 	if (inc_recurse) {
@@ -2192,7 +2192,7 @@
 		rprintf(FLOG, "receiving file list\n");
 	if (show_filelist_p())
 		start_filelist_progress("receiving file list");
-	else if (inc_recurse && verbose && !am_server && !first_flist)
+	else if (inc_recurse && INFO_GTE(FLIST, 1) && !am_server && !first_flist)
 		rprintf(FCLIENT, "receiving incremental file list\n");
 
 	start_read = stats.total_read;
@@ -2231,14 +2231,14 @@
 
 		maybe_emit_filelist_progress(flist->used);
 
-		if (verbose > 2) {
+		if (DEBUG_GTE(FLIST, 2)) {
 			rprintf(FINFO, "recv_file_name(%s)\n",
 				f_name(file, NULL));
 		}
 	}
 	file_total += flist->used;
 
-	if (verbose > 2)
+	if (DEBUG_GTE(FLIST, 2))
 		rprintf(FINFO, "received %d names\n", flist->used);
 
 	if (show_filelist_p())
@@ -2292,10 +2292,10 @@
 			flist->parent_ndx = -1;
 	}
 
-	if (verbose > 3)
+	if (DEBUG_GTE(FLIST, 3))
 		output_flist(flist);
 
-	if (verbose > 2)
+	if (DEBUG_GTE(FLIST, 2))
 		rprintf(FINFO, "recv_file_list done\n");
 
 	stats.flist_size += stats.total_read - start_read;
@@ -2323,7 +2323,7 @@
 				NDX_FLIST_OFFSET - dir_flist->used + 1);
 			exit_cleanup(RERR_PROTOCOL);
 		}
-		if (verbose > 3) {
+		if (DEBUG_GTE(FLIST, 3)) {
 			rprintf(FINFO, "[%s] receiving flist for dir %d\n",
 				who_am_i(), ndx);
 		}
@@ -2545,7 +2545,7 @@
 				keep = j, drop = i;
 
 			if (!am_sender) {
-				if (verbose > 1) {
+				if (DEBUG_GTE(DUP, 1)) {
 					rprintf(FINFO,
 					    "removing duplicate name %s from file list (%d)\n",
 					    f_name(file, fbuf), drop + flist->ndx_start);
@@ -2903,7 +2903,7 @@
 	send_directory(ignore_filter_rules ? -2 : -1, dirlist, dirname, dlen, 0);
 	xfer_dirs = save_xfer_dirs;
 	recurse = save_recurse;
-	if (do_progress)
+	if (INFO_GTE(PROGRESS, 1))
 		flist_count_offset += dirlist->used;
 
 	prune_empty_dirs = 0;
@@ -2911,7 +2911,7 @@
 	flist_sort_and_clean(dirlist, 0);
 	prune_empty_dirs = save_prune_empty_dirs;
 
-	if (verbose > 3)
+	if (DEBUG_GTE(FLIST, 3))
 		output_flist(dirlist);
 
 	return dirlist;
diff --git a/generator.c b/generator.c
index 11cba3c..22f2851 100644
--- a/generator.c
+++ b/generator.c
@@ -22,7 +22,6 @@
 
 #include "rsync.h"
 
-extern int verbose;
 extern int dry_run;
 extern int do_xfers;
 extern int stdout_format_has_i;
@@ -31,7 +30,6 @@
 extern int am_server;
 extern int am_daemon;
 extern int inc_recurse;
-extern int do_progress;
 extern int relative_paths;
 extern int implied_dirs;
 extern int keep_dirlinks;
@@ -160,7 +158,7 @@
 	char *what;
 	int ok;
 
-	if (verbose > 2) {
+	if (DEBUG_GTE(DEL, 2)) {
 		rprintf(FINFO, "delete_item(%s) mode=%o flags=%d\n",
 			fbuf, (int)mode, (int)flags);
 	}
@@ -251,7 +249,7 @@
 	int j, dlen;
 	char *p;
 
-	if (verbose > 3) {
+	if (DEBUG_GTE(DEL, 3)) {
 		rprintf(FINFO, "delete_dir_contents(%s) flags=%d\n",
 			fname, flags);
 	}
@@ -284,7 +282,7 @@
 		struct file_struct *fp = dirlist->files[j];
 
 		if (fp->flags & FLAG_MOUNT_DIR && S_ISDIR(fp->mode)) {
-			if (verbose > 1) {
+			if (DEBUG_GTE(DEL, 1)) {
 				rprintf(FINFO,
 				    "mount point, %s, pins parent directory\n",
 				    f_name(fp, NULL));
@@ -479,7 +477,7 @@
 		return;
 	}
 
-	if (verbose > 2)
+	if (DEBUG_GTE(DEL, 2))
 		rprintf(FINFO, "delete_in_dir(%s)\n", fbuf);
 
 	if (allowed_lull)
@@ -516,7 +514,7 @@
 		if (!F_IS_ACTIVE(fp))
 			continue;
 		if (fp->flags & FLAG_MOUNT_DIR && S_ISDIR(fp->mode)) {
-			if (verbose > 1)
+			if (INFO_GTE(MOUNT, 1))
 				rprintf(FINFO, "cannot delete mount point: %s\n",
 					f_name(fp, NULL));
 			continue;
@@ -564,7 +562,7 @@
 			continue;
 		}
 
-		if (verbose > 1 && file->flags & FLAG_TOP_DIR)
+		if (DEBUG_GTE(DEL, 1) && file->flags & FLAG_TOP_DIR)
 			rprintf(FINFO, "deleting in %s\n", fbuf);
 
 		if (link_stat(fbuf, &st, keep_dirlinks) < 0
@@ -575,7 +573,7 @@
 	}
 	delete_in_dir(NULL, NULL, &dev_zero);
 
-	if (do_progress && !am_server)
+	if (INFO_GTE(FLIST, 2) && !am_server)
 		rprintf(FINFO, "                    \r");
 }
 
@@ -686,7 +684,7 @@
 	}
 
 	iflags &= 0xffff;
-	if ((iflags & (SIGNIFICANT_ITEM_FLAGS|ITEM_REPORT_XATTR) || verbose > 1
+	if ((iflags & (SIGNIFICANT_ITEM_FLAGS|ITEM_REPORT_XATTR) || INFO_GTE(NAME, 2)
 	  || stdout_format_has_i > 1 || (xname && *xname)) && !read_batch) {
 		if (protocol_version >= 29) {
 			if (ndx >= 0)
@@ -805,7 +803,7 @@
 	if ((int64)sum->count != l)
 		sum->count = -1;
 
-	if (sum->count && verbose > 2) {
+	if (sum->count && DEBUG_GTE(CHKSUM, 2)) {
 		rprintf(FINFO,
 			"count=%.0f rem=%ld blength=%ld s2length=%d flength=%.0f\n",
 			(double)sum->count, (long)sum->remainder, (long)sum->blength,
@@ -857,7 +855,7 @@
 		sum1 = get_checksum1(map, n1);
 		get_checksum2(map, n1, sum2);
 
-		if (verbose > 3) {
+		if (DEBUG_GTE(CHKSUM, 3)) {
 			rprintf(FINFO,
 				"chunk[%.0f] offset=%.0f len=%ld sum1=%08lx\n",
 				(double)i, (double)offset - n1, (long)n1,
@@ -899,7 +897,7 @@
 
 		if (F_LENGTH(fp) == F_LENGTH(file)
 		    && cmp_time(fp->modtime, file->modtime) == 0) {
-			if (verbose > 4) {
+			if (DEBUG_GTE(FUZZY, 2)) {
 				rprintf(FINFO,
 					"fuzzy size/modtime match for %s\n",
 					name);
@@ -914,7 +912,7 @@
 		/* Add some extra weight to how well the suffixes match. */
 		dist += fuzzy_distance(suf, suf_len, fname_suf, fname_suf_len)
 		      * 10;
-		if (verbose > 4) {
+		if (DEBUG_GTE(FUZZY, 2)) {
 			rprintf(FINFO, "fuzzy distance for %s = %d.%05d\n",
 				name, (int)(dist>>16), (int)(dist&0xFFFF));
 		}
@@ -947,7 +945,7 @@
 	}
 	cleanup_set(copy_to, NULL, NULL, -1, -1);
 	if (copy_file(src, copy_to, fd_w, file->mode, 0) < 0) {
-		if (verbose) {
+		if (INFO_GTE(COPY, 1)) {
 			rsyserr(FINFO, errno, "copy_file %s => %s",
 				full_fname(src), copy_to);
 		}
@@ -1017,7 +1015,7 @@
 				goto try_a_copy;
 			if (preserve_hard_links && F_IS_HLINKED(file))
 				finish_hard_link(file, fname, ndx, &sxp->st, itemizing, code, j);
-			if (!maybe_ATTRS_REPORT && (verbose > 1 || stdout_format_has_i > 1)) {
+			if (!maybe_ATTRS_REPORT && (INFO_GTE(NAME, 2) || stdout_format_has_i > 1)) {
 				itemize(cmpbuf, file, ndx, 1, sxp,
 					ITEM_LOCAL_CHANGE | ITEM_XNAME_FOLLOWS,
 					0, "");
@@ -1026,7 +1024,7 @@
 #endif
 		if (itemizing)
 			itemize(cmpbuf, file, ndx, 0, sxp, 0, 0, NULL);
-		if (verbose > 1 && maybe_ATTRS_REPORT)
+		if (INFO_GTE(NAME, 2) && maybe_ATTRS_REPORT)
 			rprintf(FCLIENT, "%s is uptodate\n", fname);
 		return -2;
 	}
@@ -1040,8 +1038,8 @@
 		if (itemizing)
 			itemize(cmpbuf, file, ndx, 0, sxp, ITEM_LOCAL_CHANGE, 0, NULL);
 		if (maybe_ATTRS_REPORT
-		 && ((!itemizing && verbose && match_level == 2)
-		  || (verbose > 1 && match_level == 3))) {
+		 && ((!itemizing && INFO_GTE(NAME, 1) && match_level == 2)
+		  || (INFO_GTE(NAME, 2) && match_level == 3))) {
 			code = match_level == 3 ? FCLIENT : FINFO;
 			rprintf(code, "%s%s\n", fname,
 				match_level == 3 ? " is uptodate" : "");
@@ -1181,13 +1179,13 @@
 #endif
 			match_level = 2;
 		if (itemizing && stdout_format_has_i
-		 && (verbose > 1 || stdout_format_has_i > 1)) {
+		 && (INFO_GTE(NAME, 2) || stdout_format_has_i > 1)) {
 			int chg = compare_dest && type != TYPE_DIR ? 0
 			    : ITEM_LOCAL_CHANGE + (match_level == 3 ? ITEM_XNAME_FOLLOWS : 0);
 			char *lp = match_level == 3 ? "" : NULL;
 			itemize(cmpbuf, file, ndx, 0, sxp, chg + ITEM_MATCHED, 0, lp);
 		}
-		if (verbose > 1 && maybe_ATTRS_REPORT) {
+		if (INFO_GTE(NAME, 2) && maybe_ATTRS_REPORT) {
 			rprintf(FCLIENT, "%s%s is uptodate\n",
 				fname, type == TYPE_DIR ? "/" : "");
 		}
@@ -1273,7 +1271,7 @@
 		   : inc_recurse && ndx != cur_flist->ndx_start - 1 ? -1
 		   : 1;
 
-	if (verbose > 2)
+	if (DEBUG_GTE(GENR, 1))
 		rprintf(FINFO, "recv_generator(%s,%d)\n", fname, ndx);
 
 	if (list_only) {
@@ -1377,7 +1375,7 @@
 		else if (F_IS_HLINKED(file))
 			handle_skipped_hlink(file, itemizing, code, f_out);
 #endif
-		if (verbose > 1) {
+		if (INFO_GTE(SKIP, 1)) {
 			rprintf(FINFO, "not creating new %s \"%s\"\n",
 				is_dir ? "directory" : "file", fname);
 		}
@@ -1390,7 +1388,7 @@
 
 	if (ignore_existing > 0 && statret == 0
 	 && (!is_dir || !S_ISDIR(sx.st.st_mode))) {
-		if (verbose > 1 && is_dir >= 0)
+		if (INFO_GTE(SKIP, 1) && is_dir >= 0)
 			rprintf(FINFO, "%s exists\n", fname);
 #ifdef SUPPORT_HARD_LINKS
 		if (F_IS_HLINKED(file))
@@ -1473,7 +1471,7 @@
 			copy_xattrs(fnamecmpbuf, fname);
 #endif
 		if (set_file_attrs(fname, file, real_ret ? NULL : &real_sx, NULL, 0)
-		    && verbose && code != FNONE && f_out != -1)
+		    && INFO_GTE(NAME, 1) && code != FNONE && f_out != -1)
 			rprintf(code, "%s/\n", fname);
 
 		/* We need to ensure that the dirs in the transfer have writable
@@ -1528,7 +1526,7 @@
 #ifdef SUPPORT_LINKS
 		const char *sl = F_SYMLINK(file);
 		if (safe_symlinks && unsafe_symlink(sl, fname)) {
-			if (verbose) {
+			if (INFO_GTE(NAME, 1)) {
 				if (solo_file)
 					fname = f_name(file, NULL);
 				rprintf(FINFO,
@@ -1592,7 +1590,7 @@
 				itemize(fname, file, ndx, statret, &sx,
 					ITEM_LOCAL_CHANGE|ITEM_REPORT_CHANGE, 0, NULL);
 			}
-			if (code != FNONE && verbose)
+			if (code != FNONE && INFO_GTE(NAME, 1))
 				rprintf(code, "%s -> %s\n", fname, sl);
 #ifdef SUPPORT_HARD_LINKS
 			if (preserve_hard_links && F_IS_HLINKED(file))
@@ -1662,7 +1660,7 @@
 			goto cleanup;
 		}
 #endif
-		if (verbose > 2) {
+		if (DEBUG_GTE(GENR, 1)) {
 			rprintf(FINFO, "mknod(%s, 0%o, [%ld,%ld])\n",
 				fname, (int)file->mode,
 				(long)major(rdev), (long)minor(rdev));
@@ -1676,7 +1674,7 @@
 				itemize(fname, file, ndx, statret, &sx,
 					ITEM_LOCAL_CHANGE|ITEM_REPORT_CHANGE, 0, NULL);
 			}
-			if (code != FNONE && verbose)
+			if (code != FNONE && INFO_GTE(NAME, 1))
 				rprintf(code, "%s\n", fname);
 #ifdef SUPPORT_HARD_LINKS
 			if (preserve_hard_links && F_IS_HLINKED(file))
@@ -1696,7 +1694,7 @@
 	}
 
 	if (max_size > 0 && F_LENGTH(file) > max_size) {
-		if (verbose > 1) {
+		if (INFO_GTE(SKIP, 1)) {
 			if (solo_file)
 				fname = f_name(file, NULL);
 			rprintf(FINFO, "%s is over max-size\n", fname);
@@ -1704,7 +1702,7 @@
 		goto cleanup;
 	}
 	if (min_size > 0 && F_LENGTH(file) < min_size) {
-		if (verbose > 1) {
+		if (INFO_GTE(SKIP, 1)) {
 			if (solo_file)
 				fname = f_name(file, NULL);
 			rprintf(FINFO, "%s is under min-size\n", fname);
@@ -1714,7 +1712,7 @@
 
 	if (update_only > 0 && statret == 0
 	    && cmp_time(sx.st.st_mtime, file->modtime) > 0) {
-		if (verbose > 1)
+		if (INFO_GTE(SKIP, 1))
 			rprintf(FINFO, "%s is newer\n", fname);
 #ifdef SUPPORT_HARD_LINKS
 		if (F_IS_HLINKED(file))
@@ -1764,7 +1762,7 @@
 		if (j >= 0) {
 			fuzzy_file = fuzzy_dirlist->files[j];
 			f_name(fuzzy_file, fnamecmpbuf);
-			if (verbose > 2) {
+			if (DEBUG_GTE(FUZZY, 1)) {
 				rprintf(FINFO, "fuzzy basis selected for %s: %s\n",
 					fname, fnamecmpbuf);
 			}
@@ -1905,12 +1903,12 @@
 		fnamecmp_type = FNAMECMP_BACKUP;
 	}
 
-	if (verbose > 3) {
+	if (DEBUG_GTE(CHKSUM, 3)) {
 		rprintf(FINFO, "gen mapped %s of size %.0f\n",
 			fnamecmp, (double)sx.st.st_size);
 	}
 
-	if (verbose > 2)
+	if (DEBUG_GTE(CHKSUM, 2))
 		rprintf(FINFO, "generating and sending sums for %d\n", ndx);
 
   notify_others:
@@ -1981,7 +1979,7 @@
 #endif
 		set_file_attrs(backupptr, back_file, NULL, NULL, 0);
 		preserve_xattrs = save_preserve_xattrs;
-		if (verbose > 1) {
+		if (INFO_GTE(BACKUP, 1)) {
 			rprintf(FINFO, "backed up %s to %s\n",
 				fname, backupptr);
 		}
@@ -2041,7 +2039,7 @@
 		if (!S_ISDIR(file->mode)
 		 || (!implied_dirs && file->flags & FLAG_IMPLIED_DIR))
 			continue;
-		if (verbose > 3) {
+		if (DEBUG_GTE(TIME, 2)) {
 			fname = f_name(file, NULL);
 			rprintf(FINFO, "touch_up_dirs: %s (%d)\n",
 				NS(fname), i);
@@ -2155,7 +2153,8 @@
 	char fbuf[MAXPATHLEN];
 	int itemizing;
 	enum logcode code;
-	int save_do_progress = do_progress;
+	int save_info_flist = info_levels[INFO_FLIST];
+	int save_info_progress = info_levels[INFO_PROGRESS];
 
 	if (protocol_version >= 29) {
 		itemizing = 1;
@@ -2182,7 +2181,7 @@
 	    | (protocol_version >= 30 || !am_server ? ITEM_REPORT_TIMEFAIL : 0);
 	implied_dirs_are_missing = relative_paths && !implied_dirs && protocol_version < 30;
 
-	if (verbose > 2)
+	if (DEBUG_GTE(GENR, 1))
 		rprintf(FINFO, "generator starting pid=%ld\n", (long)getpid());
 
 	if (delete_before && !solo_file && cur_flist->used > 0)
@@ -2193,11 +2192,11 @@
 		if (!deldelay_buf)
 			out_of_memory("delete-delay");
 	}
-	do_progress = 0;
+	info_levels[INFO_FLIST] = info_levels[INFO_PROGRESS] = 0;
 
 	if (append_mode > 0 || whole_file < 0)
 		whole_file = 0;
-	if (verbose >= 2) {
+	if (DEBUG_GTE(FLIST, 1)) {
 		rprintf(FINFO, "delta-transmission %s\n",
 			whole_file
 			? "disabled for local transfer or --whole-file"
@@ -2286,7 +2285,7 @@
 	if (delete_during)
 		delete_in_dir(NULL, NULL, &dev_zero);
 	phase++;
-	if (verbose > 2)
+	if (DEBUG_GTE(GENR, 1))
 		rprintf(FINFO, "generate_files phase=%d\n", phase);
 
 	while (1) {
@@ -2297,7 +2296,7 @@
 	}
 
 	phase++;
-	if (verbose > 2)
+	if (DEBUG_GTE(GENR, 1))
 		rprintf(FINFO, "generate_files phase=%d\n", phase);
 
 	write_ndx(f_out, NDX_DONE);
@@ -2315,7 +2314,7 @@
 
 	if (protocol_version >= 29) {
 		phase++;
-		if (verbose > 2)
+		if (DEBUG_GTE(GENR, 1))
 			rprintf(FINFO, "generate_files phase=%d\n", phase);
 		if (delay_updates)
 			write_ndx(f_out, NDX_DONE);
@@ -2324,7 +2323,9 @@
 			wait_for_receiver();
 	}
 
-	do_progress = save_do_progress;
+	info_levels[INFO_FLIST] = save_info_flist;
+	info_levels[INFO_PROGRESS] = save_info_progress;
+
 	if (delete_during == 2)
 		do_delayed_deletions(fbuf);
 	if (delete_after && !solo_file && file_total > 0)
@@ -2341,6 +2342,6 @@
 		io_error |= IOERR_DEL_LIMIT;
 	}
 
-	if (verbose > 2)
+	if (DEBUG_GTE(GENR, 1))
 		rprintf(FINFO, "generate_files finished\n");
 }
diff --git a/hlink.c b/hlink.c
index 1e8adc8..d6d0ecf 100644
--- a/hlink.c
+++ b/hlink.c
@@ -22,7 +22,6 @@
 
 #include "rsync.h"
 
-extern int verbose;
 extern int dry_run;
 extern int list_only;
 extern int am_sender;
@@ -215,7 +214,7 @@
 					ITEM_LOCAL_CHANGE | ITEM_XNAME_FOLLOWS,
 					0, "");
 			}
-			if (verbose > 1 && maybe_ATTRS_REPORT)
+			if (INFO_GTE(NAME, 2) && maybe_ATTRS_REPORT)
 				rprintf(FCLIENT, "%s is uptodate\n", fname);
 			file->flags |= FLAG_HLINK_DONE;
 			return 0;
@@ -236,7 +235,7 @@
 				ITEM_LOCAL_CHANGE | ITEM_XNAME_FOLLOWS, 0,
 				realname);
 		}
-		if (code != FNONE && verbose)
+		if (code != FNONE && INFO_GTE(NAME, 1))
 			rprintf(code, "%s => %s\n", fname, realname);
 		return 0;
 	}
@@ -377,10 +376,10 @@
 					continue;
 				statret = 1;
 				if (stdout_format_has_i == 0
-				 || (verbose < 2 && stdout_format_has_i < 2)) {
+				 || (!INFO_GTE(NAME, 2) && stdout_format_has_i < 2)) {
 					itemizing = 0;
 					code = FNONE;
-					if (verbose > 1 && maybe_ATTRS_REPORT)
+					if (INFO_GTE(NAME, 2) && maybe_ATTRS_REPORT)
 						rprintf(FCLIENT, "%s is uptodate\n", fname);
 				}
 				break;
@@ -426,7 +425,7 @@
 	if (do_link(oldname, fname) < 0) {
 		enum logcode code;
 		if (terse) {
-			if (!verbose)
+			if (!INFO_GTE(NAME, 1))
 				return 0;
 			code = FINFO;
 		} else
diff --git a/io.c b/io.c
index 6a91ffd..b21dff4 100644
--- a/io.c
+++ b/io.c
@@ -408,7 +408,7 @@
 		/* Read extra file list from receiver. */
 		assert(iobuf_in != NULL);
 		assert(iobuf_f_in == fd);
-		if (verbose > 3) {
+		if (DEBUG_GTE(FLIST, 2)) {
 			rprintf(FINFO, "[%s] receiving flist for dir %d\n",
 				who_am_i(), IVAL(buf,0));
 		}
diff --git a/log.c b/log.c
index 9100f5d..4a4c404 100644
--- a/log.c
+++ b/log.c
@@ -22,7 +22,6 @@
 #include "rsync.h"
 #include "ifuncs.h"
 
-extern int verbose;
 extern int dry_run;
 extern int am_daemon;
 extern int am_server;
@@ -37,7 +36,6 @@
 extern int preserve_times;
 extern int uid_ndx;
 extern int gid_ndx;
-extern int progress_is_active;
 extern int stdout_format_has_i;
 extern int stdout_format_has_o_or_i;
 extern int logfile_format_has_i;
@@ -64,6 +62,7 @@
 struct stats stats;
 
 int got_xfer_error = 0;
+int output_needs_newline = 0;
 
 struct {
         int code;
@@ -321,9 +320,9 @@
 		exit_cleanup(RERR_MESSAGEIO);
 	}
 
-	if (progress_is_active) {
+	if (output_needs_newline) {
 		fputc('\n', f);
-		progress_is_active = 0;
+		output_needs_newline = 0;
 	}
 
 	trailing_CR_or_NL = len && (buf[len-1] == '\n' || buf[len-1] == '\r')
@@ -445,7 +444,7 @@
 	if (am_daemon || code == FLOG)
 		return;
 
-	if (code == FINFO && !am_server)
+	if (!am_server && (code == FINFO || code == FCLIENT))
 		f = stdout;
 	else
 		f = stderr;
@@ -767,7 +766,7 @@
 {
 	int significant_flags = iflags & SIGNIFICANT_ITEM_FLAGS;
 	int see_item = itemizing && (significant_flags || *buf
-		|| stdout_format_has_i > 1 || (verbose > 1 && stdout_format_has_i));
+		|| stdout_format_has_i > 1 || (INFO_GTE(NAME, 2) && stdout_format_has_i));
 	int local_change = iflags & ITEM_LOCAL_CHANGE && significant_flags;
 	if (am_server) {
 		if (logfile_name && !dry_run && see_item
@@ -791,7 +790,7 @@
 
 	x.file.mode = mode;
 
-	if (!verbose && !stdout_format)
+	if (!INFO_GTE(DEL, 1) && !stdout_format)
 		;
 	else if (am_server && protocol_version >= 29 && len < MAXPATHLEN) {
 		if (S_ISDIR(mode))
diff --git a/main.c b/main.c
index 6881c8e..5ca8210 100644
--- a/main.c
+++ b/main.c
@@ -27,7 +27,6 @@
 #include <locale.h>
 #endif
 
-extern int verbose;
 extern int dry_run;
 extern int list_only;
 extern int am_root;
@@ -38,9 +37,9 @@
 extern int inc_recurse;
 extern int blocking_io;
 extern int remove_source_files;
+extern int output_needs_newline;
 extern int need_messages_from_generator;
 extern int kluge_around_eof;
-extern int do_stats;
 extern int got_xfer_error;
 extern int module_id;
 extern int copy_links;
@@ -183,7 +182,7 @@
 	total_read = stats.total_read;
 	total_written = stats.total_written;
 
-	if (do_stats && verbose > 1) {
+	if (INFO_GTE(STATS, 3)) {
 		/* These come out from every process */
 		show_malloc_stats();
 		show_flist_stats();
@@ -239,7 +238,7 @@
 
 static void output_summary(void)
 {
-	if (do_stats) {
+	if (INFO_GTE(STATS, 2)) {
 		rprintf(FCLIENT, "\n");
 		rprintf(FINFO,"Number of files: %d\n", stats.num_files);
 		rprintf(FINFO,"Number of files transferred: %d\n",
@@ -268,7 +267,7 @@
 			human_num(total_read));
 	}
 
-	if (verbose || do_stats) {
+	if (INFO_GTE(STATS, 1)) {
 		rprintf(FCLIENT, "\n");
 		rprintf(FINFO,
 			"sent %s bytes  received %s bytes  %s bytes/sec\n",
@@ -434,7 +433,7 @@
 
 	args[argc] = NULL;
 
-	if (verbose > 3) {
+	if (DEBUG_GTE(CMD, 2)) {
 		for (i = 0; i < argc; i++)
 			rprintf(FCLIENT, "cmd[%d]=%s ", i, args[i]);
 		rprintf(FCLIENT, "\n");
@@ -499,7 +498,7 @@
 	int statret;
 	char *cp;
 
-	if (verbose > 2) {
+	if (DEBUG_GTE(RECV, 1)) {
 		rprintf(FINFO, "get_local_name count=%d %s\n",
 			file_total, NS(dest_path));
 	}
@@ -580,7 +579,7 @@
 		 && strcmp(flist->files[flist->low]->basename, ".") == 0)
 			flist->files[0]->flags |= FLAG_DIR_CREATED;
 
-		if (verbose)
+		if (INFO_GTE(NAME, 1))
 			rprintf(FINFO, "created directory %s\n", dest_path);
 
 		if (dry_run) {
@@ -682,7 +681,7 @@
 	struct file_list *flist;
 	char *dir = argv[0];
 
-	if (verbose > 2) {
+	if (DEBUG_GTE(SEND, 1)) {
 		rprintf(FINFO, "server_sender starting pid=%ld\n",
 			(long)getpid());
 	}
@@ -775,6 +774,11 @@
 		io_flush(FULL_FLUSH);
 		handle_stats(f_in);
 
+		if (output_needs_newline) {
+			fputc('\n', stdout);
+			output_needs_newline = 0;
+		}
+
 		send_msg(MSG_DONE, "", 1, 0);
 		write_varlong(error_pipe[1], stats.total_read, 3);
 		io_flush(FULL_FLUSH);
@@ -848,15 +852,17 @@
 	int exit_code;
 	struct file_list *flist;
 	char *local_name = NULL;
-	int save_verbose = verbose;
+	int negated_levels;
 
 	if (filesfrom_fd >= 0) {
 		/* We can't mix messages with files-from data on the socket,
-		 * so temporarily turn off verbose messages. */
-		verbose = 0;
-	}
+		 * so temporarily turn off info/debug messages. */
+		negate_output_levels();
+		negated_levels = 1;
+	} else
+		negated_levels = 0;
 
-	if (verbose > 2) {
+	if (DEBUG_GTE(RECV, 1)) {
 		rprintf(FINFO, "server_recv(%d) starting pid=%ld\n",
 			argc, (long)getpid());
 	}
@@ -901,7 +907,9 @@
 	}
 	if (inc_recurse && file_total == 1)
 		recv_additional_file_list(f_in);
-	verbose = save_verbose;
+
+	if (negated_levels)
+		negate_output_levels();
 
 	if (argc > 0)
 		local_name = get_local_name(flist,argv[0]);
@@ -1017,7 +1025,7 @@
 			start_write_batch(f_out);
 		flist = send_file_list(f_out, argc, argv);
 		set_msg_fd_in(-1);
-		if (verbose > 3)
+		if (DEBUG_GTE(FLIST, 3))
 			rprintf(FINFO,"file list sent\n");
 
 		if (protocol_version >= 23)
@@ -1030,7 +1038,7 @@
 		if (protocol_version >= 24)
 			read_final_goodbye(f_in);
 		if (pid != -1) {
-			if (verbose > 3)
+			if (DEBUG_GTE(EXIT, 2))
 				rprintf(FINFO,"client_run waiting on %d\n", (int) pid);
 			io_flush(FULL_FLUSH);
 			wait_process_with_flush(pid, &exit_code);
@@ -1072,7 +1080,7 @@
 	}
 
 	if (pid != -1) {
-		if (verbose > 3)
+		if (DEBUG_GTE(RECV, 1))
 			rprintf(FINFO,"client_run2 waiting on %d\n", (int) pid);
 		io_flush(FULL_FLUSH);
 		wait_process_with_flush(pid, &exit_code);
@@ -1254,7 +1262,7 @@
 		}
 	}
 
-	if (verbose > 3) {
+	if (DEBUG_GTE(CMD, 2)) {
 		rprintf(FINFO,"cmd=%s machine=%s user=%s path=%s\n",
 			NS(shell_cmd), NS(shell_machine), NS(shell_user),
 			remote_argv ? NS(remote_argv[0]) : "");
diff --git a/match.c b/match.c
index 280e09f..bb40ebf 100644
--- a/match.c
+++ b/match.c
@@ -21,8 +21,6 @@
 
 #include "rsync.h"
 
-extern int verbose;
-extern int do_progress;
 extern int checksum_seed;
 extern int append_mode;
 
@@ -108,7 +106,7 @@
 	int32 n = (int32)(offset - last_match); /* max value: block_size (int32) */
 	int32 j;
 
-	if (verbose > 2 && i >= 0) {
+	if (DEBUG_GTE(CHKSUM, 2) && i >= 0) {
 		rprintf(FINFO,
 			"match at %.0f last_match=%.0f j=%d len=%ld n=%ld\n",
 			(double)offset, (double)last_match, i,
@@ -133,7 +131,7 @@
 	else
 		last_match = offset;
 
-	if (buf && do_progress)
+	if (buf && INFO_GTE(PROGRESS, 1))
 		show_progress(last_match, buf->file_size);
 }
 
@@ -152,7 +150,7 @@
 	 * coding of the output to work more efficiently. */
 	want_i = 0;
 
-	if (verbose > 2) {
+	if (DEBUG_GTE(CHKSUM, 2)) {
 		rprintf(FINFO, "hash search b=%ld len=%.0f\n",
 			(long)s->blength, (double)len);
 	}
@@ -164,14 +162,14 @@
 	sum = get_checksum1((char *)map, k);
 	s1 = sum & 0xFFFF;
 	s2 = sum >> 16;
-	if (verbose > 3)
+	if (DEBUG_GTE(CHKSUM, 3))
 		rprintf(FINFO, "sum=%.8x k=%ld\n", sum, (long)k);
 
 	offset = 0;
 
 	end = len + 1 - s->sums[s->count-1].len;
 
-	if (verbose > 3) {
+	if (DEBUG_GTE(CHKSUM, 3)) {
 		rprintf(FINFO, "hash search s->blength=%ld len=%.0f count=%.0f\n",
 			(long)s->blength, (double)len, (double)s->count);
 	}
@@ -180,7 +178,7 @@
 		int done_csum2 = 0;
 		int32 i;
 
-		if (verbose > 4) {
+		if (DEBUG_GTE(CHKSUM, 4)) {
 			rprintf(FINFO, "offset=%.0f sum=%04x%04x\n",
 				(double)offset, s2 & 0xFFFF, s1 & 0xFFFF);
 		}
@@ -213,7 +211,7 @@
 			    && !(s->sums[i].flags & SUMFLG_SAME_OFFSET))
 				continue;
 
-			if (verbose > 3) {
+			if (DEBUG_GTE(CHKSUM, 3)) {
 				rprintf(FINFO,
 					"potential match at %.0f i=%ld sum=%08x\n",
 					(double)offset, (long)i, sum);
@@ -344,7 +342,7 @@
 		if (append_mode == 2) {
 			OFF_T j = 0;
 			for (j = CHUNK_SIZE; j < s->flength; j += CHUNK_SIZE) {
-				if (buf && do_progress)
+				if (buf && INFO_GTE(PROGRESS, 1))
 					show_progress(last_match, buf->file_size);
 				sum_update(map_ptr(buf, last_match, CHUNK_SIZE),
 					   CHUNK_SIZE);
@@ -352,7 +350,7 @@
 			}
 			if (last_match < s->flength) {
 				int32 n = (int32)(s->flength - last_match);
-				if (buf && do_progress)
+				if (buf && INFO_GTE(PROGRESS, 1))
 					show_progress(last_match, buf->file_size);
 				sum_update(map_ptr(buf, last_match, n), n);
 			}
@@ -364,12 +362,12 @@
 	if (len > 0 && s->count > 0) {
 		build_hash_table(s);
 
-		if (verbose > 2)
+		if (DEBUG_GTE(CHKSUM, 2))
 			rprintf(FINFO,"built hash table\n");
 
 		hash_search(f, s, buf, len);
 
-		if (verbose > 2)
+		if (DEBUG_GTE(CHKSUM, 2))
 			rprintf(FINFO,"done hash search\n");
 	} else {
 		OFF_T j;
@@ -384,11 +382,11 @@
 	if (buf && buf->status != 0)
 		file_sum[0]++;
 
-	if (verbose > 2)
+	if (DEBUG_GTE(CHKSUM, 2))
 		rprintf(FINFO,"sending file_sum\n");
 	write_buf(f, file_sum, sum_len);
 
-	if (verbose > 2)
+	if (DEBUG_GTE(CHKSUM, 2))
 		rprintf(FINFO, "false_alarms=%d hash_hits=%d matches=%d\n",
 			false_alarms, hash_hits, matches);
 
@@ -400,7 +398,7 @@
 
 void match_report(void)
 {
-	if (verbose <= 1)
+	if (!DEBUG_GTE(CHKSUM, 1))
 		return;
 
 	rprintf(FINFO,
diff --git a/options.c b/options.c
index d534cec..1bdea18 100644
--- a/options.c
+++ b/options.c
@@ -99,8 +99,6 @@
 int allow_inc_recurse = 1;
 int xfer_dirs = -1;
 int am_daemon = 0;
-int do_stats = 0;
-int do_progress = 0;
 int connect_timeout = 0;
 int keep_partial = 0;
 int safe_symlinks = 0;
@@ -178,7 +176,6 @@
 int remote_option_cnt = 0;
 const char **remote_options = NULL;
 
-int verbose = 0;
 int quiet = 0;
 int output_motd = 1;
 int log_before_transfer = 0;
@@ -199,6 +196,93 @@
 
 struct chmod_mode_struct *chmod_modes = NULL;
 
+static char *debug_verbosity[] = {
+	/*0*/ NULL,
+	/*1*/ NULL,
+	/*2*/ "bind,cmd,chksum,connect,del,dup,filter,flist",
+	/*3*/ "acl,backup,chksum2,del2,exit,filter2,flist2,fuzzy,genr,own,recv,send,time",
+	/*4*/ "cmd2,chksum3,del3,exit2,flist3,iconv,own2,proto,time2",
+	/*5*/ "chdir,chksum4,flist4,fuzzy2,hlink",
+};
+
+#define MAX_VERBOSITY ((int)(sizeof debug_verbosity / sizeof debug_verbosity[0]) - 1)
+
+static char *info_verbosity[MAX_VERBOSITY] = {
+	/*0*/ NULL,
+	/*1*/ "copy,del,flist,misc,name,stats,symsafe",
+	/*2*/ "backup,misc2,mount,name2,remove,skip",
+};
+
+#define MAX_OUT_LEVEL 4 /* The largest N allowed for any flagN word. */
+
+short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG];
+
+#define DEFAULT_PRIORITY 0 	/* Default/implied/--verbose set values. */
+#define HELP_PRIORITY 1		/* The help output uses this level. */
+#define USER_PRIORITY 2		/* User-specified via --info or --debug */
+#define LIMIT_PRIORITY 3	/* Overriding priority when limiting values. */
+
+#define W_CLI (1<<0)	/* client side */
+#define W_SRV (1<<1)	/* server side */
+#define W_SND (1<<2)	/* sending side */
+#define W_REC (1<<3)	/* receiving side */
+
+struct output_struct {
+	char *name;	/* The name of the info/debug flag. */
+	char *help;	/* The description of the info/debug flag. */
+	short flag;	/* The flag's value, for consistency check. */
+	short where;	/* Bits indicating where the flag is used. */
+	short priority; /* See *_PRIORITY defines. */
+};
+
+#define INFO_WORD(flag, where, help) { #flag, help, INFO_##flag, where, 0 }
+
+static struct output_struct info_words[] = {
+	INFO_WORD(BACKUP, W_REC, "Mention files backed up"),
+	INFO_WORD(COPY, W_REC, "Mention files copied locally on the receiving side"),
+	INFO_WORD(DEL, W_REC, "Mention deletions on the receiving side"),
+	INFO_WORD(FLIST, W_CLI, "Mention file-list receiving/sending (levels 1-2)"),
+	INFO_WORD(MISC, W_SND|W_REC, "Mention miscellaneous information (levels 1-2)"),
+	INFO_WORD(MOUNT, W_SND|W_REC, "Mention mounts that were found or skipped"),
+	INFO_WORD(NAME, W_SND|W_REC, "Mention 1) updated file/dir names, 2) unchanged names"),
+	INFO_WORD(PROGRESS, W_CLI, "Mention 1) per-file progress or 2) total transfer progress"),
+	INFO_WORD(REMOVE, W_SND, "Mention files removed on the sending side"),
+	INFO_WORD(SKIP, W_REC, "Mention files that are skipped due to options used"),
+	INFO_WORD(STATS, W_CLI|W_SRV, "Mention statistics at end of run (levels 1-3)"),
+	INFO_WORD(SYMSAFE, W_SND|W_REC, "Mention symlinks that are unsafe"),
+	{ NULL, "--info", 0, 0, 0 }
+};
+
+#define DEBUG_WORD(flag, where, help) { #flag, help, DEBUG_##flag, where, 0 }
+
+static struct output_struct debug_words[] = {
+	DEBUG_WORD(ACL, W_SND|W_REC, "Debug extra ACL info"),
+	DEBUG_WORD(BACKUP, W_REC, "Debug backup actions (levels 1-2)"),
+	DEBUG_WORD(BIND, W_CLI, "Debug socket bind actions"),
+	DEBUG_WORD(CHDIR, W_CLI|W_SRV, "Debug when the current directory changes"),
+	DEBUG_WORD(CONNECT, W_CLI, "Debug connection events"),
+	DEBUG_WORD(CHKSUM, W_SND|W_REC, "Debug delta-transfer checksumming (levels 1-4)"),
+	DEBUG_WORD(CMD, W_CLI, "Debug commands+options that are issued (levels 1-2)"),
+	DEBUG_WORD(DEL, W_REC, "Debug delete actions (levels 1-3)"),
+	DEBUG_WORD(DUP, W_REC, "Debug weeding of duplicate names"),
+	DEBUG_WORD(EXIT, W_CLI|W_SRV, "Debug exit events (levels 1-2)"),
+	DEBUG_WORD(FILTER, W_SND|W_REC, "Debug filter actions (levels 1-2)"),
+	DEBUG_WORD(FLIST, W_SND|W_REC, "Debug file-list operations (levels 1-4)"),
+	DEBUG_WORD(FUZZY, W_REC, "Debug fuzzy scoring (levels 1-2)"),
+	DEBUG_WORD(GENR, W_REC, "Debug generator functions"),
+	DEBUG_WORD(HLINK, W_SND|W_REC, "Debug hard-link actions"),
+	DEBUG_WORD(ICONV, W_CLI|W_SRV, "Debug iconv (character conversion)"),
+	DEBUG_WORD(OWN, W_REC, "Debug ownership changes in users & groups (levels 1-2)"),
+	DEBUG_WORD(PROTO, W_CLI|W_SRV, "Debug protocol information"),
+	DEBUG_WORD(RECV, W_REC, "Debug receiver functions"),
+	DEBUG_WORD(SEND, W_SND, "Debug sender functions"),
+	DEBUG_WORD(TIME, W_REC, "Debug setting of modified times (levels 1-2)"),
+	{ NULL, "--debug", 0, 0, 0 }
+};
+
+static int verbose = 0;
+static int do_stats = 0;
+static int do_progress = 0;
 static int daemon_opt;   /* sets am_daemon after option error-reporting */
 static int omit_dir_times = 0;
 static int F_option_cnt = 0;
@@ -216,6 +300,252 @@
  * address, or a hostname. **/
 char *bind_address;
 
+static void output_item_help(struct output_struct *words);
+
+/* This constructs a string that represents all the options set for either
+ * the --info or --debug setting, skipping any implied options (by -v, etc.).
+ * This is used both when conveying the user's options to the server, and
+ * when the help output wants to tell the user what options are implied. */
+static char *make_output_option(struct output_struct *words, short *levels, short where)
+{
+	char *str = words == info_words ? "--info=" : "--debug=";
+	int j, counts[MAX_OUT_LEVEL+1], pos, skipped = 0, len = 0, max = 0, lev = 0;
+	int word_count = words == info_words ? COUNT_INFO : COUNT_DEBUG;
+	char *buf;
+
+	memset(counts, 0, sizeof counts);
+
+	for (j = 0; words[j].name; j++) {
+		if (words[j].flag != j) {
+			rprintf(FERROR, "rsync: internal error on %s%s: %d != %d\n",
+				words == info_words ? "INFO_" : "DEBUG_",
+				words[j].name, words[j].flag, j);
+			exit_cleanup(RERR_UNSUPPORTED);
+		}
+		if (!(words[j].where & where))
+			continue;
+		if (words[j].priority == DEFAULT_PRIORITY) {
+			/* Implied items don't need to be mentioned. */
+			skipped++;
+			continue;
+		}
+		len += len ? 1 : strlen(str);
+		len += strlen(words[j].name);
+		len += levels[j] == 1 ? 0 : 1;
+
+		if (words[j].priority == HELP_PRIORITY)
+			continue; /* no abbreviating for help */
+
+		assert(levels[j] <= MAX_OUT_LEVEL);
+		if (++counts[levels[j]] > max) {
+			/* Determine which level has the most items. */
+			lev = levels[j];
+			max = counts[lev];
+		}
+	}
+
+	/* Sanity check the COUNT_* define against the length of the table. */
+	if (j != word_count) {
+		rprintf(FERROR, "rsync: internal error: %s is wrong! (%d != %d)\n",
+			words == info_words ? "COUNT_INFO" : "COUNT_DEBUG",
+			j, word_count);
+		exit_cleanup(RERR_UNSUPPORTED);
+	}
+
+	if (!len)
+		return NULL;
+
+	len++;
+	if (!(buf = new_array(char, len)))
+		out_of_memory("make_output_option");
+	pos = 0;
+
+	if (skipped || max < 5)
+		lev = -1;
+	else {
+		if (lev == 0)
+			pos += snprintf(buf, len, "%sNONE", str);
+		else if (lev == 1)
+			pos += snprintf(buf, len, "%sALL", str);
+		else
+			pos += snprintf(buf, len, "%sALL%d", str, lev);
+	}
+
+	for (j = 0; words[j].name && pos < len; j++) {
+		if (words[j].priority == DEFAULT_PRIORITY || levels[j] == lev || !(words[j].where & where))
+			continue;
+		if (pos)
+			buf[pos++] = ',';
+		else
+			pos += strlcpy(buf+pos, str, len-pos);
+		if (pos < len)
+			pos += strlcpy(buf+pos, words[j].name, len-pos);
+		/* Level 1 is implied by the name alone. */
+		if (levels[j] != 1 && pos < len)
+			buf[pos++] = '0' + levels[j];
+	}
+
+	buf[pos] = '\0';
+
+	return buf;
+}
+
+static void parse_output_words(struct output_struct *words, short *levels,
+			       const char *str, int priority)
+{
+	const char *s;
+	int j, len, lev;
+
+	if (!str)
+		return;
+
+	while (*str) {
+		if ((s = strchr(str, ',')) != NULL)
+			len = s++ - str;
+		else
+			len = strlen(str);
+		while (len && isDigit(str+len-1))
+			len--;
+		lev = isDigit(str+len) ? atoi(str+len) : 1;
+		if (lev > MAX_OUT_LEVEL)
+			lev = MAX_OUT_LEVEL;
+		if (len == 4 && strncasecmp(str, "help", 4) == 0) {
+			output_item_help(words);
+			exit_cleanup(0);
+		}
+		if (len == 4 && strncasecmp(str, "none", 4) == 0)
+			len = lev = 0;
+		else if (len == 3 && strncasecmp(str, "all", 3) == 0)
+			len = 0;
+		for (j = 0; words[j].name; j++) {
+			if (!len
+			 || (strncasecmp(str, words[j].name, len) == 0 && !words[j].name[len])) {
+				if (priority >= words[j].priority) {
+					words[j].priority = priority;
+					levels[j] = lev;
+				}
+				if (len)
+					break;
+			}
+		}
+		if (len && !words[j].name) {
+			rprintf(FERROR, "Unknown %s item: %.*s\n", words[j].help, len, str);
+			exit_cleanup(RERR_SYNTAX);
+		}
+		if (!s)
+			break;
+		str = s;
+	}
+}
+
+/* Tell the user what all the info or debug flags mean. */
+static void output_item_help(struct output_struct *words)
+{
+	short *levels = words == info_words ? info_levels : debug_levels;
+	char buf[128], *opt, *fmt = "%-10s %s\n";
+	int j;
+
+	rprintf(FINFO, "Use OPT or OPT1 for level 1 output, OPT2 for level 2, etc.; OPT0 silences.\n");
+	rprintf(FINFO, "\n");
+	for (j = 0; words[j].name; j++)
+		rprintf(FINFO, fmt, words[j].name, words[j].help);
+	rprintf(FINFO, "\n");
+
+	snprintf(buf, sizeof buf, "Set all %s options (e.g. all%d)",
+		 words[j].help, MAX_OUT_LEVEL);
+	rprintf(FINFO, fmt, "ALL", buf);
+
+	snprintf(buf, sizeof buf, "Silence all %s options (same as all0)",
+		 words[j].help);
+	rprintf(FINFO, fmt, "NONE", buf);
+
+	rprintf(FINFO, fmt, "HELP", "Output this help message");
+	rprintf(FINFO, "\n");
+	rprintf(FINFO, "Options added for each increase in verbose level:\n");
+
+	for (j = 1; j <= MAX_VERBOSITY; j++) {
+		reset_output_levels();
+		if (words == info_words)
+			parse_output_words(info_words, levels, info_verbosity[j], HELP_PRIORITY);
+		else
+			parse_output_words(debug_words, levels, debug_verbosity[j], HELP_PRIORITY);
+		opt = make_output_option(words, levels, W_CLI|W_SRV|W_SND|W_REC);
+		if (opt) {
+			rprintf(FINFO, "%d) %s\n", j, strchr(opt, '=')+1);
+			free(opt);
+		}
+	}
+
+	reset_output_levels();
+}
+
+/* The --verbose option now sets info+debug flags. */
+static void set_output_verbosity(int level, int priority)
+{
+	int j;
+
+	if (level > MAX_VERBOSITY)
+		level = MAX_VERBOSITY;
+
+	for (j = 1; j <= level; j++) {
+		parse_output_words(info_words, info_levels, info_verbosity[j], priority);
+		parse_output_words(debug_words, debug_levels, debug_verbosity[j], priority);
+	}
+}
+
+/* Limit the info+debug flag levels given a verbose-option level limit. */
+void limit_output_verbosity(int level)
+{
+	short info_limits[COUNT_INFO], debug_limits[COUNT_DEBUG];
+	int j;
+
+	if (level > MAX_VERBOSITY)
+		return;
+
+	memset(info_limits, 0, sizeof info_limits);
+	memset(debug_limits, 0, sizeof debug_limits);
+
+	/* Compute the level limits in the above arrays. */
+	for (j = 1; j <= level; j++) {
+		parse_output_words(info_words, info_limits, info_verbosity[j], LIMIT_PRIORITY);
+		parse_output_words(debug_words, debug_limits, debug_verbosity[j], LIMIT_PRIORITY);
+	}
+
+	for (j = 0; j < COUNT_INFO; j++) {
+		if (info_levels[j] > info_limits[j])
+			info_levels[j] = info_limits[j];
+	}
+
+	for (j = 0; j < COUNT_DEBUG; j++) {
+		if (debug_levels[j] > debug_limits[j])
+			debug_levels[j] = debug_limits[j];
+	}
+}
+
+void reset_output_levels(void)
+{
+	int j;
+
+	memset(info_levels, 0, sizeof info_levels);
+	memset(debug_levels, 0, sizeof debug_levels);
+
+	for (j = 0; j < COUNT_INFO; j++)
+		info_words[j].priority = DEFAULT_PRIORITY;
+
+	for (j = 0; j < COUNT_DEBUG; j++)
+		debug_words[j].priority = DEFAULT_PRIORITY;
+}
+
+void negate_output_levels(void)
+{
+	int j;
+
+	for (j = 0; j < COUNT_INFO; j++)
+		info_levels[j] *= -1;
+
+	for (j = 0; j < COUNT_DEBUG; j++)
+		debug_levels[j] *= -1;
+}
 
 static void print_rsync_version(enum logcode f)
 {
@@ -318,6 +648,8 @@
   rprintf(F,"\n");
   rprintf(F,"Options\n");
   rprintf(F," -v, --verbose               increase verbosity\n");
+  rprintf(F,"     --info=FLAGS            fine-grained informational verbosity\n");
+  rprintf(F,"     --debug=FLAGS           fine-grained debug verbosity\n");
   rprintf(F," -q, --quiet                 suppress non-error messages\n");
   rprintf(F,"     --no-motd               suppress daemon-mode MOTD (see manpage caveat)\n");
   rprintf(F," -c, --checksum              skip based on checksum, not mod-time & size\n");
@@ -451,7 +783,7 @@
       OPT_FILTER, OPT_COMPARE_DEST, OPT_COPY_DEST, OPT_LINK_DEST, OPT_HELP,
       OPT_INCLUDE, OPT_INCLUDE_FROM, OPT_MODIFY_WINDOW, OPT_MIN_SIZE, OPT_CHMOD,
       OPT_READ_BATCH, OPT_WRITE_BATCH, OPT_ONLY_WRITE_BATCH, OPT_MAX_SIZE,
-      OPT_NO_D, OPT_APPEND, OPT_NO_ICONV,
+      OPT_NO_D, OPT_APPEND, OPT_NO_ICONV, OPT_INFO, OPT_DEBUG,
       OPT_SERVER, OPT_REFUSED_BASE = 9000};
 
 static struct poptOption long_options[] = {
@@ -461,6 +793,8 @@
   {"verbose",         'v', POPT_ARG_NONE,   0, 'v', 0, 0 },
   {"no-verbose",       0,  POPT_ARG_VAL,    &verbose, 0, 0, 0 },
   {"no-v",             0,  POPT_ARG_VAL,    &verbose, 0, 0, 0 },
+  {"info",             0,  POPT_ARG_STRING, 0, OPT_INFO, 0, 0 },
+  {"debug",            0,  POPT_ARG_STRING, 0, OPT_DEBUG, 0, 0 },
   {"quiet",           'q', POPT_ARG_NONE,   0, 'q', 0, 0 },
   {"motd",             0,  POPT_ARG_VAL,    &output_motd, 1, 0, 0 },
   {"no-motd",          0,  POPT_ARG_VAL,    &output_motd, 0, 0, 0 },
@@ -1254,6 +1588,16 @@
 			}
 			break;
 
+		case OPT_INFO:
+			arg = poptGetOptArg(pc);
+			parse_output_words(info_words, info_levels, arg, USER_PRIORITY);
+			break;
+
+		case OPT_DEBUG:
+			arg = poptGetOptArg(pc);
+			parse_output_words(debug_words, debug_levels, arg, USER_PRIORITY);
+			break;
+
 		case OPT_HELP:
 			usage(FINFO);
 			exit_cleanup(0);
@@ -1307,6 +1651,13 @@
 		exit_cleanup(0);
 	}
 
+	set_output_verbosity(verbose, DEFAULT_PRIORITY);
+
+	if (do_stats && !am_server) {
+		parse_output_words(info_words, info_levels,
+			verbose > 1 ? "stats3" : "stats2", DEFAULT_PRIORITY);
+	}
+
 #ifdef ICONV_OPTION
 	if (iconv_opt && protect_args != 2) {
 		if (!am_server && strcmp(iconv_opt, "-") == 0)
@@ -1523,7 +1874,7 @@
 			backup_dir_buf[backup_dir_len++] = '/';
 			backup_dir_buf[backup_dir_len] = '\0';
 		}
-		if (verbose > 1 && !am_sender)
+		if (INFO_GTE(BACKUP, 1) && !am_sender)
 			rprintf(FINFO, "backup_dir is %s\n", backup_dir_buf);
 	} else if (!backup_suffix_len && (!am_server || !am_sender)) {
 		snprintf(err_buf, sizeof err_buf,
@@ -1558,11 +1909,10 @@
 		log_before_transfer = !am_server;
 	}
 
-	if (do_progress) {
-		if (am_server)
-			do_progress = 0;
-		else if (!verbose && !log_before_transfer && !am_server)
-			verbose = 1;
+	if (do_progress && !am_server) {
+		if (!log_before_transfer && INFO_EQ(NAME, 0))
+			parse_output_words(info_words, info_levels, "name", DEFAULT_PRIORITY);
+		parse_output_words(info_words, info_levels, "flist2,progress", DEFAULT_PRIORITY);
 	}
 
 	if (dry_run)
@@ -1570,7 +1920,7 @@
 
 	set_io_timeout(io_timeout);
 
-	if (verbose && !stdout_format) {
+	if (INFO_GTE(NAME, 1) && !stdout_format) {
 		stdout_format = "%n%L";
 		log_before_transfer = !am_server;
 	}
@@ -1733,6 +2083,7 @@
 {
 	static char argstr[64];
 	int ac = *argc_p;
+	short where;
 	char *arg;
 	int i, x;
 
@@ -1860,13 +2211,13 @@
 #endif
 	}
 
-	argstr[x] = '\0';
-
-	if (x > (int)sizeof argstr) { /* Not possible... */
+	if (x >= (int)sizeof argstr) { /* Not possible... */
 		rprintf(FERROR, "argstr overflow in server_options().\n");
 		exit_cleanup(RERR_MALLOC);
 	}
 
+	argstr[x] = '\0';
+
 	args[ac++] = argstr;
 
 #ifdef ICONV_OPTION
@@ -2049,7 +2400,6 @@
 			 *   and it may be an older version that doesn't know this
 			 *   option, so don't send it if client is the sender.
 			 */
-			int i;
 			for (i = 0; i < basis_dir_cnt; i++) {
 				args[ac++] = dest_option;
 				args[ac++] = basis_dir[i];
@@ -2057,6 +2407,16 @@
 		}
 	}
 
+	/* What flags do we need to send to the other side? */
+	where = (am_server ? W_CLI : W_SRV) | (am_sender ? W_REC : W_SND);
+	arg = make_output_option(info_words, info_levels, where);
+	if (arg)
+		args[ac++] = arg;
+
+	arg = make_output_option(debug_words, debug_levels, where);
+	if (arg)
+		args[ac++] = arg;
+
 	if (append_mode) {
 		if (append_mode > 1)
 			args[ac++] = "--append";
diff --git a/pipe.c b/pipe.c
index f32d63e..755da54 100644
--- a/pipe.c
+++ b/pipe.c
@@ -51,7 +51,7 @@
 	int to_child_pipe[2];
 	int from_child_pipe[2];
 
-	if (verbose >= 2)
+	if (DEBUG_GTE(CMD, 1))
 		print_child_argv("opening connection using:", command);
 
 	if (fd_pair(to_child_pipe) < 0 || fd_pair(from_child_pipe) < 0) {
diff --git a/progress.c b/progress.c
index 25033b8..afd8ef6 100644
--- a/progress.c
+++ b/progress.c
@@ -24,6 +24,7 @@
 
 extern int am_server;
 extern int need_unsorted_flist;
+extern int output_needs_newline;
 extern struct stats stats;
 extern struct file_list *cur_flist;
 
@@ -40,8 +41,6 @@
 	OFF_T ofs;
 };
 
-int progress_is_active = 0;
-
 static struct progress_history ph_start;
 static struct progress_history ph_list[PROGRESS_HISTORY_SECS];
 static int newest_hpos, oldest_hpos;
@@ -66,16 +65,26 @@
 {
 	char rembuf[64], eol[128];
 	const char *units;
-	int pct = ofs == size ? 100 : (int) (100.0 * ofs / size);
 	unsigned long diff;
 	double rate, remain;
+	int pct;
 
 	if (is_last) {
-		snprintf(eol, sizeof eol,
+		int len = snprintf(eol, sizeof eol,
 			" (xfer#%d, to-check=%d/%d)\n",
 			stats.num_transferred_files,
 			stats.num_files - current_file_index - 1,
 			stats.num_files);
+		if (INFO_GTE(PROGRESS, 2)) {
+			static int last_len = 0;
+			/* Drop \n and pad with spaces if line got shorter. */
+			if (last_len < --len)
+				last_len = len;
+			eol[last_len] = '\0';
+			while (last_len > len)
+				eol[--last_len] = ' ';
+			is_last = 0;
+		}
 		/* Compute stats based on the starting info. */
 		if (!ph_start.time.tv_sec
 		    || !(diff = msdiff(&ph_start.time, now)))
@@ -112,18 +121,21 @@
 			 (int) remain % 60);
 	}
 
-	progress_is_active = 0;
+	output_needs_newline = 0;
+	pct = ofs == size ? 100 : (int) (100.0 * ofs / size);
 	rprintf(FCLIENT, "\r%12s %3d%% %7.2f%s %s%s",
 		human_num(ofs), pct, rate, units, rembuf, eol);
 	if (!is_last) {
-		progress_is_active = 1;
-		fflush(stdout);
+		output_needs_newline = 1;
+		rflush(FCLIENT);
 	}
 }
 
 void set_current_file_index(struct file_struct *file, int ndx)
 {
-	if (need_unsorted_flist)
+	if (!file)
+		current_file_index = cur_flist->used + cur_flist->ndx_start - 1;
+	else if (need_unsorted_flist)
 		current_file_index = flist_find(cur_flist, file) + cur_flist->ndx_start;
 	else
 		current_file_index = ndx;
@@ -135,9 +147,14 @@
 	if (!am_server) {
 		struct timeval now;
 		gettimeofday(&now, NULL);
-		rprint_progress(size, size, &now, True);
+		if (INFO_GTE(PROGRESS, 2)) {
+			rprint_progress(stats.total_transferred_size,
+					stats.total_size, &now, True);
+		} else {
+			rprint_progress(size, size, &now, True);
+			memset(&ph_start, 0, sizeof ph_start);
+		}
 	}
-	memset(&ph_start, 0, sizeof ph_start);
 }
 
 void show_progress(OFF_T ofs, OFF_T size)
@@ -193,5 +210,9 @@
 		return;
 #endif
 
-	rprint_progress(ofs, size, &now, False);
+	if (INFO_GTE(PROGRESS, 2)) {
+		rprint_progress(stats.total_transferred_size,
+				stats.total_size, &now, False);
+	} else
+		rprint_progress(ofs, size, &now, False);
 }
diff --git a/receiver.c b/receiver.c
index ce6b739..2ba1ab9 100644
--- a/receiver.c
+++ b/receiver.c
@@ -21,11 +21,9 @@
 
 #include "rsync.h"
 
-extern int verbose;
 extern int dry_run;
 extern int do_xfers;
 extern int am_server;
-extern int do_progress;
 extern int inc_recurse;
 extern int log_before_transfer;
 extern int stdout_format_has_i;
@@ -180,7 +178,7 @@
 	if (fd_r >= 0 && size_r > 0) {
 		int32 read_size = MAX(sum.blength * 2, 16*1024);
 		mapbuf = map_file(fd_r, size_r, read_size, sum.blength);
-		if (verbose > 2) {
+		if (DEBUG_GTE(CHKSUM, 2)) {
 			rprintf(FINFO, "recv mapped %s of size %.0f\n",
 				fname_r, (double)size_r);
 		}
@@ -196,7 +194,7 @@
 			sum.flength -= sum.blength - sum.remainder;
 		if (append_mode == 2) {
 			for (j = CHUNK_SIZE; j < sum.flength; j += CHUNK_SIZE) {
-				if (do_progress)
+				if (INFO_GTE(PROGRESS, 1))
 					show_progress(offset, total_size);
 				sum_update(map_ptr(mapbuf, offset, CHUNK_SIZE),
 					   CHUNK_SIZE);
@@ -204,7 +202,7 @@
 			}
 			if (offset < sum.flength) {
 				int32 len = (int32)(sum.flength - offset);
-				if (do_progress)
+				if (INFO_GTE(PROGRESS, 1))
 					show_progress(offset, total_size);
 				sum_update(map_ptr(mapbuf, offset, len), len);
 			}
@@ -218,11 +216,11 @@
 	}
 
 	while ((i = recv_token(f_in, &data)) != 0) {
-		if (do_progress)
+		if (INFO_GTE(PROGRESS, 1))
 			show_progress(offset, total_size);
 
 		if (i > 0) {
-			if (verbose > 3) {
+			if (DEBUG_GTE(CHKSUM, 3)) {
 				rprintf(FINFO,"data recv %d at %.0f\n",
 					i,(double)offset);
 			}
@@ -246,7 +244,7 @@
 
 		stats.matched_data += len;
 
-		if (verbose > 3) {
+		if (DEBUG_GTE(CHKSUM, 3)) {
 			rprintf(FINFO,
 				"chunk[%d] of size %ld at %.0f offset=%.0f\n",
 				i, (long)len, (double)offset2, (double)offset);
@@ -288,7 +286,7 @@
 		ftruncate(fd, offset);
 #endif
 
-	if (do_progress)
+	if (INFO_GTE(PROGRESS, 1))
 		end_progress(total_size);
 
 	if (fd != -1 && offset > 0 && sparse_end(fd) != 0) {
@@ -304,7 +302,7 @@
 		unmap_file(mapbuf);
 
 	read_buf(f_in, file_sum2, sum_len);
-	if (verbose > 2)
+	if (DEBUG_GTE(CHKSUM, 2))
 		rprintf(FINFO,"got file_sum\n");
 	if (fd != -1 && memcmp(file_sum1, file_sum2, sum_len) != 0)
 		return 0;
@@ -328,7 +326,7 @@
 		if ((partialptr = partial_dir_fname(fname)) != NULL) {
 			if (make_backups > 0 && !make_backup(fname))
 				continue;
-			if (verbose > 2) {
+			if (DEBUG_GTE(RECV, 1)) {
 				rprintf(FINFO, "renaming %s to %s\n",
 					partialptr, fname);
 			}
@@ -396,7 +394,7 @@
 #endif
 	int ndx, recv_ok;
 
-	if (verbose > 2)
+	if (DEBUG_GTE(RECV, 1))
 		rprintf(FINFO, "recv_files(%d) starting\n", cur_flist->used);
 
 	if (delay_updates)
@@ -409,6 +407,10 @@
 		ndx = read_ndx_and_attrs(f_in, &iflags, &fnamecmp_type,
 					 xname, &xlen);
 		if (ndx == NDX_DONE) {
+			if (!am_server && INFO_GTE(PROGRESS, 2) && cur_flist) {
+				set_current_file_index(NULL, 0);
+				end_progress(0);
+			}
 			if (inc_recurse && first_flist) {
 				flist_free(first_flist);
 				if (first_flist)
@@ -423,7 +425,7 @@
 			}
 			if (++phase > max_phase)
 				break;
-			if (verbose > 2)
+			if (DEBUG_GTE(RECV, 1))
 				rprintf(FINFO, "recv_files phase=%d\n", phase);
 			if (phase == 2 && delay_updates)
 				handle_delayed_updates(local_name);
@@ -437,7 +439,7 @@
 			file = dir_flist->files[cur_flist->parent_ndx];
 		fname = local_name ? local_name : f_name(file, fbuf);
 
-		if (verbose > 2)
+		if (DEBUG_GTE(RECV, 1))
 			rprintf(FINFO, "recv_files(%s)\n", fname);
 
 #ifdef SUPPORT_XATTRS
@@ -482,7 +484,7 @@
 			}
 		}
 
-		if (!am_server && do_progress)
+		if (!am_server && INFO_GTE(PROGRESS, 1))
 			set_current_file_index(file, ndx);
 		stats.num_transferred_files++;
 		stats.total_transferred_size += F_LENGTH(file);
@@ -671,7 +673,7 @@
 		/* log the transfer */
 		if (log_before_transfer)
 			log_item(FCLIENT, file, &initial_stats, iflags, NULL);
-		else if (!am_server && verbose && do_progress)
+		else if (!am_server && INFO_GTE(NAME, 1) && INFO_EQ(PROGRESS, 1))
 			rprintf(FINFO, "%s\n", fname);
 
 		/* recv file data */
@@ -722,7 +724,7 @@
 			break;
 		case 0: {
 			enum logcode msgtype = redoing ? FERROR_XFER : FWARNING;
-			if (msgtype == FERROR_XFER || verbose) {
+			if (msgtype == FERROR_XFER || INFO_GTE(NAME, 1)) {
 				char *errstr, *redostr, *keptstr;
 				if (!(keep_partial && partialptr) && !inplace)
 					keptstr = "discarded";
@@ -762,7 +764,7 @@
 	if (phase == 2 && delay_updates) /* for protocol_version < 29 */
 		handle_delayed_updates(local_name);
 
-	if (verbose > 2)
+	if (DEBUG_GTE(RECV, 1))
 		rprintf(FINFO,"recv_files finished\n");
 
 	return 0;
diff --git a/rsync.c b/rsync.c
index 85244c8..734a373 100644
--- a/rsync.c
+++ b/rsync.c
@@ -27,7 +27,6 @@
 #include <langinfo.h>
 #endif
 
-extern int verbose;
 extern int dry_run;
 extern int preserve_acls;
 extern int preserve_xattrs;
@@ -84,7 +83,7 @@
 		/* It's OK if this fails... */
 		ic_chck = iconv_open(defset, defset);
 
-		if (verbose > 3) {
+		if (DEBUG_GTE(ICONV, 1)) {
 			if (ic_chck == (iconv_t)-1) {
 				rprintf(FINFO,
 					"note: iconv_open(\"%s\", \"%s\") failed (%d)"
@@ -126,7 +125,7 @@
 		exit_cleanup(RERR_UNSUPPORTED);
 	}
 
-	if (verbose > 1) {
+	if (INFO_GTE(MISC, 2)) {
 		rprintf(FINFO, "%s charset: %s\n",
 			am_server ? "server" : "client",
 			*charset ? charset : "[LOCALE]");
@@ -218,7 +217,7 @@
 
 	for (i = 0; args[i]; i++) {} /* find first NULL */
 	args[i] = "rsync"; /* set a new arg0 */
-	if (verbose > 1)
+	if (DEBUG_GTE(CMD, 1))
 		print_child_argv("protected args:", args + i + 1);
 	do {
 #ifdef ICONV_OPTION
@@ -247,7 +246,7 @@
 	int len, iflags = 0;
 	struct file_list *flist;
 	uchar fnamecmp_type = FNAMECMP_FNAME;
-	int ndx, save_verbose = verbose;
+	int ndx;
 
   read_loop:
 	while (1) {
@@ -275,17 +274,17 @@
 		}
 
 		/* Send everything read from f_in to msg_fd_out. */
-		if (verbose > 3) {
+		if (DEBUG_GTE(FLIST, 2)) {
 			rprintf(FINFO, "[%s] receiving flist for dir %d\n",
 				who_am_i(), ndx);
 		}
-		verbose = 0;
+		negate_output_levels(); /* turn off all info/debug output */
 		send_msg_int(MSG_FLIST, ndx);
 		start_flist_forward(f_in);
 		flist = recv_file_list(f_in);
 		flist->parent_ndx = ndx;
 		stop_flist_forward();
-		verbose = save_verbose;
+		negate_output_levels(); /* restore info/debug output */
 	}
 
 	iflags = protocol_version >= 29 ? read_shortint(f_in)
@@ -448,7 +447,7 @@
 	} else
 #endif
 	if (change_uid || change_gid) {
-		if (verbose > 2) {
+		if (DEBUG_GTE(OWN, 1)) {
 			if (change_uid) {
 				rprintf(FINFO,
 					"set uid of %s from %u to %u\n",
@@ -507,7 +506,7 @@
 	}
 #endif
 
-	if (verbose > 1 && flags & ATTRS_REPORT) {
+	if (INFO_GTE(NAME, 2) && flags & ATTRS_REPORT) {
 		if (updated)
 			rprintf(FCLIENT, "%s\n", fname);
 		else
@@ -555,7 +554,7 @@
 	const char *temp_copy_name = partialptr && *partialptr != '/' ? partialptr : NULL;
 
 	if (inplace) {
-		if (verbose > 2)
+		if (DEBUG_GTE(RECV, 1))
 			rprintf(FINFO, "finishing %s\n", fname);
 		fnametmp = fname;
 		goto do_set_file_attrs;
@@ -573,7 +572,7 @@
 		       ok_to_set_time ? 0 : ATTRS_SKIP_MTIME);
 
 	/* move tmp file over real file */
-	if (verbose > 2)
+	if (DEBUG_GTE(RECV, 1))
 		rprintf(FINFO, "renaming %s to %s\n", fnametmp, fname);
 	ret = robust_rename(fnametmp, fname, temp_copy_name,
 			    file->mode & INITACCESSPERMS);
diff --git a/rsync.h b/rsync.h
index 77f9b3a..fdc7a03 100644
--- a/rsync.h
+++ b/rsync.h
@@ -1120,7 +1120,51 @@
 #define FD_ZERO(fdsetp) memset(fdsetp, 0, sizeof (fd_set))
 #endif
 
-extern int verbose;
+extern short info_levels[], debug_levels[];
+
+#define INFO_GTE(flag, lvl) (info_levels[INFO_##flag] >= (lvl))
+#define INFO_EQ(flag, lvl) (info_levels[INFO_##flag] == (lvl))
+#define DEBUG_GTE(flag, lvl) (debug_levels[DEBUG_##flag] >= (lvl))
+#define DEBUG_EQ(flag, lvl) (debug_levels[DEBUG_##flag] == (lvl))
+
+#define INFO_BACKUP 0
+#define INFO_COPY (INFO_BACKUP+1)
+#define INFO_DEL (INFO_COPY+1)
+#define INFO_FLIST (INFO_DEL+1)
+#define INFO_MISC (INFO_FLIST+1)
+#define INFO_MOUNT (INFO_MISC+1)
+#define INFO_NAME (INFO_MOUNT+1)
+#define INFO_PROGRESS (INFO_NAME+1)
+#define INFO_REMOVE (INFO_PROGRESS+1)
+#define INFO_SKIP (INFO_REMOVE+1)
+#define INFO_STATS (INFO_SKIP+1)
+#define INFO_SYMSAFE (INFO_STATS+1)
+
+#define COUNT_INFO (INFO_SYMSAFE+1)
+
+#define DEBUG_ACL 0
+#define DEBUG_BACKUP (DEBUG_ACL+1)
+#define DEBUG_BIND (DEBUG_BACKUP+1)
+#define DEBUG_CHDIR (DEBUG_BIND+1)
+#define DEBUG_CONNECT (DEBUG_CHDIR+1)
+#define DEBUG_CHKSUM (DEBUG_CONNECT+1)
+#define DEBUG_CMD (DEBUG_CHKSUM+1)
+#define DEBUG_DEL (DEBUG_CMD+1)
+#define DEBUG_DUP (DEBUG_DEL+1)
+#define DEBUG_EXIT (DEBUG_DUP+1)
+#define DEBUG_FILTER (DEBUG_EXIT+1)
+#define DEBUG_FLIST (DEBUG_FILTER+1)
+#define DEBUG_FUZZY (DEBUG_FLIST+1)
+#define DEBUG_GENR (DEBUG_FUZZY+1)
+#define DEBUG_HLINK (DEBUG_GENR+1)
+#define DEBUG_ICONV (DEBUG_HLINK+1)
+#define DEBUG_OWN (DEBUG_ICONV+1)
+#define DEBUG_PROTO (DEBUG_OWN+1)
+#define DEBUG_RECV (DEBUG_PROTO+1)
+#define DEBUG_SEND (DEBUG_RECV+1)
+#define DEBUG_TIME (DEBUG_SEND+1)
+
+#define COUNT_DEBUG (DEBUG_TIME+1)
 
 #ifndef HAVE_INET_NTOP
 const char *inet_ntop(int af, const void *src, char *dst, size_t size);
diff --git a/rsync.yo b/rsync.yo
index 3377312..1b30c7a 100644
--- a/rsync.yo
+++ b/rsync.yo
@@ -314,6 +314,8 @@
 Here is a short summary of the options available in rsync. Please refer
 to the detailed description below for a complete description.  verb(
  -v, --verbose               increase verbosity
+     --info=FLAGS            fine-grained informational verbosity
+     --debug=FLAGS           fine-grained debug verbosity
  -q, --quiet                 suppress non-error messages
      --no-motd               suppress daemon-mode MOTD (see caveat)
  -c, --checksum              skip based on checksum, not mod-time & size
@@ -465,23 +467,61 @@
 dit(bf(-v, --verbose)) This option increases the amount of information you
 are given during the transfer.  By default, rsync works silently. A
 single bf(-v) will give you information about what files are being
-transferred and a brief summary at the end. Two bf(-v) flags will give you
+transferred and a brief summary at the end. Two bf(-v) options will give you
 information on what files are being skipped and slightly more
-information at the end. More than two bf(-v) flags should only be used if
+information at the end. More than two bf(-v) options should only be used if
 you are debugging rsync.
 
-Note that the names of the transferred files that are output are done using
-a default bf(--out-format) of "%n%L", which tells you just the name of the
-file and, if the item is a link, where it points.  At the single bf(-v)
-level of verbosity, this does not mention when a file gets its attributes
-changed.  If you ask for an itemized list of changed attributes (either
-bf(--itemize-changes) or adding "%i" to the bf(--out-format) setting), the
-output (on the client) increases to mention all items that are changed in
-any way.  See the bf(--out-format) option for more details.
+In a modern rsync, the bf(-v) option is equivalent to the setting of groups
+of bf(--info) and bf(--debug) options.  You can choose to use these newer
+options in addition to, or in place of using bf(--verbose), as any
+fine-grained settings override the implied settings of bf(-v).  Both
+bf(--info) and bf(--debug) have a way to ask for help that tells you
+exactly what flags are set for each increase in verbosity.
+
+dit(bf(--info=FLAGS))
+This option lets you have fine-grained control over the
+information
+output you want to see.  An individual flag name may be followed by a level
+number, with 0 meaning to silence that output, 1 being the default output
+level, and higher numbers increasing the output of that flag (for those
+that support higher levels).  Use
+bf(--info=help)
+to see all the available flag names, what they output, and what flag names
+are added for each increase in the verbose level.  Some examples:
+
+verb(    rsync -a --info=progress2 src/ dest/
+    rsync -avv --info=stats2,misc1,flist0 src/ dest/ )
+
+Note that bf(--info=name)'s output is affected by the bf(--out-format) and
+bf(--itemize-changes) (bf(-i)) options.  See those options for more
+information on what is output and when.
+
+This option was added to 3.1.0, so an older rsync on the server side might
+reject your attempts at fine-grained control (if one or more flags needed
+to be send to the server and the server was too old to understand them).
+
+dit(bf(--debug=FLAGS))
+This option lets you have fine-grained control over the
+debug
+output you want to see.  An individual flag name may be followed by a level
+number, with 0 meaning to silence that output, 1 being the default output
+level, and higher numbers increasing the output of that flag (for those
+that support higher levels).  Use
+bf(--debug=help)
+to see all the available flag names, what they output, and what flag names
+are added for each increase in the verbose level.  Some examples:
+
+verb(    rsync -avvv --debug=none src/ dest/
+    rsync -avA --del --debug=del2,acl src/ dest/ )
+
+This option was added to 3.1.0, so an older rsync on the server side might
+reject your attempts at fine-grained control (if one or more flags needed
+to be send to the server and the server was too old to understand them).
 
 dit(bf(-q, --quiet)) This option decreases the amount of information you
 are given during the transfer, notably suppressing information messages
-from the remote server. This flag is useful when invoking rsync from
+from the remote server. This option name is useful when invoking rsync from
 cron.
 
 dit(bf(--no-motd)) This option affects the information that is output
@@ -1756,22 +1796,22 @@
 outputting them as a verbose message).
 
 dit(bf(--out-format=FORMAT)) This allows you to specify exactly what the
-rsync client outputs to the user on a per-update basis.  The format is a text
-string containing embedded single-character escape sequences prefixed with
-a percent (%) character.  For a list of the possible escape characters, see
-the "log format" setting in the rsyncd.conf manpage.
+rsync client outputs to the user on a per-update basis.  The format is a
+text string containing embedded single-character escape sequences prefixed
+with a percent (%) character.   A default format of "%n%L" is assumed if
+either bf(--info=name) or bf(-v) is specified (this tells you just the name
+of the file and, if the item is a link, where it points).  For a full list
+of the possible escape characters, see the "log format" setting in the
+rsyncd.conf manpage.
 
-Specifying this option will mention each file, dir, etc. that gets updated
-in a significant way (a transferred file, a recreated symlink/device, or a
-touched directory).  In addition, if the itemize-changes escape (%i) is
-included in the string, the logging of names increases to mention any
-item that is changed in any way (as long as the receiving side is at least
-2.6.4).  See the bf(--itemize-changes) option for a description of the
-output of "%i".
-
-The bf(--verbose) option implies a format of "%n%L", but you can use
-bf(--out-format) without bf(--verbose) if you like, or you can override
-the format of its per-file output using this option.
+Specifying the bf(--out-format) option implies the bf(--info=name) option,
+which will mention each file, dir, etc. that gets updated in a significant
+way (a transferred file, a recreated symlink/device, or a touched
+directory).  In addition, if the itemize-changes escape (%i) is included in
+the string (e.g. if the bf(--itemize-changes) option was used), the logging
+of names increases to mention any item that is changed in any way (as long
+as the receiving side is at least 2.6.4).  See the bf(--itemize-changes)
+option for a description of the output of "%i".
 
 Rsync will output the out-format string prior to a file's transfer unless
 one of the transfer-statistic escapes is requested, in which case the
@@ -1804,7 +1844,9 @@
 
 dit(bf(--stats)) This tells rsync to print a verbose set of statistics
 on the file transfer, allowing you to tell how effective rsync's delta-transfer
-algorithm is for your data.
+algorithm is for your data.  This option is equivalent to bf(--info=stats2)
+if combined with 0 or 1 bf(-v) options, or bf(--info=stats3) if combined
+with 2 or more bf(-v) options.
 
 The current statistics are as follows: quote(itemization(
   it() bf(Number of files) is the count of all "files" (in the generic
@@ -1977,7 +2019,9 @@
 dit(bf(--progress)) This option tells rsync to print information
 showing the progress of the transfer. This gives a bored user
 something to watch.
-Implies bf(--verbose) if it wasn't already specified.
+With a modern rsync this is the same as specifying
+bf(--info=flist2,name,progress), but any user-supplied settings for those
+info flags takes precedence (e.g. "--info=flist0 --progress").
 
 While rsync is transferring a regular file, it updates a progress line that
 looks like this:
@@ -2012,6 +2056,13 @@
 purpose is to make it much easier to specify these two options for a long
 transfer that may be interrupted.
 
+There is also a bf(--info=progress2) option that outputs statistics based
+on the whole transfer, rather than individual files.  Use this flag without
+outputting a filename (e.g. avoid bf(-v) or specify bf(--info=name0) if you
+want to see how the transfer is doing without scrolling the screen with a
+lot of names.  (You don't need to specify the bf(--progress) option in
+order to use bf(--info=progress2).)
+
 dit(bf(--password-file)) This option allows you to provide a password in a
 file for accessing an rsync daemon.  The file must not be world readable.
 It should contain just the password as a single line.
diff --git a/sender.c b/sender.c
index f772877..6aa5aeb 100644
--- a/sender.c
+++ b/sender.c
@@ -21,7 +21,6 @@
 
 #include "rsync.h"
 
-extern int verbose;
 extern int dry_run;
 extern int do_xfers;
 extern int am_server;
@@ -39,7 +38,6 @@
 extern int remove_source_files;
 extern int updating_basis_file;
 extern int make_backups;
-extern int do_progress;
 extern int inplace;
 extern int batch_fd;
 extern int write_batch;
@@ -71,7 +69,7 @@
 
 	s->sums = NULL;
 
-	if (verbose > 3) {
+	if (DEBUG_GTE(CHKSUM, 3)) {
 		rprintf(FINFO, "count=%.0f n=%ld rem=%ld\n",
 			(double)s->count, (long)s->blength, (long)s->remainder);
 	}
@@ -105,7 +103,7 @@
 		if (allowed_lull && !(i % lull_mod))
 			maybe_send_keepalive();
 
-		if (verbose > 3) {
+		if (DEBUG_GTE(CHKSUM, 3)) {
 			rprintf(FINFO,
 				"chunk[%d] len=%d offset=%.0f sum1=%08x\n",
 				i, s->sums[i].len, (double)s->sums[i].offset,
@@ -140,7 +138,7 @@
 	f_name(file, fname);
 
 	if (do_unlink(fname) == 0) {
-		if (verbose > 1)
+		if (INFO_GTE(REMOVE, 1))
 			rprintf(FINFO, "sender removed %s\n", fname);
 	} else
 		rsyserr(FERROR, errno, "sender failed to remove %s", fname);
@@ -182,7 +180,7 @@
 	int f_xfer = write_batch < 0 ? batch_fd : f_out;
 	int ndx, j;
 
-	if (verbose > 2)
+	if (DEBUG_GTE(SEND, 1))
 		rprintf(FINFO, "send_files starting\n");
 
 	while (1) {
@@ -193,6 +191,10 @@
 		ndx = read_ndx_and_attrs(f_in, &iflags, &fnamecmp_type,
 					 xname, &xlen);
 		if (ndx == NDX_DONE) {
+			if (!am_server && INFO_GTE(PROGRESS, 2) && cur_flist) {
+				set_current_file_index(NULL, 0);
+				end_progress(0);
+			}
 			if (inc_recurse && first_flist) {
 				flist_free(first_flist);
 				if (first_flist) {
@@ -202,7 +204,7 @@
 			}
 			if (++phase > max_phase)
 				break;
-			if (verbose > 2)
+			if (DEBUG_GTE(SEND, 1))
 				rprintf(FINFO, "send_files phase=%d\n", phase);
 			write_ndx(f_out, NDX_DONE);
 			continue;
@@ -225,7 +227,7 @@
 			continue;
 		f_name(file, fname);
 
-		if (verbose > 2)
+		if (DEBUG_GTE(SEND, 1))
 			rprintf(FINFO, "send_files(%d, %s%s%s)\n", ndx, path,slash,fname);
 
 #ifdef SUPPORT_XATTRS
@@ -265,7 +267,7 @@
 		updating_basis_file = inplace && (protocol_version >= 29
 			? fnamecmp_type == FNAMECMP_FNAME : make_backups <= 0);
 
-		if (!am_server && do_progress)
+		if (!am_server && INFO_GTE(PROGRESS, 1))
 			set_current_file_index(file, ndx);
 		stats.num_transferred_files++;
 		stats.total_transferred_size += F_LENGTH(file);
@@ -321,7 +323,7 @@
 		} else
 			mbuf = NULL;
 
-		if (verbose > 2) {
+		if (DEBUG_GTE(CHKSUM, 2)) {
 			rprintf(FINFO, "send_files mapped %s%s%s of size %.0f\n",
 				path,slash,fname, (double)st.st_size);
 		}
@@ -330,18 +332,18 @@
 				    fnamecmp_type, xname, xlen);
 		write_sum_head(f_xfer, s);
 
-		if (verbose > 2)
+		if (DEBUG_GTE(CHKSUM, 2))
 			rprintf(FINFO, "calling match_sums %s%s%s\n", path,slash,fname);
 
 		if (log_before_transfer)
 			log_item(FCLIENT, file, &initial_stats, iflags, NULL);
-		else if (!am_server && verbose && do_progress)
+		else if (!am_server && INFO_GTE(NAME, 1) && INFO_EQ(PROGRESS, 1))
 			rprintf(FCLIENT, "%s\n", fname);
 
 		set_compression(fname);
 
 		match_sums(f_xfer, s, mbuf, st.st_size);
-		if (do_progress)
+		if (INFO_GTE(PROGRESS, 1))
 			end_progress(st.st_size);
 
 		log_item(log_code, file, &initial_stats, iflags, NULL);
@@ -359,7 +361,7 @@
 
 		free_sums(s);
 
-		if (verbose > 2)
+		if (DEBUG_GTE(SEND, 1))
 			rprintf(FINFO, "sender finished %s%s%s\n", path,slash,fname);
 
 		/* Flag that we actually sent this entry. */
@@ -368,7 +370,7 @@
 	if (make_backups < 0)
 		make_backups = -make_backups;
 
-	if (verbose > 2)
+	if (DEBUG_GTE(SEND, 1))
 		rprintf(FINFO, "send files finished\n");
 
 	match_report();
diff --git a/socket.c b/socket.c
index 8d4a89d..2b3271c 100644
--- a/socket.c
+++ b/socket.c
@@ -232,7 +232,7 @@
 		}
 		*cp++ = '\0';
 		strlcpy(portbuf, cp, sizeof portbuf);
-		if (verbose >= 2) {
+		if (DEBUG_GTE(CONNECT, 1)) {
 			rprintf(FINFO, "connection via http proxy %s port %s\n",
 				h, portbuf);
 		}
@@ -361,7 +361,7 @@
 		*t = '\0';
 	}
 
-	if (verbose >= 2) {
+	if (DEBUG_GTE(CONNECT, 1)) {
 		rprintf(FINFO, "%sopening tcp connection to %s port %d\n",
 			prog ? "Using RSYNC_CONNECT_PROG instead of " : "",
 			host, port);
@@ -473,7 +473,7 @@
 	/* Only output the socket()/bind() messages if we were totally
 	 * unsuccessful, or if the daemon is being run with -vv. */
 	for (s = 0; s < ecnt; s++) {
-		if (!i || verbose > 1)
+		if (!i || DEBUG_GTE(BIND, 1))
 			rwrite(FLOG, errmsgs[s], strlen(errmsgs[s]), 0);
 		free(errmsgs[s]);
 	}
@@ -829,7 +829,7 @@
 		rsyserr(FERROR, errno, "socketpair_tcp failed");
 		return -1;
 	}
-	if (verbose >= 2)
+	if (DEBUG_GTE(CMD, 1))
 		rprintf(FINFO, "Running socket program: \"%s\"\n", prog);
 	if (fork() == 0) {
 		close(fd[0]);
diff --git a/t_unsafe.c b/t_unsafe.c
index 44662bd..943185a 100644
--- a/t_unsafe.c
+++ b/t_unsafe.c
@@ -27,9 +27,9 @@
 int am_root = 0;
 int read_only = 0;
 int list_only = 0;
-int verbose = 0;
 int preserve_perms = 0;
 int preserve_executability = 0;
+short info_levels[10], debug_levels[10];
 
 int
 main(int argc, char **argv)
diff --git a/testsuite/00-hello.test b/testsuite/00-hello.test
index d7774bb..d4a8475 100644
--- a/testsuite/00-hello.test
+++ b/testsuite/00-hello.test
@@ -2,4 +2,8 @@
 
 echo $0 running
 
-$RSYNC --version || exit 1
+$RSYNC --version || test_fail '--version output failed'
+
+$RSYNC --info=help || test_fail '--info=help output failed'
+
+$RSYNC --debug=help || test_fail '--debug=help output failed'
diff --git a/testsuite/rsync.fns b/testsuite/rsync.fns
index e17fe5a..2947a5f 100644
--- a/testsuite/rsync.fns
+++ b/testsuite/rsync.fns
@@ -268,7 +268,7 @@
 log format = %i %h [%a] %m (%u) %l %f%L
 transfer logging = yes
 exclude = ? foobar.baz
-max verbosity = 9
+max verbosity = 4
 uid = 0
 gid = 0
 
diff --git a/uidlist.c b/uidlist.c
index 3d9c774..d9ca7a2 100644
--- a/uidlist.c
+++ b/uidlist.c
@@ -26,7 +26,6 @@
 #include "rsync.h"
 #include "io.h"
 
-extern int verbose;
 extern int am_root;
 extern int preserve_uid;
 extern int preserve_gid;
@@ -126,7 +125,7 @@
 		}
 		if (n == ngroups)
 			gidset[ngroups++] = mygid;
-		if (verbose > 3) {
+		if (DEBUG_GTE(OWN, 2)) {
 			int pos;
 			char *gidbuf = new_array(char, ngroups*21+32);
 			if (!gidbuf)
@@ -152,7 +151,7 @@
 	static gid_t mygid = GID_NONE;
 	if (mygid == GID_NONE) {
 		mygid = MY_GID();
-		if (verbose > 3)
+		if (DEBUG_GTE(OWN, 2))
 			rprintf(FINFO, "process has gid %u\n", (unsigned)mygid);
 	}
 	return gid == mygid;
@@ -167,7 +166,7 @@
 
 	node = add_to_list(&uidlist, id, name, id2, 0);
 
-	if (verbose > 3) {
+	if (DEBUG_GTE(OWN, 2)) {
 		rprintf(FINFO, "uid %u(%s) maps to %u\n",
 			(unsigned)id, name ? name : "", (unsigned)id2);
 	}
@@ -184,7 +183,7 @@
 	node = add_to_list(&gidlist, id, name, id2,
 		!am_root && !is_in_group(id2) ? FLAG_SKIP_GROUP : 0);
 
-	if (verbose > 3) {
+	if (DEBUG_GTE(OWN, 2)) {
 		rprintf(FINFO, "gid %u(%s) maps to %u\n",
 			(unsigned)id, name ? name : "", (unsigned)id2);
 	}
diff --git a/util.c b/util.c
index c71830b..0b3dc38 100644
--- a/util.c
+++ b/util.c
@@ -23,7 +23,6 @@
 #include "rsync.h"
 #include "ifuncs.h"
 
-extern int verbose;
 extern int dry_run;
 extern int module_id;
 extern int modify_window;
@@ -130,7 +129,7 @@
 		return 1;
 #endif
 
-	if (verbose > 2) {
+	if (DEBUG_GTE(TIME, 1)) {
 		rprintf(FINFO, "set modtime of %s to (%ld) %s",
 			fname, (long)modtime,
 			asctime(localtime(&modtime)));
@@ -397,7 +396,7 @@
 			counter = 1;
 	} while ((rc = access(path, 0)) == 0 && counter != start);
 
-	if (verbose > 0) {
+	if (INFO_GTE(MISC, 1)) {
 		rprintf(FWARNING, "renaming %s to %s because of text busy\n",
 			fname, path);
 	}
@@ -1019,7 +1018,7 @@
 		curr_dir_depth = count_dir_elements(curr_dir + module_dirlen);
 	}
 
-	if (verbose >= 5 && !set_path_only)
+	if (DEBUG_GTE(CHDIR, 1) && !set_path_only)
 		rprintf(FINFO, "[%s] change_dir(%s)\n", who_am_i(), curr_dir);
 
 	return 1;
@@ -1583,7 +1582,7 @@
 			overflow_exit("expand_item_list");
 		/* Using _realloc_array() lets us pass the size, not a type. */
 		new_ptr = _realloc_array(lp->items, item_size, new_size);
-		if (verbose >= 4) {
+		if (DEBUG_GTE(FLIST, 3)) {
 			rprintf(FINFO, "[%s] expand %s to %.0f bytes, did%s move\n",
 				who_am_i(), desc, (double)new_size * item_size,
 				new_ptr == lp->items ? " not" : "");