Cómo usar argumentos de nombre de archivo o usar de forma predeterminada stdin, stdout (breve)

Cómo usar argumentos de nombre de archivo o usar de forma predeterminada stdin, stdout (breve)

Quiero manejar nombres de archivos como argumentos en un script bash de una manera más limpia y flexible, tomando 0, 1 o 2 argumentos para los nombres de archivos de entrada y salida.

  • cuando args = 0, lee desde stdin, escribe en stdout
  • cuando args = 1, lee desde $1, escribe en stdout
  • cuando args = 2, lee desde $1, escribe en $2

¿Cómo puedo hacer que la versión del script bash sea más limpia y más corta?

Esto es lo que tengo ahora, que funciona, pero no está limpio.

#!/bin/bash
if [ $# -eq 0 ] ; then #echo "args 0"
    fgrep -v "stuff"
elif [ $# -eq 1 ] ; then #echo "args 1"
    f1=${1:-"null"}
    if [ ! -f $f1 ]; then echo "file $f1 dne"; exit 1; fi
    fgrep -v "stuff" $f1 
elif [ $# -eq 2 ]; then #echo "args 2"
    f1=${1:-"null"}
    if [ ! -f $f1 ]; then echo "file $f1 dne"; exit 1; fi
    f2=${2:-"null"}
    fgrep -v "stuff" $f1 > $f2
fi

La versión de Perl es más limpia,

#!/bin/env perl
use strict; 
use warnings;
my $f1=$ARGV[0]||"-";
my $f2=$ARGV[1]||"-";
my ($fh, $ofh);
open($fh,"<$f1") or die "file $f1 failed";
open($ofh,">$f2") or die "file $f2 failed";
while(<$fh>) { if( !($_ =~ /stuff/) ) { print $ofh "$_"; } }

Respuesta1

Yo haría un mayor uso deredirección de E/S:

#!/bin/bash
[[ $1 ]] && [[ ! -f $1 ]] && echo "file $1 dne" && exit 1
[[ $1 ]] && exec 3<$1 || exec 3<&0
[[ $2 ]] && exec 4>$2 || exec 4>&1
fgrep -v "stuff" <&3 >&4

Explicación

  • [[ $1 ]] && [[ ! -f $1 ]] && echo "file $1 dne" && exit 1

    Pruebe si se ha especificado un archivo de entrada como argumento de línea de comando y si el archivo existe.

  • [[ $1 ]] && exec 3<$1 || exec 3<&0

    Si $1está configurado, es decir, se ha especificado un archivo de entrada, el archivo especificado se abre en el descriptor de archivo 3; de lo contrario, stdinse duplica en el descriptor de archivo 3.

  • [[ $2 ]] && exec 4>$2 || exec 4>&1

    De manera similar, si $2está configurado, es decir, se ha especificado un archivo de salida, el archivo especificado se abre en el descriptor de archivo 4; de lo contrario, stdoutse duplica en el descriptor de archivo 4.

  • fgrep -v "stuff" <&3 >&4

    Por último fgrepse invoca, redirigiendo su stdiny stdouta los descriptores de archivo previamente establecidos 3y 4respectivamente.

Reapertura de entrada y salida estándar

Si prefiere no abrir descriptores de archivos intermedios, una alternativa es reemplazar los descriptores de archivos correspondientes stdiny stdoutdirectamente con los archivos de entrada y salida especificados:

#!/bin/bash
[[ $1 ]] && [[ ! -f $1 ]] && echo "file $1 dne" && exit 1
[[ $1 ]] && exec 0<$1
[[ $2 ]] && exec 1>$2
fgrep -v "stuff"

Un inconveniente de este enfoque es que se pierde la capacidad de diferenciar la salida del script en sí de la salida del comando que es el objetivo de la redirección. En el enfoque original, puede dirigir la salida del script al archivo stdinand no modificado stdout, que a su vez podría haber sido redirigido por la persona que llama al script. Aún se puede acceder a los archivos de entrada y salida especificados a través de los descriptores de archivo correspondientes, que son distintos del script stdiny stdout.

Respuesta2

Qué tal si:

  input="${1:-/dev/stdin}"
  output="${2:-/dev/stdout}"
  err="${3:-/dev/stderr}"

  foobar <"$input" >"$output" 2>"$err"

Debes tener en cuenta que /dev/std(in|out|err)sonno en el estándar POSIXpor lo que esto sólo funcionará en sistemas que admitan estos archivos especiales.

Esto también supone una entrada sensata: no comprueba la existencia de archivos antes de redirigir.

Respuesta3

si no te importa que la salida seasiempreredirigido a la salida estándar, puede utilizar la siguiente frase:

cat $1 |fgrep -v "stuff" | tee  

Respuesta4

No sé si esto es "más limpio", pero aquí hay algunas sugerencias (este no es un código probado). El uso de exec(según Thomas Nyman) puede generar problemas de seguridad y debe tratarse con cuidado.

Primero coloque código repetitivo en una función.

# die <message>
function die(){
    echo "Fatal error: $1, exiting ..." >&2
    exit 1
}

# is_file <file-path>
function is_file(){
    [[ -n "$1" && -f "$1" ]] && return 0
    die 'file not found'
}

Aquí, en lugar de usar fgrep, catestá tu amigo. Luego use seleccionar caso:

case $# in
    0) cat ;;                                  # accepts stdin to stdout.
    1) is_file "$1"; cat "$1" ;;               # puts $1 to stdout.
    2) is_file "$1"; cat "$1" > "$2" ;;        # puts $1 to $2.
    *) die 'too many arguments' ;;
esac

Otra alternativa (que es limpia y muy compacta) es cargar las instrucciones en una matriz y luego acceder a ella mediante el valor de $#, algo así como un puntero de función. Dada la función is_fileanterior, el código Bash es algo como:

# action array.
readonly do_stuff=(
    'cat'                                  # 0 arg.
    'is_file \"$1\"; cat \"$1\"'           # 1 arg.
    'is_file \"$1\"; cat \"$1\" > \"$2\";' # 2 args.
)

# Main - just do:
[[ $# -le 2 ]] && ${do_stuff[$#]} || die 'too many arguments' 

No estoy al 100% con la sintaxis, pero es necesario evitar las comillas dobles. Es mejor poner entre comillas las variables que contienen rutas de archivos.

Una nota adicional, al escribir en $2: probablemente debería verificar que el archivo no existe o se sobrescribirá.

información relacionada