重定向标准输出和标准错误到函数

26

我需要帮助将系统命令的输出(stdin和stdout)发送到bash函数中,同时仍然接受来自参数的输入。就像以下示例一样。有人能给我指点一下正确的方向吗?

LogMsg()
{
  DateTime=`date "+%Y/%m/%d %H:%M:%S"`
  echo '*****'$DateTime' ('$QMAKESPEC'): '$1 >> "$LogFile"
  echo $DateTime' ('$QMAKESPEC'): '$1
}

# Already works
LogMsg "This statement is sent directly"

# Wish I could do this:
# Capture both stdout & stderr of a system function to the logfile
# I do not presume that any of the syntax that follows is good
make 2>&1 >(LogMsg)

你还想要使用命令行参数调用LogMsg的能力吗? - chepner
所有的答案都使用管道,所以LogMsg只在make完成后调用,而不是像一个假设的>(LogMsg)一样实时调用。 - pmiguelpinto
7个回答

29

你可以使用 Bash 内置命令 read来完成此操作:

LogMsg()
{
  read IN # This reads a string from stdin and stores it in a variable called IN
  DateTime=`date "+%Y/%m/%d %H:%M:%S"`
  echo '*****'$DateTime' ('$QMAKESPEC'): '$IN >> "$LogFile"
  echo $DateTime' ('$QMAKESPEC'): '$IN
}

然后使用一个管道符:

make 2>&1 | LogMsg

更新:

根据chepner的评论,为了能够使用标准输入或参数作为输入,您可以这样做:

LogMsg()
{
  if [ -n "$1" ]
  then
      IN="$1"
  else
      read IN # This reads a string from stdin and stores it in a variable called IN
  fi

  DateTime=`date "+%Y/%m/%d %H:%M:%S"`
  echo '*****'$DateTime' ('$QMAKESPEC'): '$IN >> "$LogFile"
  echo $DateTime' ('$QMAKESPEC'): '$IN
}

4
这种方法唯一的问题是,如果没有提供标准输入,就无法再调用LogMsg。不清楚Ryan是否希望保留这种灵活性。 - chepner
你想要能够同时执行 echo foo | LogMsg baz 吗?我一直无法想出一个可以处理其中一个或两个的解决方案。 - chepner
现在是一个二选一的情况。我想要能够捕获显式参数或捕获STDOUT和STDERR。 - Ryan
@Lee 是的,我先处理参数(如果有),然后处理标准输入(如果有)。但很高兴知道不必支持这种情况,你的解决方案会起作用。 - chepner
非常有帮助,谢谢。还发现了这个:如果您想记录一个空行,请调用 LogMsg " "。如果完全省略参数,则会挂起等待从 stdin 提供某些内容。 - Mark Berry
显示剩余4条评论

6

这是一个旧的主题..但我用它来帮助我编写日志函数,该函数还将输出命令输出的多行:

# Defines function to grab a time stamp #
get_Time () { Time=$(date +%Y-%m-%d\ %H:%M:%S) ; }

write_Log()
{
get_Time
if [ -n "${1}" ]; then         # If it's from a "<message>" then set it
    IN="${1}"
    echo "${Time} ${IN}" | tee -a ${log_File}
else
    while read IN               # If it is output from command then loop it
    do
        echo "${Time} ${IN}" | tee -a ${log_File}
    done
fi
}

2

根据之前的回答,我整理了一些通用函数,可以与日志文件一起或单独使用,如本帖末尾所列。这些函数对于更复杂的脚本非常方便。我通常将终端窗口消息打印到stderr中,以不干扰可能需要重定向的合法程序输出。函数的调用方式如下:

scriptFolder=$(cd $(dirname "$0") && pwd)
scriptName=$(basename $scriptFolder)
# Start a log file that will be used by the logging functions
logFileStart ${scriptName} "${scriptFolder)/${scriptName}.log"

