Get last executed command in bash - bash

I need to know what was the last command executed while setting my bash prompt in the function corresponding to PROMPT_COMMAND. I have code as follows
function bash_prompt_command () {
...
local last_cmd="$(history | tail -n 2 | head -n 1 | tr -s ' ' | cut -d ' ' -f3-)"
[[ ${last_cmd} =~ .*git\s+checkout.* ]] && ( ... )
...
}
Is there is faster(bash built-in way) to know the what was the command which invoked PROMPT_COMMAND.
I tried using BASH_COMMAND, but that too does not return the command which actually invoked PROMPT_COMMAND.

General case: Collecting all commands
You can use a DEBUG trap to store each command before it's run.
store_command() {
declare -g last_command current_command
last_command=$current_command
current_command=$BASH_COMMAND
return 0
}
trap store_command DEBUG
...and thereafter you can check "$last_command"
Special case: Only trying to shadow one (sub)command
If you only want to change how one command operates, you can just shadow that one command. For git checkout:
git() {
# if $1 is not checkout, just run real git and pretend we weren't here
[[ $1 = checkout ]] || { command git "$#"; return; }
# if $1 _is_ checkout, run real git and do our own thing
local rc=0
command git "$#" || rc=$?
ran_checkout=1 # ...put the extra code you want to run here...
return "$rc"
}
...potentially used from something like:
bash_prompt_command() {
if (( ran_checkout )); then
ran_checkout=0
: "do special thing here"
else
: "do other thing here"
fi
}

Related

Why does the exit status check in my Bash prompt function also evaluate to true?

I'm new to the bash commands and the functions. I have two functions and trying to display them in my terminal
git_branch()
and
highlightExitCode()
Here I want to change prompt setting by export PS1, with these two functions. I'm able to get branch name and also the emoji from highlightExitCode() but the emoji displayed is always from the else and never executes if part.
Can someone let me know what part I'm doing wrong here.
export PS1='[\#][\u] [\W]$(git_branch) $(highlightExitCode)\$ '
git_branch()
{
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1)/'
}
highlightExitCode()
{
exit_code=$?
if [ $exit_code -ne 0 ]
then
echo -en '\xf0\x9f\x98\xb1 '
else
echo -en '\xf0\x9f\x98\x80 '
fi
}
highlightExitCode is getting the exit status of the last command run, which is git_branch while constructing the value of the prompt.
Use PROMPT_COMMAND instead to build a prompt dynamically. In your .bashrc file,
PROMPT_COMMAND=build_prompt
git_branch()
{
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1)/'
}
highlightExitCode()
{
if [ "$1" -ne 0 ]; then
echo -en '\xf0\x9f\x98\xb1 '
else
echo -en '\xf0\x9f\x98\x80 '
fi
}
build_prompt () {
last_exit=$?
PS1='[\#][\u] [\W]'
PS1+=$(git_branch)
PS1+=" $(highlightExitCode "$last_exit")"
PS1+='\$ '
}
$? is the exit status of the "most recently executed foreground pipeline". Since you always (successfully) run git_branch right before highlightExitCode, $? is always 0. The behaviour you want is roughly as follows:
$ exitcode() { (($? == 0)) && echo ":)" || echo ":("; }
$ PS1='$(exitcode) \$ '
:) $ false
:( $ true
:) $
but with an additional command as part of PS1.
To achieve that, you can do the following:
Create these three functions, for example as part of your ~/.bashrc:
git_branch() {
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1)/'
}
smiley() {
(($1 == 0)) && echo ':)' || echo ':('
}
set_prompt() {
local exitstatus=$?
PS1='$(git_branch) '"$(smiley "$exitstatus")"' \$ '
}
Set the PROMPT_COMMAND environment variable to set_prompt (probably also in ~/.bashrc)
Now you have a prompt that behaves as follows:
(master) :) $ false
(master) :( $ true
(master) :) $
The main trick is to store the exit status that determines the smiley behaviour before doing anything else.
As an aside, a cleaner way of getting the name of the current branch is
git symbolic-ref -q --short HEAD 2>/dev/null

"alias method chain" in Bash or Zsh

