Como usar argumentos de nome de arquivo ou padrão para stdin, stdout (breve)

Como usar argumentos de nome de arquivo ou padrão para stdin, stdout (breve)

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 $1estiver definido, ou seja, um arquivo de entrada foi especificado, o arquivo especificado será aberto no descritor de arquivo 3, caso contrário, stdinserá duplicado no descritor de arquivo 3.

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

    Da mesma forma, se $2estiver definido, ou seja, um arquivo de saída foi especificado, o arquivo especificado será aberto no descritor de arquivo 4, caso contrário, stdoutserá duplicado no descritor de arquivo 4.

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

    Por último fgrepé invocado, redirecionando seu stdine stdoutpara os descritores de arquivo previamente definidos 3e 4respectivamente.

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 stdine stdoutdiretamente 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 stdinand 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 stdine do stdoutarquivo .

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, catestá 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_fileacima, 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.

informação relacionada