I need to perform some logic based on if shell_var_1 OR shell_var_2 is set under bash.
If I use #!/bin/sh, I could just use:
if [ "$shell_var_1" -ne "0" -o "$shell_var_2" -ne "0" ] ; then
logic
fi
This just uses "-o" from test (http://unixhelp.ed.ac.uk/CGI/man-cgi?test)
However, if I specify #!/bin/bash, and want to use the '-v' option to test the shell variable's existence (added from bash 4.2 onwards), how'd I go about doing the same?
Ref: https://tiswww.case.edu/php/chet/bash/bash.html
if [ -v shell_var_1 -o -v shell_var_2 ] ; then
logic
fi
^ Is this considered correct? Am I not mixing bash and the test operator?
[[ $shell_var_1 || $shell_var_2 ]]
...is a very succinct and idiomatic way to write this in bash, compatible back to ancient releases, assuming that you consider a variable set to an empty value not to exist.
If you want a portable alternative which treats variables set to zero-byte values as unset, then
[ -n "$shell_var_1" ] || [ -n "$shell_var_2" ]
...will serve.
If you want a portable alternative that treats variables set to empty values as existing, then
[ -n "${shell_var_1+set}" ] || [ -n "${shell_var_2+set}" ]
...will do this.
If you really want to use -v (thus, treating variables set to empty values as existing, but being needlessly incompatible with POSIX shells and older bash releases -- including those shipped with MacOS), then use [[ ]] -- which makes it unmistakably clear to readers that you're using bash-only syntax, and isn't prone to some obscure bugs which test or [ (even the bash built-in form of them) are prone to when used in more than 2-arg form:
[[ -v shell_var_1 || -v shell_var_2 ]]
Related
This question already has answers here:
Bash operators: "!" vs "-z"
(2 answers)
Closed 3 years ago.
I recently found this "bug" in my bash script;
if [ "$var" ]; then
echo "Condition is true"
fi
where what I meant was to check if $var is non-empty, i.e. [ -n "$var" ]. As it happens, the code seems to work perfectly fine without the -n. Similarly, I find that I can replace [ -z "$var" ] with [ ! "$var" ].
I tend to like this implicit falseyness (truthiness) based on the (non-)emptyness of a variable in other languages, and so I might adopt this pattern in bash as well. Are there any danger to doing so, i.e. edge cases where the two set of syntaxes are inequivalent?
So we substitute:
-n "$var" -> "$var"
-z "$var" -> ! "$var"
That looks ok and some people do that. There are corner cases where the removal will be harmful and result in syntax errors. These corner cases specially include times, where var is equal to a valid test argument. Ex. var=-n or var="!" etc.
Ex:
$ v1="-n" v2=""; [ -n "$v1" -o -z "$v2" ]; echo $?
0
but
$ v1="-n" v2=""; [ "$v1" -o ! "$v2" ]; echo $?
bash: [: too many arguments
2
That said, I couldn't find a way on my system (bash 5.0.0) to break it without using -o and -a. And anyway, the test man page advises to use && and || instead of -a and -o.
But in POSIX specification test we can find the following application note:
The two commands:
test "$1"
test ! "$1"
could not be used reliably on some historical systems. Unexpected
results would occur if such a string expression were used and $1
expanded to '!', '(', or a known unary primary. Better constructs are:
test -n "$1"
test -z "$1"
So I think as long as you don't use "some historical systems", you are safe. But is it worth the risk? You have to answer yourself.
That said, subjective: I value maintainability and readability much more then saving to type 3 characters and find -n and -z more readable. The intent with -n is clear - test if the string has -nonzero length. The intent with -z is also clear - test if the string has -zero length. I find it confusing to others to write [ "$var" ] and [ ! "$var" ]. At first it would look like $var has some special meaning inside the [ ].
I recently found this "bug" in my bash script
It's not a bug, it's a feature!
First of all, you make use of single brackets. This implies that you are using the test command and not the Bash-builtin function. From the manual :
test EXPRESSION or [ EXPRESSION ]: this exits with the status returned by EXPRESSION
-n STRING: the length of STRING is nonzero.
STRING: equivalent to -n STRING
source: man test
This should answer your question.
Furthermore:
! EXPRESSION: test returns true of EXPRESSION is false
-z STRING: test returns true if the length of STRING is zero.
Which implies that [ ! STRING ] is equivalent to [ -z STRING ].
I looked some other posts and learnt to match file extension in the following way but why my code is not working? Thanks.
1 #!/bin/sh
2
3 for i in `ls`
4 do
5 if [[ "$i" == *.txt ]]
6 then
7 echo "$i is .txt file"
8 else
9 echo "$i is NOT .txt file"
10 fi
11 done
eidt:
I realized #!/bin/sh and #!/bin/bash are different, if you are looking at this post later, remember to check which one you are using.
The [[ ]] expression is only available in some shells, like bash and zsh. Some more basic shells, like dash, do no support it. I'm guessing you're running this on a recent version of Ubuntu or Debian, where /bin/sh is actually dash, and hence doesn't recognize [[. And actually, you shouldn't use [[ ]] with a #!/bin/sh shebang anyway, since it's unsafe to depend on a feature that the shebang doesn't request.
So, what to do about it? You'll have the [ ] type of test expression available, but it doesn't do pattern matching (like *.txt). There are a number of alternate ways to do it:
The case statement is available in even basic shells, and has the same pattern matching capability as [[ = ]]. This is the most common way to do this type of thing, especially when you have a list of different patterns to check against.
More indirectly, you can use ${var%pattern} to try remove .txt from the end of the end of the value (see "Remove Smallest Suffix Pattern" here), and then check to see if that changed the value:
if [ "$i" != "${i%.txt}" ]
More explanation: suppose $i is "file.txt"; then this expands to [ "file.txt" != "file" ], so they're not equal, and the test (for !=) succeeds. On the other hand, if $i is "file.pdf", then it expands to [ "file.pdf" != "file.pdf" ], which fails because the strings are the same.
Other notes: when using [ ], use a single equal sign for string comparison, and be sure to properly double-quote all variable references to avoid confusion. Also, if you use anything that has special meaning to the shell (like < or >), you need to quote or escape them.
You could use the expr command's : operator to do regular expression matching. (Regular expressions are a different type of pattern from the basic wildcard or "glob" expression.) You could do this, but don't.
#!/bin/sh
for i in `ls`
do
if [[ "$i" = *".txt" ]] ; then
echo "$i is .txt file"
else
echo "$i is NOT .txt file"
fi
done
You don't have to loop in ls output, and sh implementation might vary among OS distributions.
Consider:
#! /bin/sh
for i in *
do
if [[ "$i" == *.txt ]]
then
echo "$i is txt file"
else
echo "$i is NOT txt file"
fi
done
I was doing some tests, and I couldn't understand why there two results are different. The first one seems correct, since in a crescent ordenation, the character 'f' comes first than 'F'
$ [[ "Foo" < "foo" ]]
$ echo $?
1
So why this one is incorrect?
$ [ "Foo" \< "foo" ]
$ echo $?
0
Both [[ and [ are built into bash. [ is equivalent to the test command, also a builtin. (Well, almost equivalent; [ requires a matching ], test doesn't.)
According to the bash manual:
When used with test or [, the < and > operators sort
lexicographically using ASCII ordering.
(Bourne shell builtins are documented here).
But the expression in [[ ... ]] follows the rules of Bash conditional expressions:
When used with [[, the < and > operators sort
lexicographically using the current locale. The test command uses
ASCII ordering.
(There's another test / [ command provided as part of the GNU coreutils package, typically /usr/bin/test. This command doesn't provide < and > operators, which are not specified by POSIX. The external command should be irrelevant if you're using bash, unless you explicitly give the full path /usr/bin/test.)
Your question is tagged both "bash" and "sh". If you're using bash, you should probably use the bash-specific [[ feature rather than [. If you want greater portability (e.g., if your script might be used with a shell other than bash), you can't rely on the [[ builtin, and you can't assume that [ or test supports < and >.
The BSD test implementation of <[1], like the bash-builtin one, is not character-collation-order aware; it refers only to "the binary value" of the characters in question.
The bash [[ ]] implementation of < is characterset-aware: It honors the current language/locale's selected collation order.
If you set LC_COLLATE=C (specifying ASCII sort order), then [[ ]] will do the same:
$ (export LC_COLLATE=C; [[ "Foo" < "foo" ]]; echo $?)
0
[1] - > is not a POSIX-standardized test operator, so all answers must be in the context of a specific implementation.
I'm confused about this conditional:
if [[ ! -z "$1" ]]
What is this language?
Here is what I'm familiar with for my terminal and bash_profile:
Bash is the shell, or command language interpreter, for the GNU
operating system.
and
Simply put, the shell is a program that takes your commands from the
keyboard and gives them to the operating system to perform. In the old
days, it was the only user interface available on a Unix computer.
Nowadays, we have graphical user interfaces (GUIs) in addition to
command line interfaces (CLIs) such as the shell.
On most Linux systems a program called bash (which stands for Bourne
Again SHell, an enhanced version of the original Bourne shell program,
sh, written by Steve Bourne) acts as the shell program.
function parse_git_branch {
branch=`git rev-parse --abbrev-ref HEAD 2>/dev/null`
if [ "HEAD" = "$branch" ]; then
echo "(no branch)"
else
echo "$branch"
fi
}
function prompt_segment {
# for colours: http://en.wikipedia.org/wiki/ANSI_escape_code#Colors
# change the 37 to change the foreground
# change the 45 to change the background
if [[ ! -z "$1" ]]; then
echo "\[\033[${2:-37};45m\]${1}\[\033[0m\]"
fi
}
function build_mah_prompt {
# time
ps1="$(prompt_segment " \# ")"
# cwd
ps1="${ps1} $(prompt_segment " \w ")"
# git branch
git_branch=`parse_git_branch`
if [[ ! -z "$git_branch" ]]
then
ps1="${ps1} $(prompt_segment " $git_branch " 32)"
fi
# next line
ps1="${ps1}\n\$ "
# set prompt output
PS1="$ps1"
}
PROMPT_COMMAND='build_mah_prompt'
The language is Bash, a modern shell based on the old Bourne shell and
(mostly) compatible with POSIX standards.
test aka [
[[ is a Bash extension of the test command also known as [. The test command is a separate executable but since it’s so useful for shell programming, most (if not all) modern shells implement it as a shell builtin. The following commands show that both versions are available on many systems:
$ type -a test
test is a shell builtin
test is /usr/bin/test
$ type -a [
[ is a shell builtin
[ is /usr/bin/[
For more info, see man test or help test (with Bash).
[[
[[ is implemented as a Bash keyword (not an external command). It originally came from the Korn shell and works similarly but has many improvements over the original [ command. See the following for more info:
What is the difference between test, [ and [[ ?
What's the difference between [ and [[ in bash?
Is [[ ]] preferable over [ ] in bash scripts?
Specific example
According to man test (POSIX specification)
−z string True if the length of string string is zero; otherwise, false.
Thus, the [[ -z "$1" ]] construct returns 0 (value for True in Unix shells) if $1, the first positional parameter to a script or function is an empty string. Introducing the negation ! operator converts the expression to its Boolean opposite, i.e, False if the following expression evaluates to True and vice versa.
To sum up, the whole expression evaluates to True if the first argument to the function is a non-empty string and False if it’s empty (or possibly not set at all).
If you read the above links, you’ll notice that [[ ! -z "$1" ]] is actually equivalent to [[ -n "$1" ]] which return True if $1contains anything, i.e., is not empty. This can be further shortened to [[ $1 ]] as quotes aren’t required for variables within [[.
Note: the portable version (for POSIX shells) is [ -n "$1" ] or [ "$1" ] (where the variables have to be quoted to protect from pathname expansion, word splitting and other potential side effects). See http://mywiki.wooledge.org/Quotes for more info.
Functions
The remaining code are shell functions which look like they’re used to build up a colourful prompt which provides details of the status of a git repository if the current working directory is under version control.
I currently assigned my thinkvantage button to turn off tap to click on my trackpad, but I'd like to convert it to a toggle on/off switch.
For the moment, this is the bash command I use to turn it off (or on with CTRL):
gsettings set org.gnome.settings-daemon.peripherals.touchpad tap-to-click true
gsettings set org.gnome.settings-daemon.peripherals.touchpad tap-to-click false
In other words, ho do I turn this into a conditional toggle switch bash statement?
Presumably something like this:
#!/bin/bash
class=org.gnome.settings-daemon.peripherals.touchpad
name=tap-to-click
status=$(gsettings get "$class" "$name")
status=${status,,} # normalize to lower case; this is a modern bash extension
if [[ $status = true ]]; then
new_status=false
else
new_status=true
fi
gsettings set "$class" "$name" "$new_status"
Breaking it down into pieces:
#!/bin/bash ensures that the interpreter for this script is bash, enabling extended syntax such as [[ ]].
The syntax $( ) is "command substitution"; this runs a command, and substitutes the output of that command. Thus, if the output is true, then status=$(...) becomes status=true.
The parameter expansion ${name,,} expands the contents of name while converting those contents to all-lowercase, and is only available in newer versions of bash. If you want to support /bin/sh or older releases of bash, consider status=$(printf '%s\n' "$status" | tr '[:upper:]' '[:lower:]') instead, or just remove this line if the output of gsettings get is always lowercase anyhow.
The comparison [[ $status = true ]] relies on the bash extension [[ ]] (also available in other modern ksh-derived shells) to avoid the need for quoting. If you wanted it to work with #!/bin/sh, you'd use [ "$status" = true ] instead. (Note that == is allowable inside [[ ]], but is not allowable inside of [ ] on pure POSIX shells; this is why it's best not to be in the habit of using it).
Note that whitespace is important in bash! foo = bar, foo= bar and foo=bar are completely different statements, and all three of them do different things from the other two. Be sure to be cognizant of the differences in copying from this example.
Toggling seconds on the Ubuntu Unity clock (directly pastable as a command for a keybinding):
bash -c 'gsettings set com.canonical.indicator.datetime show-seconds $(gsettings get com.canonical.indicator.datetime show-seconds | perl -neprint/true/?false:true)'
For GNOME:
bash -c 'gsettings set org.gnome.desktop.interface clock-show-seconds $(gsettings get org.gnome.desktop.interface clock-show-seconds | perl -neprint/true/?false:true)'
Source: https://www.commandlinefu.com/commands/view/24256/toggle-the-touchpad-on-or-off
synclient TouchpadOff=$(synclient -l | grep -q 'TouchpadOff.*1'; echo $?)
or
tp=$(synclient -l | grep TouchpadOff | awk '{ print $3 }') && tp=$((tp==0)) && synclient TouchpadOff=$tp