Reputation: 7443
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.
Upvotes: 4
Views: 3516
Reputation: 66899
The system forks a process in which it runs a shell, which is non-login and non-interactive; so no initialization is done and you get no aliases. Also note that the shell used is /bin/sh
, which is generally a link to another shell. This is often bash
but not always, so run bash
explicitly.
To circumvent this you need to source the file with aliases, but as bash
man page says
Aliases are not expanded when the shell is not interactive, unless the expand_aliases shell option is set using shopt (see the description of shopt under SHELL BUILTIN COMMANDS below).
Thus you need shopt -s expand_aliases
, as mentioned. But there is another screw: on that same physical line aliases are not yet available; so it won't work like this in a one-liner.
I'd also recommend to put aliases in .bashrc
, or in a separate file that is sourced.
Solutions
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.
Finally, is this necessary? It asks for trouble, and there is probably a better design.
Other comments
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.
Upvotes: 3
Reputation: 189648
The proper solution is to have your Perl script just print the commands, and make your current interactive shell eval
the string printed from your history. (I would probably get rid of Perl entirely but that's beside the point here.)
If the commands get evaluated in the current shell, you avoid many contextual problems which would be very hard or even intractable with system()
or generally anything involving a new process. For example, a subprocess cannot have access to non-exported variables in the current shell. var="foo", echo "$var"; r 1
is going to be very hard to solve correctly with your current approach. Using the current interactive shell will also naturally and easily solve the problems you were having with trying to get a noninteractive subshell act like an interactive one.
Aliases suck anyway, so let's redefine r
as a function:
r(){
history -w
eval $(printlast "$1")
}
... where refactoring runlast
into a different script printlast
is a trivial additional requirement. Or maybe just turn it into a (much simpler!) shell function:
printlast () {
history "$1" |
perl -ne 's/^\s*\d+\s+\*?//; print unless m/^(history|rm?)($|\s)'
}
With this, you can also get rid of history -w
from the r
definition.
Notice how we are using Perl where it is useful; but the main functionality makes sense to keep in the shell when you're dealing with the shell.
Upvotes: 2
Reputation: 43039
You can't source in a Bash script into a Perl script. The bash_profile has to be sourced in by the shell that executes the command. When Perl runs system
, it forks a new shell each time.
You have to source in the bash_profile for each command that you run through system
:
system('source ~/.bash_profile; ' + $cmd);
One more thing, system
invokes a non-interactive shell. So, your Bash aliases defined in .bash_profile won't work unless you invoke:
shopt -s expand_aliases
inside that script
Upvotes: 1