What is the difference between "$a" and $a in unix [duplicate] - shell

This question already has answers here:
When to wrap quotes around a shell variable?
(5 answers)
Closed 6 years ago.
For example:
#!/bin/sh
a=0
while [ "$a" -lt 10 ]
b="$a"
while [ "$b" -ge 0 ] do
echo -n "$b "
b=`expr $b - 1`
done
echo
a=`expr $a + 1`
done*
The above mentioned script gives the answer in triangle while with out the double quotes, it falls one after the other on diff lines.

After a variable is expanded to its value, word splitting (i.e. separating the value into tokens at whitespace) and filename wildcard expansion takes place unless the variable is inside double quotes.
Example:
var='foo bar'
echo No quotes: $var
echo With quotes: "$var"
will output:
No quotes: foo bar
With quotes: foo bar

Here the difference is how the argument is passed to echo function. Effectively " " will preserve whitespaces.
This:
echo -n "$b "
Is translated to:
echo -n "<number><space>"
While this:
echo -n $b<space>
Will ignore the trailing space and will just output the number:
echo -n <number>
Therefore removing all the spaces that are needed for output to look "triangular".

There are errors in your script:
no do after 1st while
no ; before do after 2nd while
why asterisk on done* at the end?
Now to answer your question.
If used as a paramenter:
"$a" is one argument.
$a (without quotes) is possibly multiple arguments:
Compare:
v='a b'; set $v; echo "\$#=$#, \$1=\"$1\", \$2=\"$2\""
$#=2, $1="a", $2="b"
v='a b'; set "$v"; echo "\$#=$#, \$1=\"$1\", \$2=\"$2\""
$#=1, $1="a b", $2=""

Related

BASH:Why is printf to a var removing spaces? [duplicate]

This question already has answers here:
When to wrap quotes around a shell variable?
(5 answers)
I just assigned a variable, but echo $variable shows something else
(7 answers)
Closed 2 years ago.
Why is the output of these two different? I want the spaces to be preserved.
#!/bin/bash
x="foo"
printf "%-12s *" $x
echo " "
y=$(printf "%-12s" $x)
echo $y "*"
Running it gives
foo *
foo *
I want the second line to look like the first.
You'll need to quote the $y:
echo "${y} *"
#!/bin/bash
x="foo"
printf "%-12s *" $x
echo " "
y=$(printf "%-12s" $x)
echo "$y" "*"
➜ ./test.sh
foo *
foo *
➜
More information about when to use double quotes
Ypu can use printf too
printf '%s *\n' "$y"

How to compare a variable to a string in bash? [duplicate]

This question already has answers here:
How do I compare two string variables in an 'if' statement in Bash? [duplicate]
(12 answers)
Closed 2 years ago.
here is how i tried it
while IFS= read line
do
var=$(cut -d ":" -f 3 $line)
if [ "$var" = "L2" ]
then :here is my action:
fi
done < myfile.txt
What i want to do is read a file line by line, read the third word of each line, and do a special action if the third word = a certaine string, i've tried a lot of syntax but it doesn't work. i've also tried to echo "$var" just to see if my variable get the right value, and it does. i don't know what to do anymore
It is better to use double brackets for if condition & for String comparison double equals (==)
And the line which has "cut" command wouldn't have worked. Please find below the corrected code which is working.
while IFS= read line
do
echo "Line is $line"
var=`echo $line | cut -d ":" -f 3`
echo $var
if [[ "$var" == "L2" ]]
then
echo "Some Action"
fi
done < myfile.txt

Iterate over a list of quoted strings

