it-swarm.com.de

Gibt es eine Möglichkeit zu wissen, wie der Benutzer ein Programm aus Bash aufgerufen hat?

Hier ist das Problem: Ich habe dieses Skript foo.py, und wenn der Benutzer es ohne die Option --bar aufruft, möchte ich die folgende Fehlermeldung anzeigen:

Please add the --bar option to your command, like so:
    python foo.py --bar

Der schwierige Teil ist, dass der Benutzer den Befehl auf verschiedene Arten aufrufen kann:

  • Sie haben möglicherweise python foo.py wie im Beispiel verwendet
  • Sie haben möglicherweise /usr/bin/foo.py verwendet
  • Sie haben möglicherweise einen Shell-Alias ​​frob='python foo.py' und haben tatsächlich frob ausgeführt.
  • Vielleicht ist es sogar ein Git-Alias ​​flab=!/usr/bin/foo.py, und sie haben git flab verwendet

In jedem Fall möchte ich, dass die Nachricht widerspiegelt, wie der Benutzer den Befehl aufgerufen hat, damit das von mir bereitgestellte Beispiel Sinn ergibt.

sys.argv enthält immer foo.py und /proc/$$/cmdline kennt keine Aliase. Es scheint mir, dass die einzig mögliche Quelle für diese Information die Bash selbst ist, aber ich weiß nicht, wie ich sie fragen soll.

Irgendwelche Ideen?

UPDATE Wie wäre es, wenn wir mögliche Szenarien auf die oben aufgeführten beschränken?

UPDATE 2 : Viele Leute haben sehr gute Erklärungen geschrieben, warum dies im allgemeinen Fall nicht möglich ist, deshalb möchte ich meine Frage hierauf beschränken:

Unter folgenden Annahmen:

  • Das Skript wurde interaktiv von Bash aus gestartet
  • Das Skript wurde auf eine der folgenden drei Arten gestartet :
    1. foo <args> wobei foo ein symbolischer Link/usr/bin/foo -> foo.py ist
    2. git foo where alias.foo =!/usr/bin/foo in ~/.gitconfig
    3. git baz where alias.baz =!/usr/bin/foo in ~/.gitconfig

Gibt es eine Möglichkeit, zwischen 1 und (2,3) innerhalb des Skripts zu unterscheiden? Gibt es eine Möglichkeit, zwischen 2 und 3 innerhalb des Skripts zu unterscheiden?

Ich weiß, dass dies ein langer Weg ist, also akzeptiere ich Charles Duffys Antwort vorerst.

UPDATE 3 : Der bisher vielversprechendste Winkel wurde von Charles Duffy in den Kommentaren unten vorgeschlagen. Wenn ich meine User dazu bringen kann

trap 'export LAST_BASH_COMMAND=$(history 1)' DEBUG

in ihrem .bashrc kann ich dann so etwas in meinem Code verwenden:

like_so = None
cmd = os.environ['LAST_BASH_COMMAND']
if cmd is not None:
    cmd = cmd[8:]  # Remove the history counter
    if cmd.startswith("foo "):
        like_so = "foo --bar " + cmd[4:]
    Elif cmd.startswith(r"git foo "):
        like_so = "git foo --bar " + cmd[8:]
    Elif cmd.startswith(r"git baz "):
        like_so = "git baz --bar " + cmd[8:]
if like_so is not None:
    print("Please add the --bar option to your command, like so:")
    print("    " + like_so)
else:
    print("Please add the --bar option to your command.")

Auf diese Weise zeige ich die allgemeine Nachricht an, wenn ich ihre Aufrufmethode nicht abrufen kann. Wenn ich mich darauf verlassen möchte, die Umgebung meiner Benutzer zu ändern, kann ich natürlich auch sicherstellen, dass die verschiedenen Aliase ihre eigenen Umgebungsvariablen exportieren, die ich anzeigen kann, aber zumindest kann ich auf diese Weise dieselbe Technik für alle verwenden ein anderes Skript könnte ich später hinzufügen.

14
itsadok

