如何使用文件名参数或默认为 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

我会更多地使用I/O 重定向:

#!/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到先前设置的文件描述符34

重新打开标准输入和输出

如果您不想打开中间文件描述符,另一种方法是直接用指定的输入和输出文件替换对应的stdin文件描述符stdout

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

这种方法的缺点是您无法区分脚本本身的输出和作为重定向目标的命令的输出。在原始方法中,您可以将脚本输出定向到未修改的stdinstdout,而这又可能已被脚本的调用者重定向。指定的输入和输出文件仍然可以通过相应的文件描述符访问,这与脚本stdinstdout.

答案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'
}

这里,不是使用fgrepcat而是你的朋友。然后使用选择案例:

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 时 - 可能应该检查该文件不存在,否则它将被覆盖。

相关内容