web-dev-qa-db-fra.com

Existe-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'appelle 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

La difficulté réside dans le fait qu’il peut invoquer la commande de plusieurs manières:

  • 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 alias Shell frob='python foo.py', et en fait, ils ont exécuté frob
  • Peut-être que c'est même un alias de git flab=!/usr/bin/foo.py, et ils ont utilisé git flab

Dans tous les cas, j'aimerais que le message indique comment l'utilisateur a appelé la commande afin que l'exemple que je donne ait un 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 elle-même, mais je ne sais pas comment la demander.

Des idées?

UPDATEEt si on limitait les scénarios possibles à ceux listés ci-dessus?

UPDATE 2 : Beaucoup de personnes ont très bien expliqué pourquoi cela n’est pas possible dans le cas général. Je voudrais donc limiter ma question à celle-ci:

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: ces 3 manières :
    1. foo <args> 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 1 et (2,3) du script? Y at-il un moyen de distinguer entre 2 et 3 à partir du script?

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

UPDATE 3 : Jusqu'à présent, l'angle le plus prometteur avait é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 ceci 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 n'arrive pas à obtenir leur méthode d'invocation. Bien sûr, si je compte changer l’environnement de mes utilisateurs, je peux tout aussi bien m'assurer que les divers alias exportent leurs propres variables d'environnement que je peux consulter, mais au moins, cette méthode me permet d'utiliser la même technique autre script que je pourrais ajouter plus tard.

14
itsadok

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 s’effectue comme suit au niveau du syscall sous-jacent:

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

Notamment, il y a trois arguments:

  • Le chemin de l'exécutable
  • Un tableau argv (le premier élément - 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

Il n'y a nulle part ici de chaîne fournissant la commande Shell d'origine saisie par l'utilisateur à partir de laquelle l'appel du nouveau processus a été demandé. Ceci est particulièrement vrai puisque tous les programmes ne sont pas démarrés à partir d'un shell ; Considérons le cas où votre programme est lancé à partir d'un autre script Python avec Shell=False.


Sous UNIX, il est tout à fait classique de supposer que votre programme a été démarré avec le nom donné dans argv[0]; cela fonctionne pour les liens symboliques.

Vous pouvez même voir les outils UNIX standard faire ceci:

$ 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 do 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éveloppera" pas les alias, reviendra au code qui a été appelé pour appeler une fonction qui a appelé votre commande, etc. il n'y a pas de sonnerie qui sonne.


Ce can peut être utilisé pour rechercher une instance de git dans l’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
16
Charles Duffy

Voir la note en bas à propos du script de wrapper proposé à l'origine.

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èrent voir dans les messages d'erreur.

Par exemple, si un utilisateur préfère appeler le script python 'myPyScript.py' via un alias, il peut modifier la définition de cet alias de la manière suivante:

  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 développées, voici le 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.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

Après avoir enregistré ceci dans un fichier nommé 'pytest.py', puis l'avoir appelé à partir d'un script bash appelé 'pytest.sh' avec divers arguments, voici le résultat:

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

REMARQUE: les critiques du script wrapper d'origine aliasTest.sh étaient valides. Bien que l’existence d’un pseudonyme prédéfini fasse partie de la spécification de la question et puisse être présumée exister dans l’environnement de l’utilisateur, la proposition a défini le pseudonyme (créant l’impression trompeuse qu’il faisait partie de la recommandation plutôt qu’un paramètre spécifié). 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 trouver le wrapper, soit définir l’alias dans celui-ci, et le script python devrait déléguer l’impression des messages d’erreur à plusieurs scripts d’appel personnalisés (où se trouvent les informations d’appel), et les clients appeler les scripts wrapper. La résolution de ces problèmes a conduit à une approche plus simple, pouvant être étendue à un nombre quelconque de cas d'utilisation supplémentaires.

Voici une version moins déroutante 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]
4
philwalk

Il n'y a aucun moyen de distinguer entre le moment où un interpréteur pour un script est explicitement spécifié sur la ligne de commande et le moment où il est déduit par 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'y a pas de moyen raisonnable d'identifier les autres moyens (médiés) d'appeler une commande.

3
Leon

Je peux voir deux façons de faire cela:

  • Le plus simple, comme suggéré par 3sky, serait d’analyser la ligne de commande depuis le 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, serait de 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 façon 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 figure parmi les arguments, et si c'est le cas, vérifiez si --bar l'est également. Sinon, imprimez le message et revenez.

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

Maintenant, espérons-le, l’exécution de python se fait par le Shebang suivant: #!/usr/bin/env python3, ou par le python foo.py, plutôt que par une variante de #!/usr/bin/python ou /usr/bin/python foo.py. De cette façon, vous pouvez modifier la variable $PATH et ajouter un répertoire dans lequel votre false python réside.

Dans le cas contraire, vous pouvez remplacer le /usr/bin/python executable, au risque de ne pas jouer à Nice avec des mises à jour.

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


Exemple de ce qui pourrait fonctionner comme 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 avoir relu votre question, il est évident que je l’ai mal comprise. À mon avis, il est correct d'imprimer soit "foo.py doit s'appeler comme foo.py --bar", "veuillez ajouter une barre à vos arguments" ou "veuillez essayer (au lieu de)", quel que soit le utilisateur entré:

  • S'il s'agit d'un alias (git), il s'agit d'une erreur unique, et l'utilisateur essaiera de le remplacer après l'avoir créé afin de savoir où placer la partie --bar.
  • avec soit avec /usr/bin/foo.py ou python foo.py:
    • Si l'utilisateur n'est pas vraiment habitué à 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.
    • S'ils le sont, ils devraient pouvoir comprendre le message sans problème et ajuster leur ligne de commande.
0
MayeulC