bug-coreutils
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [PATCH] stdbuf work in progress


From: Pádraig Brady
Subject: Re: [PATCH] stdbuf work in progress
Date: Tue, 9 Jun 2009 11:43:57 +0100
User-agent: Thunderbird 2.0.0.6 (X11/20071008)

Latest iteration of stdbuf command.

Notes:
 * It installs an unversioned shared lib to PKGLIBDIR
   (/usr/local/lib/coreutils by default). This is searched
   for by stdbuf itself rather than relying on the system,
   which allows us to select the uninstalled lib alongside
   the stdbuf binary when running tests for example.
 * Controlling fully buffered mode doesn't work with glibc-2.7:
   http://sources.redhat.com/bugzilla/show_bug.cgi?id=10108
 * It only installs to ELF systems with GCC currently
   (I'm not using libtool at present).

cheers,
Pádraig.

>From 1607d653df3401c286b0e5a9e51378221d49a388 Mon Sep 17 00:00:00 2001
From: =?utf-8?q?P=C3=A1draig=20Brady?= <address@hidden>
Date: Wed, 17 Dec 2008 11:30:03 +0000
Subject: [PATCH] stdbuf: A new program to run a command with modified stdio 
buffering

* AUTHORS: Register as the author.
* NEWS: Mention this change.
* README: Add stdbuf command to list.
* configure.ac: Only enable on ELF systems with GCC.
* cfg.mk (sc_system_h_headers): Use VC_LIST_EXCEPT rather than
VC_LIST, so we can add an exception, if needed.
* .x-sc_system_h_headers: New file.  Exempt libstdbuf.c.
* Makefile.am (syntax_check_exceptions): Add .x-sc_system_h_headers.
* doc/coreutils.texi (stdbuf invocation): Add stdbuf info.
* man/.gitignore: Ignore generated manpage.
* src/.gitignore: Ignore stdbuf and libstdbuf.so binaries.
* man/Makefile.am (stdbuf.1): Add dependency.
* man/stdbuf.x: New file.
* po/POTFILES.in: Reference new command and shared library sources.
* src/Makefile.am (build_if_possible__progs): Add stdbuf and libstdbuf,
(pkglib_PROGRAMS): Reference optional shared lib,
(libstdbuf_so_LDFLAGS): Add -shared GCC option,
(libstdbuf_so_CFLAGS): Add -fPIC GCC option.
* src/libstdbuf.c: shared library to LD_PRELOAD to control buffering.
* src/stdbuf.c: New file to setup env variables before execing command.
* tests/Makefile.am: Reference new test file.
* tests/misc/help-version: Set expected exit codes.
* tests/misc/invalid-opt: ditto.
* tests/misc/stdbuf: Add 9 tests.
---
 .x-sc_system_h_headers  |    3 +
 AUTHORS                 |    1 +
 Makefile.am             |    1 +
 NEWS                    |    5 +
 README                  |    6 +-
 cfg.mk                  |    3 +-
 configure.ac            |   15 ++
 doc/coreutils.texi      |   83 +++++++++++-
 man/.gitignore          |    1 +
 man/Makefile.am         |    1 +
 man/stdbuf.x            |   16 ++
 po/POTFILES.in          |    2 +
 src/.gitignore          |    2 +
 src/Makefile.am         |   22 +++-
 src/libstdbuf.c         |  103 ++++++++++++++
 src/stdbuf.c            |  354 +++++++++++++++++++++++++++++++++++++++++++++++
 tests/Makefile.am       |    1 +
 tests/misc/help-version |    2 +
 tests/misc/invalid-opt  |    1 +
 tests/misc/stdbuf       |   86 ++++++++++++
 20 files changed, 698 insertions(+), 10 deletions(-)
 create mode 100644 .x-sc_system_h_headers
 create mode 100644 man/stdbuf.x
 create mode 100644 src/libstdbuf.c
 create mode 100644 src/stdbuf.c
 create mode 100755 tests/misc/stdbuf

diff --git a/.x-sc_system_h_headers b/.x-sc_system_h_headers
new file mode 100644
index 0000000..14e020f
--- /dev/null
+++ b/.x-sc_system_h_headers
@@ -0,0 +1,3 @@
+^src/libstdbuf\.c$
+^src/system\.h$
+^src/copy\.h$
diff --git a/AUTHORS b/AUTHORS
index fa3c029..7095db0 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -76,6 +76,7 @@ sleep: Jim Meyering, Paul Eggert
 sort: Mike Haertel, Paul Eggert
 split: Torbjörn Granlund, Richard M. Stallman
 stat: Michael Meskes
+stdbuf: Pádraig Brady
 stty: David MacKenzie
 su: David MacKenzie
 sum: Kayvan Aghaiepour, David MacKenzie
diff --git a/Makefile.am b/Makefile.am
index 97be46a..99fc937 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -52,6 +52,7 @@ syntax_check_exceptions =             \
   .x-sc_require_config_h_first         \
   .x-sc_space_tab                      \
   .x-sc_sun_os_names                   \
+  .x-sc_system_h_headers               \
   .x-sc_trailing_blank                 \
   .x-sc_unmarked_diagnostics           \
   .x-sc_useless_cpp_parens
diff --git a/NEWS b/NEWS
index 29b09a0..14a24b0 100644
--- a/NEWS
+++ b/NEWS
@@ -7,6 +7,11 @@ GNU coreutils NEWS                                    -*- 
outline -*-
   truncate -s failed to skip all whitespace in the option argument in
   some locales.
 
+** New programs
+
+  stdbuf: Run a command with modified buffering operations
+  for its standard streams.
+
 ** New features
 
   chroot now accepts the options --userspec and --groups.
diff --git a/README b/README
index 08e0bab..7545eab 100644
--- a/README
+++ b/README
@@ -13,9 +13,9 @@ The programs that can be built with this package are:
   link ln logname ls md5sum mkdir mkfifo mknod mktemp mv nice nl nohup
   od paste pathchk pinky pr printenv printf ptx pwd readlink rm rmdir
   runcon seq sha1sum sha224sum sha256sum sha384sum sha512sum shred shuf
-  sleep sort split stat stty su sum sync tac tail tee test timeout touch tr
-  true truncate tsort tty uname unexpand uniq unlink uptime users vdir wc who
-  whoami yes
+  sleep sort split stat stdbuf stty su sum sync tac tail tee test timeout
+  touch tr true truncate tsort tty uname unexpand uniq unlink uptime users
+  vdir wc who whoami yes
 
 See the file NEWS for a list of major changes in the current release.
 
diff --git a/cfg.mk b/cfg.mk
index d3ec9de..34123d5 100644
--- a/cfg.mk
+++ b/cfg.mk
@@ -160,8 +160,7 @@ sc_system_h_headers: .re-list
        @if test -f $(srcdir)/src/system.h; then                        \
          trap 'rc=$$?; rm -f .re-list; exit $$rc' 0 1 2 3 15;          \
          grep -nE -f .re-list                                          \
-             $$($(VC_LIST) src |                                       \
-                grep -Ev '((copy|system)\.h|parse-gram\.c)$$')         \
+             $$($(VC_LIST_EXCEPT) | grep '^src/')                      \
            && { echo '$(ME): the above are already included via system.h'\
                  1>&2;  exit 1; } || :;                                \
        fi
diff --git a/configure.ac b/configure.ac
index 4eb640e..32d2958 100644
--- a/configure.ac
+++ b/configure.ac
@@ -321,6 +321,19 @@ if test $gl_cv_list_mounted_fs = yes && test 
$gl_cv_fs_space = yes; then
   gl_ADD_PROG([optional_bin_progs], [df])
 fi
 
+# Limit stdbuf to ELF systems with GCC
+optional_pkglib_progs=
+AC_MSG_CHECKING([whether this is an ELF system])
+AC_EGREP_CPP([yes], [#if __ELF__
+yes
+#endif], [elf_sys=yes], [elf_sys=no])
+AC_MSG_RESULT([$elf_sys])
+if test "$elf_sys" = "yes" && \
+   test "$GCC" = "yes"; then
+  gl_ADD_PROG([optional_bin_progs], [stdbuf])
+  gl_ADD_PROG([optional_pkglib_progs], [libstdbuf.so])
+fi
+
 ############################################################################
 mk="$srcdir/src/Makefile.am"
 # Extract all literal names from the definition of $(EXTRA_PROGRAMS)
@@ -393,6 +406,8 @@ MAN=`echo "$MAN"|sed 's/\@<:@\.1//'`
 
 OPTIONAL_BIN_PROGS=`echo "$optional_bin_progs "|sed 's/ /\$(EXEEXT) /g;s/ $//'`
 AC_SUBST([OPTIONAL_BIN_PROGS])
+OPTIONAL_PKGLIB_PROGS=`echo "$optional_pkglib_progs " | sed 's/ $//'`
+AC_SUBST([OPTIONAL_PKGLIB_PROGS])
 NO_INSTALL_PROGS_DEFAULT=$no_install_progs_default
 AC_SUBST([NO_INSTALL_PROGS_DEFAULT])
 
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index 155858b..e0c9c7f 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -105,6 +105,7 @@
 * sort: (coreutils)sort invocation.             Sort text files.
 * split: (coreutils)split invocation.           Split into fixed-size pieces.
 * stat: (coreutils)stat invocation.             Report file(system) status.
+* stdbuf: (coreutils)stdbuf invocation.         Modify stdio buffering.
 * stty: (coreutils)stty invocation.             Print/change terminal settings.
 * su: (coreutils)su invocation.                 Modify user and group ID.
 * sum: (coreutils)sum invocation.               Print traditional checksum.
@@ -197,7 +198,7 @@ Free Documentation License''.
 * User information::                   id logname whoami groups users who
 * System context::                     date uname hostname hostid uptime
 * SELinux context::                    chcon runcon
-* Modified command invocation::        chroot env nice nohup su timeout
+* Modified command invocation::        chroot env nice nohup stdbuf su timeout
 * Process control::                    kill
 * Delaying::                           sleep
 * Numeric operations::                 factor seq
@@ -434,6 +435,7 @@ Modified command invocation
 * env invocation::               Run a command in a modified environment
 * nice invocation::              Run a command with modified niceness
 * nohup invocation::             Run a command immune to hangups
+* stdbuf invocation::            Run a command with modified I/O buffering
 * su invocation::                Run a command with substitute user and group 
ID
 * timeout invocation::           Run a command with a time limit
 
@@ -14138,6 +14140,7 @@ user, etc.
 * env invocation::              Modify environment variables.
 * nice invocation::             Modify niceness.
 * nohup invocation::            Immunize to hangups.
+* stdbuf invocation::           Modify buffering of standard streams.
 * su invocation::               Modify user and group ID.
 * timeout invocation::          Run with time limit.
 @end menu
@@ -14501,6 +14504,84 @@ the exit status of @var{command} otherwise
 @end display
 
 
address@hidden stdbuf invocation
address@hidden @command{stdbuf}: Run a command with modified I/O stream 
buffering
+
address@hidden stdbuf
address@hidden standard streams, buffering
address@hidden line buffered
+
address@hidden allows one modify the buffering operations of the
+3 standard I/O streams associated with a program.  Synopsis:
+
address@hidden
+stdbuf @address@hidden @var{command}
address@hidden example
+
+Any additional @var{arg}s are passed as additional arguments to the
address@hidden
+
+The program accepts the following options.  Also see @ref{Common options}.
+
address@hidden @samp
+
address@hidden -i @var{mode}
address@hidden address@hidden
address@hidden -i
address@hidden --input
+Setup the standard input stream buffering.
+
address@hidden -o @var{mode}
address@hidden address@hidden
address@hidden -o
address@hidden --output
+Setup the standard output stream buffering.
+
address@hidden -e @var{mode}
address@hidden address@hidden
address@hidden -e
address@hidden --error
+Setup the standard error stream buffering.
+
address@hidden table
+
+The @var{mode} can be specified as follows:
+
address@hidden @samp
+
address@hidden L
+Set the stream to line buffered mode.
+In this mode data is coalesced until a newline is output or
+input is read from any stream attached to a terminal device.
+This option is invalid with standard input.
+
address@hidden 0
+Set the stream to unbuffered mode.
+In this mode data is output immediately and only the
+amount of data requested is read from input.
+
address@hidden @var{size}
+Specify the size of the buffer to use in fully buffered mode.
address@hidden
+
address@hidden table
+
+NOTE: If @var{command} controls the buffering of its standard streams
+(like @command{tee}) then this will override corresponding settings changed
+by this command.  Also some filters (like @command{dd} and @command{cat} etc.)
+don’t use streams for I/O, and are thus unaffected.
+
address@hidden exit status of @command{stdbuf}
+Exit status:
+
address@hidden
+125 if @command{stdbuf} itself fails
+126 if @var{command} is found but cannot be invoked
+127 if @var{command} cannot be found
+the exit status of @var{command} otherwise
address@hidden display
+
+
 @node su invocation
 @section @command{su}: Run a command with substitute user and group ID
 
diff --git a/man/.gitignore b/man/.gitignore
index e9e270d..1085ff0 100644
--- a/man/.gitignore
+++ b/man/.gitignore
@@ -72,6 +72,7 @@ sleep.1
 sort.1
 split.1
 stat.1
+stdbuf.1
 stty.1
 su.1
 sum.1
diff --git a/man/Makefile.am b/man/Makefile.am
index ee16a3f..cacaba6 100644
--- a/man/Makefile.am
+++ b/man/Makefile.am
@@ -105,6 +105,7 @@ sleep.1:    $(common_dep)   $(srcdir)/sleep.x       
../src/sleep.c
 sort.1:                $(common_dep)   $(srcdir)/sort.x        ../src/sort.c
 split.1:       $(common_dep)   $(srcdir)/split.x       ../src/split.c
 stat.1:                $(common_dep)   $(srcdir)/stat.x        ../src/stat.c
+stdbuf.1:      $(common_dep)   $(srcdir)/stdbuf.x      ../src/stdbuf.c
 stty.1:                $(common_dep)   $(srcdir)/stty.x        ../src/stty.c
 su.1:          $(common_dep)   $(srcdir)/su.x          ../src/su.c
 sum.1:         $(common_dep)   $(srcdir)/sum.x         ../src/sum.c
diff --git a/man/stdbuf.x b/man/stdbuf.x
new file mode 100644
index 0000000..93f0b8e
--- /dev/null
+++ b/man/stdbuf.x
@@ -0,0 +1,16 @@
+'\" Copyright (C) 2009 Free Software Foundation, Inc.
+'\"
+'\" This is free software.  You may redistribute copies of it under the terms
+'\" of the GNU General Public License <http://www.gnu.org/licenses/gpl.html>.
+'\" There is NO WARRANTY, to the extent permitted by law.
+[NAME]
+stdbuf \- Run COMMAND, with modified buffering operations for its standard 
streams.
+[DESCRIPTION]
+.\" Add any additional description here
+[EXAMPLES]
+.B tail -f access.log | stdbuf -oL cut -d \(aq \(aq -f1 | uniq
+.br
+This will immedidately display unique entries from access.log
+[BUGS]
+On GLIBC platforms, specifying a buffer size, i.e. using fully buffered mode
+will result in undefined operation.
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 6c291cc..6ded568 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -73,6 +73,7 @@ src/id.c
 src/install.c
 src/join.c
 src/kill.c
+src/libstdbuf.c
 src/link.c
 src/ln.c
 src/logname.c
@@ -109,6 +110,7 @@ src/sleep.c
 src/sort.c
 src/split.c
 src/stat.c
+src/stdbuf.c
 src/stty.c
 src/su.c
 src/sum.c
diff --git a/src/.gitignore b/src/.gitignore
index bc14523..f2886de 100644
--- a/src/.gitignore
+++ b/src/.gitignore
@@ -40,6 +40,7 @@ hostname
 id
 join
 kill
+libstdbuf.so
 libver.a
 link
 ln
@@ -81,6 +82,7 @@ sleep
 sort
 split
 stat
+stdbuf
 stty
 su
 sum
diff --git a/src/Makefile.am b/src/Makefile.am
index 3bed7b1..85a3841 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -24,7 +24,7 @@ no_install__progs = \
   arch hostname su
 
 build_if_possible__progs = \
-  chroot df hostid nice pinky stty su uname uptime users who
+  chroot df hostid nice pinky stdbuf libstdbuf.so stty su uname uptime users 
who
 
 AM_CFLAGS = $(WARN_CFLAGS) $(WERROR_CFLAGS)
 
@@ -48,6 +48,8 @@ bin_PROGRAMS = $(OPTIONAL_BIN_PROGS)
 
 noinst_PROGRAMS = setuidgid getlimits
 
+pkglib_PROGRAMS = $(OPTIONAL_PKGLIB_PROGS)
+
 noinst_HEADERS = \
   chown-core.h \
   copy.h \
@@ -91,6 +93,7 @@ du_LDADD = $(LDADD)
 getlimits_LDADD = $(LDADD)
 ptx_LDADD = $(LDADD)
 split_LDADD = $(LDADD)
+stdbuf_LDADD = $(LDADD)
 timeout_LDADD = $(LDADD)
 truncate_LDADD = $(LDADD)
 
@@ -170,6 +173,7 @@ du_LDADD += $(LIBICONV)
 getlimits_LDADD += $(LIBICONV)
 ptx_LDADD += $(LIBICONV)
 split_LDADD += $(LIBICONV)
+stdbuf_LDADD += $(LIBICONV)
 timeout_LDADD += $(LIBICONV)
 truncate_LDADD += $(LIBICONV)
 
@@ -286,6 +290,12 @@ sha512sum_CPPFLAGS = -DHASH_ALGO_SHA512=1 $(AM_CPPFLAGS)
 
 ginstall_CPPFLAGS = -DENABLE_MATCHPATHCON=1 $(AM_CPPFLAGS)
 
+# Note libstdbuf is only compiled if GCC is available
+# (as per the check in configure.ac). libtool is probably required
+# to relax this dependency.
+libstdbuf_so_LDFLAGS = -shared
+libstdbuf_so_CFLAGS = -fPIC $(AM_CFLAGS)
+
 editpl = sed -e 's,@''PERL''@,$(PERL),g'
 
 BUILT_SOURCES += dircolors.h
@@ -369,6 +379,7 @@ check-README:
        rm -rf $(pr) $(pm)
        echo $(all_programs) \
         | tr -s ' ' '\n' | sed -e 's,$(EXEEXT)$$,,;s/ginstall/install/' \
+        | sed /libstdbuf/d \
         | $(ASSORT) -u > $(pm) && \
        sed -n '/^The programs .* are:/,/^[a-zA-Z]/p' $(top_srcdir)/README \
          | sed -n '/^   */s///p' | tr -s ' ' '\n' > $(pr)
@@ -416,7 +427,9 @@ check-AUTHORS: $(all_programs)
 # Most functions in src/*.c should have static scope.
 # Any that don't must be marked with `extern', but `main'
 # and `usage' are exceptions.  They're always extern, but
-# don't need to be marked.
+# don't need to be marked. Also functions starting with __
+# are exempted due to possibly being added by the compiler
+# (when compiled as a shared library for example).
 #
 # The second nm|grep checks for file-scope variables with `extern' scope.
 .PHONY: sc_tight_scope
@@ -424,10 +437,11 @@ sc_tight_scope: $(bin_PROGRAMS)
        $(AM_V_GEN)t=exceptions-$$$$;                                   \
        trap "s=$$?; rm -f $$t; exit $$s" 0 1 2 13 15;                  \
        src=`for f in $(SOURCES); do                                    \
-              test -f $$f && d= || d=$(srcdir)/; echo $$d$$f; done`;   \
+              test -f $$f && d= || d=$(srcdir)/;                       \
+              test $$f = libstdbuf.c || echo $$d$$f; done`;            \
        hdr=`for f in $(noinst_HEADERS); do                             \
               test -f $$f && d= || d=$(srcdir)/; echo $$d$$f; done`;   \
-       ( printf 'main\nusage\n';                                       \
+       ( printf 'main\nusage\n_.*\n';                                  \
          grep -h -A1 '^extern .*[^;]$$' $$src                          \
            | grep -vE '^(extern |--)' | sed 's/ .*//';                 \
          perl -ne '/^extern \S+ (\S*) \(/ and print "$$1\n"' $$hdr;    \
diff --git a/src/libstdbuf.c b/src/libstdbuf.c
new file mode 100644
index 0000000..bf28092
--- /dev/null
+++ b/src/libstdbuf.c
@@ -0,0 +1,103 @@
+/* Assume all input validation has been done by stdbuf.  */
+
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include "system.h"
+#include "error.h"
+#include "quote.h"
+
+/* Note currently for glibc (2.3.5) the following call does not change the
+   the buffer size, and more problematically does not give any indication
+   that the new size request was ignored:
+
+       setvbuf (stdout, (char*)NULL, _IOFBF, 8192);
+
+   The ISO C99 standard section 7.19.5.6 on the setvbuf function says:
+
+   ... If buf is not a null pointer, the array it points to _may_ be used
+   instead of a buffer allocated by the setvbuf function and the argument
+   size specifies the size of the array; otherwise, size _may_ determine
+   the size of a buffer allocated by the setvbuf function. ...
+
+   Obviously some interpret the above to mean setvbuf(....,size)
+   is only a hint from the application which I don't agree with.
+
+   FreeBSD's libc seems more sensible in this regard. From the man page:
+
+   The size argument may be given as zero to obtain deferred optimal-size
+   buffer allocation as usual.  If it is not zero, then except for
+   unbuffered files, the buf argument should point to a buffer at least size
+   bytes long; this buffer will be used instead of the current buffer.  (If
+   the size argument is not zero but buf is NULL, a buffer of the given size
+   will be allocated immediately, and released on close.  This is an extension
+   to ANSI C; portable code should use a size of 0 with any NULL buffer.)
+   --------------------
+   Another issue is that on glibc-2.7 the following doesn't buffer
+   the first write if it's greater than 1 byte.
+
+       setvbuf(stdout,buf,_IOFBF,127);
+
+   Now the POSIX standard says that "allocating a buffer of size bytes does
+   not necessarily imply that all of size bytes are used for the buffer area".
+   However I think it's just a buggy implementation due to the various
+   inconsistencies with write sizes and subsequent writes.  */
+
+static const char *
+fileno_to_name (const int fd)
+{
+  switch (fd)
+    {
+    case 0:
+      return "stdin";
+    case 1:
+      return "stdout";
+    case 2:
+      return "stderr";
+    default:
+      return "unknown";
+    }
+}
+
+static void
+apply_mode (FILE *stream, const char *mode)
+{
+  char *buf = NULL;
+  int setvbuf_mode;
+  size_t size = 0;
+
+  if (*mode == '0')
+    setvbuf_mode = _IONBF;
+  else if (*mode == 'L')
+    setvbuf_mode = _IOLBF;      /* FIXME: should we allow 1ML  */
+  else
+    {
+      setvbuf_mode = _IOFBF;
+      /* Note this is done elsewhere in coreutils:
+         verify (SIZE_MAX <= ULONG_MAX);  */
+      size = strtoul (mode, NULL, 10);
+      buf = malloc (size);      /* will be free by fclose()  */
+    }
+
+  errno = 0;
+  if (setvbuf (stream, buf, setvbuf_mode, size) != 0)
+    {
+      error (0, errno, _("could not set buffering of %s to mode %s"),
+             fileno_to_name (fileno (stream)), quote (mode));
+    }
+}
+
+__attribute__ ((constructor)) static void
+stdbuf (void)
+{
+  char *e_mode = getenv ("_STDBUF_E");
+  char *i_mode = getenv ("_STDBUF_I");
+  char *o_mode = getenv ("_STDBUF_O");
+  if (e_mode) /* Do first so can write errors to stderr  */
+    apply_mode (stderr, e_mode);
+  if (i_mode)
+    apply_mode (stdin, i_mode);
+  if (o_mode)
+    apply_mode (stdout, o_mode);
+}
diff --git a/src/stdbuf.c b/src/stdbuf.c
new file mode 100644
index 0000000..1b15327
--- /dev/null
+++ b/src/stdbuf.c
@@ -0,0 +1,354 @@
+/* stdbuf -- setup the standard streams for a command
+   Copyright (C) 2009 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+/* Written by Pádraig Brady.  */
+
+#include <config.h>
+#include <stdio.h>
+#include <getopt.h>
+#include <sys/types.h>
+
+#include "system.h"
+#include "error.h"
+#include "posixver.h"
+#include "quote.h"
+#include "xstrtol.h"
+
+/* The official name of this program (e.g., no `g' prefix).  */
+#define PROGRAM_NAME "stdbuf"
+#define LIB_NAME "libstdbuf.so" //FIXME: don't hardcode
+
+#define AUTHORS proper_name_utf8 ("Padraig Brady", "P\303\241draig Brady")
+
+static char *program_path;
+
+extern char **environ;
+
+/* Set size to the value of STR, interpreted as a decimal integer,
+   optionally multiplied by various values.
+   Return -1 on error, 0 on success.
+
+   This supports dd BLOCK size suffixes.
+   Note we don't support dd's b=512, c=1, w=2 or 21x512MiB formats.  */
+static int
+parse_size (char const *str, size_t *size)
+{
+  enum strtol_error e;
+  uintmax_t tmp_size;
+  e = xstrtoumax (str, NULL, 10, &tmp_size, "EGkKMPTYZ0");
+  if (e == LONGINT_OK && tmp_size > SIZE_MAX)
+    e = LONGINT_OVERFLOW;
+
+  if (e == LONGINT_OK)
+    {
+      errno = 0;
+      *size = tmp_size;
+      return 0;
+    }
+
+  errno = (e == LONGINT_OVERFLOW ? EOVERFLOW : 0);
+  return -1;
+}
+
+static struct option const longopts[] =
+{
+  {"input", required_argument, NULL, 'i'},
+  {"output", required_argument, NULL, 'o'},
+  {"error", required_argument, NULL, 'e'},
+  {GETOPT_HELP_OPTION_DECL},
+  {GETOPT_VERSION_OPTION_DECL},
+  {NULL, 0, NULL, 0}
+};
+
+void
+usage (int status)
+{
+  if (status != EXIT_SUCCESS)
+    fprintf (stderr, _("Try `%s --help' for more information.\n"),
+             program_name);
+  else
+    {
+      printf (_("Usage: %s OPTION... COMMAND\n"), program_name);
+      fputs (_("\
+Run COMMAND, with modified buffering operations for its standard streams.\n\
+\n\
+"), stdout);
+      fputs (_("\
+Mandatory arguments to long options are mandatory for short options too.\n\
+"), stdout);
+      fputs (_("\
+  -i, --input=MODE   Setup standard input stream buffering\n\
+  -o, --output=MODE  Setup standard output stream buffering\n\
+  -e, --error=MODE   Setup standard error stream buffering\n\
+"), stdout);
+      fputs (HELP_OPTION_DESCRIPTION, stdout);
+      fputs (VERSION_OPTION_DESCRIPTION, stdout);
+      fputs (_("\n\
+If MODE is `L' then corresponding stream will be line buffered.\n\
+This option is invalid with standard input.\n"), stdout);
+      fputs (_("\n\
+If MODE is `0' then corresponding stream will be unbuffered.\n\
+"), stdout);
+      fputs (_("\n\
+Otherwise MODE is a number which may be followed by one of the following:\n\
+KB 1000, K 1024, MB 1000*1000, M 1024*1024, and so on for G, T, P, E, Z, Y.\n\
+In this case the corresponding stream will be fully buffered with the buffer\n\
+size set to MODE bytes.\n\
+"), stdout);
+      fputs (_("\n\
+NOTE: If COMMAND controls the buffering of its standard streams (like `tee')\n\
+then this will override corresponding settings changed by this command.\n\
+Also some filters (like `dd' and `cat' etc.) don't use streams for I/O,\n\
+and are thus unaffected.\n\
+"), stdout);
+      emit_bug_reporting_address ();
+    }
+  exit (status);
+}
+
+static struct {
+    size_t size;
+    int optc;
+    char *optarg;
+} stdbuf[3];
+
+static int
+optc_to_fileno (int c)
+{
+  if (c == 'e')
+    return STDERR_FILENO;
+  if (c == 'i')
+    return STDIN_FILENO;
+  if (c == 'o')
+    return STDOUT_FILENO;
+  return -1;
+}
+
+/* Internal error  */
+#define EXIT_CANCELED 125
+
+static void
+set_LD_PRELOAD(void)
+{
+  int ret;
+  char* old_libs = getenv("LD_PRELOAD");
+  char* LD_PRELOAD;
+
+  //Note this would auto add the appropriate search path for "libstdbuf.so":
+  //  gcc stdbuf.c -Wl,-rpath,'$ORIGIN' -Wl,-rpath,$PKGLIBDIR
+  //However we want the lookup done for the exec'd command not stdbuf.
+  //
+  //Since we don't link against libstdbuf.so add it to LIBDIR rather than
+  //LIBEXECDIR, as we'll search for it in the "sys default" case below.
+  char const* const search_path[] = {
+      program_path,
+      PKGLIBDIR,
+      "", /* sys default */
+      NULL
+  };
+
+  char const* const* path = search_path;
+  char* libstdbuf;
+
+  do
+    {
+      struct stat sb;
+
+      if (!**path) /* system default  */
+        {
+          libstdbuf = xstrdup(LIB_NAME);
+          break;
+        }
+      ret = asprintf (&libstdbuf, "%s/%s", *path, LIB_NAME);
+      if (ret < 0)
+        xalloc_die ();
+      if (stat (libstdbuf, &sb) == 0) /* file_exists  */
+        break;
+      free (libstdbuf);
+    } while (*++path);
+
+  //FIXME: Do we need to support libstdbuf.dll, c:, '\' separators etc?
+
+  if (old_libs)
+    ret = asprintf (&LD_PRELOAD, "LD_PRELOAD=%s:%s", old_libs, libstdbuf);
+  else
+    ret = asprintf (&LD_PRELOAD, "LD_PRELOAD=%s", libstdbuf);
+
+  if (ret < 0)
+    xalloc_die ();
+
+  free(libstdbuf);
+
+  ret = putenv(LD_PRELOAD);
+
+  if (ret < 0)
+    xalloc_die ();
+}
+
+/* argv[0] can be anything really, but generally it contains
+   the path to the executable or just a name if it was executed
+   using $PATH. In the latter case to get the path we can:
+   search getenv("PATH"), readlink("/prof/self/exe"), getenv("_"),
+   dladdr(), pstat_getpathname(), etc.  */
+
+static void
+set_program_path (const char *arg)
+{
+  if (strchr (arg, '/'))        /* Use absolute or relative paths directly.  */
+    {
+      program_path = dir_name (arg);
+    }
+  else
+    {
+      char *path;
+      char tmppath[PATH_MAX + 1];
+      int len = readlink ("/proc/self/exe", tmppath, sizeof (tmppath) - 1);
+      if (len > 0)
+        {
+          tmppath[len] = '\0';
+          program_path = dir_name (tmppath);
+        }
+      else if ((path = getenv ("PATH")))
+        {
+          char *dir;
+          path = strdup (path);
+          for (dir = strtok (path, ":"); dir != NULL; dir = strtok (NULL, ":"))
+            {
+              int req = snprintf (tmppath, sizeof (tmppath), "%s/%s", dir, 
arg);
+              if (req >= sizeof (tmppath))
+                {
+                  error (0, 0, _("path truncated when looking for %s"),
+                         quote (arg));
+                }
+              else if (access (tmppath, X_OK) == 0)
+                {
+                  program_path = dir_name (tmppath);
+                  break;
+                }
+            }
+          free (path);
+        }
+    }
+}
+
+int
+main (int argc, char **argv)
+{
+  int c, i;
+
+  initialize_main (&argc, &argv);
+  set_program_name (argv[0]);
+  setlocale (LC_ALL, "");
+  bindtextdomain (PACKAGE, LOCALEDIR);
+  textdomain (PACKAGE);
+
+  initialize_exit_failure (EXIT_CANCELED);
+  atexit (close_stdout);
+
+  /* Try to get path to this program, so we can load
+     libstdbuf.so from the same directory as a convenience.  */
+  set_program_path (argv[0]);
+  if (!program_path)
+    program_path = strdup(PKGLIBDIR); /* Need to init to non NULL.  */
+
+  while ((c = getopt_long (argc, argv, "+i:o:e:", longopts, NULL)) != -1)
+    {
+      int opt_fileno;
+
+      switch (c)
+        {
+        /* Old McDonald had a farm ei...  */
+        case 'e':
+        case 'i':
+        case 'o':
+          opt_fileno = optc_to_fileno(c);
+          stdbuf[opt_fileno].optc=c;
+          while (isspace (*optarg))
+            optarg++;
+          stdbuf[opt_fileno].optarg = optarg;
+          if (c == 'i' && *optarg == 'L')
+            {
+              /* -oL will be by far the most common use of this utility,
+               * but one could easily think -iL might have the same affect,
+               * so disallow it as it could be confusing. */
+              error (0, 0, _("line buffering stdin is meaninglesss"));
+              usage (EXIT_CANCELED);
+            }
+
+          if (!STREQ(optarg,"L") &&
+              parse_size (optarg, &stdbuf[opt_fileno].size) == -1)
+            error (EXIT_CANCELED, errno, _("invalid mode %s"),
+                   quote (optarg));
+
+          break;
+
+        case_GETOPT_HELP_CHAR;
+
+        case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS);
+
+        default:
+          usage (EXIT_CANCELED);
+        }
+    }
+
+  argv += optind;
+  argc -= optind;
+
+  /* must specify at least 1 command.  */
+  if (argc < 1)
+    {
+      error (0, 0, _("missing operand"));
+      usage (EXIT_CANCELED);
+    }
+
+  /* FIXME: Should we mandate at least one option?  */
+
+  set_LD_PRELOAD();
+
+  /* Populate environ with _STDBUF_I=$MODE _STDBUF_O=$MODE _STDBUF_E=$MODE  */
+  for (i = 0; i < sizeof (stdbuf) / sizeof *(stdbuf); i++)
+    {
+      if (stdbuf[i].optarg)
+        {
+          char *var;
+          int ret;
+
+          if (*stdbuf[i].optarg == 'L')
+            ret = asprintf (&var, "%s%c=L", "_STDBUF_",
+                            toupper(stdbuf[i].optc));
+          else
+            ret = asprintf (&var, "%s%c=%"PRIuMAX, "_STDBUF_",
+                            toupper(stdbuf[i].optc), (uintmax_t) 
stdbuf[i].size);
+          if (ret < 0)
+            xalloc_die ();
+          putenv(var);
+        }
+    }
+
+  execvp (*argv, argv);
+
+  {
+    int exit_status = (errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE);
+    error (0, errno, "%s", *argv);
+    exit (exit_status);
+  }
+}
+
+/*
+ * Local variables:
+ *  indent-tabs-mode: nil
+ * End:
+ */
diff --git a/tests/Makefile.am b/tests/Makefile.am
index a0ed986..a2be194 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -216,6 +216,7 @@ TESTS =                                             \
   misc/split-l                                 \
   misc/stat-fmt                                        \
   misc/stat-printf                             \
+  misc/stdbuf                                  \
   misc/stty                                    \
   misc/stty-invalid                            \
   misc/stty-row-col                            \
diff --git a/tests/misc/help-version b/tests/misc/help-version
index ee74600..983148b 100755
--- a/tests/misc/help-version
+++ b/tests/misc/help-version
@@ -28,6 +28,7 @@ export SHELL
 . $srcdir/test-lib.sh
 
 expected_failure_status_nohup=127
+expected_failure_status_stdbuf=125
 expected_failure_status_timeout=125
 expected_failure_status_printenv=2
 expected_failure_status_tty=3
@@ -148,6 +149,7 @@ printf_args=foo
 seq_args=10
 sleep_args=0
 su_args=--version
+stdbuf_args="-oL true"
 timeout_args=--version
 
 # I'd rather not run sync, since it spins up disks that I've
diff --git a/tests/misc/invalid-opt b/tests/misc/invalid-opt
index cbd41ca..9af2dd2 100755
--- a/tests/misc/invalid-opt
+++ b/tests/misc/invalid-opt
@@ -32,6 +32,7 @@ my %exit_status =
     expr => 0,
     nohup => 127,
     sort => 2,
+    stdbuf => 125,
     test => 0,
     timeout => 125,
     true => 0,
diff --git a/tests/misc/stdbuf b/tests/misc/stdbuf
new file mode 100755
index 0000000..eb9f6bf
--- /dev/null
+++ b/tests/misc/stdbuf
@@ -0,0 +1,86 @@
+#!/bin/sh
+# Exercise stdbuf functionality
+
+# Copyright (C) 2009 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+if test "$VERBOSE" = yes; then
+  set -x
+  stdbuf --version
+fi
+
+. $srcdir/test-lib.sh
+getlimits_
+
+# Use a fifo rather than a pipe in the tests below
+# so that the producer (uniq) will wait until the
+# consumer (dd) opens the fifo therefore increasing
+# the chance that dd will read the data from each
+# write separately.
+mkfifo fifo || framework_failure
+
+fail=0
+
+# Verify input parameter checking
+stdbuf -o1 true || fail=1 #verify size syntax
+stdbuf -oK true || fail=1 #verify size syntax
+stdbuf -o0 true || fail=1 #verify unbuffered syntax
+stdbuf -oL true || fail=1 #verify line buffered syntax
+stdbuf -ol true && fail=1 #Capital 'L' required
+stdbuf -o$SIZE_OFLOW true && fail=1 #size too large
+stdbuf -iL true && fail=1 #line buffering stdin disallowed
+
+# Ensure line buffering stdout takes effect
+printf '1\n' > exp
+dd count=1 if=fifo > out 2> err&
+(printf '1\n'; sleep .2; printf '2\n') | stdbuf -oL uniq > fifo
+wait #for dd to complete
+compare out exp || fail=1
+
+# Ensure un buffering stdout takes effect
+printf '1\n' > exp
+dd count=1 if=fifo > out 2> err&
+(printf '1\n'; sleep .2; printf '2\n') | stdbuf -o0 uniq > fifo
+wait #for dd to complete
+compare out exp || fail=1
+
+# Ensure un buffering stdin takes effect
+#  The following works for me, but is racy. I.E. we're depending
+#  on dd to run and close the fifo before the second write by uniq.
+#  If we add a sleep, then we're just testing -oL
+    #printf '3\n' > exp
+    #dd count=1 if=fifo > /dev/null 2> err&
+    #printf '1\n\2\n3\n' | (stdbuf -i0 -oL uniq > fifo; cat) > out
+    #wait #for dd to complete
+    #compare out exp || fail=1
+#  One could remove the need for dd (used to close the fifo to get uniq to quit
+#  early), if head -n1 read stdin char by char. Note uniq | head -c2 doesn't
+#  suffice due to the buffering implicit in the pipe. sed currently does read
+#  stdin char by char, so we can test with `sed 1q`. However I'm wary about
+#  adding this dependency on a program outside of coreutils.
+    #printf '2\n' > exp
+    #printf '1\n2\n' | (stdbuf -i0 sed 1q >/dev/null; cat) > out
+    #compare out exp || fail=1
+
+# Ensure block buffering stdout takes effect
+# We don't currently test block buffering failures as
+# this doesn't work on on GLIBC-2.7 or GLIBC-2.9 at least.
+   #printf '1\n2\n' > exp
+   #dd count=1 if=fifo > out 2> err&
+   #(printf '1\n'; sleep .2; printf '2\n') | stdbuf -o4 uniq > fifo
+   #wait #for dd to complete
+   #compare out exp || fail=1
+
+Exit $fail
-- 
1.5.3.6


reply via email to

[Prev in Thread] Current Thread [Next in Thread]