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:
python foo.py
comme dans l'exemple/usr/bin/foo.py
frob='python foo.py'
, et en fait, ils ont exécuté frob
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:
foo <args>
où foo est un lien symbolique/usr/bin/foo -> foo.pygit foo
où alias.foo =!/usr/bin/foo dans ~/.gitconfig
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.
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:
argv[0]
ou $0
- est transmis à cet exécutable pour refléter le nom sous lequel il a été démarré)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
.
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
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.
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 à 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]
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.
Je peux voir deux façons de faire cela:
argparse
peut être utilisé pour le faire de manière fiable. Cela ne fonctionne que si vous pouvez modifier ce script.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é:
--bar
./usr/bin/foo.py
ou python foo.py
: