如何使用檔案名稱參數或預設為 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 時 - 應該檢查該文件不存在,否則它將被覆蓋。

相關內容