How to disable special characters in bash? [duplicate] - bash

This question already has an answer here:
Prevent globbing in a bash script
(1 answer)
Closed 6 years ago.
I've created a script which performs a calculation. For example:
count 1 + 3
1 + 3 = 4,
- and / also works, but if I type
count 1 * 3, I got
Should be number operand number
Here is a part of a script:
if [ "$#" -ne 3 ]; then
echo "Should be number operand number"
exit 1
fi
...
elif [ "$2" = "*" ]; then
result=`echo $1 \* $3 | bc`
echo "$1 * $3 = $result"
Yes, if I type \ before * in command line it would work, but I need it to run just with a *.
I've tried to use set -f inside the script it did not work (yes it disables special characters if I type in bash itself but it is not what I need). There is also a shopt command that controls shell behavior, so I've tried to enable some options like shopt -s globstar, nullglob, promptvars, etc. It did not work. I put the command before if statement maybe it is wrong.
I would appreciate if someone corrects me or tells the other way to disable interpretation of a special character from inside the script.

This exact problem is mentioned in the POSIX specification of expr, saying that the tool has a "rather difficult syntax" for the reason you describe.
This is definitely a known problem, in other words, and POSIX itself leaves it unsolved. You should really consider working with Unix and do the same, rather than trying to work against it.
Some options are:
Require the entire expression to one quoted argument, e.g. count "1 * 2"
This is a pragmatic, fail-fast way of ensuring that the argument is always quoted, so that any introduction of * still works. The bash builtin let does this.
read the expression yourself, e.g. just run count and then type in 1 * 2.
This avoids the shell touching your data, so that you can interpret it however you want. It's what bc and dc do.
Live with the fact that passing * will require careful quoting.
This is what expr does.
Use a magic alias hack that only works on certain shells under very specific circumstances.
This is what no one does, because it's fragile and unpredictable, even though it in some cases allows the syntax you want.

Always quote when you echo. You need to quote the variable reference as well:
me$ FOO="BAR * BAR"
me$ echo "$FOO"
BAR * BAR
If you don't use double quote, bash will see * and perform filename expansion. * expands to all files in your current directory.
However in saying this you would have to be using single quotes for the *:
#!/bin/bash
if [ "$2" = "*" ]; then
result=`echo $1 \* $3 | bc`
echo "$1 * $3 = $result"
fi
The following would work:
» count.sh 2 '*' 1
2 * 2 = 4
There is nothing you can do in your program, because the * expansion is done by the shell (in contrast to Windows, where it's done by the program).
Here's another code example using a menu-driven approach instead:
#!/bin/bash
while true; do
read -p "what's the first number? " n1
read -p "what's the second number? " n2
PS3="what's the operation? "
select ans in add subtract multiply divide; do
case $ans in
add) op='+' ; break ;;
subtract) op='-' ; break ;;
multiply) op='*' ; break ;;
divide) op='/' ; break ;;
*) echo "invalid response" ;;
esac
done
ans=$(echo "$n1 $op $n2" | bc -l)
printf "%s %s %s = %s\n\n" "$n1" "$op" "$n2" "$ans"
done
what's the first number? 5
what's the second number? 4
1) add
2) subtract
3) multiply
4) divide
what's the operation? /
invalid response
what's the operation? 4
5 / 4 = 1.25000000000000000000
In short, quote everything where you do not require the shell to perform token splitting and wildcard expansion.

Related

Get variable outside while loop shell script

