Есть ли способ узнать, как пользователь вызвал программу из bash? - программирование
Подтвердить что ты не робот

Есть ли способ узнать, как пользователь вызвал программу из bash?

Здесь проблема: у меня есть этот скрипт foo.py, и если пользователь вызывает его без опции --bar, я бы хотел отобразить следующее сообщение об ошибке:

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

Теперь сложная часть состоит в том, что пользователь может вызвать несколько способов:

  • Возможно, они использовали python foo.py как в примере
  • Возможно, они использовали /usr/bin/foo.py
  • У них может быть псевдоним оболочки frob='python foo.py', и на самом деле он запускал frob
  • Возможно, это даже git alias flab=!/usr/bin/foo.py, и они использовали git flab

В каждом случае я хотел бы, чтобы сообщение отражало, как пользователь вызывается этой командой, так что пример, который я предоставляю, имеет смысл.

sys.argv всегда содержит foo.py, а /proc/$$/cmdline не знает о псевдонимах. Мне кажется, что единственным возможным источником этой информации будет сам bash, но я не знаю, как его спросить.

Есть идеи?

ОБНОВЛЕНИЕ Как насчет того, ограничиваем ли мы возможные сценарии только перечисленными выше?

ОБНОВЛЕНИЕ 2: Множество людей написало очень хорошее объяснение, почему это невозможно в общем случае, поэтому я хотел бы ограничить свой вопрос:

При следующих предположениях:

  • Сценарий запускался интерактивно, начиная с bash
  • Сценарий запускался одним из следующих трех способов:
    1. foo <args> где foo - символическая ссылка /usr/bin/foo → foo.py
    2. git foo где alias.foo =!/usr/bin/foo in ~/.gitconfig
    3. git baz где alias.baz =!/usr/bin/foo in ~/.gitconfig

Есть ли способ отличить 1 и (2,3) от сценария? Есть ли способ различать 2 и 3 из сценария?

Я знаю, что это длинный выстрел, поэтому я сейчас принимаю ответ Чарльза Даффи.

ОБНОВЛЕНИЕ 3: До сих пор наиболее перспективный угол был предложен Чарльзом Даффи в комментариях ниже. Если я могу заставить своих пользователей иметь

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

в их .bashrc, то я могу использовать что-то вроде этого в моем коде:

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

Таким образом, я показываю общее сообщение, если мне не удается получить их метод вызова. Конечно, если я буду полагаться на изменение среды моих пользователей, я мог бы также гарантировать, что различные псевдонимы будут экспортировать собственные переменные среды, на которые я могу смотреть, но по крайней мере этот способ позволяет мне использовать ту же технику для любых другой скрипт, который я могу добавить позже.

4b9b3361

Ответ 1

Нет, нет способа увидеть исходный текст (перед псевдонимами/функциями/и т.д.).

Запуск программы в UNIX выполняется следующим образом на уровне базовой системы:

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

Примечательно, что есть три аргумента:

  • Путь к исполняемому файлу
  • Массив argv (первый элемент которого - argv[0] или $0 - передается этому исполняемому файлу, чтобы отразить имя, в котором он был запущен)
  • Список переменных среды

Нигде здесь нет строки, которая предоставляет исходную введенную пользователем команду оболочки, из которой было запрошено новое обращение к процессу. Это особенно верно, поскольку не все программы запускаются из оболочки вообще; рассмотрите случай, когда ваша программа запускается из другого сценария Python с shell=False.


В UNIX вполне условно предположить, что ваша программа была запущена через любое имя в argv[0]; это работает для символических ссылок.

Вы даже можете увидеть стандартные инструменты UNIX:

$ 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 

Если вы хотите создать командную строку UNIX, используйте pipes.quote() (Python 2) или shlex.quote() (Python 3), чтобы сделать это безопасно.

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

Опять же, это не будет "нерасширять" псевдонимы, вернется к коду, который был вызван, чтобы вызвать функцию, вызвавшую вашу команду, и т. Д.; в этом колокольчике нет никакого звонка.


Это можно использовать для поиска экземпляра git в родительском дереве процессов и поиска его списка аргументов:

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

Ответ 2

См. Примечание внизу относительно первоначально предложенного сценария оболочки.

Новый более гибкий подход заключается в том, чтобы скрипт python предоставил новую опцию командной строки, позволяющую пользователям указывать пользовательскую строку, которую они предпочли бы видеть в сообщениях об ошибках.

Например, если пользователь предпочитает вызывать скрипт python " myPyScript.py " через псевдоним, он может изменить определение псевдонима следующим образом:

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