Nein, es gibt keine Möglichkeit, den Originaltext (vor Aliases/Funktionen/etc) anzuzeigen.

Das Starten eines Programms unter UNIX erfolgt auf der zugrunde liegenden Systemaufrufebene wie folgt:

int execve(const char *path, char *const argv[], char *const envp[]);

Insbesondere gibt es drei Argumente:

  • Der Pfad zur ausführbaren Datei
  • Ein argv-Array (das erste Element, argv[0] oder $0 -, das an die ausführbare Datei übergeben wird, um den Namen anzuzeigen, unter dem es gestartet wurde)
  • Eine Liste von Umgebungsvariablen

Es gibt nirgends eine Zeichenfolge, die den vom Benutzer eingegebenen ursprünglichen Shell-Befehl enthält, von dem aus der Aufruf des neuen Prozesses angefordert wurde. Dies gilt insbesondere, da nicht alle Programme von einer Shell aus gestartet werden ; Betrachten Sie den Fall, in dem Ihr Programm von einem anderen Python-Skript mit Shell=False gestartet wird.


Unter UNIX ist es völlig üblich, davon auszugehen, dass Ihr Programm mit dem in argv[0] angegebenen Namen gestartet wurde. das funktioniert für symlinks.

Sie können sogar Standard-UNIX-Tools sehen, die dies tun:

$ ls '*.txt'         # sample command to generate an error message; note "ls:" at the front
ls: *.txt: No such file or directory
$ (exec -a foobar ls '*.txt')   # again, but tell it that its name is "foobar"
foobar: *.txt: No such file or directory
$ alias somesuch=ls             # this **doesn't** happen with an alias
$ somesuch '*.txt'              # ...the program still sees its real name, not the alias!
ls: *.txt: No such file 

Wenn Sie do eine UNIX-Befehlszeile generieren möchten, verwenden Sie pipes.quote() (Python 2) oder shlex.quote() (Python 3), um dies sicher auszuführen.

try:
    from pipes import quote # Python 2
except ImportError:
    from shlex import quote # Python 3

cmd = ' '.join(quote(s) for s in open('/proc/self/cmdline', 'r').read().split('\0')[:-1])
print("We were called as: {}".format(cmd))

Auch hier werden die Aliase nicht "entpackt", sondern der Code, der aufgerufen wurde, um eine Funktion aufzurufen, die Ihren Befehl aufgerufen hat, usw .; Es ist kein Klingeln los.


Das can kann verwendet werden, um in Ihrem übergeordneten Prozessbaum nach einer git-Instanz zu suchen und deren Argumentliste zu ermitteln:

def find_cmdline(pid):
    return open('/proc/%d/cmdline' % (pid,), 'r').read().split('\0')[:-1]

def find_ppid(pid):
    stat_data = open('/proc/%d/stat' % (pid,), 'r').read()
    stat_data_sanitized = re.sub('[(]([^)]+)[)]', '_', stat_data)
    return int(stat_data_sanitized.split(' ')[3])

def all_parent_cmdlines(pid):
    while pid > 0:
        yield find_cmdline(pid)
        pid = find_ppid(pid)

def find_git_parent(pid):
    for cmdline in all_parent_cmdlines(pid):
        if cmdline[0] == 'git':
            return ' '.join(quote(s) for s in cmdline)
    return None
16
Charles Duffy

Lesen Sie den Hinweis am Ende des ursprünglich vorgeschlagenen Wrapper-Skripts.

Ein neuer, flexiblerer Ansatz besteht darin, dass das Python-Skript eine neue Befehlszeilenoption bereitstellt, mit der Benutzer eine benutzerdefinierte Zeichenfolge angeben können, die in Fehlermeldungen angezeigt werden soll.

Wenn ein Benutzer beispielsweise das Python-Skript 'myPyScript.py' über einen Aliasnamen aufrufen möchte, kann er die Aliasdefinition wie folgt ändern:

  alias myAlias='myPyScript.py [email protected]'

zu diesem:

  alias myAlias='myPyScript.py --caller=myAlias [email protected]'

