有没有办法知道用户如何从bash调用程序?

时间:2018-07-12 09:07:47

标签: linux bash command-line-interface

这是问题所在:我有这个脚本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别名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,其中~/.gitconfig中的alias.foo =!/ usr / bin / foo
    3. git baz,其中~/.gitconfig中的alias.baz =!/ usr / bin / foo

有没有办法从脚本中区分1和(2,3)?有没有办法从脚本中区分2和3?

我知道这是一个远景,所以我现在接受Charles Duffy的回答。

更新3 :到目前为止,最有希望的角度是Charles Duffy在下面的评论中提出的。如果我可以让我的用户拥有

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

这样,如果我无法获取其调用方法,则会显示一般消息。当然,如果我要依赖于更改用户的环境,则最好确保各种别名导出自己可以查看的环境变量,但是至少这样可以允许我对任何其他对象使用相同的技术其他脚本,我稍后可能会添加。

5 个答案:

答案 0 :(得分:16)

否,无法看到原始文本(在别名/函数/等之前)。

在底层syscall级别上,按照以下步骤在UNIX中启动程序:

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

值得注意的是,有三个参数:

  • 可执行文件的路径
  • 一个argv数组(其中的第一项-argv[0]$0-传递到该可执行文件以反映启动它的名称)
  • 环境变量列表

这里没有任何地方提供一个字符串,该字符串提供了原始用户输入的shell命令,从中请求了新进程的调用。 尤其如此,因为并非所有程序都是从外壳启动的;考虑您的程序是使用shell=False从另一个Python脚本启动的情况。


在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

答案 1 :(得分:4)

有关最初建议的包装器脚本,请参见底部的注释。

一种新的更灵活的方法是让python脚本提供一个新的命令行选项,允许用户指定他们希望在错误消息中看到的自定义字符串。

例如,如果用户希望通过别名调用python脚本“ myPyScript.py”,则可以从中更改别名定义:

  alias myAlias='myPyScript.py $@'

对此:

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

如果他们希望从shell脚本中调用python脚本,则可以使用其他命令行选项,如下所示:

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

此方法的其他可能应用:

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

  myPyScript.py --caller=myPyScript.py

要列出扩展的命令行,下面是一个脚本'pyTest.py',该脚本基于@CharlesDuffy的反馈,其中列出了正在运行的python脚本的cmdline以及生成该脚本的父进程。 如果使用了新的-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 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 $@
_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]

答案 2 :(得分: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

这可防止您检测到列表中前两种情况之间的差异。

我也相信,没有合理的方法来识别调用命令的其他(中介)方法。

答案 3 :(得分:0)

我可以看到两种方法:

  • 3sky建议,最简单的方法是从python脚本内部解析命令行。 argparse可用于可靠地这样做。仅当您可以更改该脚本时,此方法才有效。
  • 更通用,更复杂的方式是更改系统上的python可执行文件。

由于第一个选项已被详细记录,因此以下是有关第二个选项的更多详细信息:

无论调用脚本的方式如何,python都会运行。这里的目标是用一个脚本替换python可执行文件,该脚本检查foo.py是否在参数中,如果存在,则检查--bar是否也在参数中。如果没有,请打印消息并返回。

在其他所有情况下,都执行真正的python可执行文件。

现在,希望可以通过以下#!/usr/bin/env python3python foo.py来运行python,而不是#!/usr/bin/python/usr/bin/python foo.py的变体。这样,您可以更改$PATH变量,并在错误的python所在的目录之前添加一个目录。

在另一种情况下,您可以替换/usr/bin/python executable,以免对更新不满意。

更复杂的方法可能是使用名称空间和挂载,但是以上内容可能就足够了,尤其是如果您具有管理员权限。


可以用作脚本的示例:

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

但是,重新阅读您的问题后,很明显我误解了它。我认为,无论打印了什么,都可以只打印“ foo.py必须像foo.py --bar一样被调用”,“请在您的参数中添加bar”或“请尝试(而不是)”。用户输入:

  • 如果这是(git)别名,则这是一次错误,用户将在创建别名后尝试使用其别名,以便他们知道将--bar部分放在哪里
  • /usr/bin/foo.pypython foo.py一起使用:
    • 如果用户不是非常了解命令行,即使他们不知道区别,他们也可以粘贴显示的有效命令
    • 如果是的话,他们应该能够轻松理解消息并调整命令行。

答案 4 :(得分:-1)

我知道这是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