Я хочу обрабатывать имена файлов как аргументы в скрипте 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, вероятно, следует проверить, не существует ли этот файл, иначе он будет перезаписан.