The first thing in my bashrc file is this expression:
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac
Can somebody explain what this means?
all these symbols make it really hard to google, and there is no Haskell "hoogle" equivalent for bash so I can search for symbol expressions.
The intended behavior seems to be similar to this.
nonsourced=0;
# if sourced,
if [[ "$0" == "$BASH_SOURCE" ]]; then
nonsourced=1;
else
nonsourced=0;
fi
echo "nonsourced? $nonsourced";
case $- in
*i*)
# this case is entered if "$-" contains "i".
if [[ "$nonsourced" -eq "0" ]]; then
echo "1. " "$-";
fi
;; # leave case?
*) # this case is entered in all other cases.
if [[ "$nonsourced" -eq "0" ]]; then
echo "2. " "$-";
return
else
# cannot return from nonsourced, use exit.
echo "avoided return from nonsourced #2";
exit 0;
fi
;; # leave case?
esac
echo "3";
Can somebody explain what this means?
$- The list of options set in the shell at the point of evaluation.
When a shell (bash) starts it accepts some options:
LESS=+'/^ *OPTIONS' man bash
All of the single-character shell options documented in the description of the set builtin command can be used as options when the shell is invoked. In addition, bash interprets the following options when it is invoked:
One of such options is -i. So calling bash as bash -i … should [a] trigger that option inside[a] the shell.
[a] I say should as some other conditions are also required to have an effective interactive shell. Also, an interactive shell may be started by simply writing bash in a terminal (no -i option used)
[b] The way to print some options that have been set is with echo $-
*i*) ;; tests if the string from $- contains an i, if so, do nothing.
*) return;; On any other value of $- return (go out the script[c] ).
[c] Please read this answer for return vs. exit.
In overall, it does what the comment says:
# If not running interactively, don't do anything
Or with a clearer wording:
# If running interactively, exit[d].
[d] It may be more technically correct to use the word return instead of exit, but the idea is cleaner, I believe.
Note that there is a quite similar construct with $PS1 (used in /etc/bash.bashrc and repeated in ~/.bashrc in debian based systems, for example):
# If not running interactively, don't do anything
[ -z "$PS1" ] && return
As for the problem of finding symbols:
> all these symbols make it really hard to google
even if it doesn't cover so many pages, SymbolHound may be of help here.
If we try it
we find this
Which clearly explains what you are asking.
See the documentation for Bash variables:
$-: A hyphen expands to the current option flags as specified upon invocation, by the set built-in command, or those set by the shell itself (such as the -i).
The asterisks in the case patterns are wildcards, so essentially the whole case says “if there’s an i [as interactive] somewhere in the arguments, go on, otherwise return”.
$- lists the current shell options.
The two cases are whether the -i interactive flag is present anywhere in that list of options.
Related
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.
I want to detect if either no arguments or an invalid argument is passed and print a help message. A separate check for an empty argument is possible, but not so elegant.
My bash script looks like this:
COMMAND="$1"
shift
case "$COMMAND" in
loop)
loop_
;;
...
*)
echo $"Usage: $0 {loop|...}"
exit 1
esac
When no arguments are passed, nothing executes; if I pass "" then the proper case is triggered. If I use $1 directly instead of using the temporary variable, then it works as expected.
I've even tried adding a specific case for "") but to no avail.
The only way your case statement isn't going to match with no $1 given is if it isn't entered in the first place.
Consider the following:
#!/usr/bin/env bash
set -e
command=$1
shift
case $command in
*) echo "Default case was entered";;
esac
This emits no output when $1 is unset -- but not because anything wrong with the case statement.
Rather, the issue is that shift exits with a nonzero exit status when there's nothing available to shift, and the set -e causes the script as a whole to exit on that failure.
First Moral Of This Story: Don't Use set -e (or #!/bin/bash -e)
See BashFAQ #105 for an extended discussion -- or the exercises included therein if in a hurry. set -e is wildly incompatible between different "POSIX-compliant" shells, and thus makes behavior hard to predict. Manual error handling may not be fun, but it's much more reliable.
Second: Consider A Usage Function
This gives you a terse way to have your usage message in one place, and re-use it where necessary (for example, if you don't have a $1 to shift):
#!/usr/bin/env bash
usage() { echo "Usage: $0 {loop|...}" >&2; exit 1; }
command=$1
shift || usage
case $command in
*) usage ;;
esac
Because of the || usage, the exit status of shift is considered "checked", so even if you do run your script with set -e, it will no longer constitute a fatal error.
Alternately, Mark The shift As Checked Explicitly
Similarly:
shift ||:
...will run shift, but then fall back to running : (a synonym for true, which historically/conventionally implies placeholder use) should shift fail, similarly preventing set -e from triggering.
Aside: Use Lower-Case Names For Your Own Variables
POSIX specifies that the shell (and other tools to which the standards applies) have their behavior modified only by environment variables with all-caps names:
Environment variable names used by the utilities in the Shell and Utilities volume of POSIX.1-2017 consist solely of uppercase letters, digits, and the ( '_' ) from the characters defined in Portable Character Set and do not begin with a digit. Other characters may be permitted by an implementation; applications shall tolerate the presence of such names. Uppercase and lowercase letters shall retain their unique identities and shall not be folded together. The name space of environment variable names containing lowercase letters is reserved for applications. Applications can define any environment variables with names from this name space without modifying the behavior of the standard utilities.
This applies even to regular, non-exported shell variables because specifying a shell variable with the same name as an environment variable overwrites the latter.
BASH_COMMAND, for example, has a distinct meaning in bash -- and thus can be set to a non-empty value at the front of your script. There's nothing stopping COMMAND from similarly being meaningful to, and already used by, a POSIX-compliant shell interpreter.
If you want to avoid side effects from cases where your shell has set a built-in variable with a name your script uses, or where your script accidentally overwrites a variable meaningful to the shell, stick to lowercase or mixed-case names when writing scripts for POSIX-compliant shells.
The easiest way to solve your problem without altering your general pattern is to use “advanced” parameter expansion features from the Bourne shell (this is not bash-specific). In this case, we can use the :- modifier to supply default values:
COMMAND="${1:-triggerusagemessage}"
shift
case "$COMMAND" in
loop)
loop_
;;
...
triggerusagemessage)
echo $"Usage: $0 {loop|...}"
exit 64
;;
esac
See the paragraph “Parameter Expansion” in the man page of your shell for a short presentation of the available parameter expansion modifiers.
(Note the exit code 64, which is reserved for this case on some operating systems.)
You can simply use $#. It represents the number of given arguments:
if [ $# = 0 ]
then
echo "help ..."
fi
in the line: echo $"Usage: $0 {loop|...}" what's the first $ for?
If you don't want to repeat the message, just put it in a function and check for an empty string before the case statement.
#! /bin/bash
die()
{
"Usage: $0 {loop|...}"
exit 1
}
COMMAND="$1"
[ -z $COMMAND ] && die
shift
case "$COMMAND" in
loop)
loop_
;;
*)
die
exit 1
;;
esac
Is it possible to implement a boolean cli option using getopts in bash? Basically I want to do one thing if -x is specified and another if it is not.
Of course it is possible. #JonathanLeffler already pretty much gave the answer in the comments to the question, so all I'm going to do here is add an example of the implementation and a few niceties to consider:
#!/usr/bin/env bash
# Initialise option flag with a false value
OPT_X='false'
# Process all options supplied on the command line
while getopts ':x' 'OPTKEY'; do
case ${OPTKEY} in
'x')
# Update the value of the option x flag we defined above
OPT_X='true'
;;
'?')
echo "INVALID OPTION -- ${OPTARG}" >&2
exit 1
;;
':')
echo "MISSING ARGUMENT for option -- ${OPTARG}" >&2
exit 1
;;
*)
echo "UNIMPLEMENTED OPTION -- ${OPTKEY}" >&2
exit 1
;;
esac
done
# [optional] Remove all options processed by getopts.
shift $(( OPTIND - 1 ))
[[ "${1}" == "--" ]] && shift
# "do one thing if -x is specified and another if it is not"
if ${OPT_X}; then
echo "Option x was supplied on the command line"
else
echo "Option x was not supplied on the command line"
fi
A few notes about the above example:
true and false are used as option x indicators because both are valid UNIX commands. This makes the test for the option presence more readable, in my opinion.
getopts is configured to run in silent error reporting mode because it suppressed default error messages and allows for a more precise error handling.
the example includes fragments of code for dealing with missing option arguments and post-getopts command line arguments. These are not part of the OP's question.
They are added for the sake of completeness as this code will be required in any reasonably complex script.
For more information about getopts see Bash Hackers Wiki: Small getopts tutorial
I was trying to modify the bd script to use getopts. I am a newbie at bash scripting
my script is
while getopts ":hvis:d:" opt
do
...
done
...
echo $somedirpath
cd "$somedirpath"
this runs fine when doing
$ ./bd -v -i -s search
or
$ ./bd -is search -d dir
But when running it like this
$ . ./bd -s search
getopts doesn't read the arguments at all. And all the variables I set in the while loop according to the arguments are all not set, so the script no longer works. Please help!
Setting OPTIND=1 before invoking getopts works fine.
The problem is that getopts relies on OPTIND to loop through the arguments provided, and after sourcing the script, it will be set to some value greater than 1 by getopts according to how many arguments you pass. This value gets carried over even after the script ends(because its being sourced). So the next time its sourced, getopts will pick up from that OPTIND, rather than starting from 1!
This might cause strange behaviour with other scripts, and I don't know how safe this is. But it works!
For a better workaround, I think what #tripleee suggests looks safe and robust.
When you source a script, the arguments parsed by getopts are those of the current shell, not the parameters on the source command line.
The common workaround is to have your script merely print the path, and invoke it like cd "$(bd)" instead (perhaps indirectly through a function or alias).
Setting OPTIND=1 may not work reliably on zsh. Try to use something different than getopts:
while [ "$#" -gt 0 ]
do
case "$1" in
-h|--help)
help
return 0
;;
-o|--option)
option
return 0
;;
-*)
echo "Invalid option '$1'. Use -h|--help to see the valid options" >&2
return 1
;;
*)
echo "Invalid option '$1'. Use -h|--help to see the valid options" >&2
return 1
;;
esac
shift
done
Is it possible to pass command line arguments into a function from within a bourne script, in order to allow getopts to process them.
The rest of my script is nicely packed into functions, but it's starting to look like I'll have to move the argument processing into the main logic.
The following is how it's written now, but it doesn't work:
processArgs()
{
while getopts j:f: arg
do
echo "${arg} -- ${OPTARG}"
case "${arg}" in
j) if [ -z "${filename}" ]; then
job_number=$OPTARG
else
echo "Filename ${filename} already set."
echo "Job number ${OPTARG} will be ignored.
fi;;
f) if [ -z "${job_number}" ]; then
filename=$OPTARG
else
echo "Job number ${job_number} already set."
echo "Filename ${OPTARG} will be ignored."
fi;;
esac
done
}
doStuff1
processArgs
doStuff2
Is it possible to maybe define the function in a way that it can read the scripts args? Can this be done some other way? I like the functionality of getopts, but it looks like in this case I'm going to have to sacrifice the beauty of the code to get it.
You can provide args to getopts after the variable. The default is $#, but that's also what shell functions use to represent their arguments. Solution is to pass "$#" — representing all the script's command-line arguments as individual strings — to processArgs:
processArgs "$#"
Adding that to your script (and fixing the quoting in line 11), and trying out some gibberish test args:
$ ./try -j asdf -f fooo -fasdfasdf -j424pyagnasd
j -- asdf
f -- fooo
Job number asdf already set.
Filename fooo will be ignored.
f -- asdfasdf
Job number asdf already set.
Filename asdfasdf will be ignored.
j -- 424pyagnasd