[PATCH v2] tar/cpio: fix verbose output with to-stdout option

ThorstenB via busybox <[email protected]> Sat, 1 Aug 2026 12:13:28 +0200
Newsgroups gmane.linux.busybox
Message-ID <[email protected]>
When verbose/-v and -O/--to-stdout options are combined, verbose output
needs to be sent to stderr instead.
Verbose output also needs to be line-buffered, so for each archive member,
verbose output is emitted before extracting the corresponding file data (tar),
(after the file data for cpio).
Aligns the behavior with the GNU tar/cpio utilities.
Includes test cases.

function                                             old     new   delta
cpio_main                                            565     603     +38
header_list                                           17      52     +35
header_verbose_list                                  283     305     +22
tar_main                                            1195    1210     +15
init_handle                                           69      80     +11
------------------------------------------------------------------------------
(add/remove: 0/0 grow/shrink: 5/0 up/down: 121/0)             Total: 121 bytes

Signed-off-by: ThorstenB <[email protected]>
---
 archival/cpio.c                               |  5 ++-
 archival/libarchive/header_list.c             |  4 +-
 archival/libarchive/header_verbose_list.c     | 11 +++--
 archival/libarchive/init_handle.c             |  1 +
 archival/tar.c                                |  5 ++-
 include/bb_archive.h                          |  1 +
 .../cpio/cpio-verbose-to-standard-output      | 44 +++++++++++++++++++
 .../tar-verbose-extract-to-standard-output    | 41 +++++++++++++++++
 ...ar-very-verbose-extract-to-standard-output | 39 ++++++++++++++++
 9 files changed, 144 insertions(+), 7 deletions(-)
 create mode 100644 testsuite/cpio/cpio-verbose-to-standard-output
 create mode 100644 testsuite/tar/tar-verbose-extract-to-standard-output
 create mode 100644 testsuite/tar/tar-very-verbose-extract-to-standard-output

diff --git a/archival/cpio.c b/archival/cpio.c
index b033b3733..eb7a2eeaa 100644
--- a/archival/cpio.c
+++ b/archival/cpio.c
@@ -543,8 +543,11 @@ int cpio_main(int argc UNUSED_PARAM, char **argv)
 	}
 	if (opt & OPT_EXTRACT) {
 		archive_handle->action_data = data_extract_all;
-		if (opt & OPT_2STDOUT)
+		if (opt & OPT_2STDOUT) {
 			archive_handle->action_data = data_extract_to_stdout;
+			/* If data goes to stdout, verbose goes to stderr */
+			archive_handle->file_header->verbose_fp = stderr;
+		}
 	}
 	if (opt & OPT_UNCONDITIONAL) {
 		archive_handle->ah_flags |= ARCHIVE_UNLINK_OLD;
diff --git a/archival/libarchive/header_list.c b/archival/libarchive/header_list.c
index 9490b3635..c2105d223 100644
--- a/archival/libarchive/header_list.c
+++ b/archival/libarchive/header_list.c
@@ -8,5 +8,7 @@
 void FAST_FUNC header_list(const file_header_t *file_header)
 {
 //TODO: cpio -vp DIR should output "DIR/NAME", not just "NAME" */
-	puts(printable_string(file_header->name));
+	fputs(printable_string(file_header->name), file_header->verbose_fp);
+	fputc('\n', file_header->verbose_fp);
+	fflush(file_header->verbose_fp);
 }
diff --git a/archival/libarchive/header_verbose_list.c b/archival/libarchive/header_verbose_list.c
index e7a09430d..303606b9c 100644
--- a/archival/libarchive/header_verbose_list.c
+++ b/archival/libarchive/header_verbose_list.c
@@ -29,7 +29,8 @@ void FAST_FUNC header_verbose_list(const file_header_t *file_header)
 		/*sprintf(gid, "%u", (unsigned)file_header->gid);*/
 		group = utoa(file_header->gid);
 	}
