Y a-t-il un moyen de savoir comment l’utilisateur a appelé un programme depuis bash?

Voici le problème: J’ai ce script foo.py , et si l’utilisateur l’invoque sans l’option --bar , j’aimerais afficher le message d’erreur suivant:

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

Maintenant, la partie délicate est que l’utilisateur peut invoquer la commande de plusieurs façons:

  • Ils ont peut-être utilisé python foo.py comme dans l’exemple
  • Ils ont peut-être utilisé /usr/bin/foo.py
  • Ils peuvent avoir un shell alias frob='python foo.py' , et ont en fait lancé frob
  • Peut-être que c’est même un alias git flab=!/usr/bin/foo.py , et ils ont utilisé git flab

Dans tous les cas, je souhaiterais que le message reflète la manière dont l’utilisateur a invoqué la commande, de sorte que l’exemple que je propose aurait du sens.

sys.argv contient toujours foo.py et /proc/$$/cmdline ne connaît pas les alias. Il me semble que la seule source possible pour cette information serait bash lui-même, mais je ne sais pas comment le demander.

Des idées?

MISE À JOUR Que diriez-vous si nous limitons les scénarios possibles à ceux énumérés ci-dessus?

MISE À JOUR 2 : Beaucoup de personnes ont écrit de très bonnes explications sur la raison pour laquelle cela n’est pas possible dans le cas général, alors je voudrais limiter ma question à ceci:

Sous les hypothèses suivantes:

  • Le script a été lancé de manière interactive, à partir de bash
  • Le script a été lancé de l’une des manières suivantes :
    1. foo où foo est un lien symbolique / usr / bin / foo -> foo.py
    2. git foo où alias.foo =! / usr / bin / foo dans ~/.gitconfig
    3. git baz où alias.baz =! / usr / bin / foo dans ~/.gitconfig

Existe-t-il un moyen de distinguer entre 1 et (2,3) du script? Existe-t-il un moyen de distinguer 2 et 3 du script?

Je sais que c’est un long coup, alors j’accepte la réponse de Charles Duffy pour le moment.

MISE À JOUR 3 : Jusqu’à présent, le point le plus prometteur a été suggéré par Charles Duffy dans les commentaires ci-dessous. Si je peux obtenir mes utilisateurs d’avoir

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

dans leur .bashrc , alors je peux utiliser quelque chose comme ça dans mon code:

 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.") 

De cette façon, je montre le message général si je ne parviens pas à obtenir leur méthode d’invocation. Bien sûr, si je compte sur la modification de l’environnement de mes utilisateurs, je pourrais tout aussi bien m’assurer que les différents alias exportent leurs propres variables d’environnement, mais au moins, cette technique me permet d’utiliser la même technique autre script que je pourrais append plus tard.

Non, il n’y a aucun moyen de voir le texte original (avant les alias / fonctions / etc).

Le démarrage d’un programme sous UNIX se fait comme suit au niveau de l’appel système sous-jacent:

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

Notamment, il y a trois arguments:

  • Le chemin d’access à l’exécutable
  • Un tableau argv (le premier élément dont – argv[0] ou $0 – est transmis à cet exécutable pour refléter le nom sous lequel il a été démarré)
  • Une liste de variables d’environnement

Nulle part ici, il n’y a une chaîne qui fournit la commande shell originale saisie par l’utilisateur à partir de laquelle l’appel du nouveau processus a été demandé. Ceci est particulièrement vrai car tous les programmes ne sont pas du tout lancés depuis un shell ; Considérez le cas où votre programme est démarré à partir d’un autre script Python avec shell=False .


Il est tout à fait conventionnel sous UNIX de supposer que votre programme a été démarré par le nom donné dans argv[0] ; Cela fonctionne pour les liens symboliques.

Vous pouvez même voir les outils UNIX standard en procédant comme suit:

 $ 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 

Si vous souhaitez générer une ligne de commande UNIX, utilisez pipes.quote() (Python 2) ou shlex.quote() (Python 3) pour le faire en toute sécurité.

 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)) 

Encore une fois, cela ne “dé-développez” pas les alias, revenez au code qui a été appelé pour appeler une fonction qui a invoqué votre commande, etc. il n’y a pas de sonnerie qui ne sonne pas.


Cela peut être utilisé pour rechercher une instance de git dans votre arborescence de processus parent et découvrir sa liste d’arguments:

 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 

Voir la note en bas concernant le script wrapper initialement proposé.

Une nouvelle approche plus flexible consiste pour le script python à fournir une nouvelle option de ligne de commande, permettant aux utilisateurs de spécifier une chaîne personnalisée qu’ils préféreraient voir dans les messages d’erreur.

Par exemple, si un utilisateur préfère appeler le script python ‘ myPyScript.p y’ via un alias, il peut modifier la définition d’alias à partir de ceci:

  alias myAlias='myPyScript.py $@' 

pour ça:

  alias myAlias='myPyScript.py --caller=myAlias $@' 

S’ils préfèrent appeler le script python à partir d’un script shell, il peut utiliser l’option de ligne de commande supplémentaire comme suit:

  #!/bin/bash exec myPyScript.py "$@" --caller=${0##*/} 

Autres applications possibles de cette approche:

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

Pour répertorier les lignes de commande étendues, voici un script ” pyTest.py “, basé sur les commentaires de @CharlesDuffy, qui répertorie cmdline pour le script python en cours d’exécution, ainsi que le processus parent qui l’a généré. Si le nouvel argument -caller est utilisé, il apparaîtra dans la ligne de commande, bien que les alias aient été développés, etc.

 #!/usr/bin/env python import os, re with open ("/proc/self/stat", "r") as myfile: data = [x.ssortingp() 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.ssortingp() 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 