Wenn sie es vorziehen, das Python-Skript von einem Shell-Skript aus aufzurufen, kann es die zusätzliche Befehlszeilenoption wie folgt verwenden:

  #!/bin/bash
  exec myPyScript.py "[email protected]" --caller=${0##*/}

Andere mögliche Anwendungen dieses Ansatzes:

  bash -c myPyScript.py --caller="bash -c myPyScript.py"

  myPyScript.py --caller=myPyScript.py

Zum Auflisten erweiterter Befehlszeilen finden Sie hier ein Skript 'pyTest.py', das auf Rückmeldungen von @CharlesDuffy basiert. Es listet Cmdline für das ausgeführte Python-Skript sowie den übergeordneten Prozess auf, der es erzeugt hat Wird verwendet, wird es in der Befehlszeile angezeigt, obwohl Aliasnamen erweitert wurden usw.

#!/usr/bin/env python

import os, re

with open ("/proc/self/stat", "r") as myfile:
  data = [x.strip() for x in str.split(myfile.readlines()[0],' ')]

pid = data[0]
ppid = data[3]

def commandLine(pid):
  with open ("/proc/"+pid+"/cmdline", "r") as myfile:
    return [x.strip() for x in str.split(myfile.readlines()[0],'\x00')][0:-1]

pid_cmdline = commandLine(pid)
ppid_cmdline = commandLine(ppid)

print "%r" % pid_cmdline
print "%r" % ppid_cmdline

Nachdem Sie dies in einer Datei mit dem Namen 'pytest.py' gespeichert und dann mit einem Bash-Skript namens 'pytest.sh' mit verschiedenen Argumenten aufgerufen haben, folgt hier die Ausgabe:

$ ./pytest.sh a b "c d" e
['python', './pytest.py']
['/bin/bash', './pytest.sh', 'a', 'b', 'c d', 'e']

HINWEIS: Kritik am ursprünglichen Wrapper-Skript aliasTest.sh war gültig. Das Vorhandensein eines vordefinierten Alias ​​ist zwar Teil der Spezifikation der Frage und kann in der Benutzerumgebung vermutet werden. Der Vorschlag definierte den Alias ​​(erweckte den irreführenden Eindruck, dass er Teil der Empfehlung war und nicht eine Angabe war) Teil der Benutzerumgebung), und es zeigte sich nicht, wie der Wrapper mit dem aufgerufenen Python-Skript kommunizieren würde. In der Praxis müsste der Benutzer entweder den Wrapper beziehen oder den Alias ​​innerhalb des Wrappers definieren, und das Python-Skript müsste das Drucken von Fehlermeldungen an mehrere benutzerdefinierte aufrufende Skripts (wo sich die aufrufenden Informationen befinden) und Clients übergeben um die Wrapper-Skripte aufzurufen. Die Lösung dieser Probleme führte zu einem einfacheren Ansatz, der auf eine beliebige Anzahl zusätzlicher Anwendungsfälle erweitert werden kann.

Hier ist eine weniger verwirrende Version des Originalskripts als Referenz:

#!/bin/bash
shopt -s expand_aliases
alias myAlias='myPyScript.py'

# called like this:
set -o history
myAlias [email protected]
_EXITCODE=$?
CALL_HISTORY=( `history` )
_CALLING_MODE=${CALL_HISTORY[1]}

case "$_EXITCODE" in
0) # no error message required
  ;;
1)
  echo "customized error message #1 [$_CALLING_MODE]" 1>&2
  ;;
2)
  echo "customized error message #2 [$_CALLING_MODE]" 1>&2
  ;;
esac

Hier ist die Ausgabe:

$ aliasTest.sh 1 2 3
['./myPyScript.py', '1', '2', '3']
customized error message #2 [myAlias]
4
philwalk

Es gibt keine Möglichkeit, zu unterscheiden, wann ein Interpreter für ein Skript explizit in der Befehlszeile angegeben wird und wann er vom Betriebssystem aus der Hashbang-Zeile abgeleitet wird. 

