Quero lidar com nomes de arquivos como argumentos em um script bash de uma forma mais limpa e flexível, usando 0, 1 ou 2 argumentos para nomes de arquivos de entrada e saída.
- quando args = 0, leia de stdin, escreva em stdout
- quando args = 1, leia $1, escreva em stdout
- quando args = 2, leia de $1, escreva em $2
Como posso tornar a versão do script bash mais limpa e mais curta?
Aqui está o que tenho agora, que funciona, mas não está limpo,
#!/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
A versão perl é mais limpa,
#!/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 "$_"; } }
Responder1
Eu faria uso mais pesadoRedirecionamento 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
Explicação
[[ $1 ]] && [[ ! -f $1 ]] && echo "file $1 dne" && exit 1
Teste se um arquivo de entrada foi especificado como argumento de linha de comando e se o arquivo existe.
[[ $1 ]] && exec 3<$1 || exec 3<&0
Se
$1
estiver definido, ou seja, um arquivo de entrada foi especificado, o arquivo especificado será aberto no descritor de arquivo3
, caso contrário,stdin
será duplicado no descritor de arquivo3
.[[ $2 ]] && exec 4>$2 || exec 4>&1
Da mesma forma, se
$2
estiver definido, ou seja, um arquivo de saída foi especificado, o arquivo especificado será aberto no descritor de arquivo4
, caso contrário,stdout
será duplicado no descritor de arquivo4
.fgrep -v "stuff" <&3 >&4
Por último
fgrep
é invocado, redirecionando seustdin
estdout
para os descritores de arquivo previamente definidos3
e4
respectivamente.
Reabrindo entrada e saída padrão
Se você preferir não abrir descritores de arquivos intermediários, uma alternativa é substituir os descritores de arquivo correspondentes stdin
e stdout
diretamente pelos arquivos de entrada e saída especificados:
#!/bin/bash
[[ $1 ]] && [[ ! -f $1 ]] && echo "file $1 dne" && exit 1
[[ $1 ]] && exec 0<$1
[[ $2 ]] && exec 1>$2
fgrep -v "stuff"
Uma desvantagem dessa abordagem é que você perde a capacidade de diferenciar a saída do próprio script da saída do comando que é o alvo do redirecionamento. Na abordagem original, você pode direcionar a saída do script para o stdin
and não modificado stdout
, que por sua vez pode ter sido redirecionado pelo chamador do script. Os arquivos de entrada e saída especificados ainda podem ser acessados por meio dos descritores de arquivo correspondentes, que são distintos do script stdin
e do stdout
arquivo .
Responder2
Que tal:
input="${1:-/dev/stdin}"
output="${2:-/dev/stdout}"
err="${3:-/dev/stderr}"
foobar <"$input" >"$output" 2>"$err"
Você deve observar que /dev/std(in|out|err)
sãonão está no padrão POSIXentão isso só funcionará em sistemas que suportam esses arquivos especiais.
Isso também pressupõe uma entrada sensata: não verifica a existência de arquivos antes de redirecionar.
Responder3
se você não se importa que a saída sejasempreredirecionado para stdout, você pode usar a seguinte linha:
cat $1 |fgrep -v "stuff" | tee
Responder4
Não sei se isso é 'mais limpo', mas aqui estão algumas sugestões (este não é um código testado). O uso de exec
(de acordo com Thomas Nyman) pode levar a problemas de segurança e deve ser tratado com cuidado.
Primeiro coloque o código repetitivo em uma função.
# 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'
}
Aqui, em vez de usar fgrep
, cat
está seu amigo. Em seguida, use o caso selecionado:
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
Outra alternativa (limpa e muito compacta) é carregar as instruções em um array e acessá-lo através do valor $#, algo como um ponteiro de função. Dada a função is_file
acima, o código Bash é 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'
Não estou 100% na sintaxe, mas as aspas duplas precisam ser escapadas. É melhor colocar aspas duplas nas variáveis que contêm caminhos de arquivo.
Uma nota adicional, ao escrever em $2 - provavelmente deve verificar se o arquivo não existe ou será sobrescrito.