Как использовать аргументы имени файла или по умолчанию stdin, stdout (кратко)

Как использовать аргументы имени файла или по умолчанию stdin, stdout (кратко)

Я хочу обрабатывать имена файлов как аргументы в скрипте bash более понятным и гибким способом, принимая 0, 1 или 2 аргумента для входных и выходных имен файлов.

  • когда args = 0, чтение из stdin, запись в stdout
  • когда args = 1, прочитать из $1, записать в stdout
  • когда args = 2, прочитать из $1, записать в $2

Как сделать версию bash-скрипта чище и короче?

Вот что у меня сейчас есть, что работает, но не чисто,

#!/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

Версия Perl чище,

#!/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 "$_"; } }

решение1

Я бы использовал интенсивнееПеренаправление ввода-вывода:

#!/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

Объяснение

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

    Проверьте, был ли указан входной файл в качестве аргумента командной строки и существует ли этот файл.

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

    Если $1установлено, т.е. указан входной файл, указанный файл открывается в файловом дескрипторе 3, в противном случае stdinдублируется в файловом дескрипторе 3.

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

    Аналогично, если установлено $2, т.е. указан выходной файл, указанный файл открывается в файловом дескрипторе 4, в противном случае stdoutдублируется в файловом дескрипторе 4.

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

    Наконец fgrep, вызывается, перенаправляя свои stdinи stdoutна ранее заданные файловые дескрипторы 3и 4соответственно.

Повторное открытие стандартного ввода и вывода

Если вы предпочитаете не открывать промежуточные файловые дескрипторы, альтернативой является замена файловых дескрипторов, соответствующих указанным входным и выходным файлам, непосредственно на них stdin:stdout

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

Недостатком этого подхода является то, что вы теряете возможность отличать вывод самого скрипта от вывода команды, которая является целью перенаправления. В исходном подходе вы можете направить вывод скрипта в неизмененный stdinи stdout, который в свою очередь может быть перенаправлен вызывающей стороной скрипта. Указанные входные и выходные файлы по-прежнему могут быть доступны через соответствующие файловые дескрипторы, которые отличаются от скрипта stdinи stdout.

решение2

Как насчет:

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

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

Вы должны отметить, /dev/std(in|out|err)чтоне в стандарте POSIXпоэтому это будет работать только в системах, которые поддерживают эти специальные файлы.

Это также предполагает разумные входные данные: перед перенаправлением не проверяется наличие файлов.

решение3

если вас не смущает, что вывод будетвсегдаперенаправлены на stdout, можно использовать следующую однострочник:

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

решение4

Я не знаю, «чище» ли это, но вот несколько предложений (это не проверенный код). Использование exec(согласно Томасу Найману) может привести к проблемам безопасности и должно быть сделано с осторожностью.

Сначала поместите повторяющийся код в функцию.

# 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'
}

Здесь вместо использования fgrep, catваш друг. Затем используйте select case:

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

Другая альтернатива (чистая и очень компактная) — загрузить инструкции в массив, а затем получить к нему доступ через значение $#, что-то вроде указателя функции. Учитывая функцию is_fileвыше, код Bash будет примерно таким:

# 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' 

Я не на 100% уверен в синтаксисе, но двойные кавычки нужно экранировать. Лучше всего заключать в двойные кавычки переменные, которые содержат пути к файлам.

Добавленное замечание: при записи в $2, вероятно, следует проверить, не существует ли этот файл, иначе он будет перезаписан.

Связанный контент