14

I wrote this script :

#!/bin/bash
while [ true ] 
do
    currentoutput="$(lsusb)"
    if [ "$currentoutput" != "$lastoutput" ]
    then
        echo "" date and Time >> test.log
        date +%x_r >> test.log
        lastoutput="$(lsusb)"
        lsusb >> test.log
    fi
    sleep 5
done

I'm a newbie trying to learn fast and I got a question about the variable’s quotation marks.

Put a variable between $(), I get it, but why are the quotation marks needed even in the if statement? Is it to make a nested command?

dessert
  • 39,982
Shankhara
  • 143
  • 5
    Note that while [ true ] does produce an infinite loop, but perhaps not for the reason you think it does; while [ false ] also produces an infinite loop, because with a single argument, [ ... ] succeeds if that argument is a non-empty string. while true will actually run a command named true (which always succeeds). – chepner Apr 12 '19 at 13:06
  • 4
    You don't put a variable inside $(). You put a command inside $(). – Wildcard Apr 12 '19 at 20:19
  • Try this: $ python3 -c "import sys; print(sys.argv)" $(lsusb) versus the same thing, but with double quotes $ python3 -c "import sys; print(sys.argv)" "$(lsusb)". For extra xp, also try the following: "\lsusb`"`lsusb`` '$(lsusb)' Quiz: what will \$(lsusb)`` do? – Evgeni Sergeev Oct 16 '21 at 01:53

4 Answers4

24

Quotation marks prevent "word splitting". That is: breaking down variables into multiple items at whitespace characters (or to be more exact, at spaces, tabs, and newlines as defined in the value of the default $IFS shell variable).

For example,

$ var="one two"
$ howmany(){ echo $#;  }
$ howmany $var
2
$ howmany "$var"
1

Here we define the howmany function which just lets us know how many positional parameters are given. As you can see, there are two items being passed to the variable, and with the quotes the text in the variable is treated as one unit.

This is important for accurate passing of information. For example, if the variable contains path to file, and the filename contains spaces anywhere in the path, the command you are trying to run may fail or give inaccurate results. If we were trying to create a file with the $var variable, touch $var would create two files, but touch "$var" just one.

Same goes for your [ "$currentoutput" != "$lastoutput" ] part. This particular test performs a comparison on two strings. When the test runs, the [ command would need to see 3 arguments - a text string, the != operator, and another text string. Keeping double quotes prevents word splitting, and the [ command sees exactly those 3 arguments. Now what happens if variables are unquoted ?

$ var="hello world"
$ foo="hi world"
$ [ $var != $foo ]
bash: [: too many arguments
$ 

Here, word splitting occurs, and instead [ sees two strings hello and world followed by !=, followed by two other strings hi world. Key point is that without double quotes, the contents of variables are understood as separate units rather than one whole item.

Assigning command substitution doesn't require double quotes as in

var=$( df )

where you have the df command's output saved to var. However, it is a good habit to always double quote variables and command substitution $(...) unless you do in fact want the output to be treated as separate items.


On a side note, the

while [ true ]

part can be

while true

[ is a command which evaluates its arguments, and [ whatever ] is always true regardless of what is inside. By contrast, while true uses the command true which always returns success exit status (and that's exactly what while loop needs). The difference is a bit more clarity and less testing performed. Alternatively, you could also use : instead of true

The double quotes in echo "" date and Time part could probably be removed. They merely insert an empty string and an add extra space to the output. If that's desired, feel free to keep them there, but there's no particular functional value in this case.

lsusb >> test.log

This part could probably be replaced with echo "$currentoutput" >> test.log. There's no reason to run lsusb again after it has been run already in currentoutput=$(lsusb). In cases where trailing newlines have to be preserved in the output - one could see the value in running a command multiple times, but in case of lsusb there's no need for that. The less external commands you call, the better, because every call to a non-built-in command incurs costs in CPU, memory usage, and execution time ( even though the commands are probably pre-loaded from memory).


See also:

terdon
  • 100,812
Sergiy Kolodyazhnyy
  • 105,154
  • 20
  • 279
  • 497
  • 1
    Great answer, if you didn't already, you should write a book about bash. But in the last part shouldn't echo "$currentoutput" >> test.log better be printf '%s\n' "$currentoutput" >> test.log ? – pLumo Apr 12 '19 at 07:05
  • 2
    @RoVo Yes, printf should be preferred for portability. Since we're using bash-specific script here it can be excused to use echo. But your observation is very much correct – Sergiy Kolodyazhnyy Apr 12 '19 at 13:26
  • 1
    @SergiyKolodyazhnyy, ...I'm not sure that echo is completely reliable even when the shell is known to be bash (and the value of the xpg_echo and posix flags is known); if your value could be -n or -e, it can disappear instead of being printed. – Charles Duffy Apr 12 '19 at 16:13
4

The following runs the external command command and returns its output.

"$(command)"

Without the brackets/parentheses, this would look for a variable instead of running a command:

"$variable"

As for the difference between $variable and "$variable", this becomes relevant when $variable contains spaces. When using "$variable", the entire variable contents will be inserted into a single string even if the contents include spaces. When using $variable the contents of the variable may be expanded into an argument list of multiple arguments.

thomasrutter
  • 36,774
4

In currentoutput="$(lsusb)" lsusb is not a variable, it is a command. What this statement does, it executes lsusb command and assigns its output to currentoutput variable.

Older syntax for this was

currentoutput=`lsusb`

you can find it in many examples and scripts

To answer the other part of your question, if [ ] is just how syntax for if is defined in bash. See more in https://www.tldp.org/LDP/Bash-Beginners-Guide/html/sect_07_01.html

marosg
  • 1,303
  • 3
    I think it's important to say that [ ] is actually the test command. You can use if statements with other commands too, as it relies on a 0 or non-zero test of their exit code. – Arronical Apr 12 '19 at 09:08
  • ...indeed, it's much better to run if grep -qe "somestring" file than to run grep -qe "somestring" file; if [ $? = 0 ]; then ..., so the claim that if [ ... is part of the definition of if syntax is not just misleading but leads to bad practices. – Charles Duffy Apr 12 '19 at 16:16
2

To be contrarian - bash recommends you use the [[ ... ]] over the [ ... ] construct to avoid having to quote variables in the test and so the associated problems of word splitting that the others have pointed out.

[ is provided in bash for POSIX compatibility with scripts meant to run under #!/bin/sh or those being ported over to bash - for the most part, you should avoid it in favour of [[.

e.g.

# With [ - quotes are needed

$ foo='one two'; bar='one two'; [ $foo = $bar ] && echo "they're equal"
-bash: [: too many arguments

$ foo='one two'; bar='one two'; [ "$foo" = "$bar" ] && echo "they're equal"                                                                                                              
they're equal

# versus [[ - quotes not needed

$ foo='one two'; bar='one two'; [[ $foo = $bar ]] && echo "they're equal"
they're equal

shalomb
  • 251
  • bash makes no such recommendation; it provides [[ ... ]] which can be more convenient and has some functionality that [ ... ] does not, but there is no problem with using [ ... ] correctly. – chepner Apr 12 '19 at 13:01
  • 1
    @chepner - Maybe I should be clearer here. Sure it's not an explicit recommendation in the bash manpage but given [[ does everything [ does and more and still the many times [ trips people up and then try and retrofit features of [[ back to [ only to fail and leave bugs scattered about - it's fair to say it is a community recommendation in as far as most relevant bash style guides will advise never use [. – shalomb Apr 12 '19 at 14:36