Você deve conseguir fazer isso com multitail
facilmente.
Existe uma ferramenta que recebe entradas de vários arquivos ou pipes e os grava no stdout, sem bloquear as leituras, de modo que as linhas de entrada individuais saiam intactas? Eu basicamente quero multiplexar um monte de entradas em uma saída sem linhas de sobra.
$ combine file1 <(prog2) ... > nice-output.txt
Você deve conseguir fazer isso com multitail
facilmente.
Se os processos gravarem as linhas em uma única chamada write
, o que requer que os processos usem o buffer de linha (geralmente desativado se a saída padrão não for um terminal), basta apontar todos para um pipe. / p>
{ { sleep .1; echo one; sleep .1; echo two; } &
{ echo hello; sleep .15; echo world; };
wait; } | cat
Se os processos só executarem o buffer de linha ao gravar em um terminal, a maneira mais fácil é usar script
. É um pouco desajeitado: só pode gravar em um arquivo.
script -q -c '
{ { sleep .1; echo one; sleep .1; echo two; } &
{ echo hello; sleep .15; echo world; };
wait; }'
tail -n +2 typescript
Se os programas escreverem linhas longas ou simplesmente não usarem buffer de linha, essa abordagem não funcionará. Você precisará de um programa coletor que leia e armazene buffers de linhas de cada entrada separadamente e execute a sincronização nos finais de linha. Não há utilitário padrão com essa funcionalidade. I second sugestão de Caleb de multitail
.
Aqui está um script em Python que lê linhas produzidas por vários comandos e as cospe em sua saída padrão, sem dividir uma linha. Eu não testei muito, então deixe de usar o usuário. Eu não fiz o benchmarking.
#!/usr/bin/env python
import Queue, itertools, os, subprocess, sys, threading
# Queue of (producer_id, line). line==None indicates the end of a producer.
lq = Queue.Queue()
# Line producer
def run_task(i, cmd):
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
line = p.stdout.readline()
while line <> "":
lq.put((i, line))
line = p.stdout.readline()
lq.put((i, None))
# Start a producer for each command passed as an argument
for i in range(1,len(sys.argv)):
threading.Thread(target=run_task, args=(i, sys.argv[i])).start()
sources = len(sys.argv) - 1
# Consumer: print lines as they come in, until no producer is left.
while sources > 0:
(k, line) = lq.get()
if line == None: sources -= 1
else: sys.stdout.write(str(k) + ":" + line)
Uso da amostra:
./collect.py 'sleep 1; ls /; sleep 1; ls /' \
'/bin/echo -n foo; sleep 1; /bin/echo -n bar; sleep 1; /bin/echo qux'
Sim multitail parece vinculado à noção de uma "janela" como um subconjunto de um terminal; Eu não consegui fazer isso funcionar como um componente de pipeline.
Então, parece que nós mesmos fazemos isso rachaduras
/* Copyright © 2015 [email protected]
** Use/modify as you see fit but leave this attribution.
** If you change the interface and want to distribute the
** result please change the binary name too! */
#include <err.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/select.h>
/* typedefs are for pussies */
struct {
char *filename; /* for clarity of errors */
char *data;
long len;
long cap;
} saved[FD_SETSIZE] = {0};
void
ewriten(int fd, char *buf, int n)
{
int done = 0, c;
while (done < n) {
if ((c=write(fd, buf + done, n - done)) <= 0 && errno != EINTR) {
err(1, "write");
}
done += c;
}
}
int
empty(fd_set *fdset, int maxfd)
{
int i;
for (i=0; i <= maxfd; i++) {
if (FD_ISSET(i, fdset)) return 0;
}
return 1;
}
void
combine(fd_set *fdset, int maxfd)
{
char buf[4096], *cp;
fd_set ready;
int n, i, fd, left;
while (!empty(fdset, maxfd)) {
ready = *fdset;
/* timeouts are for pussies */
if (select(maxfd + 1, &ready, NULL, NULL, NULL) == -1) err(1, "select");
for (fd=0; fd <= maxfd; fd++) {
if (!FD_ISSET(fd, &ready)) continue;
switch (n=read(fd, &buf, sizeof(buf))) {
case -1:
if (errno == EINTR)
break; /* ignore interrupts; we'll re-read next iteration */
if (saved[fd].filename) err(1, "read: %s", saved[fd].filename);
err(1, "read: %d", fd);
case 0:
if (saved[fd].len > 0) {
/* someone forgot their newline at EOF... */
ewriten(1, saved[fd].data, saved[fd].len);
saved[fd].data[0] = '\n'; /* put it back for them */
ewriten(1, saved[fd].data, 1);
}
free(saved[fd].data);
FD_CLR(fd, fdset);
break;
default:
for (cp=buf + n - 1; cp >= buf && *cp != '\n'; cp--); /* find last newline */
left = n - (cp - buf + 1);
if (cp >= buf) {
/* we found one! first dump any saved data from the last read */
if (saved[fd].len > 0) {
ewriten(1, saved[fd].data, saved[fd].len);
saved[fd].len = 0;
}
ewriten(1, buf, cp - buf + 1);
}
if (left > 0) {
/* now save any leftover data for later */
int need = saved[fd].len + left;
if (saved[fd].cap < need &&
(saved[fd].data=realloc(saved[fd].data, need)) == NULL) {
errx(1, "realloc: failed on %d bytes", need);
/* it was good enough for quake... */
}
saved[fd].cap = need;
memcpy(saved[fd].data + saved[fd].len, buf + n - 1 - left, left);
saved[fd].len += left;
}
}
}
}
}
void
addfd(int fd, fd_set *fdset, int *maxfd)
{
FD_SET(fd, fdset);
if (*maxfd < fd) {
*maxfd = fd;
}
}
int
main(int argc, char **argv)
{
fd_set fdset;
char **arg = argv + 1;
char *cp;
struct stat st;
int fd, maxfd = -1;
FD_ZERO(&fdset);
while (*arg != NULL) {
/* getopt is for pussies */
if (strncmp("-u", *arg, 2) == 0) {
*arg += 2;
if (**arg == '/* Copyright © 2015 [email protected]
** Use/modify as you see fit but leave this attribution.
** If you change the interface and want to distribute the
** result please change the binary name too! */
#include <err.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/select.h>
/* typedefs are for pussies */
struct {
char *filename; /* for clarity of errors */
char *data;
long len;
long cap;
} saved[FD_SETSIZE] = {0};
void
ewriten(int fd, char *buf, int n)
{
int done = 0, c;
while (done < n) {
if ((c=write(fd, buf + done, n - done)) <= 0 && errno != EINTR) {
err(1, "write");
}
done += c;
}
}
int
empty(fd_set *fdset, int maxfd)
{
int i;
for (i=0; i <= maxfd; i++) {
if (FD_ISSET(i, fdset)) return 0;
}
return 1;
}
void
combine(fd_set *fdset, int maxfd)
{
char buf[4096], *cp;
fd_set ready;
int n, i, fd, left;
while (!empty(fdset, maxfd)) {
ready = *fdset;
/* timeouts are for pussies */
if (select(maxfd + 1, &ready, NULL, NULL, NULL) == -1) err(1, "select");
for (fd=0; fd <= maxfd; fd++) {
if (!FD_ISSET(fd, &ready)) continue;
switch (n=read(fd, &buf, sizeof(buf))) {
case -1:
if (errno == EINTR)
break; /* ignore interrupts; we'll re-read next iteration */
if (saved[fd].filename) err(1, "read: %s", saved[fd].filename);
err(1, "read: %d", fd);
case 0:
if (saved[fd].len > 0) {
/* someone forgot their newline at EOF... */
ewriten(1, saved[fd].data, saved[fd].len);
saved[fd].data[0] = '\n'; /* put it back for them */
ewriten(1, saved[fd].data, 1);
}
free(saved[fd].data);
FD_CLR(fd, fdset);
break;
default:
for (cp=buf + n - 1; cp >= buf && *cp != '\n'; cp--); /* find last newline */
left = n - (cp - buf + 1);
if (cp >= buf) {
/* we found one! first dump any saved data from the last read */
if (saved[fd].len > 0) {
ewriten(1, saved[fd].data, saved[fd].len);
saved[fd].len = 0;
}
ewriten(1, buf, cp - buf + 1);
}
if (left > 0) {
/* now save any leftover data for later */
int need = saved[fd].len + left;
if (saved[fd].cap < need &&
(saved[fd].data=realloc(saved[fd].data, need)) == NULL) {
errx(1, "realloc: failed on %d bytes", need);
/* it was good enough for quake... */
}
saved[fd].cap = need;
memcpy(saved[fd].data + saved[fd].len, buf + n - 1 - left, left);
saved[fd].len += left;
}
}
}
}
}
void
addfd(int fd, fd_set *fdset, int *maxfd)
{
FD_SET(fd, fdset);
if (*maxfd < fd) {
*maxfd = fd;
}
}
int
main(int argc, char **argv)
{
fd_set fdset;
char **arg = argv + 1;
char *cp;
struct stat st;
int fd, maxfd = -1;
FD_ZERO(&fdset);
while (*arg != NULL) {
/* getopt is for pussies */
if (strncmp("-u", *arg, 2) == 0) {
*arg += 2;
if (**arg == '%pre%' && *++arg == NULL ) errx(1, "-u requires argument (comma separated FD list)");
/* reentrancy is for pussies */
for (cp=strtok(*arg, ","); cp != NULL; cp=strtok(NULL, ",")) {
fd = atoi(cp);
if (fstat(fd, &st) != 0) err(1, "%d", fd);
addfd(fd, &fdset, &maxfd);
}
arg++;
} else if (strcmp("-", *arg) == 0) {
if (fstat(0, &st) != 0) err(1, "stdin", fd);
addfd(0, &fdset, &maxfd);
saved[0].filename = "stdin";
arg++;
} else if (strcmp("--", *arg) == 0) {
arg++;
break;
} else if (**arg == '-') {
errx(1, "unrecognized argument %s", *arg);
} else {
break; /* treat as filename */
}
}
/* remaining args are filenames */
for (; *arg != NULL; arg++) {
/* stdio is for pussies */
if ((fd=open(*arg, O_RDONLY)) == -1) err(1, "open: %s", *arg);
addfd(fd, &fdset, &maxfd);
saved[fd].filename = *arg;
}
combine(&fdset, maxfd);
return 0;
}
' && *++arg == NULL ) errx(1, "-u requires argument (comma separated FD list)");
/* reentrancy is for pussies */
for (cp=strtok(*arg, ","); cp != NULL; cp=strtok(NULL, ",")) {
fd = atoi(cp);
if (fstat(fd, &st) != 0) err(1, "%d", fd);
addfd(fd, &fdset, &maxfd);
}
arg++;
} else if (strcmp("-", *arg) == 0) {
if (fstat(0, &st) != 0) err(1, "stdin", fd);
addfd(0, &fdset, &maxfd);
saved[0].filename = "stdin";
arg++;
} else if (strcmp("--", *arg) == 0) {
arg++;
break;
} else if (**arg == '-') {
errx(1, "unrecognized argument %s", *arg);
} else {
break; /* treat as filename */
}
}
/* remaining args are filenames */
for (; *arg != NULL; arg++) {
/* stdio is for pussies */
if ((fd=open(*arg, O_RDONLY)) == -1) err(1, "open: %s", *arg);
addfd(fd, &fdset, &maxfd);
saved[fd].filename = *arg;
}
combine(&fdset, maxfd);
return 0;
}
Ahhh isso foi bom.
(nota: ele é testado em dois conjuntos de entradas. Os erros podem ou não existir)