Ich möchte Dateinamen als Argumente in einem Bash-Skript sauberer und flexibler behandeln und dabei 0, 1 oder 2 Argumente für Eingabe- und Ausgabedateinamen verwenden.
- wenn args = 0, von stdin lesen, in stdout schreiben
- wenn args = 1, von $1 lesen, in stdout schreiben
- wenn args = 2, von $1 lesen, nach $2 schreiben
Wie kann ich die Bash-Skriptversion übersichtlicher und kürzer machen?
Hier ist, was ich jetzt habe, was funktioniert, aber nicht sauber ist,
#!/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
Die Perl-Version ist sauberer,
#!/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 "$_"; } }
Antwort1
Ich würde mehr nutzen vonE/A-Umleitung:
#!/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
Erläuterung
[[ $1 ]] && [[ ! -f $1 ]] && echo "file $1 dne" && exit 1
Testen Sie, ob eine Eingabedatei als Befehlszeilenargument angegeben wurde und ob die Datei existiert.
[[ $1 ]] && exec 3<$1 || exec 3<&0
Wenn
$1
gesetzt ist, d. h. eine Eingabedatei angegeben wurde, wird die angegebene Datei am Dateideskriptor geöffnet3
, andernfallsstdin
wird am Dateideskriptor dupliziert3
.[[ $2 ]] && exec 4>$2 || exec 4>&1
Entsprechend wird, wenn
$2
gesetzt ist, d. h. eine Ausgabedatei angegeben wurde, die angegebene Datei am Dateideskriptor geöffnet4
, andernfallsstdout
wird am Dateideskriptor dupliziert4
.fgrep -v "stuff" <&3 >&4
Zuletzt
fgrep
wird aufgerufen, wobeistdin
undstdout
zu den zuvor festgelegten Dateideskriptoren3
bzw. umgeleitet werden4
.
Standard-Ein- und Ausgabe erneut öffnen
Wenn Sie keine Zwischendateideskriptoren öffnen möchten, können Sie alternativ die Dateideskriptoren direkt stdin
und stdout
entsprechend den angegebenen Eingabe- und Ausgabedateien ersetzen:
#!/bin/bash
[[ $1 ]] && [[ ! -f $1 ]] && echo "file $1 dne" && exit 1
[[ $1 ]] && exec 0<$1
[[ $2 ]] && exec 1>$2
fgrep -v "stuff"
Ein Nachteil dieses Ansatzes besteht darin, dass Sie die Möglichkeit verlieren, die Ausgabe des Skripts selbst von der Ausgabe des Befehls zu unterscheiden, der das Ziel der Umleitung ist. Beim ursprünglichen Ansatz können Sie die Skriptausgabe an das unveränderte stdin
und weiterleiten stdout
, das wiederum vom Aufrufer des Skripts umgeleitet worden sein könnte. Auf die angegebenen Eingabe- und Ausgabedateien konnte weiterhin über die entsprechenden Dateideskriptoren zugegriffen werden, die sich vom Skript stdin
und unterscheiden stdout
.
Antwort2
Wie wäre es mit:
input="${1:-/dev/stdin}"
output="${2:-/dev/stdout}"
err="${3:-/dev/stderr}"
foobar <"$input" >"$output" 2>"$err"
Beachten Sie bitte, /dev/std(in|out|err)
dassnicht im POSIX-StandardDies funktioniert also nur auf Systemen, die diese speziellen Dateien unterstützen.
Dies setzt auch vernünftige Eingaben voraus: Vor der Umleitung wird nicht geprüft, ob Dateien vorhanden sind.
Antwort3
wenn es Ihnen nichts ausmacht, dass die Ausgabestetsauf stdout umgeleitet werden, können Sie den folgenden Einzeiler verwenden:
cat $1 |fgrep -v "stuff" | tee
Antwort4
Ich weiß nicht, ob das „sauberer“ ist, aber hier sind einige Vorschläge (das ist kein getesteter Code). Die Verwendung von exec
(laut Thomas Nyman) kann zu Sicherheitsproblemen führen und sollte mit Vorsicht behandelt werden.
Platzieren Sie zunächst sich wiederholenden Code in einer Funktion.
# 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'
}
Hier verwenden Sie statt fgrep
, cat
Ihr Freund. Verwenden Sie dann 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
Eine weitere Alternative (die sauber und sehr kompakt ist) besteht darin, die Anweisungen in ein Array zu laden und dann über den Wert von $# darauf zuzugreifen, so etwas wie einen Funktionszeiger. Bei der is_file
obigen Funktion sieht der Bash-Code etwa so aus:
# 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'
Ich bin mir mit der Syntax nicht 100 % sicher, aber die Anführungszeichen müssen maskiert werden. Variablen, die Dateipfade enthalten, werden am besten in doppelte Anführungszeichen gesetzt.
Ein zusätzlicher Hinweis: Beim Schreiben nach $2 sollte wahrscheinlich überprüft werden, dass die Datei nicht existiert, da sie sonst überschrieben wird.