Canalizar / redirecionar um grupo de comandos

8

Atualmente, uso a seguinte configuração para redirecionar a saída de vários comandos:

echo "Some normal commands"
(
echo "Error: something happened"
echo "Warning: this incident will be logged"
) >> logfile
echo "More normal commands"

Isso é bastante útil e também funciona com tubos.

Essa é a melhor maneira de fazer isso? Existe uma alternativa que eu deva considerar?

wchargin
fonte
É assim que eu faria. Você está enfrentando um problema específico com essa abordagem?
Bratchley 27/10
@ JoelDavis Nope, só queria saber se havia uma maneira melhor de fazê-lo. Das respostas que recebi, parece que sim! :)
wchargin

Respostas:

15

A alternativa é usar chaves em vez de parênteses. Essa alteração executa os comandos no shell atual , não em um subshell

echo "Some normal commands"
{
echo "Error: something happened"
echo "Warning: this incident will be logged"
} >> logfile
echo "More normal commands"

ref: https://www.gnu.org/software/bash/manual/bashref.html#Command-Grouping

Isso é particularmente relevante quando você está modificando variáveis ​​dentro do grupo:

$ x=5; ( x=10; echo inside: $x; ); echo outside: $x
inside: 10
outside: 5

$ x=5; { x=10; echo inside: $x; }; echo outside: $x
inside: 10
outside: 10
Glenn Jackman
fonte
Excelente - obrigado! Isso funciona melhor com o meu recuo também. ( { }( )
Recuos
2
Observe que você pode recolher o grupo de comandos em uma única linha de qualquer maneira; por exemplo, (echo msg1; echo msg2)- mas, com chaves, deve ser { echo msg1; echo msg2;}, com um espaço após o {ponto-e-vírgula ( ;) ou um e comercial ( &) antes do }.
G-Man diz 'Reinstate Monica'
4

A resposta de Glenn é boa - a distinção entre ( ... )e { ... }é importante.

Uma estratégia que costumo usar para gerar erros como o que está em sua pergunta é o teecomando. Você poderia fazer algo assim:

echo "Normal output"
{
  printf "[%s] %s\n" "$(date '+%Y-%m-%d %T')" "Warning text"
  printf "[%s] %s\n" "$(date '+%Y-%m-%d %T')" "This event is logged."
} | tee -a $logfile >&2
echo "More normal output"

O teecomando enviará a saída para dois lugares; -aA opção "anexa" a saída ao arquivo nomeado, e o comando também passará a entrada para o stdout. O >&2no final da linha redireciona teeo stdout do stderr, que pode ser tratado de maneira diferente (ou seja, em um trabalho cron).

Uma outra dica que costumo usar em scripts de shell é alterar o comportamento da depuração ou saída detalhada, com base no fato de o script estar sendo executado em um terminal ou ter uma -vopção fornecida. Por exemplo:

#!/bin/sh

# Set defaults
if [ -t 0 ]; then
  Verbose=true; vflag="-v"
else
  Verbose=false; vflag=""
fi
Debug=false; AskYN=true; Doit=true

# Detect options (altering defaults)
while getopts vdqbn opt; do
  case "$opt" in
    v)  Verbose=true; vflag="-v" ;;             # Verbose mode
    d)  Debug=true; Verbose=true; vflag="-v" ;; # Very Verbose
    q)  Verbose=false; vflag="" ;;              # quiet mode (non-verbose)
    b)  AskYN=false ;;                          # batch mode
    n)  Doit=false ;;                           # test mode
    *)  usage; exit 1 ;;
  esac
done

# Shift our options for further processing
shift $(($OPTIND - 1))

$Verbose && echo "INFO: Verbose output is turned on." >&2
$Debug && echo "INFO: In fact, expect to be overrun." >&2

# Do your thing here
if $AskYN; then
  read -p "Continue? " choice
  case "$choice" in
    Y|y) $Doit && somecommand ;;
    *) echo "Done." ;;
  esac
fi

Os scripts podem começar com algo genérico como esse no topo, com a saída Verbose e Debug espalhadas por todo o script. É apenas uma maneira de fazer isso - existem muitos, e pessoas diferentes terão seu próprio jeito de lidar com essas coisas, especialmente se elas já existem há algum tempo. :)

Mais uma opção é manipular sua saída com um "manipulador" - uma função de shell que pode fazer coisas mais inteligentes. Por exemplo:

#!/bin/bash

logme() {
  case "${1^^}" in
    [IN]*)  level=notice ;;
    W*)     level=warning ;;
    A*)     level=alert ;;
    E*)     level=emerg ;;
    *)      level=notice ;;
  esac
  if [[ "$#" -eq 1 ]]; then
    # Strip off unnecessary prefixes like "INFO:"
    string="${1#+([A-Z])?(:) }"
  else
    shift
    string="$@"
  fi
  logger -p "${facility}.${level}" -t "$(hostname -s)" "$string"
}

echo "Normal output"
logme INFO "Here we go..."
somecommand | logme
echo "Additional normal output"

(Observe que ${var^^}é apenas para o bash.)

Isso cria uma função shell que pode usar as syslogfunções do seu sistema (com o loggercomando ) to send things to system logs. Thelogme () `, a função pode ser usada com opções que geram linhas únicas de dados de log ou com várias linhas de entrada que são processadas no stdin. parece atraente.

Observe que este é um exemplo e provavelmente não deve ser copiado literalmente, a menos que você o entenda e saiba que ele faz exatamente o que você precisa. Uma idéia melhor é pegar os conceitos aqui e implementá-los em seus próprios scripts.

ghoti
fonte
Muito obrigado pela sua resposta detalhada! Na verdade, eu tenho usado um function log () { cat >> $logfile }, que é basicamente uma versão mais simples do seu logme.
wchargin
Além disso, um pode estar interessado ts(1); uso: { printf '%s\n' "Warning text"; printf '%s\n' "This event will be logged"; } | ts '[%Y-%m-%d %T]' | tee -a "$logfile" >&2.
wchargin
@wchargin - ts(1)não está instalado nos sistemas que uso - FreeBSD, OSX e uma caixa antiga do Ubuntu. Você pode nos dizer o que fornece?
ghoti
É parte do moreutils, que também inclui algumas ferramentas interessantes como sponge(1)(gravar no arquivo somente depois que o stdin for fechado, para que você possa fazersomething < foo | sponge foo evitar o fooredirecionamento) e vipe(1)(inserir um editor de texto em um canal).
precisa saber é o seguinte
1

A maneira mais apropriada de fazer isso é com { command; }e não (command). O motivo é que, quando os comandos são agrupados com ()um subshell, é aberto para executar esses comandos e, portanto, as variáveis ​​inicializadas durante esse bloco não ficam disponíveis para outras seções do script.

Em vez disso, quando usamos o {}agrupamento de comandos, os comandos são executados no mesmo shell e, portanto, as variáveis ​​estarão disponíveis para outras seções do script.

echo "Some normal commands"

{
    var=1
    echo "Error: something happened"
    echo "Warning: this incident will be logged"
} >> logfile

echo "The value of var is: $var"
echo "More normal commands"

Aqui, quando esta seção é executada, a $varvariável retém seu valor, e, como no outro caso, não.

Kannan Mohan
fonte
Além disso, ao usar em uma linha, lembre-se de ter espaços no meio e finalize o comando com ponto e vírgula. Exemplo: { command; }.
CMCDragonkai