I'm trying to run a for loop over a list of strings where some of them are quoted and others are not like so:
STRING='foo "bar_no_space" "baz with space"'
for item in $STRING; do
echo "$item"
done
Expected result:
foo
bar_no_space
baz with space
Actual result:
foo
"bar_no_space"
"baz
with
space"
I can achieve the expected result by running the following command:
bash -c 'for item in '"$STRING"'; do echo "$item"; done;'
I would like to do this without spawning a new bash process or using eval because I do not want to take the risk of having random commands executed.
Please note that I do not control the definition of the STRING variable, I receive it through an environment variable. So I can't write something like:
array=(foo "bar_no_space" "baz with space")
for item in "${array[#]}"; do
echo "$item"
done
If it helps, what I am actually trying to do is split the string as a list of arguments that I can pass to another command.
I have:
STRING='foo "bar_no_space" "baz with space"'
And I want to run:
my-command --arg foo --arg "bar_no_space" --arg "baz with space"
Use an array instead of a normal variable.
arr=(foo "bar_no_space" "baz with space")
To print the values:
print '%s\n' "${arr[#]}"
And to call your command:
my-command --arg "${arr[0]}" --arg "${arr[1]}" --arg "{$arr[2]}"
Can you try something like this:
sh-4.4$ echo $string
foo "bar_no_space" "baz with space"
sh-4.4$ echo $string|awk 'BEGIN{FS="\""}{for(i=1;i<NF;i++)print $i}'|sed '/^ $/d'
foo
bar_no_space
baz with space
Solved: xargs + subshell
A few years late to the party, but...
Malicious Input:
SSH_ORIGINAL_COMMAND='echo "hello world" foo '"'"'bar'"'"'; sudo ls -lah /; say -v Ting-Ting "evil cackle"'
Note: I originally had an rm -rf in there, but then I realized that would be a recipe for disaster when testing variations of the script.
Converted perfectly into safe args:
# DO NOT put IFS= on its own line
IFS=$'\r\n' GLOBIGNORE='*' args=($(echo "$SSH_ORIGINAL_COMMAND" \
| xargs bash -c 'for arg in "$#"; do echo "$arg"; done'))
echo "${args[#]}"
See that you can indeed pass these arguments just like $#:
for arg in "${args[#]}"
do
echo "$arg"
done
Output:
hello world
foo
bar;
sudo
rm
-rf
/;
say
-v
Ting-Ting
evil cackle
I'm too embarrassed to say how much time I spent researching this to figure it out, but once you get the itch... y'know?
Defeating xargs
It is possible to fool xargs by providing escaped quotes:
SSH_ORIGINAL_COMMAND='\"hello world\"'
This can make a literal quote part of the output:
"hello
world"
Or it can cause an error:
SSH_ORIGINAL_COMMAND='\"hello world"'
xargs: unmatched double quote; by default quotes are special to xargs unless you use the -0 option
In either case, it doesn't enable arbitrary execution of code - the parameters are still escaped.
Pure bash parser
Here's a quoted-string parser written in pure bash (what terrible fun)!
Caveat: just like the xargs example above, this errors in the case of an escaped quoted.
Usage
MY_ARGS="foo 'bar baz' qux * "'$(dangerous)'" sudo ls -lah"
# Create array from multi-line string
IFS=$'\r\n' GLOBIGNORE='*' args=($(parseargs "$MY_ARGS"))
# Show each of the arguments array
for arg in "${args[#]}"; do
echo "$arg"
done
Output:
$#: foo bar baz qux *
foo
bar baz
qux
*
Parse Argument Function
Literally going character-by-character and adding to the current string, or adding to the array.
set -u
set -e
# ParseArgs will parse a string that contains quoted strings the same as bash does
# (same as most other *nix shells do). This is secure in the sense that it doesn't do any
# executing or interpreting. However, it also doesn't do any escaping, so you shouldn't pass
# these strings to shells without escaping them.
parseargs() {
notquote="-"
str=$1
declare -a args=()
s=""
# Strip leading space, then trailing space, then end with space.
str="${str## }"
str="${str%% }"
str+=" "
last_quote="${notquote}"
is_space=""
n=$(( ${#str} - 1 ))
for ((i=0;i<=$n;i+=1)); do
c="${str:$i:1}"
# If we're ending a quote, break out and skip this character
if [ "$c" == "$last_quote" ]; then
last_quote=$notquote
continue
fi
# If we're in a quote, count this character
if [ "$last_quote" != "$notquote" ]; then
s+=$c
continue
fi
# If we encounter a quote, enter it and skip this character
if [ "$c" == "'" ] || [ "$c" == '"' ]; then
is_space=""
last_quote=$c
continue
fi
# If it's a space, store the string
re="[[:space:]]+" # must be used as a var, not a literal
if [[ $c =~ $re ]]; then
if [ "0" == "$i" ] || [ -n "$is_space" ]; then
echo continue $i $is_space
continue
fi
is_space="true"
args+=("$s")
s=""
continue
fi
is_space=""
s+="$c"
done
if [ "$last_quote" != "$notquote" ]; then
>&2 echo "error: quote not terminated"
return 1
fi
for arg in "${args[#]}"; do
echo "$arg"
done
return 0
}
I may or may not keep this updated at:
https://git.coolaj86.com/coolaj86/git-scripts/src/branch/master/git-proxy
Seems like a rather stupid thing to do... but I had the itch... oh well.
Here is a way without an array of strings or other difficulties (but with bash calling and eval):
STRING='foo "bar_no_space" "baz with space"'
eval "bash -c 'while [ -n \"\$1\" ]; do echo \$1; shift; done' -- $STRING"
Output:
foo
bar_no_space
baz with space
If You want to do with the strings something more difficult then just echo You can split Your script:
split_qstrings.sh
#!/bin/bash
while [ -n "$1" ]
do
echo "$1"
shift
done
Another part with more difficult processing (capitalizing of a characters for example):
STRING='foo "bar_no_space" "baz with space"'
eval "split_qstrings.sh $STRING" | while read line
do
echo "$line" | sed 's/a/A/g'
done
Output:
foo
bAr_no_spAce
bAz with spAce

multiplicator operation bash issue

from stdin I read string and if it's like this:
"numberOne * numberTwo"
I have to execute the multiplicator between numberOne and numberTwo.
This is my code:
read string
regex2="^[1-9]+ \*{1,1} [1-9]+$"
if [[ $string =~ $regex2 ]]; then
val=1
val1=`echo $string|cut -d " " -f 1`
val2=`echo $string|cut -d " " -f 3`
((val=$val1*$val2))#comment
echo $val
fi
but I get two errors:
1) on the line where calculate the operation ((val=$val1*$val2)), it says syntax error : arithmetic operator invalid
2) where , by shell , I insert the input string, for example 3 * 2 on shell it prints a list of files, then I thought it was for jolly character "*", and for this reason I substuited the input string with this:
3 \* 2
but the result doesn't change
Always, always quote your expansions.
echo $string, when $string contains a * surrounded by whitespace, treats that * as a glob, replacing it with a list of filenames in the current directory. Your filenames are not likely to be part of a legitimate math operation.
Use echo "$string" instead, if you must use echo at all; printf '%s\n' "$string" is the alternative that works in corner cases where echo fails (and/or behaves in ways unspecified by POSIX).
That said, there's no legitimate reason to use cut here at all; your regex will split your string into pieces perfectly well on its own.
regex2='^([1-9][0-9]*) [*] ([1-9][0-9]*)$'
read -r string
if [[ $string =~ $regex2 ]]; then
val=$(( ${BASH_REMATCH[1]} * ${BASH_REMATCH[2]} ))
echo "$val"
fi
...and even if you couldn't do that, it would be a better practice to use read:
read val1 _ val2 <<<"$string"
echo "$(( val1 * val2 ))"

number of tokens in bash variable

how can I know the number of tokens in a bash variable (whitespace-separated tokens) - or at least, wether it is one or there are more.
The $# expansion will tell you the number of elements in a variable / array. If you're working with a bash version greater than 2.05 or so you can:
VAR='some string with words'
VAR=( $VAR )
echo ${#VAR[#]}
This effectively splits the string into an array along whitespace (which is the default delimiter), and then counts the members of the array.
EDIT:
Of course, this recasts the variable as an array. If you don't want that, use a different variable name or recast the variable back into a string:
VAR="${VAR[*]}"
I can't understand why people are using those overcomplicated bashisms all the time. There's almost always a straight-forward, no-bashism solution.
howmany() { echo $#; }
myvar="I am your var"
howmany $myvar
This uses the tokenizer built-in to the shell, so there's no discrepancy.
Here's one related gotcha:
myvar='*'
echo $myvar
echo "$myvar"
set -f
echo $myvar
echo "$myvar"
Note that the solution from #guns using bash array has the same gotcha.
The following is a (supposedly) super-robust version to work around the gotcha:
howmany() ( set -f; set -- $1; echo $# )
If we want to avoid the subshell, things start to get ugly
howmany() {
case $- in *f*) set -- $1;; *) set -f; set -- $1; set +f;; esac
echo $#
}
These two must be used WITH quotes, e.g. howmany "one two three" returns 3
set VAR='hello world'
echo $VAR | wc -w
here is how you can check.
if [ `echo $VAR | wc -w` -gt 1 ]
then
echo "Hello"
fi
Simple method:
$ VAR="a b c d"
$ set $VAR
$ echo $#
4
To count:
sentence="This is a sentence, please count the words in me."
words="${sentence//[^\ ]} "
echo ${#words}
To check:
sentence1="Two words"
sentence2="One"
[[ "$sentence1" =~ [\ ] ]] && echo "sentence1 has more than one word"
[[ "$sentence2" =~ [\ ] ]] && echo "sentence2 has more than one word"
For a robust, portable sh solution, see #JoSo's functions using set -f.
(Simple bash-only solution for answering (only) the "Is there at least 1 whitespace?" question; note: will also match leading and trailing whitespace, unlike the awk solution below:
[[ $v =~ [[:space:]] ]] && echo "\$v has at least 1 whitespace char."
)
Here's a robust awk-based bash solution (less efficient due to invocation of an external utility, but probably won't matter in many real-world scenarios):
# Functions - pass in a quoted variable reference as the only argument.
# Takes advantage of `awk` splitting each input line into individual tokens by
# whitespace; `NF` represents the number of tokens.
# `-v RS=$'\3'` ensures that even multiline input is treated as a single input
# string.
countTokens() { awk -v RS=$'\3' '{print NF}' <<<"$1"; }
hasMultipleTokens() { awk -v RS=$'\3' '{if(NF>1) ec=0; else ec=1; exit ec}' <<<"$1"; }
# Example: Note the use of glob `*` to demonstrate that it is not
# accidentally expanded.
v='I am *'
echo "\$v has $(countTokens "$v") token(s)."
if hasMultipleTokens "$v"; then
echo "\$v has multiple tokens."
else
echo "\$v has just 1 token."
fi
Not sure if this is exactly what you meant but:
$# = Number of arguments passed to the bash script
Otherwise you might be looking for something like man wc

Resources