This is (or was, at least) a common pattern in Ruby, but I can't figure out how to do it in Zsh or Bash.
Let's suppose I have a shell function called "whoosiwhatsit", and I want to override it in a specific project, while still keeping the original available under a different name.
If I didn't know better, I might try creating an alias to point to whoosiwhatsit, and then create a new "whoosiwhatsit" function that uses the alias. Of course that work, since the alias will refer to the new function instead.
Is there any way to accomplish what I'm talking about?
Aliases are pretty weak. You can do this with functions though. Consider the following tools:
#!/usr/bin/env bash
PS4=':${#FUNCNAME[#]}:${BASH_SOURCE}:$LINENO+'
rename_function() {
local orig_definition new_definition new_name retval
retval=$1; shift
orig_definition=$(declare -f "$1") || return 1
new_name="${1}_"
while declare -f "$new_name" >/dev/null 2>&1; do
new_name+="_"
done
new_definition=${orig_definition/"$1"/"$new_name"}
eval "$new_definition" || return
unset -f "$orig_definition"
printf -v "$retval" %s "$new_name"
}
# usage: shadow_function target_name shadowing_func [...]
# ...replaces target_name with a function which will call:
# shadowing_func target_renamed_to_this number_of_args_in_[...] [...] "$#"
shadow_function() {
local shadowed_func eval_code shadowed_name shadowing_func shadowed_func_renamed
shadowed_name=$1; shift
shadowing_func=$1; shift
rename_function shadowed_func_renamed "$shadowed_name" || return
if (( $# )); then printf -v const_args '%q ' "$#"; else const_args=''; fi
printf -v eval_code '%q() { %q %q %s "$#"; }' \
"$shadowed_name" "$shadowing_func" "$shadowed_func_renamed" "$# $const_args"
eval "$eval_code"
}
...and the following example application of those tools:
whoosiwhatsit() { echo "This is the original implementation"; }
override_in_directory() {
local shadowed_func=$1; shift
local override_cmd_len=$1; shift
local override_dir=$1; shift
local -a override_cmd=( )
local i
for (( i=1; i<override_cmd_len; i++)); do : "$1"
override_cmd+=( "$1" ); shift
done
: PWD="$PWD" override_dir="$override_dir" shadowed_func="$shadowed_func"
: override_args "${override_args[#]}"
if [[ $PWD = $override_dir || $PWD = $override_dir/* ]]; then
[[ $- = *x* ]] && declare -f shadowed_func >&2 # if in debugging mode
"${override_cmd[#]}"
else
"$shadowed_func" "$#"
fi
}
ask_the_user_first() {
local shadowed_func=$1; shift;
shift # ignore static-argument-count parameter
if [[ -t 0 ]]; then
read -r -p "Press ctrl+c if you are unsure, or enter if you are"
fi
"$shadowed_func" "$#"
}
shadow_function whoosiwhatsit ask_the_user_first
shadow_function whoosiwhatsit \
override_in_directory /tmp echo "Not in the /tmp!!!"
shadow_function whoosiwhatsit \
override_in_directory /home echo "Don't try this at home"
The end result is a whoosiwhatsit function that asks the user before it does anything when its stdin is a TTY, and aborts (with different messages) when run under either /tmp or /home.
That said, I don't condone this practice. Consider the above provided as an intellectual exercise. :)
In bash, there is a built-in variable called BASH_ALIASES that is an associative array containing the current aliases. The semantics are a bit inconsistent when you update it (RTM!) but if you restrict yourself to reading BASH_ALIASES, you should be able to write yourself a shell function that implements alias chaining.
It's common and well supported to create a single level of overrides through functions that optionally invoke their overridden builtin or command:
# Make all cd commands auto-exit on failure
cd() { builtin cd "$#" || exit; }
# Make all ssh commands verbose
ssh() { command ssh -vv "$#"; }
It doesn't chain beyond the one link, but it's completely POSIX and often works better in practice than trying to write Ruby in Bash.

Symfony based autocomplete breaks SCP autocomplete

I am using the PHP tools http://robo.li and n98-magerun.phar - both are based on Sympfony's CLI components.
When I use such a autocomplete script:
https://gist.github.com/caseyfw/51bdbcb37e5dfb91b74e
#!/bin/sh
function __robo_list_cmds ()
{
robo list --raw | awk '{print $1}' | sort
}
function __robo_list_opts ()
{
robo list --no-ansi | sed -e '1,/Options:/d' -e '/^$/,$d' -e 's/^ *//' -e 's/ .*//' | sort
}
_robo()
{
local cur="${COMP_WORDS[COMP_CWORD]}"
COMPREPLY=($(compgen -W "$(__robo_list_opts) $(__robo_list_cmds)" -- ${cur}))
return 0;
}
complete -o default -F _robo robo
COMP_WORDBREAKS=${COMP_WORDBREAKS//:}
It breaks the autocomplete of the scp command (which usually completes files on a remote server - but with this - unrelated - robo completion in place, scp removes the host name from the command)
Why is that? How to fix?
EDIT
based on the answer, the fixed version is here:
https://gist.github.com/amenk/d68f1fe54b156952ced621f771ff48ba
This has probably nothing to do with your robo compspec. Nor with the _scp completion function associated with scp.
It is probably due to your COMP_WORDBREAKS=${COMP_WORDBREAKS//:}.
You removed : from the list of separators. Apparently _scp is robust enough to behave the same with or without : as a word separator. It returns the same list of candidate completions. But the token that gets substituted when _scp returns only one candidate for the completion of, e.g. scp host:public_ht, is host:public_ht, instead of just public_ht. Proof:
$ _foobar () { COMPREPLY=bazcux; return 0; }
$ complete -o default -F _foobar foobar
$ echo $COMP_WORDBREAKS
"'><=;|&(:
If you try to complete foobar host:public_ht, you get foobar host:bazcux because the substituted token is just public_ht. While with:
$ COMP_WORDBREAKS=${COMP_WORDBREAKS//:}
$ echo $COMP_WORDBREAKS
"'><=;|&(
if you try to complete foobar host:public_ht, you get foobar bazcux because it is the complete host:public_ht that is replaced by bazcux.
The solution to your problem is probably to adapt your _robo completion function such that it does not require that : is not a word separator. Something like:
_stem () {
local lcur lprev
lcur="$cur"
stem="$lcur"
for (( i = cword - 1; i >= 0; i -= 1 )); do
lprev="${words[i]}"
[[ $lcur == ":" ]] && [[ $lprev == ":" ]] && break
[[ $lcur != ":" ]] && [[ $lprev != ":" ]] && break
stem="$lprev$stem"
lcur="$lprev"
done
}
_robo () {
local cur prev words cword
_init_completion || return
local stem options
options=($(__robo_list_opts) $(__robo_list_cmds))
COMPREPLY=()
_stem
COMPREPLY=($(compgen -W '${options[#]}' -- "$stem"))
[[ $stem =~ : ]] && stem=${stem%:*}: && COMPREPLY=(${COMPREPLY[#]#"$stem"})
return 0
}
complete -o default -F _robo robo
A much (apparently) simpler solution consists in replacing the _stem function above by the existing __reassemble_comp_words_by_ref function of the bash_completion library:
_robo () {
local cur prev words cword
_init_completion || return
__reassemble_comp_words_by_ref ":" words cword
options=($(__robo_list_opts) $(__robo_list_cmds))
COMPREPLY=($(compgen -W '${options[#]}' -- "$cur"))
return 0
}
complete -o default -F _robo robo
All this is probably not exactly what you want. I do not know robo.il and there are probably many improvements that would take more context into account to propose specific completions. But it may be a starting point.

bash script not running properly

When I run this by its self in the command line it seems to work fine, but when I have another script execute this, it doesn't work. Any ideas? I'm guessing it has to do with quotes, but not sure.
#!/bin/sh
#Required csvquote from https://github.com/dbro/csvquote
#TODO: Clean CSV File using CSVFix
#Version 3
echo "File Name: $1"
function quit {
echo "Quitting Script"
exit 1
}
function fileExists {
if [ ! -f "$1" ]
then
echo "File $1 does not exists"
quit
fi
}
function getInfo {
#Returns website url like: "http://www.website.com/info"
#Reads last line of a csv file, and gets the 2nd item.
RETURN=$(tail -n 1 $1 | csvquote | cut -d ',' -f 2 | csvquote -u)
echo $RETURN
}
function work {
CURLURL="http://127.0.0.1:9200/cj/_query"
URL=$(getInfo)
echo "URL: $URL"
CURLDATA='{ "query" : { "match" : { "PROGRAMURL" : '$URL' } } }'
#URL shows up as blank...???
echo "Curl Data: $CURLDATA"
RESPONSE=$(curl -XDELETE "$CURLURL" -d "$CURLDATA" -vn)
echo $RESPONSE
echo "Sleeping Allowing Time To Delete"
sleep 5s
}
fileExists $1
work $1
I cant see why a simpler version wont work: functions are useful, but I think there are too many, overcomplicating things, if what you are posting is the entirety of your script (in my opinion)
Your script is doing things using a broken lucky pattern: $1 variables are also arguments to shell functions as well as the main script. Think of them as local variables to a function. So when you are calling $(getInfo) it is calling that function with no argument, so actually runs tail -n 1 which falls back to stdin, which you are specifying to work as < $1. You could see this for yourself by putting echo getInfo_arg_1="$1" >&2 inside the function...
Note also you are not quoting $1 anywhere, this script is not whitespace in file safe, although this is only more likely to be a problem if you are having to deal with files sent to you from a Windows computer.
In the absence of other information, the following 'should' work:
#!/bin/bash
test -z "$1" && { echo "Please specify a file." ; exit 1; }
test -f "$1" || { echo "Cant see file '$1'." ; exit 1; }
FILE="$1"
function getInfo() {
#Returns website url like: "http://www.website.com/info"
#Reads last line of a csv file, and gets the 2nd item.
tail -n 1 "$1" | csvquote | cut -d ',' -f 2 | csvquote -u
}
CURLURL="http://127.0.0.1:9200/cj/_query"
URL=$(getInfo "$FILE")
echo "URL: $URL"
CURLDATA='{ "query" : { "match" : { "PROGRAMURL" : '$URL' } } }'
curl -XDELETE "$CURLURL" -d "$CURLDATA" -vn
echo "Sleeping Allowing Time To Delete"
sleep 5s
If it still fails you really need to post your error messages.
One other thing, especially if you are calling this from another script, chmod +x the script so you can run it without having to invoke it with bash directly. If you want to turn on debugging then put set -x near the start somewhere.

SSH commands via bash script

I've been trying several fails to perform the following:
Basically, what I need is to execute several sequenced commands on a remote unix shell, such as setting environment variables with variables that I have on the script, move to a particular directory and run a script there and so on.
I've tried using a printf with the portion of the script and then piped the ssh command, but it didn't work quite well, also, I've read about the "ssh ... >> END" marker, which is great but since I'm using functions, it doesn't work well.
Do you have any thoughts?
Here's an excerpt of the code:
deployApp() {
inputLine=$1;
APP_SPECIFIC_DEPLOY_SCRIPT="$(echo $inputLine | cut -d ' ' -s -f1)";
BRANCH="$(echo $inputLine | cut -d ' ' -s -f2)";
JBOSS_HOME="$(echo $inputLine | cut -d ' ' -s -f3)";
BASE_PORT="$(echo $inputLine | cut -d ' ' -s -f4)";
JAVA_HOME_FOR_JBOSS="$(echo $inputLine | cut -d ' ' -s -f5)";
JAVA_HEAP="$(echo $inputLine | cut -d ' ' -s -f6)";
echo "DEPLOYING $APP_SPECIFIC_DEPLOY_SCRIPT"
echo "FROM BRANCH $BRANCH"
echo "IN JBOSS $JBOSS_HOME"
echo "WITH BASE PORT $BASE_PORT"
echo "USING $JAVA_HOME_FOR_JBOSS"
if [[ -n "$JAVA_HEAP" ]]; then
echo "WITH $JAVA_HEAP"
fi
echo
echo "Exporting jboss to $JBOSS_HOME"
ssh me#$SERVER <<END
cleanup() {
rm -f $JBOSS_SERVER/log/*.log
rm -Rf $JBOSS_SERVER/deploy/
rm -Rf $JBOSS_SERVER/tmp/
mkdir $JBOSS_SERVER/deploy
}
startJboss() {
cd $JBOSS_SERVER/bin
./jbossctl.sh start
return 0;
}
export JBOSS_HOME
export JBOSS_SERVER=$JBOSS_HOME/server/default
END
return 0;
}
With that "HERE" approach, I'm getting this error: "syntax error: unexpected end of file"
Thanks a lot in advance!
Just put the functions in your here document, too:
var="Hello World"
ssh user#host <<END
x() {
print "x function with args=$*"
}
x "$var"
END
[EDIT] Some comments:
You say "export JBOSS_HOME" but you never define a value for the variable in the here document. You should use export JBOSS_HOME="$JBOSS_HOME". BASH will take all text between the two END, replace all variables, and send the result to SSH for processing.
That also means the other side will see rm -f /path/to/jboss/server/*.log; the assignment to JBOSS_SERVER in the last line of the here document has no effect (at least not to the code in cleanup()).
If you want to pass $ unmodified to the remote server, you have to escape it with \: rm -f \$JBOSS_SERVER/log/*.log
You never call cleanup()
There is a } missing after return 0 to finish the definition of deployapp()
There may be other problems as well. Run the script with bash -x to see what the shell actually executes. You can also add echo commands in the here document to see what the values of the variables are or you can add set -x before cleanup() to get the same output as with bash -x but from the remote side.
I don't understand why you're using cut to split the arguments to your function. Just do
APP_SPECIFIC_DEPLOY_SCRIPT=$1
BRANCH=$2
JBOSS_HOME=$3
# etc.
If you don't quote your here document delimiter, the contents are expanded before they're sent to the server. That may be what you want. If you don't and you want all expansion to be done on the server side, then quote it like this:
ssh me#$SERVER <<'END'
# etc.
END
If you wan't a mixture, don't quote the delimiter, but do escape those things that you want delayed expansion for:
ssh me#$SERVER <<END
echo $EXPAND_ME_NOW \$EXPAND_ME_LATER
END
What are the export statements supposed to do? I can't see that they would have any effect at all.

Resources