Wie kann ich einen „grep | grep“-Befehl als Zeichenfolge in einer Bash-Funktion ausführen?

Wie kann ich einen „grep | grep“-Befehl als Zeichenfolge in einer Bash-Funktion ausführen?

Ich versuche, einen Befehl zu erstellen, der die Ergebnisse eines Grep-Befehls an einen anderen Grep-Befehl in einer Bash-Funktion weiterleitet. Letztendlich möchte ich, dass der ausgeführte Befehl so aussieht:

grep -I -r FooBar /code/internal/dev/ /code/public/dev/ | grep .c:\|.h:

Die Funktion, die ich schreibe, speichert den ersten Teil des Befehls in einer Zeichenfolge und hängt dann den zweiten Teil an:

grep_cmd="grep -I -r $pattern $@"

if (( ${#file_types[@]} > 0 )); then
    file_types="${file_types[@]}"
    file_types=.${file_types// /':\|.'}:

    grep_cmd="$grep_cmd | grep $file_types"
fi

echo "$grep_cmd"
${grep_cmd}

Dies löst nach der Ausgabe des ersten Teils einen Fehler aus:

grep: |: No such file or directory
grep: grep: No such file or directory
grep: .c:\|.h:: No such file or directory

Wenn Sie die letzte Zeile von ${grep_cmd}in ändern "$grep_cmd", wird keine Ausgabe des ersten Teils angezeigt und ein anderer Fehler ausgelöst:

bash: grep -I -r FooBar /code/internal/dev/ /code/public/dev/ | grep .c:\|.h:: No such file or directory

Prodiese SO-Antwort, ich habe versucht, die letzte Zeile in zu ändern $(grep_cmd). Dies führt zu einem weiteren Fehler:

bash: grep_cmd: command not found

Diese SO-Antwortschlägt die Verwendung vor eval $grep_cmd. Dadurch werden die Fehler, aber auch die Ausgabe unterdrückt.

Dieses hierschlägt vor, zu verwenden eval ${grep_cmd}. Dies hat die gleichen Ergebnisse (unterdrückt die Fehler und die Ausgabe). Ich habe versucht, das Debuggen in Bash zu aktivieren (mit set -x), was mir Folgendes gibt:

+ eval grep -I -r FooBar /code/internal/dev/ /code/public/dev/ '|' grep '.c:\|.h:'
++ grep -I -r FooBar /code/internal/dev/ /code/public/dev/
++ grep '.c:|.h:'

Es sieht so aus, als ob das Pipe-Zeichen maskiert wird, sodass die Shell den Befehl als zwei Befehle interpretiert. Wie maskiere ich das Pipe-Zeichen richtig, sodass es als ein Befehl interpretiert wird?

Antwort1

Wie im Kommentar erwähnt, liegen viele Ihrer Schwierigkeiten darin, dass Sie versuchen, einen Befehl in einer Variablen zu speichern und diesen Befehl dann später auszuführen.

Sie haben wesentlich mehr Erfolg, wenn Sie den Befehl einfach sofort ausführen, anstatt zu versuchen, ihn zu speichern.

Das Folgende sollte beispielsweise das erreichen, was Sie erreichen möchten:

if (( ${#file_types[@]} > 0 )); then
    regex="${file_types[*]}"
    regex="\.\(${regex// /\|}\):"
    grep -I -r "$pattern" "$@" | grep "$regex"
else
    grep -I -r "$pattern" "$@"
fi

Antwort2

Bei der Shell-Programmierung ist zu beachten, dass es zwei Arten von Daten gibt, was in Tutorials oft nicht klar erklärt wird:Zeichenfolgen und Listen von Zeichenfolgen. Eine Liste von Zeichenfolgen ist nicht dasselbe wie Zeichenfolgen mit einem Zeilenumbruch- oder Leerzeichentrennzeichen, sondern etwas Eigenes.

Außerdem sollten Sie bedenken, dass die meisten Erweiterungen nur angewendet werden, wenn die Shell eine Datei analysiert. Die Ausführung eines Befehls erfordert keine Erweiterung.

Es findet eine gewisse Erweiterung des Wertes einer Variablen statt: $foobedeutet „nimm den Wert der Variablen foo, teile ihn in eine Liste von Zeichenfolgen auf, wobei du Leerzeichen¹ als Trennzeichen verwendest, und interpretiere jedes Element der Liste als Platzhaltermuster, das dann erweitert wird“. Diese Erweiterung findet nur statt, wenn die Variable in einem Kontext verwendet wird, der eine Liste erfordert. In einem Kontext, der eine Zeichenfolge erfordert, $foobedeutet „nimm den Wert der Variablen foo“. Doppelte Anführungszeichen erzwingen einen Zeichenfolgenkontext, daher der Rat:Verwenden Sie Variablen- und Befehlsersetzungen immer in Anführungszeichen: "$foo","$(somecommand)"². (Bei ungeschützten Befehlsersetzungen treten die gleichen Erweiterungen auf wie bei Variablen.)

Eine Konsequenz der Unterscheidung zwischen Parsen und Ausführen ist, dass Sie einen Befehl nicht einfach in eine Zeichenfolge packen und ausführen können. Wenn Sie schreiben ${grep_cmd}, findet nur das Aufteilen und Globbing statt, kein Parsen, daher |hat ein Zeichen wie keine besondere Bedeutung.

Wenn Sie unbedingt einen Shell-Befehl in eine Zeichenfolge packen müssen, können Sie evaldies tun:

eval "$grep_cmd"

Beachten Sie die Anführungszeichen – der Wert der Variablen enthält einen Shell-Befehl, daher benötigen wir dessen exakten String-Wert. Dieser Ansatz ist jedoch tendenziell kompliziert: Sie müssen dort wirklich etwas in Shell-Quellsyntax haben. Wenn Sie beispielsweise einen Dateinamen benötigen, muss dieser Dateiname richtig in Anführungszeichen gesetzt werden. Sie können also nicht einfach „ $patternund“ $@dort einfügen, sondern müssen einen String erstellen, der beim Parsen ein einzelnes Wort mit dem Muster und eine Liste von Wörtern mit den Argumenten ergibt.

Zusammenfassen:Stopfen Sie keine Shell-Befehle in Variablen. Stattdessen,Funktionen verwenden. Wenn Sie einen einfachen Befehl mit Argumenten und nicht etwas Komplexeres wie eine Pipeline benötigen, können Sie stattdessen ein Array verwenden (eine Array-Variable speichert eine Liste von Zeichenfolgen).

Hier ist ein möglicher Ansatz. Die run_grepFunktion wird bei dem von Ihnen gezeigten Code nicht wirklich benötigt. Ich füge sie hier ein, weil ich davon ausgehe, dass dies ein kleiner Teil eines größeren Skripts ist und es viel mehr Zwischencode gibt. Wenn dies wirklich das gesamte Skript ist, führen Sie grep einfach an der Stelle aus, an der Sie wissen, wohin es weitergeleitet werden soll. Ich habe auch den Code korrigiert, der den Filter erstellt, der nicht richtig schien (zum Beispiel .bedeutet in einem regulären Ausdruck „beliebiges Zeichen“, aber ich glaube, Sie möchten einen wörtlichen Punkt).

grep_cmd=(grep -I -r "$pattern" "$@")

if (( ${#file_types[@]} > 0 )); then
    regexp='\.\('
    for file_type in "${file_types[@]}"; do
      regexp="$regexp$file_type\\|"
    done
    regexp="${regexp%?}):"
    run_grep () {
      "${grep_cmd[@]}" | grep "$file_types"
    }
else
  run_grep () {
    "${grep_cmd[@]}"
  }
fi

run_grep

¹ Allgemeiner gesagt, verwenden Sie den Wert von IFS.
² Nur für Experten: Setzen Sie Variablen- und Befehlsersetzungen immer in Anführungszeichen, es sei denn, Sie wissen, warum Sie durch Weglassen den richtigen Effekt erzielen.
³ Nur für Experten: Wenn Sie einen Shell-Befehl in eine Variable stopfen müssen, gehen Sie mit den Anführungszeichen sehr vorsichtig um.


Beachten Sie, dass Ihr Vorgehen übermäßig komplex und nicht zuverlässig erscheint. Was passiert, wenn Sie eine Datei haben, die enthält foo.c: 42? GNU grep verfügt über eine --includeOption, mit der bei einer rekursiven Durchquerung nur in bestimmten Dateien gesucht werden kann. Verwenden Sie diese einfach.

grep_cmd=(grep -I -r)
for file_type in "${file_types[@]}"; do
  grep_cmd+=(--include "*.$file_type")
done
"${grep_cmd[@]}" "$pattern" "$@"

Antwort3

command="grep $regex1 filelist | grep $regex2"
echo $command | bash

verwandte Informationen