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

5

Eu quero manipular nomes de arquivos como argumentos em um script bash de uma maneira mais limpa e flexível, usando argumentos 0, 1 ou 2 para nomes de arquivos de entrada e saída.

  • quando args = 0, leia de stdin, escreva para stdout
  • quando args = 1, leia a partir de $ 1, escreva para stdout
  • quando args = 2, leia a partir de $ 1, escreva para $ 2

Como posso tornar a versão do script bash mais limpa, mais curta?

Aqui está o que eu tenho agora, o 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 "$_"; } }
    
por ChuckCottrill 09.10.2013 / 08:35

5 respostas

8

Eu faria um uso mais pesado de redirecionamento 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 um 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 tiver sido especificado, o arquivo especificado será aberto no descritor de arquivo 3 , caso contrário, stdin será duplicado no descritor de arquivo 3 .

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

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

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

    Por fim, fgrep é invocado, redirecionando seus stdin e stdout para os descritores de arquivos previamente definidos 3 e 4 , respectivamente.

Reabertura de 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 a stdin e stdout diretamente aos 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 destino do redirecionamento. Na abordagem original, você pode direcionar a saída do script para os stdin e stdout não modificados, que, por sua vez, podem ter sido redirecionados pelo responsável pela chamada do script. Os arquivos de entrada e saída especificados ainda podem ser acessados por meio dos descritores de arquivos correspondentes, que são distintos do script stdin e stdout .

    
por 09.10.2013 / 09:40
3

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) não está no padrão POSIX , por isso só trabalhe em sistemas que suportem esses arquivos especiais.

Isso também pressupõe uma entrada sã: não verifica a existência de arquivos antes de redirecionar.

    
por 09.10.2013 / 22:52
1

se você não se importa que a saída é sempre redirecionada para stdout, você pode usar o seguinte one-liner:

cat $1 |fgrep -v "stuff" | tee  
    
por 09.10.2013 / 10:02
1

Você pode usar exec <foo para redirecionar a entrada do script do arquivo foo e, da mesma forma, exec >foo para a saída.

#!/bin/sh
set -e
if [ $# -ne 0 ]; then
  exec <"$1"
  shift
fi
if [ $# -ne 0 ]; then
  exec >"$1"
  shift
fi
fgrep -v "stuff"

Esse estilo é conciso, mas não se presta bem a relatórios detalhados de erros.

    
por 10.10.2013 / 02:01
0

Eu não sei se isso é 'mais limpo', mas aqui estão algumas sugestões (isso não é código testado). O uso de exec (por 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 é seu amigo. Então 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 (que é limpa e muito compacta) é carregar as instruções em um array, e depois acessá-lo através do valor de $ #, 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' 

Eu não estou 100% na sintaxe, mas as aspas duplas precisam ser escapadas. Melhor para aspas duplas que contêm caminhos de arquivo.

Uma anotação adicionada, ao gravar em $ 2, provavelmente deve verificar se o arquivo não existe ou será substituído.

    
por 09.10.2013 / 22:18