I would like the get the content of variable outside the while loop using shell script
For example:
::
count = 5
while [ #count -gt 0]; do
a=3
b=4
if ( a > b)
result = "UP"
else
result = "DOWN"
fi
count=$[ $count - 1]
done
echo $result
$result appears empty every time!
I just want the content of result outside the loop.
Can anyone help on this issue! I know that the variable inseide the loop is executed in sub-shell, but I tried several tips and doesn't works!
Thanks in avdance
Wow you have a huge number of syntax errors in your short script. I have detailed them in the comments below as well as adjusted where the variables are declared to make the loop do something, e.g.
#!/bin/sh
count=5 ## no spaces aroung " = "
a=3 ## a & b never change in loop
b=4
while [ "$count" -gt 0 ]; do ## always quote variables in [ .. ], spaces required
if ((a > b)); then ## there are two ((..)) in arithmetic comparison
result="UP" ## NO spaces around " = "
else
result="DOWN" ## ditto
fi
printf "%d %s\n" "$count" "$result" ## some output helps
count=$((count - 1)) ## use arithmetic ((..)), no $ required inside
((a++)) ## increment a to make it iteresting.
done
First, in shell there is no spaces allowed around the '=' sign during assignments. When using [ .. ] you must have a space after [ and before ], and always quote your variables inside. The quoting isn't required with bash [[ ... ]] or with the arithmetic comparison ((..)).
Every if and elif must be followed by a then. Every for or while must be followed by a do.
When using the arithmetic operator ((..)) (either for an arithmetic operation or comparison) there are two parens required. You can also use the increment and decrement operators ++ and --, e.g. ((a++)) to increment/decrement values within, but it you are assigning the result you must preceded the opening (( with the $, e.g. $((count - 1))
Example Use/Output
$ sh count.sh
5 DOWN
4 DOWN
3 UP
2 UP
1 UP
I think that accounts for most of the syntax issues. If you have further questions, please drop a comment below.

bash function name with dash is bad pratice? [duplicate]

What are the syntax rules for identifiers, especially function and variable names, in Bash?
I wrote a Bash script and tested it on various versions of Bash on Ubuntu, Debian, Red Hat 5 and 6, and even an old Solaris 8 box. The script ran well, so it shipped.
Yet when a user tried it on SUSE machines, it gave a "not a valid identifier" error. Fortunately, my guess that there was an invalid character in the function name was right. The hyphens were messing it up.
The fact that a script that was at least somewhat tested would have completely different behaviour on another Bash or distro was disconcerting. How can I avoid this?
From the manual:
Shell Function Definitions
...
name () compound-command [redirection]
function name [()] compound-command [redirection]
name is defined elsewhere:
name A word consisting only of alphanumeric characters and under‐
scores, and beginning with an alphabetic character or an under‐
score. Also referred to as an identifier.
So hyphens are not valid. And yet, on my system, they do work...
$ bash --version
GNU bash, version 4.2.25(1)-release (x86_64-pc-linux-gnu)
The question was about "the rules", which has been answered two different ways, each correct in some sense, depending on what you want to call "the rules". Just to flesh out #rici's point that you can shove about any character in a function name, I wrote a small bash script to try to check every possible (0-255) character as a function name, as well as as the second character of a function name:
#!/bin/bash
ASCII=( nul soh stx etx eot enq ack bel bs tab nl vt np cr so si dle \
dc1 dc2 dc3 dc4 nak syn etb can em sub esc fs gs rs us sp )
for((i=33; i < 127; ++i)); do
printf -v Hex "%x" $i
printf -v Chr "\x$Hex"
ASCII[$i]="$Chr"
done
ASCII[127]=del
for((i=128; i < 256; ++i)); do
ASCII[$i]=$(printf "0X%x" $i)
done
# ASCII table is now defined
function Test(){
Illegal=""
for((i=1; i <= 255; ++i)); do
Name="$(printf \\$(printf '%03o' $i))"
eval "function $1$Name(){ return 0; }; $1$Name ;" 2>/dev/null
if [[ $? -ne 0 ]]; then
Illegal+=" ${ASCII[$i]}"
# echo Illegal: "${ASCII[$i]}"
fi
done
printf "Illegal: %s\n" "$Illegal"
}
echo "$BASH_VERSION"
Test
Test "x"
# can we really do funky crap like this?
function [}{(){
echo "Let me take you to, funkytown!"
}
[}{ # why yes, we can!
# though editor auto-indent modes may punish us
I actually skip NUL (0x00), as that's the one character bash may object to finding in the input stream. The output from this script was:
4.4.0(1)-release
Illegal: soh tab nl sp ! " # $ % & ' ( ) * 0 1 2 3 4 5 6 7 8 9 ; < > \ ` { | } ~ del
Illegal: soh " $ & ' ( ) ; < > [ \ ` | del
Let me take you to, funkytown!
Note that bash happily lets me name my function "[}{". Probably my code is not quite rigorous enough to provide the exact rules for legality-in-practice, but it should give a flavor of what manner of abuse is possible.
I wish I could mark this answer "For mature audiences only."
Command identifiers and variable names have different syntaxes. A variable name is restricted to alphanumeric characters and underscore, not starting with a digit. A command name, on the other hand, can be just about anything which doesn't contain bash metacharacters (and even then, they can be quoted).
In bash, function names can be command names, as long as they would be parsed as a WORD without quotes. (Except that, for some reason, they cannot be integers.) However, that is a bash extension. If the target machine is using some other shell (such as dash), it might not work, since the Posix standard shell grammar only allows "NAME" in the function definition form (and also prohibits the use of reserved words).
From 3.3 Shell Functions:
Shell functions are a way to group commands for later execution using a single name for the group. They are executed just like a "regular" command. When the name of a shell function is used as a simple command name, the list of commands associated with that function name is executed. Shell functions are executed in the current shell context; no new process is created to interpret them.
Functions are declared using this syntax:
name () compound-command [ redirections ]
or
function name [()] compound-command [ redirections ]
and from 2 Definitions:
name
A word consisting solely of letters, numbers, and underscores, and beginning with a letter or underscore. Names are used as shell variable and function names. Also referred to as an identifier.
Note The biggest correction here is that newline is never allowed in a function name.
My answer:
Bash --posix: [a-zA-Z_][0-9a-zA-Z_]*
Bash 3.0-4.4: [^#%0-9\0\1\9\10 "$&'();<>\`|\x7f][^\0\1\9\10 "$&'();<>\`|\x7f]*
Bash 5.0: [^#%0-9\0\9\10 "$&'();<>\`|][^\0\9\10 "$&'();<>\`|]*
\1 and \x7f works now
Bash 5.1: [^#%\0\9\10 "$&'();<>\`|][^\0\9\10 "$&'();<>\`|]*
Numbers can come first?! Yep!
Any bash 3-5: [^#%0-9\0\1\9\10 "$&'();<>\`|\x7f][^\0\1\9\10 "$&'();<>\`|\x7f]*
Same as 3.0-4.4
My suggestion (opinion): [^#%0-9\0-\f "$&'();<>\`|\x7f-\xff][^\0-\f "$&'();<>\`|\x7f-\xff]
Positive version: [!*+,-./:=?#A-Z\[\]^_a-z{}~][#%0-9!*+,-./:=?#A-Z\[\]^_a-z{}~]*
My version of the test:
for ((x=1; x<256; x++)); do
hex="$(printf "%02x" $x)"
name="$(printf \\x${hex})"
if [ "${x}" = "10" ]; then
name=$'\n'
fi
if [ "$(echo -n "${name}" | xxd | awk '{print $2}')" != "${hex}" ]; then
echo "$x failed first sanity check"
fi
(
eval "function ${name}(){ echo ${x};}" &>/dev/null
if test "$("${name}" 2>/dev/null)" != "${x}"; then
eval "function ok${name}doe(){ echo ${x};}" &>/dev/null
if test "$(type -t okdoe 2>/dev/null)" = "function"; then
echo "${x} failed second sanity test"
fi
if test "$("ok${name}doe" 2>/dev/null)" != "${x}"; then
echo "${x}(${name}) never works"
else
echo "${x}(${name}) cannot be first"
fi
else
# Just assume everything over 128 is hard, unless this says otherwise
if test "${x}" -gt 127; then
if declare -pF | grep -q "declare -f \x${hex}"; then
echo "${x} works, but is actually not difficult"
declare -pF | grep "declare -f \x${hex}" | xxd
fi
elif ! declare -pF | grep -q "declare -f \x${hex}"; then
echo "${x} works, but is difficult in bash"
fi
fi
)
done
Some additional notes:
Characters 1-31 are less than ideal, as they are more difficult to type.
Characters 128-255 are even less ideal in bash (except on bash 3.2 on macOS. It might be compiled differently?) because commands like declare -pF do not render the special characters, even though they are there in memory. This means any introspection code will incorrectly assume that these functions are not there. However, features like compgen still correctly render the characters.
Out of my testing scope, but some unicode does work too, although it's extra hard to paste/type on macOS over ssh.
This script tests all valid chars for
function names with 1 char.
It outputs 53 valid chars (a-zA-Z and underscore) using
a POSIX shell and 220 valid chars with BASH v4.4.12.
The Answer from Ron Burk is valid, but lacks the numbers.
#!/bin/sh
FILE='/tmp/FOO'
I=0
VALID=0
while [ $I -lt 256 ]; do {
NAME="$( printf \\$( printf '%03o' $I ))"
I=$(( I + 1 ))
>"$FILE"
( eval "$NAME(){ rm $FILE;}; $NAME" 2>/dev/null )
if [ -f "$FILE" ]; then
rm "$FILE"
else
VALID=$(( VALID + 1 ))
echo "$VALID/256 - OK: $NAME"
fi
} done

Adding a list of space separated numbers

Currently stuck in a situation where I ask the user to input a line of numbers with a space in between, then have the program display those numbers with a delay, then add them. I have everything down, but can't seem to figure out a line of code to coherently calculate the sum of their input, as most of my attempts end up with an error, or have the final number multiplied by the 2nd one (not even sure how?). Any help is appreciated.
echo Enter a line of numbers to be added.
read NUMBERS
COUNTER=0
for NUM in $NUMBERS
do
sleep 1
COUNTER=`expr $COUNTER + 1`
if [ "$NUM" ]; then
echo "$NUM"
fi
done
I've tried echo expr $NUM + $NUM to little success, but this is really all I can some up with.
Start with
NUMBERS="4 3 2 6 5 1"
echo $NUMBERS
Your script can be changed into
sum=0
for NUM in ${NUMBERS}
do
sleep 1
((counter++))
(( sum += NUM ))
echo "Digit ${counter}: Sum=$sum"
done
echo Sum=$sum
Another way is using bc, usefull for input like 1.6 2.3
sed 's/ /+/g' <<< "${NUMBERS}" | bc
Set two variables n and m, store their sum in $x, print it:
n=5 m=7 x=$((n + m)) ; echo $x
Output:
12
The above syntax is POSIX compatible, (i.e. works in dash, ksh, bash, etc.); from man dash:
Arithmetic Expansion
Arithmetic expansion provides a mechanism for evaluating an arithmetic
expression and substituting its value. The format for arithmetic expan‐
sion is as follows:
$((expression))
The expression is treated as if it were in double-quotes, except that a
double-quote inside the expression is not treated specially. The shell
expands all tokens in the expression for parameter expansion, command
substitution, and quote removal.
Next, the shell treats this as an arithmetic expression and substitutes
the value of the expression.
Two one-liners that do most of the job in the OP:
POSIX:
while read x ; do echo $(( $(echo $x | tr ' ' '+') )) ; done
bash:
while read x ; do echo $(( ${x// /+} )) ; done
bash with calc, (allows summing real, rational & complex numbers, as well as sub-operations):
while read x ; do calc -- ${x// /+} ; done
Example input line, followed by output:
-8!^(1/3) 2^63 -1
9223372036854775772.7095244707464171953

I am getting expr syntax errors in bash shell for a simple program

#!/bin/bash
clear
echo "Enter a number"
read a
s = 0
while [ $a -gt 0 ]
do
r = ` expr $a % 10 `
s = ` expr $s + $r `
a = ` expr $a / 10 `
done
echo "sum of digits is = $s"
This is my code guys .
I am getting a bunch of expr syntax errors.
I am using the bash shell.
Thanks!
Your error is caused by the spaces surrounding the = in the assignments, the following replacements should work (I prefer $() to using backticks since they're much easier to nest):
s=0
r=$(expr $a % 10)
s=$(expr $s + $r)
a=$(expr $a / 10)
For example, s = 0 (with the spaces) does not set the variable s to zero, rather it tries to run the command s with the two arguments, = and 0.
However, it's not really necessary to call the external expr1 to do mathematical manipulation and capture the output to a variable. That's because bash itself can do this well enough without resorting to output capture (see ARITHMETIC EVALUATION in the bash man page):
#!/bin/bash
clear
read -p "Enter a number: " number
((sum = 0))
while [[ $number -gt 0 ]]; do
((sum += number % 10))
((number /= 10))
done
echo "Sum of digits is $sum"
You'll notice I've made some other minor changes which I believe enhances the readability, but you could revert back to the your original code if you wish and just use the ((expression)) method rather than expr.
1 If you don't mind calling external executables, there's no need for a loop in bash, you could instead use sneakier methods:
#!/bin/bash
clear
read -p "Enter a number: " number
echo "Sum of digits is $(grep -o . <<<$number | paste -sd+ | bc)"
But, to be brutally honest, I think I prefer the readable solution :-)

Bash programme for counting characters

This is the programme I use.
clear
echo enter a string
read string
length=`echo -n $string | wc -c`
v=0
cons=0
d=0
s=0
len=$length
while [ $len -gt 0 ]
do
h=`echo $string | cut -c $len`
case $h in
[AaEeIiOoUu]) v=`expr $v + 1`
;;
[BbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz]) cons=`expr $cons + 1`
;;
[0-9]) d=`expr $d + 1`
;;
' ') s=`expr $s + 1 `
;;
esac
len=`expr $len - 1 `
done
spl=`expr $len - $v - $cons - $d - $s`
echo "vowels = $v"
echo "consonants = $cons"
echo "digits = $d"
echo "spaces = $s"
echo "special character = $spl"
The program counts all other types of characters except special characters. The output shows a minus value even if there are special characters in the input value. How can I modify the programme to make it display correct number of special characters in the input?
While your script is technically (almost) correct, it is not very pretty (both visually and in terms of code beauty).
The reason your version is not working as expected #devnull has already pointed out, but there is another bug that I will explain further down.
Since you are using bash you could rewrite the whole thing in a more idiomatic, readable and shorter way.
Here is a rewritten version of your script (comments and explanations follow below):
#!/bin/bash
clear
IFS= read -p "enter a string: " string
for ((i = 0; i < ${#string}; i++)); do
case "${string:$i:1}" in
[AaEeIiOoUu]) ((vowels++)) ;;
[[:alpha:]]) ((consonants++)) ;;
[[:digit:]]) ((digits++)) ;;
[[:space:]]) ((whitespace++)) ;;
*) ((others++)) ;;
esac
done
echo "vowels = ${vowels-0}"
echo "consonants = ${consonants-0}"
echo "digits = ${digits-0}"
echo "whitespace = ${whitespace-0}"
echo "other characters = ${others-0}"
indentation
First off, you should always indent your code blocks (if constructs, loops, switch (case) statements, ...) for readability, e.g.
while [ $len -gt 0 ]; do
do_stuff
done
read, whitespace and the prompt
Since you are using bash, read is capable of displaying a prompt for you - the extra echo is not needed. Furthermore, read strips off leading and trailing whitespace which results in an incorrect calculation unless you set IFS to the null string:
IFS= read -p "this is my prompt: " string
iterating over characters in a string
You can use parameter expansion to both get the length of the string as well as slice out one character at a time using a for loop, getting rid of the unnecessary cut and avoiding a subshell.
# ${#string} = length of $string
for ((i = 0; i < ${#string}; i++)); do
c=${string:$i:1} # c is set to the character at position $i in string $string
done
character classes
First off, your consonants statement still includes Ii for the match. Technically it doesn't matter since you cannot fall-through from the vowels match, but if this is an assignment you probably want to remove it.
That being said, you could just use the short character class for readability:
[AaEeIiOoUu]) vowel_stuff ;;
[a-zA-Z]) consonant_stuff ;; # vowels already matched above, so superfluous characters don't matter here
To make your life even easier, there are so called character classes which you can use instead, e.g.
[:digit:] = [0-9]
[:space:] = tabs, newline, form feed, carriage return, space
etc.
Note that your current locale influences certain character classes.
the special characters case
Just use the default case in the switch statement to count them, then you can skip calculating those afterwards:
case ... in
vowels) handle_vowel ;;
[...]
*) handle_other_character ;;
esac
defaults
Using parameter expansion you can also get rid of initializing your variables with 0, you can on-the-fly expand the variables to 0 if they are not set, i.e. they have not been incremented during the loop.
backticks
Unless you are writing code that has to be super-portable and has to work in all kinds of old shells, use the $() syntax in stead of ``.
arithmetic expressions
Same as above, unless you really need it you can use (( )) for arithmetic expressions, e.g.
a=5
((a = a + 10)) # or even ((a += 10))
# $a is now 15
Google and the Advanced Bash-Scripting Guide as well as the bash sections of Greg's Wiki are your friends.
You are using the wrong variable while calculating spl. Using len is clearly wrong as you're decrementing it within the loop and it becomes 0 when the loop is done.
Instead of saying:
spl=`expr $len - $v - $cons - $d - $s`
say:
spl=`expr $length - $v - $cons - $d - $s`

Resources