I'm trying to solve a very mundane problem. I want PS1 to change depending upon the previous command executed. Whether success or failure isn't the issue here. I want PS1 to include \w, but only if the last command entered was cd. What I have at the moment is:
if [[ !:0 == "cd" ]]
then
PS1='(\w)[jobs: \j] > '
else
PS1='[jobs: \j] > '
The output will always be the shorter one, regardless of the last command.
I feel like I'm making a simple mistake somewhere, and this also seems mundane enough that I can't find anything related through Google.
Put this in .bashrc:
PROMPT_COMMAND='
if [[ "$NEWPWD" != "$PWD" ]]; then
PS1="(\w)[jobs: \j] > "
NEWPWD=$PWD
else
PS1="[jobs: \j] > "
fi'
You can use whichever name you want for $NEWPWD
It's simple, it works, and is not prone to errors.
The Csh-style !:0 history expansion is an interactive feature. You can use the command history -p "!:0" to execute it in a script context, though (even when you have set +H, like most sane people have); but executing it inside PROMPT_COMMAND or the prompt itself is highly unwieldy. (When I tried, it would show me the penultimate command, or something from within the PROMPT_COMMAND scriptlet itself.)
Borrowing from https://stackoverflow.com/a/6110446/874188 (currently the accepted answer to Echoing the last command run in Bash?) I would go with
trap 'prompt_previous_command=$prompt_this_command; prompt_this_command=$BASH_COMMAND' DEBUG
PS1='$([[ ${prompt_previous_command%%\ *} == "cd" ]] && echo "(${PWD/$HOME/~})")[jobs: \j] \> '
It is unfortunate that echo "\\w" doesn't produce the expanded value in this context; ${PWD/$HOME/~} is a reasonable approximation, although there are corner cases where it gets it wrong.
... Perhaps a less confusing approach is to set the value in the trap already:
trap 'prompt_previous_command=$prompt_this_command
prompt_this_command=$BASH_COMMAND
[[ "${prompt_previous_command%%\ *}" == "cd" ]] &&
prompt_cwd="(\\w)" || prompt_cwd=""
PS1="$prompt_cwd[jobs: \\j] \\> "' DEBUG
Many Bash add-ons want to hook into your PROMPT_COMMAND and might sabotage any attempt to reserve it for youself; of course, this approach has a similar problem if you have something else in your system which relies on the DEBUG trap for something.
To make this work for pushd / popd and aliases etc too, here's an adaptation of Dan's excellent answer:
trap 'case ${prompt_prev_pwd-$PWD} in
"$PWD") PS1="[jobs \\j] > ";;
*) PS1="(\\w)[jobs: \\j] > ";;
esac
prompt_prev_pwd=$PWD' DEBUG
On approach is to create a function and parse history. The PROMPT_COMMAND is also needed.
Put the code below in your ~/.bashrc file or put it in another file, just make sure you source that file from ~/.bashrc
is_cd(){
set -- $(history 1)
if [[ $2 == "cd" ]]; then
echo cd_is_the_last_command
else
echo no_cd
fi
}
if [[ $PROMPT_COMMAND != *is_cd* ]]; then
PROMPT_COMMAND="is_cd"
fi
Change the lines with echo's with the actual command you want to execute.
Source ~/.bashrc after you have edited it.
This assumes that the output of your history has the numeric number first and command as the second column.
Related
#!/bin/bash
set -exuo pipefail
# Run delorean to update the namespaces folder
main() {
if [ !$(yq -r '.random' file_that_doesnt_exist.yaml) = "true" ]; then
echo "yes"
else
echo "no"
fi
}
# shellcheck disable=SC2068
main $#
set -e pipefail based on my understanding should exit the bash script on the first occurence of error. However, I get "no" in stdout even though `echo "no" occurs after the error. How does that happen?
set -e stops the script on error, but not always:
The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test following the if or elif reserved words, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command's return value is being inverted with !.
(Quote from man bash).
Basically, don't use -e (or at least don't only use that.)
Use a trap, and carefully design your tests with it in mind.
c.f. this whole page for good discussions on alternatives and explanations, and this one for some other examples and elaborations that should give you good inspiration for a better solution.
To check the validity of lines in a file I'm using a condition which is met when egrep -v does NOT return an empty result. When there are invalid lines, then this works fine (i.e. the conditional block is executed), but when every line is valid then the script ends without further processing.
Script:
INVALID_HOSTS=$(egrep -v ${IP_REGEX} hosts)
if [[ ! -z "${INVALID_HOSTS}" ]]; then
echo "Invalid hosts:"
for entry in ${INVALID_HOSTS}
do echo ${entry}
done
exit_with_error_msg "hosts file contains invalid hosts (Pattern must be: \"\d+.\d+.\d+.\d+:\d+\"), exiting"
else
echo "all cool"
fi
echo "after if-else"
So when there are no invalid lines then neither the echo "all cool" nor echo "after if-else" get executed. The script just stops and returns to the shell.
When set -x is enabled, then it prints:
++ egrep -v '^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):([1-9]|[1-5]?[0-9]{2,4}|6[1-4][0-9]{3}|65[1-4][0-9]{2}|655[1-2][0-9]|6553[1-5])$' hosts
+ INVALID_HOSTS=
Playing around with it I'm sure that it's about the if [[ ! -z "${INVALID_HOSTS}" ]]; then, but my bash wizardry is not strong enough to overcome this magical barrier.
Thanks for any help!
This is a bit long for a comment. I'll start it as an answer and we can work our way through further details or I can scrap it entirely if not helpful. I'll make some assumptions and let us see if it hits the spot.
For starters, you do indeed use the value further, so command expansion into a variable is not entirely useless, but otherwise it's much easier to determine match (or lack thereof) of grep through it's return value. If anything matched (output would be non-empty), it returns (shell true) value of 0, otherwise it returns false (in this case 1). Not to mention the ! -z test notation should really be -n if used at all.
And this is where I'd start assuming a bit. I suspect this is not your entire script and you have errexit option turned on in that shell session (or through rc file in general). Either by means of set -o errexit or set -e or running bash with -e option. Since grep not matching anything returns as failed, your shell (script execution) would terminate after having encountered a failing command.
Observe the difference between:
$ bash -ec 'grep "BOGUS" /etc/fstab ; echo "$?"'
$ bash -c 'grep "BOGUS" /etc/fstab ; echo "$?"'
1
With errexit, bash terminates after grep has "failed" and we never even reach the echo.
Since the assumption has proven to be correct, small extension. If errexit is what you want, you'd need to either change the option value before/after a command you want to be able to fail (return non-zero value) without affecting your script:
set +o errexit
grep THIS_COULD_NOT_MATCH...
set -o errexit
Or you can ignore return value of individual commands by ensuring their success:
grep THIS_COULD_NOT_MATCH... || true
You can also still use potentially "failing" commands safely in conditionals (such as if) without terminating your shell.
man bash describes a very useful Event Designator
^string1^string2^
Quick substitution. Repeat the last command, replacing string1 with string2. Equivalent to ''!!:s/string1/string2/''
Is there a way to execute !!:gs/string1/string2/ when typing in #string1#string2# on the command line to replace all occurrences in the previous command? (# or any other designated character/string)
^string1^string2^:g&
See question Replace all occurrences of a word in the last command.
See Modifiers in History Expansion.
Shortly: no!
In fact, there may exist a way, using `trap "..." debug'...
Something like:
trap 'if [[ $BASH_COMMAND =~ ^#(.*)#(.*)#$ ]] ;then
BASH_LAST=${BASH_LAST//${BASH_REMATCH[1]}/${BASH_REMATCH[2]}};
$BASH_LAST;
unset BASH_COMMAND;
else BASH_LAST=$BASH_COMMAND;
fi;
' debug
This is quick and dirty, there left an execution error, but I think: It may be a way to do it.
Edit 01
This is a little better, but history stay quite wrong:
shopt -s extdebug
trap '
if [[ $BASH_COMMAND =~ ^#(.*)#(.*)#$ ]] ;then
BASH_LAST="${BASH_LAST//${BASH_REMATCH[1]}/${BASH_REMATCH[2]}}"
$BASH_LAST
false
else
BASH_LAST="$BASH_COMMAND"
fi' debug
But warn: I do this for fun, for playing with bash and to understand how it work... This is not intended to be used in effective final solution!!
I'm learning shell scripting, and am finding it hard finding a good way to learn. I have created a script below which lets the user search various different Internet engines through options. I would be really grateful if someone could look through this and point out what I'm doing wrong, how to improve it, etc.
#!/bin/bash
## Get user search-engine option
while getopts aegwy: OPTIONS ; do
case "$OPTIONS" in
a) ENGINE="http://www.amazon.com/s/ref=nb_sb_noss/?field-keywords";;
e) ENGINE="http://www.ebay.com/sch/i.html?_nkw";;
g) ENGINE="http://www.google.com/search?q";;
w) ENGINE="http://en.wikipedia.org/wiki/?search";;
y) ENGINE="http://www.youtube.com/results?search_query";;
?) ERRORS=true;;
esac
done &>/dev/null
## Ensure correct command usage
[ $# -ne 2 ] || [ $ERRORS ] && printf "USAGE: $(basename $0) [-a Amazon] [-e eBay] [-g Google] [-w Wikipedia] [-y YouTube] \"search query\"\n" && exit 1
## Ensure user is connected to the Internet
ping -c 1 209.85.147.103 &>/dev/null ; [ $? -eq 2 ] && printf "You are not connected to the Internet!\n" && exit 1
## Reformat the search query
QUERY=`printf "$2" | sed 's/ /+/g'`
## Execute the search and exit program
which open &>/dev/null ; [ $? -eq 0 ] && open "$ENGINE"="$QUERY" &>/dev/null && exit 0 || xdg-open "$ENGINE"="$QUERY" &>/dev/null && exit 0 || printf "Command failed!\n" && exit 1
Thanks in advance everyone, means a lot!
Best posted in codereviews, as indicated above, but here are some mostly stylistic comments. I should stress that the script is pretty much fine as-is; these are just minor improvements that I think will help make the code easier to read/maintain, more robust in a couple cases, etc.
You don't need to use all-caps for variable names just because environment variables are all-caps; shell variables and environment variables aren't the same thing.
Since your $OPTIONS variable only holds one option at a time, a singular name would be better (e.g. $option). Or you could go with $opt, which is somewhat traditional here.
The : in your getopts string (aegwy:) indicates that the -y option expects an argument, as in -ysomething instead of just -y by itself. Since you aren't doing anything with $OPTARG, I'm guessing that's not intentional.
As others have said, an if/then/elif/else would probably be clearer than the chain of && and ||.
The test [ $ERRORS ] is somewhat unclear because it can mean a lot of different things depending on the content of the $ERRORS parameter. A more explicit indication that you only care about whether or not it's set would be [ -n "$ERRORS" ].
Comparisons like [ -ne ] and friends are mostly holdovers from before the shell had built-in integer arithmetic; the more modern idiom would be (( $# != 2 )).
Your usage message implies that the -a, -e, -g, -w, and -y options take arguments of the form Amazon, eBay, Google, etc. It would be clearer what the actual syntax of the command is without those additions; you can include an extra paragraph in the help text listing what each option stands for.
As a rule, error messages should go to stderr instead of stdout (>&2).
It's fine to use basename $0 for consistency of output, but there's something to be said for leaving $0 alone as it will reflect however the user actually invoked the command. Something to consider.
Not much point in using printf if you're not using a format string; just use echo, which automatically appends the newline. Usage messages traditionally don't include quotation marks, either; it's up to the user to quote the arg or not depending on whether it's needed.
Checking a command for success is exactly how if works, so there's no need to do explicit checks of $? unless you really care about the exact exit value. In the case of your connectivity ping, you probably don't care about why it failed, only that it did:
if ! ping -c 1 209.85.147.103 >/dev/null; then
echo >&2 "$0: You are not connected to the Internet!"
exit 1
fi
Your search query reformat might need to do more than just turn spaces into plus signs; what if it has an ampersand? But if you're just doing the spaces-to-pluses thing, you could use bash parameter expansion do it without sed: QUERY="${QUERY// /+}"
If your program relies on open/xdg-open etc, you should probably check for its availability at the top; no sense doing anything else if you know you can't possibly perform the requested operation anyway. And you can use a variable so you don't wind up repeating yourself in multiple clauses:
open=
for cmd in open xdg-open; do
if type -p "$cmd" >/dev/null; then
open="$cmd"
break
fi
done
if [ -z "$open" ]; then
echo >&2 "$0: open command not found."
exit 1
fi
And then later you can finish up with just this one line:
"$open" "$ENGINE=$QUERY" &>/dev/null
http://linuxcommand.org/ is an excellent resource to improve your bash scripting skills.
http://tldp.org/LDP/abs/html/ is another great document.
Hope this helps.
is it possible to assign variable inside if conditional in bash 4? ie. in the function below I want to assign output of executing cmd to output and check whether it is an empty string - both inside test conditional. The function should output
"command returned: bar"
myfunc() {
local cmd="echo bar"
local output=
while [[ -z output=`$cmd` ]];
do
#cmd is failing so far, wait and try again
sleep 5
done
# great success
echo "command returned: $output"
}
why the above?
i prefer to run scripts with 'set -e' - which will cause script to terminate on first non-0 return/exit code that's not in an if/loop conditional.
with that in mind, imagine cmd is an unstable command that may exit with > 1 from time to time, and I want to keep calling it until it succeeds and i get some output.
You can try something like this:
myfunc() {
local cmd="echo bar"
local output=
while ! output=$($cmd) || [[ -z output ]];
do
#cmd is failing so far, wait and try again
sleep 5
done
# great success
echo "command returned: $output"
}
Note that it is strongly recommended to avoid the use of set -e.
I don't think you would be able to do it in your conditional
As yi_H pointed out, the if is equivalent to
if [[ ! -z output=bar ]];
which in turn is basically
if [[ ! -z "output=bar" ]];
So, all you are checking is if the string "output=bar" is empty or not...
So, output=bar could actually be anything like !##!#%=== and it would still do the same thing (that is, the expression isn't evaluated). You might have to assign the variable in a subshell somehow, but I'm not sure that would work.
Since assignment won't work there, you need some workaroudn.
You could temporary do a set +e...
You could use this way ...
$cmd
exit_status=$?
while [[ $exit_status -gt 0 ]];
do
#cmd is failing so far, wait and try again
sleep 5
$cmd
exit_status=$?
done
EDIT: This won't work with 'set -e' or other way around, don't use 'set -e' to begin with.