Get name of last run program in Bash - bash

I have a bash script where I trap errors using the trap command, and I would like to be able to print the name of the last command (the one that failed)
#!/bin/bash
function error
{
# echo program name
}
trap error ERR
# Some commands ...
/bin/false foo # For testing
I'm not sure what to put in the error function. I tried echo $_ but that only works if the command has no arguments. I also tried with !! but that gives me "!!: command not found". At an interactive prompt (also bash) I get:
$ /bin/false foo
$ !!
/bin/false foo
which seems to be pretty much what I want. Why the difference?
What is the easiest way to get the name of the previous command inside a script?

Try echo $BASH_COMMAND in your trap function.
From man bash:
BASH_COMMAND
The command currently being executed or about to be executed,
unless the shell is executing a command as the result of a trap,
in which case it is the command executing at the time of the
trap.

You need to set
set -o history
to quote bash manual page:
When the -o history option to the set builtin is enabled, the shell provides access to the command history, the list of commands previously typed. The value of the HISTSIZE variable is used
as the number of commands to save in a history list. The text of the last HISTSIZE commands (default 500) is saved. The shell stores each command in the history list prior to parameter and
variable expansion (see EXPANSION above) but after history expansion is performed, subject to the values of the shell variables HISTIGNORE and HISTCONTROL.
In general, read the HISTORY and HISTORY EXPANSION sections in bash man page.

Related

bash `set -e` reset inside functions when run via `$(...)`?

It seems that the set -e option in Bash gets reset inside of functions, when those functions are invoked via a $(...) expansion.
This surprises me, and I'm not sure if it is a bug or not.
I have not been able to find a description of this behavior in the (usually quite thorough) Bash manpage.
Note: here are some other similar SO posts:
Bash functions ignore set -e
Using set -e / set +e in bash with functions
But neither of them deals with $(...), which is not really discussed in the manpage either.
I also cannot find reference to this issue in the excellent Bash FAQ 105.
Here is a small program to demonstrate the issue:
echo "Initial: $-"
set -eu
echo "After set: $-"
function foo() {
echo "Inside foo: $-"
}
foo
function bar() {
false # I'd expect this to immediately fail
echo "Inside bar: $-"
}
# When a $(...) construct is involved, 'bar' runs to completion!
x=$(bar)
echo "We should never get here ... but we do."
echo "$x"
For me, on Bash version 5.0.11(0)-release, I get the following output:
Initial: hB
After set: ehuB
Inside foo: ehuB
We should never get here ... but we do.
Inside bar: huB
So, as you can see, the -u option does get "passed through" to the function in all cases. And the -e option gets passed through when the function is called normally. But only in the special case of $(bar) does the -e option get reset.
Does anyone know if this is documented behavior, or otherwise explainable?
It makes no sense to me (:
The behaviour of set -e in conjunction with Command Substitution is documented in
Command Execution Environment:
Subshells spawned to execute command substitutions inherit the value of the -e option from the parent shell. When not in POSIX mode, Bash clears the -e option in such subshells.
That seems to say that the behaviour you see is expected — unless you're running in POSIX mode, the -e option is unset in command substitution subshells in Bash (even though the -e setting is initially inherited, it is changed soon after the subshell commences execution). It's a funny way of writing it, though.
The relevant man page quotes for you, first from the Command Substiution section.
Command substitution allows the output of a command to replace the command name. There are two forms:
$(command)
or
`command`
Bash performs the expansion by executing command in a subshell environment and replacing the command substitution with the standard output of the command, with any trailing newlines deleted.
And from Command Execution Environment:
Subshells spawned to execute command substitutions inherit the value of the -e option from the parent shell. When not in posix mode, bash clears the -e option in such subshells.
So bar get executed in a subshell, and since your are not in posix mode, the -e option gets cleared.
Add set -o posix to the start of you script and it will behave as expected, although expect other differences when using this mode.

How do you load bash_profile for bash commands run from perl script?

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

What does a Bash command do to the file typed in afterwards?

I just learned that the bash command opens up a new Bash shell inside of whatever shell you're using, and uses the profile of .bashrc for its commands.
When I was installing Laravel earlier this week, I used a bash init.sh command. Now I'm wondering, what exactly did that bash init.sh command do? Why did I need to open a new shell to... execute or open whatever was in init.sh?
Quoting man bash*:
ARGUMENTS
If arguments remain after option processing, and neither the -c nor the -s option has been supplied, the first argument is assumed to be the name of a file containing shell commands. If bash is invoked in this fashion, $0 is set to the name of the file, and the positional parameters are set to the remaining arguments. Bash reads and executes commands from this file, then exits. Bash's exit status is the exit status of the last command executed in the script. If no commands are executed, the exit status is 0. An attempt is first made to open the file in the current directory, and, if no file is found, then the shell searches the directories in PATH for the script.
In other words: bash <file> executes <file> as a Bash script. You will not get a new interactive shell.
Note that this is not the only way to specify which shell (or indeed any command) should be used to execute a script: It's common to use a shebang for that, so that the script can be ‘executed’ by itself.
* The one I had at hand, of Bash v4.3.42.

fish: command substitution issue with interactive command

I'm trying to set "fzf - Fuzzy finder for your shell" for the fish shell. The problem is that interactive commands don't work when I use it in command substitution. Example:
This command works: (echoes all the files in the current dir, and I can interactively select one by fuzzy-finder)
ls | fzf
But this one doesn't work:
echo (ls | fzf)
It just immediately returns empty string.
It doesn't work for any interactive command, so if you haven't fzf, you can test it with, say, off the top of my head, chsh:
This command works: (asks for password)
chsh
but this one doesn't: (immediately returns empty string)
echo (chsh)
More, when I try to exit fish, it says that "there are stopped jobs", i.e. interactive command starts and immediately stops.
How to make it work?
This is probably just a bug. See https://github.com/fish-shell/fish-shell/issues/1362 (as discussed on the mailing list)

Output redirection using a bash variable

I would like to do something along these lines, but most combinations I have tried have failed:
export no_error="2 > /dev/null"
./some_command $no_error
To run that command and redirect the output using a variable instead of typing the command. How would I go about doing this?
The shell doesn't re-evaluate your no_error variable when you use it like that. It just gets passed to ./some_command as a command-line argument. You can get the behaviour you want by using eval. From the bash manual:
eval [arguments]
The arguments are concatenated together into a single command, which is then read and executed, and its exit status returned as the exit status of eval. If there are no arguments or only empty arguments, the return status is zero.
Here's an example for your case:
export no_error="2>/dev/null"
eval ./some_command $no_error
Note that you can't have a space between the 2 and the >. I'm guessing that's just a typo in your question, though.

Resources