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
$1
está configurado, es decir, se ha especificado un archivo de entrada, el archivo especificado se abre en el descriptor de archivo3
; de lo contrario,stdin
se duplica en el descriptor de archivo3
.[[ $2 ]] && exec 4>$2 || exec 4>&1
De manera similar, si
$2
está configurado, es decir, se ha especificado un archivo de salida, el archivo especificado se abre en el descriptor de archivo4
; de lo contrario,stdout
se duplica en el descriptor de archivo4
.fgrep -v "stuff" <&3 >&4
Por último
fgrep
se invoca, redirigiendo sustdin
ystdout
a los descriptores de archivo previamente establecidos3
y4
respectivamente.
Reapertura de entrada y salida estándar
Si prefiere no abrir descriptores de archivos intermedios, una alternativa es reemplazar los descriptores de archivos correspondientes stdin
y stdout
directamente 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 stdin
and 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 stdin
y 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
, cat
está 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_file
anterior, 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á.