к этому:

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

Если они предпочитают вызывать скрипт python из скрипта оболочки, он может использовать дополнительную опцию командной строки, например:

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

Другие возможные применения этого подхода:

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

  myPyScript.py --caller=myPyScript.py

Для перечисления расширенных командных строк, здесь есть скрипт " pyTest.py ", основанный на обратной связи @CharlesDuffy, который перечисляет cmdline для запущенного скрипта python, а также родительский процесс, который его породил. Если используется новый аргумент -caller, он появится в командной строке, хотя псевдонимы будут расширены и т.д.

#!/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

После сохранения этого файла с именем " pytest.py ", а затем вызова его из сценария bash с именем " pytest.sh " с различными аргументами, pytest.sh вывод:

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

ПРИМЕЧАНИЕ. Критика исходного сценария-оболочки aliasTest.sh была обоснованной. Хотя наличие предварительно определенного псевдонима является частью спецификации вопроса, и можно предположить, что он существует в пользовательской среде, предложение определило псевдоним (создавая ложное впечатление, что он был частью рекомендации, а не указанным часть пользовательской среды), и он не показывает, как оболочка будет взаимодействовать с вызываемым скриптом python. На практике пользователь должен был бы либо указать источник оболочки, либо определить псевдоним внутри оболочки, а сценарий python должен был бы делегировать печать сообщений об ошибках нескольким настраиваемым сценариям вызова (где находилась информация вызова), и клиенты могли вызывать сценарии оболочки. Решение этих проблем привело к более простому подходу, который можно распространить на любое количество дополнительных вариантов использования.

Вот менее запутанная версия оригинального скрипта, для справки:

#!/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

Вот вывод:

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

Ответ 3

Невозможно различать, когда интерпретатор для сценария явно указан в командной строке и когда он выводится ОС из строки hashbang.

Доказательство:

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

ps -o command $$

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

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

Это не позволит вам определить разницу между первыми двумя случаями в вашем списке.

Я также уверен, что нет разумного способа идентифицировать другие (опосредованные) способы вызова команды.

Ответ 4

Я вижу два способа сделать это:

  • Простейшим, как было предложено 3sky, было бы проанализировать командную строку внутри скрипта python. argparse можно использовать для этого надежным способом. Это работает, только если вы можете изменить этот скрипт.
  • Более сложным способом, немного более общим и вовлеченным, было бы изменение исполняемого файла python в вашей системе.

Поскольку первый вариант хорошо документирован, вот немного более подробная информация о втором:

Независимо от способа вызова вашего сценария python. Целью здесь является замена исполняемого файла python скриптом, который проверяет, является ли foo.py среди аргументов, и если это так, проверьте, есть ли --bar. Если нет, распечатайте сообщение и верните его.

В любом другом случае выполните реальный исполняемый файл python.

Теперь, надеюсь, запуск python выполняется через следующий shebang: #!/usr/bin/env python3 или через python foo.py, а не вариант #!/usr/bin/python или /usr/bin/python foo.py Таким образом, вы можете изменить $PATH и добавить каталог, в котором находится ваш ложный python.

В другом случае вы можете заменить /usr/bin/python executable, рискуя не играть с обновлениями.

Более сложный способ сделать это, вероятно, будет состоять из пространств имен и монтировок, но, возможно, этого достаточно, особенно если у вас есть права администратора.


Пример того, что может работать как скрипт:

#!/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]"

Однако, перечитав свой вопрос, очевидно, что я его неправильно понял. На мой взгляд, правильно напечатать либо "foo.py нужно вызвать как foo.py --bar", "добавьте строку в свои аргументы" или "попробуйте (вместо)", независимо от того, что введенный пользователем:

  • Если это псевдоним (git), это однократная ошибка, и пользователь попытается создать свой псевдоним после его создания, поэтому они знают, куда положить --bar часть
  • с помощью /usr/bin/foo.py или python foo.py:
    • Если пользователь не очень хорошо разбирается в командной строке, он может просто вставить рабочую команду, которая отображается, даже если они не знают разницы
    • Если они есть, они должны иметь возможность понять сообщение без проблем и настроить свою командную строку.

Ответ 5

Я знаю эту задачу bash, но я думаю, что самый простой способ - изменить "foo.py". Конечно, это зависит от уровня сложности скрипта, но, возможно, он подойдет. Вот пример кода:

#!/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'

В этом случае не имеет значения, как пользователь запускает этот код.

$ ./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