# The following logs the message string passed to the function.
# - use a space for empty lines because otherwise the logging function
#   will hang waiting for input
logInfo " "
logInfo "Starting to do some work."

# The following will log each 'stdout` and `stderr` line piped to the function.
someOtherProgram 2>&1 | logInfo

Functions...

# Echo to stderr
echoStderr() {
  # - if necessary, quote the string to be printed
  # - redirect stdout from echo to stderr
  echo "$@" 1>&2
  # Or, use an alternate echo such one that colors textT
  # ${echo2} "$@" 1>&2
}

# Print a DEBUG message
# - prints to stderr and optionally appends to log file if ${logFile} is defined globally
#   - see logFileStart() to start a log file
# - call with parameters or pipe stdout and stderr to this function: 2>&1 | logDebug
# - print empty lines with a space " " to avoid hanging the program waiting on stdin input
logDebug() {
  if [ -n "${1}" ]; then
    if [ -n "${logFile}" ]; then
      # Are using a log file
      echoStderr "[DEBUG] $@" 2>&1 | tee --append $logFile
    else
      # Are NOT using a log file
      echoStderr "[DEBUG] $@"
    fi
  else
    while read inputLine; do
      if [ -n "${logFile}" ]; then
        # Are using a log file
        echoStderr "[DEBUG] ${inputLine}" 2>&1 | tee --append $logFile
      else
        # Are NOT using a log file
        echoStderr "[DEBUG] ${inputLine}"
      fi
    done
  fi
}

# Print an ERROR message
# - prints to stderr and optionally appends to log file if ${logFile} is defined globally
#   - see logFileStart() to start a log file
# - call with parameters or pipe stdout and stderr to this function: 2>&1 | logError
# - print empty lines with a space " " to avoid hanging the program waiting on stdin input
logError() {
  if [ -n "${1}" ]; then
    if [ -n "${logFile}" ]; then
      # Are using a log file
      echoStderr "[ERROR] $@" 2>&1 | tee --append $logFile
    else
      # Are NOT using a log file
      echoStderr "[ERROR] $@"
    fi
  else
    while read inputLine; do
      if [ -n "${logFile}" ]; then
        # Are using a log file
        echoStderr "[ERROR] ${inputLine}" 2>&1 | tee --append $logFile
      else
        # Are NOT using a log file
        echoStderr "[ERROR] ${inputLine}"
      fi
    done
  fi
}

# Start a new logfile
# - name of program that is being run is the first argument
# - path to the logfile is the second argument
# - echo a line to the log file to (re)start
# - subsequent writes to the file using log*() functions will append
# - the global variable ${logFile} will be set for use by log*() functions
logFileStart() {
  local newLogFile now programBeingLogged
  programBeingLogged=$1
  # Set the global logfile, in case it was not saved
  if [ -n "${2}" ]; then
    logFile=${2}
  else
    # Set the logFile to stderr if not specified, so it is handled somehow
    logFile=/dev/stderr
  fi
  now=$(date '+%Y-%m-%d %H:%M:%S')
  # Can't use logInfo because it only appends and want to restart the file
  echo "Log file for ${programBeingLogged} started at ${now}" > ${logFile}
}

# Print an INFO message
# - prints to stderr and optionally appends to log file if ${logFile} is defined globally
#   - see logFileStart() to start a log file
# - call with parameters or pipe stdout and stderr to this function: 2>&1 | logInfo
# - print empty lines with a space " " to avoid hanging the program waiting on stdin input
logInfo() {
  if [ -n "${1}" ]; then
    if [ -n "${logFile}" ]; then
      # Are using a log file
      echoStderr "[INFO] $@" 2>&1 | tee --append $logFile
    else
      # Are NOT using a log file
      echoStderr "[INFO] $@"
    fi
  else
    while read inputLine; do
      if [ -n "${logFile}" ]; then
        # Are using a log file
        echoStderr "[INFO] ${inputLine}" 2>&1 | tee --append $logFile
      else
        # Are NOT using a log file
        echoStderr "[INFO] ${inputLine}"
      fi
    done
  fi
}

# Print an WARNING message
# - prints to stderr and optionally appends to log file if ${logFile} is defined globally
#   - see logFileStart() to start a log file
# - call with parameters or pipe stdout and stderr to this function: 2>&1 | logWarning
# - print empty lines with a space " " to avoid hanging the program waiting on stdin input
logWarning() {
  if [ -n "${1}" ]; then
    if [ -n "${logFile}" ]; then
      # Are using a log file
      echoStderr "[WARNING] $@" 2>&1 | tee --append $logFile
    else
      # Are NOT using a log file
      echoStderr "[WARNING] $@"
    fi
  else
    while read inputLine; do
      if [ -n "${logFile}" ]; then
        # Are using a log file
        echoStderr "[WARNING] ${inputLine}" 2>&1 | tee --append $logFile
      else
        # Are NOT using a log file
        echoStderr "[WARNING] ${inputLine}"
      fi
    done
  fi
}

1
您可以通过以下方式创建一个函数,该函数可选地将系统命令的STDOUT和STDERR写入日志文件,或接受将被写入日志文件的参数:

_LOG='/some_dir/some_file'

function Log_Msg {

    #If no arguments are given to function....
    if [ -z "$@" ]; then
        
        #...then take STDOUT/STDERR as input and write to log file
        read && echo "$REPLY" | tee -a $_LOG \

    else
        #Take arguments that were given to function and write that to log file
        echo "$@" | tee -a $_LOG

    fi

}

#Logging from system commands example. The "|&" operator pipes STDOUT and STDERR to Log_Msg function
bad command |& Log_Msg

or

#Taking an argument as input and writing to log file
Log_Msg "Write this to log file"

希望这可以帮助你!

1
感谢那些回复的人。我做出了我的版本,每条消息只会添加一次时间戳。
#!/bin/bash
CURRENT_PID=$$
PROCESS_NAME=$(basename $0)

LOGFILE=/var/log/backup-monitor.log
function log_message {
  if [ -n "$1" ]; then
      MESSAGE="$1"
      echo -e "$(date -Iseconds)\t$PROCESS_NAME\t$CURRENT_PID\t$MESSAGE" | tee -a $LOGFILE
  else
      MESSAGE=$(tee)
      echo -e "$(date -Iseconds)\t$PROCESS_NAME\t$CURRENT_PID\t$MESSAGE" | tee -a $LOGFILE
  fi
}

log_message "Direct arguments are working!!"

echo "stdin also working" | log_message

-1
在我看来,read命令中的100ms超时(-t 0.1)将允许LogMsg处理输入管道和参数,而不会在没有输入的情况下永远等待。
function log(){ read -t 0.1 IN1
  echo $(date "+%Y/%m/%d %H:%M:%S")' ('$QMAKESPEC'): '$IN1 $* |tee -a $LogFile ;}
#test without, with pipe , with pipe and parameters , with parameters only
log ; echo foo | log ; echo foo | log bar ; log bar
2015/01/01 16:52:17 ():
2015/01/01 16:52:17 (): foo
2015/01/01 16:52:17 (): foo bar
2015/01/01 16:52:17 (): bar

tee -a 将重复内容输出到标准输出并追加到 $LogFile 中

玩得开心!


-2

有两种方法可以做到这一点,我认为第一种方法更好,那就是创建一个bash文件并将结果传递给它,像这样:

make 2>&1 > ./LogMsg

第二种方法是将结果作为参数传递给函数:

LogMsg $(make 2>&1)

1
你的第一个选项不太清楚。你是指将make的输出导入到LogMsg中(但它无法像现在这样从stdin读取)吗?你的第二个选项只会处理make的第一行输出,因为LogMsg只处理它的第一个参数。 - chepner

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