Beweis:

$ cat test.sh 
#!/usr/bin/env bash

ps -o command $$

$ bash ./test.sh 
COMMAND
bash ./test.sh

$ ./test.sh 
COMMAND
bash ./test.sh

Dadurch wird verhindert, dass Sie den Unterschied zwischen den ersten beiden Fällen in Ihrer Liste erkennen.

Ich bin auch zuversichtlich, dass es keinen vernünftigen Weg gibt, die anderen (vermittelten) Wege eines Befehls zu identifizieren.

3
Leon

Ich sehe zwei Möglichkeiten, dies zu tun:

  • Die einfachste, wie von 3sky empfohlen, besteht darin, die Befehlszeile aus dem Python-Skript heraus zu analysieren. argparse kann dazu auf zuverlässige Weise verwendet werden. Dies funktioniert nur, wenn Sie das Skript ändern können.
  • Eine etwas komplexere und etwas allgemeinere Methode ist die Änderung der ausführbaren Datei python auf Ihrem System.

Da die erste Option gut dokumentiert ist, finden Sie hier einige Details zur zweiten:

Unabhängig davon, wie Ihr Skript aufgerufen wird, wird python ausgeführt. Das Ziel hier ist, die ausführbare Datei python durch ein Skript zu ersetzen, das überprüft, ob foo.py zu den Argumenten gehört. Wenn dies der Fall ist, prüfen Sie, ob auch --bar vorhanden ist. Wenn nicht, drucken Sie die Nachricht und kehren Sie zurück.

In allen anderen Fällen führen Sie die echte ausführbare Python-Datei aus.

Jetzt wird hoffentlich Python mit dem folgenden Shebang ausgeführt: #!/usr/bin/env python3 oder durch python foo.py statt einer Variante von #!/usr/bin/python oder /usr/bin/python foo.py. Auf diese Weise können Sie die $PATH-Variable ändern und ein Verzeichnis voranstellen, in dem sich Ihre falsche python befindet.

Im anderen Fall können Sie den /usr/bin/python executable ersetzen, da Nice nicht mit Updates abgespielt wird.

Eine komplexere Methode wäre dies wahrscheinlich mit Namespaces und Mounts, aber das ist wahrscheinlich genug, vor allem wenn Sie über Administratorrechte verfügen.


Beispiel, was als Skript funktionieren könnte:

#!/usr/bin/env bash

function checkbar
{
    for i in "[email protected]"
    do
            if [ "$i" = "--bar" ]
            then
                    echo "Well done, you added --bar!"
                    return 0
            fi
    done
    return 1
}

command=$(basename ${1:-none})
if [ $command = "foo.py" ]
then
    if ! checkbar "[email protected]"
    then
        echo "Please add --bar to the command line, like so:"
        printf "%q " $0
        printf "%q " "[email protected]"
        printf -- "--bar\n"
        exit 1
    fi
fi
/path/to/real/python "[email protected]"

Nachdem Sie Ihre Frage jedoch noch einmal gelesen haben, ist es offensichtlich, dass ich sie falsch verstanden habe. Meiner Meinung nach ist es in Ordnung, entweder nur "foo.py muss wie foo.py --bar" aufgerufen werden, "fügen Sie Ihren Argumenten eine Leiste hinzu" oder "Bitte versuchen Sie es stattdessen", unabhängig davon, was die Benutzer eingegeben:

  • Wenn es sich um einen (git) Alias ​​handelt, handelt es sich um einen einmaligen Fehler, und der Benutzer versucht seinen Alias ​​nach der Erstellung, sodass er weiß, wo er den --bar-Teil ablegt
  • entweder mit /usr/bin/foo.py oder python foo.py:
    • Wenn der Benutzer nicht wirklich mit der Befehlszeile vertraut ist, kann er einfach den angezeigten Arbeitsbefehl einfügen, auch wenn er den Unterschied nicht kennt
    • Wenn dies der Fall ist, sollten sie die Nachricht problemlos verstehen und ihre Befehlszeile anpassen können.
0
MayeulC