如何在Perl脚本中运行的Bash命令加载bash_profile?

4

I wrote simple command that lets me run the last N commands from terminal history. It looks like this: $> r 3 which will replay the last 3 commands.

I have the following alias in my bash profile: alias r="history -w; runlast $1"

And then the following simple perl script for the runlast command:

#!/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");
}

This works but my bash profile has aliases and other features (e.g. color output) I want the perl script to have access to. How do I load the bash profile for perl so it runs the bash commands with my bash profile? I read somewhere that if you "source the bash profile" for perl you can get it to work. So I tried adding source ~/.bash_profile; to my r command alias but that didn't have an effect. I'm not sure if I was doing that correctly, though.


3
在脚本中使用别名通常是一个不好的主意,因为这会导致行为随着运行环境的不同而变化,从而增加了维护的难度。最好使用函数代替。 - cdarke
1
在别名定义中的$1并不是你所希望的意思。无论如何,你真正需要知道的关于别名的事情就是,使用函数代替。 - tripleee
3个回答

3

system函数会创建一个非登录、非交互的shell过程,因此不会进行初始化操作,也无法使用别名。需要注意的是,所使用的shell一般是/bin/sh,虽然通常是bash,但不总是如此,因此需明确运行bash

为了避免这个问题,需要使用别名文件,但是根据bash手册的描述:

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

因此,需要执行shopt -s expand_aliases命令。但还有另一个问题:在同一物理行上,别名还没有可用;因此,在单行命令中无法起作用。

我还建议将别名放在.bashrc 文件中或在源文件中使用别名。

解决方案:

  • Add shopt -s expand_aliases to your ~/.bashrc, and before the aliases are defined (or the file with them sourced), and run bash as a login shell

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

    where -l is short for --login.

    In my tests the source ~/.bashrc wasn't needed; however, the man page says

    When bash is invoked as an interactive login shell, or as a non-interactive shell with the --login option, it first reads and executes commands from the file /etc/profile, if that file exists. After reading that file, it looks for ~/.bash_profile, ~/.bash_login, and ~/.profile, in that order, and reads and executes commands from the first one that exists and is readable.

    and goes on to specify that ~/.bashrc is read when an interactive shel that is not login runs. So I added explicit sourcing.

    In my tests sourcing .bashrc (with shopt added) while not running as a login shell didn't work, and I am not sure why.

    This is a little heavy-handed. Also, initialization may be undesirable to run from a script.

  • Source ~/.bashrc and issue shopt command, and then a newline before the command

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

    Really. It works.

最后,这必要吗?这样做容易出问题,而且可能有更好的设计。

其他评论。

  • The backticks (qx) is context-aware. If it's used in list context – its return assigned to an array, for example – then the command's output is returned as a list of lines. When you use it as the argument for split then it is in the scalar context though, when all output is returned in one string. Just drop split

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

    where I also use history N to get last N lines. In this case the newlines stay.

  • history N returns last N lines of history so there is no need to pipe to last

  • Regex substitution in a map can be done without changing the original

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

    With /r modifier the s/// operator returns the new string, not changing the original. This "non-destructive substitution" has been available since v5.14

  • No need to explicitly use $_ in the last grep, and no need for parenthesis in regex

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

    or

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

    where parens are now needed, but as it is only for grouping the ?: makes it not capture the match. I use [ ] to emphasize that that space is intended; this is not necessary.

    I also added ? to make space optional since history (and r?) may have no space.


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")" doesn't seem to work. I modified my perl script to return the commands separated by semicolons. The eval bash command returns -bash: 2: command not found - StevieD
1
@StevieD 感谢您的反馈,我直到现在才有机会测试代码。我去掉了对 eval 参数的引号(呵呵),并切换回 Perl 以进行 history 后处理,以实现可移植性和可预测性。 - tripleee
现在可以工作了。我的bash技能几乎为零,引用或不引用东西让我感到困惑。虽然我从未尝试过学习bash,但我想现在是时候了,因为我想更多地自动化事情。我不认为尽可能将事物保留在bash中有任何优势。Perl非常擅长这些任务,并且我认为它更易于阅读和维护。 - StevieD
第二段详细解释了为什么你需要在Bash中解决这个特定的问题。我完全同意现代脚本语言在许多方面都更受欢迎,尽管我现在更喜欢Python而不是Perl。 - tripleee
我明白你为什么需要使用bash来输出命令的结果。但是我认为在这种情况下,我不会尝试用bash来替换整个perl脚本。 - StevieD
显示剩余4条评论

1
你不能将Bash脚本源代码嵌入到Perl脚本中。必须由执行命令的shell加载bash_profile文件。当Perl运行system时,每次都会fork一个新的shell。
对于通过system运行的每个命令,您都必须加载bash_profile文件:
system('source ~/.bash_profile; ' + $cmd);

还有一件事,system会调用一个非交互式的shell。因此,除非你调用:

.bash_profile

你在.bash_profile中定义的Bash别名将不起作用。

shopt -s expand_aliases

在那个脚本里面。

谢谢。shopt在脚本中的哪个位置?在系统调用中吗? - StevieD
我尝试了 system('source ~/.bash_profile; shopt -s expand_aliases; ' . $cmd); 但是别名没有起作用。我可能做错了。我还在 .bash_profile 脚本中加入了 shopt - StevieD
1
真正的关键是“不要那样做”。 - tripleee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接