Après avoir enregistré ceci dans un fichier nommé ” pytest.py “, puis appelé depuis un script bash appelé ” pytest.sh ” avec divers arguments, voici la sortie:

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

REMARQUE: les critiques du script d’emballage original aliasTest.sh étaient valides. Bien que l’existence d’un alias prédéfini fasse partie de la spécification de la question et puisse être présumée exister dans l’environnement utilisateur, la proposition a défini l’alias (créant une impression trompeuse selon laquelle elle faisait partie de la recommandation plutôt qu’un partie de l’environnement de l’utilisateur), et il n’a pas montré comment le wrapper communiquerait avec le script appelé python. En pratique, l’utilisateur devrait soit générer le wrapper, soit définir l’alias dans l’encapsuleur, et le script python devrait déléguer l’impression des messages d’erreur à plusieurs scripts d’appel personnalisés (où résidaient les informations appelantes) et les clients auraient dû: appeler les scripts wrapper. La résolution de ces problèmes a conduit à une approche plus simple, qui peut être étendue à un nombre illimité de cas d’utilisation supplémentaires.

Voici une version moins confuse du script original, à titre de référence:

 #!/bin/bash shopt -s expand_aliases alias myAlias='myPyScript.py' # called like this: set -o history myAlias $@ _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 

Voici la sortie:

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

Il n’y a aucun moyen de distinguer quand un interpréteur pour un script est explicitement spécifié sur la ligne de commande et quand il est déduit par le système d’exploitation de la ligne de hachage.

Preuve:

 $ cat test.sh #!/usr/bin/env bash ps -o command $$ $ bash ./test.sh COMMAND bash ./test.sh $ ./test.sh COMMAND bash ./test.sh 

Cela vous empêche de détecter la différence entre les deux premiers cas de votre liste.

Je suis également convaincu qu’il n’ya pas de moyen raisonnable d’identifier les autres moyens (médiatisés) d’appeler un commandement.

Je peux voir deux façons de le faire:

  • Le plus simple, comme suggéré par 3sky, serait d’parsingr la ligne de commande depuis l’intérieur du script python. argparse peut être utilisé pour le faire de manière fiable. Cela ne fonctionne que si vous pouvez modifier ce script.
  • Une manière plus complexe, légèrement plus générique et impliquée, consisterait à changer l’exécutable python sur votre système.

La première option étant bien documentée, voici un peu plus de détails sur la seconde:

Quelle que soit la manière dont votre script est appelé, python est exécuté. Le but ici est de remplacer l’exécutable python par un script qui vérifie si foo.py fait partie des arguments, et si c’est le cas, vérifiez si --bar est également. Sinon, imprimez le message et renvoyez-le.

Dans tous les autres cas, exécutez le véritable exécutable python.

Maintenant, si tout va bien, l’exécution de python se fait via le shebang suivant: #!/usr/bin/env python3 , ou python foo.py , plutôt qu’une variante de #!/usr/bin/python ou /usr/bin/python foo.py De cette façon, vous pouvez modifier la variable $PATH et append un répertoire dans lequel réside votre faux python .

Dans l’autre cas, vous pouvez remplacer l’ /usr/bin/python executable , au risque de ne pas jouer bien avec les mises à jour.

Une manière plus complexe de le faire serait probablement avec les espaces de noms et les assemblys, mais ce qui précède est probablement suffisant, surtout si vous avez des droits d’administrateur.


Exemple de ce qui pourrait fonctionner comme un script:

 #!/usr/bin/env bash function checkbar { for i in "$@" 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 "$@" then echo "Please add --bar to the command line, like so:" printf "%q " $0 printf "%q " "$@" printf -- "--bar\n" exit 1 fi fi /path/to/real/python "$@" 

Cependant, après relecture de votre question, il est évident que je l’ai mal comprise. À mon avis, il est juste d’imprimer soit “foo.py doit être appelé comme foo.py –bar”, “s’il vous plaît append des barres à vos arguments” ou “s’il vous plaît essayez (au lieu de)”, indépendamment de ce que le utilisateur entré:

  • Si c’est un alias (git), il s’agit d’une erreur unique, et l’utilisateur essaiera son alias après l’avoir créé, afin de savoir où placer la partie --bar
  • avec soit /usr/bin/foo.py ou python foo.py :
    • Si l’utilisateur n’est pas vraiment averti par la ligne de commande, il lui suffit de coller la commande de travail affichée, même s’il ne connaît pas la différence.
    • Si tel est le cas, ils devraient pouvoir comprendre le message sans problème et ajuster leur ligne de commande.

Je sais que c’est la tâche bash , mais je pense que la manière la plus simple est de modifier ‘foo.py’. Bien sûr, cela dépend du niveau de script compliqué, mais peut-être que ça ira. Voici un exemple de code:

 #!/usr/bin/python import sys if len(sys.argv) > 1 and sys.argv[1] == '--bar': print 'make magic' else: print 'Please add the --bar option to your command, like so:' print ' python foo.py --bar' 

Dans ce cas, la manière dont l’utilisateur exécute ce code importe peu.

 $ ./a.py Please add the --bar option to your command, like so: python foo.py --bar $ ./a.py -dua Please add the --bar option to your command, like so: python foo.py --bar $ ./a.py --bar make magic $ python a.py --t Please add the --bar option to your command, like so: python foo.py --bar $ /home/3sky/test/a.py Please add the --bar option to your command, like so: python foo.py --bar $ alias a='python a.py' $ a Please add the --bar option to your command, like so: python foo.py --bar $ a --bar make magic