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

Posted

技术标签:

【中文标题】有没有办法知道用户如何从 bash 调用程序?【英文标题】:Is there a way to know how the user invoked a program from bash? 【发布时间】:2018-07-12 09:07:47 【问题描述】:

问题是:我有这个脚本foo.py,如果用户在没有--bar 选项的情况下调用它,我想显示以下错误消息:

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

现在,棘手的部分是用户可能会通过多种方式调用命令:

他们可能使用了python foo.py,就像示例中一样 他们可能使用过/usr/bin/foo.py 他们可能有一个shell别名frob='python foo.py',实际上运行frob 也许它甚至是一个git别名flab=!/usr/bin/foo.py,而他们使用git flab

在每种情况下,我都希望消息能够反映用户如何调用命令,以便我提供的示例有意义。

sys.argv 总是包含foo.py,而/proc/$$/cmdline 不知道别名。在我看来,此信息的唯一可能来源是 bash 本身,但我不知道如何询问。

有什么想法吗?

更新如果我们将可能的场景限制在上面列出的那些呢?

更新 2:很多人写了很好的解释说明为什么在一般情况下这是不可能的,所以我想把我的问题限制在这个:

在以下假设下:

脚本以交互方式从 bash 启动 脚本以以下三种方式之一之一启动:
    foo <args> 其中 foo 是符号链接 /usr/bin/foo -> foo.py git foo where alias.foo=!/usr/bin/foo in ~/.gitconfig git baz 其中 alias.baz=!/usr/bin/foo 在 ~/.gitconfig

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

我知道这是一个很长的机会,所以我现在接受 Charles Duffy 的回答。

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

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

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

【问题讨论】:

也许history 1 就够了吗? 如果--bar 是强制性的,那么总是自己在内部添加它,并为不想要它但知道如何添加参数的更熟练的用户提供--no-bar 怎么样?跨度> @MarkSetchell 我的概括性尝试在这里适得其反。我当前的用例是关于一个退出的脚本,但想告诉用户他们可以继续使用--continue。有点像git rebase 遇到冲突时。所以你的想法对我不起作用。 而不是在如何找到调用的名称上浪费很多周期(恕我直言,有很多极端情况会失败)——为什么不处理给用户的消息? Please add the --bar option to your command, like so: 'cmd --bar' 大多数人都很聪明(再说一遍,恕我直言)知道cmd 是他们输入的任何内容的填写。 请注意,可能使 shell 从 DEBUG 陷阱中将 BASH_COMMAND 的副本导出到环境中,但如果您依赖它,您的程序只有在被如此准备好的 shell 调用时才会有手头的行为......所以它看起来几乎没有用。 【参考方案1】:

不,没有办法看到原文(在别名/函数/等之前)。

在 UNIX 中启动程序是在底层系统调用级别完成的:

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

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

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

这里没有一个字符串提供原始用户输入的 shell 命令,从该命令请求新进程的调用。 尤其如此,因为并非所有程序都从 shell 启动;考虑一下你的程序是从另一个 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

【讨论】:

@philwalk,如果我正确理解了您的建议(从脚本中检索历史记录),它对运行时配置非常敏感——许多 shell 配置不会将历史记录写入文件直到完全退出。如果您对用户的 shell 配置进行了足够的控制以确保它能够正常工作,那么您可以让他们安装一个 DEBUG 陷阱来导出 $BASH_COMMAND 的副本并完成它。 @philwalk, ...此外,说我“提供一些行不通的方法作为证据”是完全不真实的——我提供了一种适用于 git 别名情况的方法(find_git_parent 函数),以及一种适用于符号链接情况的方法(sys.argv[0],正如我所演示的,它是标准 UNIX 工具使用的方法)。那些允许不依赖于shell运行时配置细节的解决方案的情况。 我指的是您答案的标题“无法查看原始文本(在别名/函数/等之前)。”。我的解决方案就是这样做的。 @philwalk,嗯?这不是“解决方案”; OP 想要从 Python 解释器中检索该文本。您只能从运行命令的同一 shell 中检索它——甚至不能从解释脚本的子 shell 中检索它,而无需交互式父 shell 的积极参与。 @philwalk, ...当您的 aliasTest 脚本可以通过别名可靠地判断 if 本身是否被调用 时,不需要执行该调用的 shell 具有任何非- 事先默认配置,那是让我印象深刻的时候——而不是之前。【参考方案2】:

请参阅底部关于最初提议的包装脚本的注释。

一种新的更灵活的方法是让 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”的文件中,然后从名为“pytest.sh”的 bash 脚本中使用各种参数调用它后,输出如下:

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

【讨论】:

我正在制作一个包含您反馈的版本,谢谢! 我对使用 bash 脚本包装 python 脚本的想法感到非常兴奋,但我发现除非我让用户始终获取它,否则 bash 脚本不会看到其父级的历史记录,我不认为是一个可行的选择。看来 Charles Duffy 关于使用 DEBUG 的建议是最有希望的角度。 @philwalk 它在 aliasTest.sh 中工作,因为您使用来自调用命令的同一 bash 进程中的 history 命令。无法查明aliasTest.sh 脚本本身是如何被调用的。 我看不出通过包装器调用如何解决问题——包装器无法访问其父进程的历史记录,只能访问它自己的历史记录,除非用户的 shell 有一些非常具体的配置(在每个命令上立即将历史刷新到磁盘,这 不是默认行为)。但是,是的,请进行编辑以进行演示。 ...回到正题,但是:如果你打算修改调用shell,你可以修改它来设置$0,因此不需要一个额外的命令行参数。例如:myAlias() exec -a myAlias myPyScript "$@"; 将在 $0 中调用 myPyScriptmyAlias【参考方案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

这会阻止您检测列表中前两个案例之间的差异。

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

【讨论】:

我同意不可能可靠地检索原始 shell 命令行,但我不同意您在这里证明了这一点。 /proc/self 中的数据比 ps 中的数据要好得多。例如,cmdline_for_pid() local -a args; local arg; args=( ); while IFS= read -r -d '' arg; do args+=( "$arg" ); done </proc/"$1"/cmdline; printf '%q ' "$args[@]"; echo; 定义了一个 cmdline_for_pid 函数,它提供了比 ps 更好的列表。 @CharlesDuffy 对于我的测试用例cmdline_for_pidps -o command 没有什么不同 使用一个更有趣的测试用例——一个带有空格、引号等的测试用例。 @CharlesDuffy 我知道区别。我只是想指出,为了说明我的陈述 cmdline_for_pid 并没有添加任何有用的东西,同时要求读者破译证明。 那么,我理解“证明”一词的意思与您的意图不同。当我读到它时,只有当数据以完整和规范的形式反映时,才能证明哪些数据可用或不可用;有损翻译并不能证明什么可以已被一个损失较小的过程检索到任何有用的信息,因此也不能证明哪些数据实际上可以检索或不可以检索。 【参考方案4】:

我可以看到两种方法:

如 3sky 所建议的,最简单的方法是从 python 脚本中解析命令行。 argparse 可用于以可靠的方式执行此操作。这仅在您可以更改该脚本时才有效。 一种更复杂、更通用、更复杂的方法是更改​​系统上的 python 可执行文件。

由于第一个选项有据可查,这里有更多关于第二个选项的详细信息:

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

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

现在,希望通过以下 shebang 运行 python:#!/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 "$@"
    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 must be called like foo.py --bar”、“please add bar to your arguments”或“please try (instead of )”都可以,不管是什么用户输入:

如果这是一个 (git) 别名,这是一次性错误,用户将在创建别名后尝试使用别名,因此他们知道将 --bar 部分放在哪里 使用/usr/bin/foo.pypython foo.py: 如果用户不是真正精通命令行,他们可以只粘贴显示的工作命令,即使他们不知道其中的区别 如果是,他们应该能够毫无困难地理解消息,并调整他们的命令行。

【讨论】:

"$@",而不是$@。不带引号的$@ 的行为与不带引号的$* 相同——也就是说,您的参数都被字符串拆分并扩展为glob。 exit -1 没有意义——UNIX 退出状态是一个单字节无符号(正)整数。而[ 并没有保证== 会起作用——唯一的POSIX-standardized 字符串比较运算符是=。而x 在你引用你的扩展并且没有使用标记为过时的test 模式时是不必要的/毫无意义的(-a-o 引入了语法歧义,但这里不存在这些歧义)。跨度> 是的,这是作为示例快速组合起来的,并没有经过全面测试;我想与x 保持安全。你是对的,我会将$@ 都更改为"$@"。符号位使返回值 255,但我可以将其更改为 1,因为它实际上并不重要。然而,这个答案实际上是题外话,因为它没有回答 OP 的担忧。【参考方案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

【讨论】:

您的“答案”不会以任何方式回答 OP 的问题。当未指定 --bar 选项时,您的解决方案总是产生相同的消息,而 OP 需要根据使用的实际命令不同的消息。

以上是关于有没有办法知道用户如何从 bash 调用程序?的主要内容,如果未能解决你的问题,请参考以下文章

当用户在 android 应用程序中执行某个操作时,有没有办法知道所有函数被调用?

如何从需要SUDO的php调用shell脚本?

有没有办法从linux内核模块调用用户空间函数?

如何从 .gitconfig 别名调用外部脚本?

如何从需要 SUDO 的 php 调用 shell 脚本?

如何从 Bash 中的文件调用多个命令行参数?