-	printf("%s %s/%s %9"OFF_FMT"u %4u-%02u-%02u %02u:%02u:%02u %s",
+	fprintf(file_header->verbose_fp,
+	        "%s %s/%s %9"OFF_FMT"u %4u-%02u-%02u %02u:%02u:%02u %s",
 		bb_mode_string(modestr, file_header->mode),
 		user,
 		group,
@@ -46,7 +47,8 @@ void FAST_FUNC header_verbose_list(const file_header_t *file_header)
 
 	localtime_r(&file_header->mtime, ptm);
 
-	printf("%s %u/%u %9"OFF_FMT"u %4u-%02u-%02u %02u:%02u:%02u %s",
+	fprintf(file_header->verbose_fp,
+	        "%s %u/%u %9"OFF_FMT"u %4u-%02u-%02u %02u:%02u:%02u %s",
 		bb_mode_string(modestr, file_header->mode),
 		(unsigned)file_header->uid,
 		(unsigned)file_header->gid,
@@ -63,7 +65,8 @@ void FAST_FUNC header_verbose_list(const file_header_t *file_header)
 
 	/* NB: GNU tar shows "->" for symlinks and "link to" for hardlinks */
 	if (file_header->link_target) {
-		printf(" -> %s", printable_string(file_header->link_target));
+		fprintf(file_header->verbose_fp, " -> %s", printable_string(file_header->link_target));
 	}
-	bb_putchar('\n');
+	fputc('\n', file_header->verbose_fp);
+	fflush(file_header->verbose_fp);
 }
diff --git a/archival/libarchive/init_handle.c b/archival/libarchive/init_handle.c
index 4c64dac58..14a8fe9c3 100644
--- a/archival/libarchive/init_handle.c
+++ b/archival/libarchive/init_handle.c
@@ -12,6 +12,7 @@ archive_handle_t* FAST_FUNC init_handle(void)
 	/* Initialize default values */
 	archive_handle = xzalloc(sizeof(archive_handle_t));
 	archive_handle->file_header = xzalloc(sizeof(file_header_t));
+	archive_handle->file_header->verbose_fp = stdout;
 	archive_handle->action_header = header_skip;
 	archive_handle->action_data = data_skip;
 	archive_handle->filter = filter_accept_all;
diff --git a/archival/tar.c b/archival/tar.c
index 87ad7bb49..54f59d527 100644
--- a/archival/tar.c
+++ b/archival/tar.c
@@ -1075,8 +1075,11 @@ int tar_main(int argc UNUSED_PARAM, char **argv)
 	if (opt & OPT_EXTRACT)
 		tar_handle->action_data = data_extract_all;
 
-	if (opt & OPT_2STDOUT)
+	if (opt & OPT_2STDOUT) {
 		tar_handle->action_data = data_extract_to_stdout;
+		/* If archive goes to stdout, verbose goes to stderr */
+		tar_handle->file_header->verbose_fp = stderr;
+	}
 
 	if (opt & OPT_2COMMAND) {
 		putenv((char*)"TAR_FILETYPE=f");
diff --git a/include/bb_archive.h b/include/bb_archive.h
index 1dc77f31d..aeca1daef 100644
--- a/include/bb_archive.h
+++ b/include/bb_archive.h
@@ -41,6 +41,7 @@ typedef struct file_header_t {
 	mode_t mode;
 	time_t mtime;
 	dev_t device;
+	FILE* verbose_fp;
 } file_header_t;
 
 struct hardlinks_t;
diff --git a/testsuite/cpio/cpio-verbose-to-standard-output b/testsuite/cpio/cpio-verbose-to-standard-output
new file mode 100644
index 000000000..9c8267bff
--- /dev/null
+++ b/testsuite/cpio/cpio-verbose-to-standard-output
@@ -0,0 +1,44 @@
+#####################################################
+# create test archive
+#####################################################
+echo "data 1" > file1.txt
+echo "data 2" > file2.txt
+echo "data 3" > file3.txt
+printf '%s\n' file1.txt file2.txt file3.txt | busybox cpio -o -H newc > archive.cpio
+
+mkdir output
+cd output
+
+#####################################################
+# cpio -v --to-stdout: verbose goes to to stderr instead
+#####################################################
+busybox cpio -iv --to-stdout < ../archive.cpio 2> stderr.txt > /dev/null
+
+cat > expected.txt << 'EOF'
+file1.txt
+file2.txt
+file3.txt
+1 blocks
+EOF
+
+cmp expected.txt stderr.txt
+
+rm -f file1.txt file2.txt file3.txt
+
+#####################################################
+# is stdout/stderr properly synchronized?
+#####################################################
+busybox cpio -iv --to-stdout < ../archive.cpio > output.txt 2>&1
+
+cat > expected.txt << 'EOF'
+data 1
+file1.txt
+data 2
+file2.txt
+data 3
+file3.txt
+1 blocks
+EOF
+
+cmp expected.txt output.txt
+
diff --git a/testsuite/tar/tar-verbose-extract-to-standard-output b/testsuite/tar/tar-verbose-extract-to-standard-output
new file mode 100644
index 000000000..3433920e2
--- /dev/null
+++ b/testsuite/tar/tar-verbose-extract-to-standard-output
@@ -0,0 +1,41 @@
+# FEATURE: CONFIG_FEATURE_TAR_CREATE
+
+#####################################################
+# create test tar
+#####################################################
+echo "data 1" > file1.txt
+echo "data 2" > file2.txt
+echo "data 3" > file3.txt
+busybox tar cf test.tar file1.txt file2.txt file3.txt
+
+
+#####################################################
+# tar vO: verbose goes to to stderr instead
+#####################################################
+busybox tar xvOf test.tar 2> stderr.txt > /dev/null
+
+cat > expected.txt << 'EOF'
+file1.txt
+file2.txt
+file3.txt
+EOF
+
+cmp expected.txt stderr.txt
+
+
+#####################################################
+# is stdout/stderr properly synchronized?
+#####################################################
+busybox tar xvOf test.tar > output.txt 2>&1
+
+cat > expected.txt << 'EOF'
+file1.txt
+data 1
+file2.txt
+data 2
+file3.txt
+data 3
+EOF
+
+cmp expected.txt output.txt
+
diff --git a/testsuite/tar/tar-very-verbose-extract-to-standard-output b/testsuite/tar/tar-very-verbose-extract-to-standard-output
new file mode 100644
index 000000000..82f88eddc
--- /dev/null
+++ b/testsuite/tar/tar-very-verbose-extract-to-standard-output
@@ -0,0 +1,39 @@
+# FEATURE: CONFIG_FEATURE_TAR_CREATE
+
+#####################################################
+# create test tar
+#####################################################
+echo "data 1" > file1.txt
+echo "data 2" > file2.txt
+echo "data 3" > file3.txt
+busybox tar cf test.tar file1.txt file2.txt file3.txt
+
+
+#####################################################
+# tar vvO: very verbose output goes to to stderr
+#####################################################
+busybox tar xvvOf test.tar 2> stderr.txt > /dev/null
+
+# create expected output
+busybox tar tvvf test.tar file1.txt > expected.txt
+busybox tar tvvf test.tar file2.txt >> expected.txt
+busybox tar tvvf test.tar file3.txt >> expected.txt
+
+cmp expected.txt stderr.txt
+
+
+#####################################################
+# is stdout/stderr properly synchronized?
+#####################################################
+busybox tar xvvOf test.tar > output.txt 2>&1
+
+# create expected output
+busybox tar tvvf test.tar file1.txt > expected.txt
+cat file1.txt >> expected.txt
+busybox tar tvvf test.tar file2.txt >> expected.txt
+cat file2.txt >> expected.txt
+busybox tar tvvf test.tar file3.txt >> expected.txt
+cat file3.txt >> expected.txt
+
+cmp expected.txt output.txt
+
-- 
2.51.0