创建一个可以写入多个文件的管道(tee)

6
我想在一个ksh脚本中创建一个管道(使用exec),将其传输到tee,并将输出发送到管道。 当前情况:
#Redirect EVERYTHING
exec 3>&1 #Save STDOUT as 3
exec 4>&2 #Save STDERR as 4
exec 1>${Log} #Redirect STDOUT to a log
exec 2>&1 #Redirect STDERR to STDOUT

我想做的事情(但我没有正确的语法):
#Redirect EVERYTHING
exec 3>&1 #Save STDOUT as 3
exec 4>&2 #Save STDERR as 4
exec 1>tee -a ${Log} >&3  #Redirect STDOUT to a log
exec 2>&1 #Redirect STDERR to STDOUT

如何创建这个管道?

在bash中,每个块的最后一行最好描述为“将STDOUT附加到STDERR”或“将STDERR重定向到STDOUT”。之后,2>dump-stdout将为空,但您使用echo >&2输出的任何内容都将与您使用echo >&1输出的内容放在同一个位置。 - dubiousjim
5个回答

6

我使用命名管道找到了一个解决方案。

#!/bin/ksh

LOG=~/testLog.log
PIPE=~/logPipe
mkfifo ${PIPE}
exec 3>&1 #Save STDOUT as 3
exec 4>&2 #Save STDERR as 4
tee -a ${LOG} <${PIPE} >&3 & #Start tee off the logpipe in the background
exec 1>${PIPE} #Redirect stdout to the pipe
exec 2>&1 #Redirect STDERR to STDOUT

echo "TEST"
echo Test 2

ls | grep -i "test"

rm -f ${PIPE} #Remove the pipe

一个非常简洁的解决方案。谢谢。 - J Jorgenson

5

这是我使用的解决方案。在我的 Mac 上,它可以在 ksh 下工作。通过将其封装为 start_logging()stop_logging() 函数,使得操作更加简便。

实际上,代码看起来像这样:

# Optional:
#   Set the name and location of the log file.
#   OUTPUT_LOG=output.log    # default
#   Set the name and location of the named pipe used.
#   OUTPUT_PIPE=output.pipe  # default

start_logging
# Default is to append to an existing log file.
# start_logging delete_existing_logfile
echo "This is on standard out"
echo "This is on standard err" >&2
stop_logging

这是整个文件。上面的启动和停止函数以及示例都在文件底部。为了更容易使用,只需将启动和停止函数放在自己的文件中,并在需要记录日志的脚本中引用它们。

#!/bin/sh

# Author: Harvey Chapman <hchapman _AT_ 3gfp.com>
# Description: POSIX shell functions that can be used with tee to simultaneously put
#              stderr and stdout to both a file and stdout
#
# Based on:
#    Re: How to redirect stderr and stdout to a file plus display at the same time
#    http://www.travishartwell.net/blog/2006/08/19_2220

#
# Original example function from Travis Hartwell's blog.
# Note: I've made minor changes to it.
example()
{
  OUTPUT_LOG=output.log
  OUTPUT_PIPE=output.pipe

  # This should really be -p to test that it's a pipe.
  if [ ! -e $OUTPUT_PIPE ]; then
      mkfifo $OUTPUT_PIPE
  fi

  # This should really be -f to test that it's a regular file.
  if [ -e $OUTPUT_LOG ]; then
      rm $OUTPUT_LOG
  fi

  exec 3>&1 4>&2
  tee $OUTPUT_LOG < $OUTPUT_PIPE >&3 &
  tpid=$!
  exec > $OUTPUT_PIPE 2>&1

  echo "This is on standard out"
  echo "This is on standard err" >&2

  exec 1>&3 3>&- 2>&4 4>&-
  wait $tpid

  rm $OUTPUT_PIPE
}

# A slightly reduced version of example()
example2()
{
  OUTPUT_LOG=output.log
  OUTPUT_PIPE=output.pipe

  rm -f $OUTPUT_PIPE
  mkfifo $OUTPUT_PIPE
  rm -f $OUTPUT_LOG

  tee $OUTPUT_LOG < $OUTPUT_PIPE &
  tpid=$!

  exec 3>&1 4>&2 >$OUTPUT_PIPE 2>&1

  echo "This is on standard out"
  echo "This is on standard err" >&2

  exec 1>&3 3>&- 2>&4 4>&-
  wait $tpid
  rm -f $OUTPUT_PIPE
}

#
# Logging methods based on above. See the example below for how to use them.
#

# Usage: start_logging [delete_existing_logfile]
start_logging()
{
  # Check to see if OUTPUT_LOG and OUTPUT_PIPE need to be defined.
  if [ -z "$OUTPUT_LOG" ]; then
    OUTPUT_LOG=output.log
  fi
  if [ -z "$OUTPUT_PIPE" ]; then
    OUTPUT_PIPE=output.pipe
  fi
  # Make sure that we're not already logging.
  if [ -n "$OUTPUT_PID" ]; then
    echo "Logging already started!"
    return 1
  fi

  # Always remove the log and pipe first.
  rm -f $OUTPUT_PIPE
  # Delete the logfile first if told to.
  if [ "$1" = delete_existing_logfile ]; then
    rm -f $OUTPUT_LOG
  fi

  mkfifo $OUTPUT_PIPE
  tee -a $OUTPUT_LOG < $OUTPUT_PIPE &
  OUTPUT_PID=$!

  exec 3>&1 4>&2 >$OUTPUT_PIPE 2>&1
}

stop_logging()
{
  # Make sure that we're currently logging.
  if [ -z "$OUTPUT_PID" ]; then
    echo "Logging not yet started!"
    return 1
  fi
  exec 1>&3 3>&- 2>&4 4>&-
  wait $OUTPUT_PID
  rm -f $OUTPUT_PIPE
  unset OUTPUT_PID
}

example3()
{
  start_logging
  #start_logging delete_existing_logfile
  echo "This is on standard out"
  echo "This is on standard err" >&2
  stop_logging
}

#example
#example2
example3

感谢您提供这个实现,我真的很喜欢那些函数。 - J Jorgenson

0

我知道Bash而不是Ksh,但是它们有很多重叠之处,因此也许这个方法也适用于那里。

process1 N> >(process2)

创建运行process2的子shell。该子shell将process1文件描述符N中的数据作为其标准输入。因此,特别地,您可以执行以下操作:
process1 1> >(tee -a mylog >&3)

我不知道如果用exec替换process1是否也能起作用,但你可以试一下。

0

ksh 中有 |&>&p,但我无法让它们做你想要的事情。也许你可以试试。


0

不要使用:

exec 1>tee -a ${Log} >&3

而是直接使用:

tee -a ${Log} >&3 &

tee 将会在后台运行,并且将消耗调用进程(即您的脚本)在 tee 分叉时的标准输入。


我的测试中它没有起作用。Tee 已经开始并写入日志,但它没有消费标准输出。 - C. Ross
你的文件句柄设置可能还有其他问题;启动整个程序并使用 lsof 查看/验证每个位置的内容。 - vladr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接