【问题标题】:How do you load bash_profile for bash commands run from perl script?如何为从 perl 脚本运行的 bash 命令加载 bash_profile?
【发布时间】:2018-08-19 04:05:09
【问题描述】:

我编写了简单的命令,让我可以运行终端历史记录中的最后 N 个命令。它看起来像这样:$> r 3,它将重播最后 3 个命令。

我的 bash 个人资料中有以下别名: alias r="history -w; runlast $1"

然后是 runlast 命令的以下简单 perl 脚本:

#!/usr/bin/env perl
use strict;
use warnings;

my $lines = $ARGV[0] || exit;

my @last_commands = split /\n/, 
    `bash -ic 'set -o history; history' | tail -$lines`;

@last_commands = 
    grep { $_ !~ /(^r |^history |^rm )/ } 
    map { local $_ = $_; s/^\s+\d+\s+//; $_ } 
    @last_commands;

foreach my $cmd (@last_commands) {
  system("$cmd");
}

这可行,但我的 bash 配置文件有别名和其他功能(例如颜色输出),我希望 perl 脚本能够访问。如何为 perl 加载 bash 配置文件,以便它使用我的 bash 配置文件运行 bash 命令?我在某处读到,如果你为 perl “获取 bash 配置文件”,你可以让它工作。所以我尝试将source ~/.bash_profile; 添加到我的r 命令别名中,但这没有效果。不过,我不确定我这样做是否正确。

【问题讨论】:

标签: bash perl


【解决方案1】:

system 派生了一个进程,在该进程中运行一个非登录和非交互的 shell;所以没有初始化完成,你没有别名。另请注意,使用的 shell 是/bin/sh,它通常是指向另一个 shell 的链接。这通常是bash,但并非总是如此,因此请明确运行bash

要避免这种情况,您需要使用别名来获取文件,但正如 bash 手册页所说

当 shell 不是交互式时,别名不会展开,除非使用 shopt 设置了 expand_aliases shell 选项(请参阅下面的 SHELL BUILTIN COMMANDS 下的 shopt 描述)。

因此,如上所述,您需要shopt -s expand_aliases。但是还有另一个问题:在同一条物理线路上,别名尚不可用;所以它不会像这样在单行中工作。

我还建议将别名放在 .bashrc 中,或者放在一个单独的文件中。

解决方案

  • shopt -s expand_aliases 添加到您的~/.bashrc,并且在定义别名之前(或将它们作为来源的文件),然后将bash 作为登录shell 运行

    system('/bin/bash', '-cl', 'source ~/.bashrc; command');
    

    其中-l--login 的缩写。

    在我的测试中,source ~/.bashrc 是不需要的;但是,手册页说

    当 bash 作为交互式登录 shell 或作为带有 --login 选项的非交互式 shell 调用时,它首先从文件 /etc/profile 中读取并执行命令(如果该文件存在)。读取该文件后,它会按顺序查找~/.bash_profile~/.bash_login~/.profile,并从第一个存在且可读的文件中读取并执行命令。

    并继续指定在非登录的交互式外壳运行时读取~/.bashrc。所以我添加了显式采购。

    在我的测试中,采购.bashrc(添加了shopt)而不是作为登录外壳运行时不起作用,我不知道为什么。

    这有点粗暴。此外,从脚本运行初始化可能是不可取的。

  • 输入~/.bashrc 并发出shopt 命令,然后在命令前换行

    system('/bin/bash', '-c', 
        'source ~/.bashrc; shopt -s expand_aliases\ncommand');
    

    真的。它有效。

最后,这有必要吗?它自找麻烦,而且可能有更好的设计。

其他cmets

  • 反引号 (qx) 是 context-aware。如果它在列表上下文中使用——例如,它的返回分配给一个数组——那么命令的输出将作为行列表返回。当您将它用作split 的参数时,它位于标量上下文中,此时所有输出都以一个字符串形式返回。直接删除split

    my @last_commands = `bash -ic 'set -o history; history $lines`;
    

    我还使用history N 来获取最后的N 行。在这种情况下,换行符会保留。

  • history N 返回最后的 N 历史行,因此无需通过管道传输到 last

  • map 中的正则表达式替换可以在不更改原始内容的情况下完成

    map { s/^\s+\d+\s+//r } @last_commands;
    

    使用/r 修饰符s/// 运算符返回新字符串,而不更改原始字符串。这个“无损替换”已经available since v5.14

  • 最后一个grep不需要显式使用$_,正则表达式不需要括号

    grep { not /^r |^history |^rm ?/ } ...
    

    grep { not /^(?:r|history|rm)[ ]?/ } ...
    

    现在需要括号,但它仅用于对?: 进行分组,使其无法捕获匹配项。我使用[ ] 强调该空间是有意的;这不是必需的。

    我还添加了? 以使空间可选,因为history(和r?)可能没有空间。

【讨论】:

    【解决方案2】:

    正确的解决方案是让您的 Perl 脚本只打印命令,并使您当前的交互式 shell eval 成为从您的历史记录中打印的字符串。 (我可能会完全摆脱 Perl,但这不是重点。)

    如果在当前 shell 中对命令进行评估,您可以避免许多上下文问题,这些问题对于 system() 或通常涉及新进程的任何事情来说都是非常困难甚至难以处理的。例如,子进程无法访问当前 shell 中未导出的变量。 var="foo", echo "$var"; r 1 将很难用您当前的方法正确解决。使用当前的交互式 shell 也会自然而轻松地解决您在尝试让非交互式子 shell 像交互式子 shell 时遇到的问题。

    反正别名很烂,所以让我们将r 重新定义为一个函数:

    r(){
        history -w
        eval $(printlast "$1")
    }
    

    ... 将runlast 重构为不同的脚本printlast 是一个微不足道的附加要求。或者也许只是把它变成一个(更简单!)shell函数:

    printlast () {
        history "$1" |
        perl -ne 's/^\s*\d+\s+\*?//; print unless m/^(history|rm?)($|\s)'
    }
    

    这样,您还可以从r 定义中删除history -w

    注意我们如何在有用的地方使用 Perl;但是在处理 shell 时,将主要功能保留在 shell 中是有意义的。

    【讨论】:

    • ` eval "$(printlast "$1")"` 似乎不起作用。我修改了我的 perl 脚本以返回用分号分隔的命令。 eval bash 命令返回-bash: 2: command not found
    • @StevieD 感谢您的反馈,直到现在我才有机会测试代码。我删除了 eval (duh) 参数周围的引号,并切换回 Perl 进行 history 后处理以实现可移植性和可预测性。
    • 现在工作。我的 bash 技能不存在,引用或不引用东西让我很困惑。不过,我从未尝试过学习 bash。猜猜是时候了,因为我想做更多的事情来自动化。不过,我认为尽可能多地使用 bash 并没有什么好处。 Perl 非常擅长这类任务,而且我认为它更具可读性和更易于维护。
    • 第二段详细解释了为什么您需要在 Bash 中解决这个特殊问题。我完全同意现代脚本语言在许多事情上更受欢迎,尽管我现在更喜欢 Python 而不是 Perl。
    • 我明白为什么需要 bash 来输出命令的结果。我只是说在这种情况下我不会尝试用 bash 替换整个 perl 脚本。
    【解决方案3】:

    您不能将 Bash 脚本中的源代码转换为 Perl 脚本。 bash_profile 必须由执行命令的 shell 提供。当 Perl 运行 system 时,它每次都会派生一个新的 shell。

    您必须在 bash_profile 中为您通过system 运行的每个命令提供源代码:

    system('source ~/.bash_profile; ' + $cmd);
    

    还有一点,system 调用了一个非交互式 shell。因此,除非您调用,否则您在 .bash_profile 中定义的 Bash 别名将不起作用:

    shopt -s expand_aliases
    

    在那个脚本里面

    【讨论】:

    • 谢谢。 shopt 在脚本中的哪个位置?在系统调用中?
    • 我试过 system('source ~/.bash_profile; shopt -s expand_aliases; ' . $cmd); 但别名不起作用。我可能做错了。我还将 shopt 放入 .bash_profile 脚本中。
    • 真正的牛肉是“不要那样做”。
    猜你喜欢
    • 2012-08-29
    • 2013-07-18
    • 2011-06-04
    • 1970-01-01
    • 1970-01-01
    • 2016-02-20
    • 2013-12-25
    • 2015-01-09
    • 1970-01-01
    相关资源
    最近更新 更多