Canalizar / redirigir un grupo de comandos

8

Actualmente uso la siguiente configuración para redirigir la salida de múltiples comandos:

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

Esto es bastante útil, y también funciona con tuberías.

¿Es ésta la mejor manera de hacer ésto? ¿Hay alguna alternativa que deba considerar?

wchargin
fuente
Así es como lo haría. ¿Se encuentra con un problema particular con este enfoque?
Bratchley
@JoelDavis Nope, solo me preguntaba si había una mejor manera de hacerlo. De las respuestas que recibí, ¡parece que las hay! :)
wchargin

Respuestas:

15

La alternativa es usar llaves en lugar de paréntesis. Este cambio ejecuta los comandos en el shell actual , no en un 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

Esto es particularmente relevante cuando está modificando variables dentro del 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
fuente
¡Excelente, gracias! Esto funciona mejor con mi sangría también. (Vim sangra { }pero no ( ).)
wchargin
2
Tenga en cuenta que puede contraer el grupo de comandos en una sola línea de cualquier manera; por ejemplo, (echo msg1; echo msg2)- pero, con llaves, tiene que ser { echo msg1; echo msg2;}, con un espacio después del {y un punto y coma ( ;) o un signo ( &) antes del }.
G-Man dice 'Restablecer a Monica' el
4

La respuesta de Glenn es buena: la distinción entre ( ... )y { ... }es importante.

Una estrategia que uso a menudo para la salida de errores, como lo que hay en su pregunta, es el teecomando. Podrías hacer algo como esto:

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"

El teecomando enviará la salida a dos lugares; -ala opción "agrega" salida al archivo nombrado, y el comando también pasará la entrada a stdout. El >&2final de la línea redirige teeel stdout de stdout a stderr, que puede manejarse de manera diferente (es decir, en un trabajo cron).

Otro consejo que uso a menudo en los scripts de shell es cambiar el comportamiento de la depuración o la salida detallada en función de si el script se ejecuta en un terminal o tiene una -vopción provista. Por ejemplo:

#!/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

Las secuencias de comandos pueden comenzar con algo genérico como este en la parte superior, con resultados detallados y de depuración esparcidos por toda la secuencia de comandos. Es solo una forma de hacerlo: hay muchas, y diferentes personas tendrán su propia manera de manejar estas cosas, especialmente si han estado alrededor por un tiempo. :)

Una opción más es manejar su salida con un "controlador", una función de shell que puede hacer cosas más inteligentes. Por ejemplo:

#!/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"

(Tenga en cuenta que solo ${var^^}es bash).

Esto crea una función de shell que puede usar las syslogfunciones de su sistema (con la función de loggercomando ) to send things to system logs. Thelogme () `puede usarse con opciones que generan líneas individuales de datos de registro o con múltiples líneas de entrada que se procesan en stdin. Juegue con ella si Parece atractivo

Tenga en cuenta que este es un ejemplo y probablemente no debería copiarse literalmente a menos que lo entienda y sepa que hace exactamente lo que necesita. Una mejor idea es tomar los conceptos aquí e implementarlos usted mismo en sus propios scripts.

ghoti
fuente
Muchas gracias por tu respuesta detallada! De hecho, he estado usando un function log () { cat >> $logfile }, que es básicamente una versión más simple de tu logme.
wchargin
Además, uno puede estar interesado en 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)no está instalado en los sistemas que uso: FreeBSD, OSX y una antigua caja de Ubuntu. ¿Puedes decirnos qué lo proporciona?
ghoti
Es parte de moreutils, que también incluye algunas buenas herramientas como sponge(1)(escribir en el archivo solo después de cerrar stdin, por lo que puede something < foo | sponge fooprescindir de clobbering foopor redireccionamiento) e vipe(1)(insertar un editor de texto en una tubería).
wchargin
1

La forma más apropiada de hacerlo es con { command; }más que con (command). La razón es que cuando los comandos se agrupan con ()una subshell se abre para ejecutar esos comandos y, por lo tanto, las variables que se inicializan durante ese bloque no estarán disponibles para otras secciones del script.

En cambio, cuando usamos {}para la agrupación de comandos, los comandos se ejecutan dentro del mismo shell y, por lo tanto, las variables estarán disponibles para otras secciones del 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"

Aquí, cuando se ejecuta esta sección, la $varvariable conserva su valor, donde, como en el otro caso, no lo hará.

Kannan Mohan
fuente
Además, cuando lo use en una línea, recuerde tener espacios en el medio y termine el comando con punto y coma. Ejemplo: { command; }.
CMCDragonkai