multiplicator operation bash issue - bash

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 ))"

Related

Identifying hash encoding

I am creating a function that will accept an input and determine if the value is a certain type of hash encoding (md5, sha1, sha256, and sha512). I have asked a few classmates and logically it makes sense, but clearly something is wrong.
#!/usr/bin/bash
function identify-hash() {
encryptinput=$(echo $1 | grep -E -i '^[a-z0-9=]+${32}')
if [[ -n $encryptinput ]]; then
echo "The $1 is a valid md5sum string"
exit
else
encryptinput=$(echo $1 | grep -E -i '^[a-z0-9=]+${40}')
if [[ -n $encryptinput ]]; then
echo "The $1 is a valid sha1sum string"
exit
else
encryptinput=$(echo $1 | grep -E -i '^[a-z0-9=]+${64}')
if [[ -n $encryptinput ]]; then
echo "The $1 is a valid sha256sum string"
exit
else
encryptinput=$(echo $1 | grep -E -i '^[a-z0-9=]+${128}')
if [[ -n $encryptinput ]]; then
echo "The $1 is a valid sha512sum string"
exit
else
echo "Unable to determine the hash function used to generate the input"
fi
fi
fi
fi
}
identify-hash $1
I know that hashes have a specific number of characters for them, but I don't know exactly why it's not working. Removing the {32} out of line 4 allows it to answer as a md5sum, but than it assumes everything is md5sum.
Suggestions?
Fixed your script. I advise you would have spotted most of the issues if you had used ShellCheck:
#!/usr/bin/env bash
identify_hash() {
# local variables
local -- encrypt_input
local -- sumname
# Regex capture the hexadecimal digits
if [[ "$1" =~ ([[:xdigit:]]+) ]]; then
encrypt_input="${BASH_REMATCH[1]}"
else
encrypt_input=''
fi
# Determine name of sum algorithm based on length of encrypt_input
case "${#encrypt_input}" in
32) sumname=md5sum ;;
40) sumname=sha1sum ;;
64) sumname=sha256sum ;;
128) sumname=sha512sum ;;
*) sumname=;;
esac
# If sum algorithm name found (sumname is not empty)
if [ -n "$sumname" ]; then
printf 'The %s is a valid %s string\n' "$encrypt_input" "$sumname"
else
printf 'Unable to determine the hash function used to generate the input\n' >&2
exit 1
fi
}
identify_hash "$1"
Something shorter, using bash:
checkHash() {
local -ar sumnames=([32]=md5sum [40]=sha1sum [64]=sha256sum [128]=sha512sum)
[[ "$1" =~ [[:xdigit:]]{32,129} ]]
echo "${sumnames[${#BASH_REMATCH}]+String $BASH_REMATCH could be }${sumnames[
${#BASH_REMATCH}]:-No hash tool match this string.}"
}
This will extract [:xdigit:] part out of any complete line:
checkHash 'Filename: 13aba32dbe4db7a7117ed40a25c29fa8 --'
String 13aba32dbe4db7a7117ed40a25c29fa8 could be md5sum
checkHash a32dba32dbe4db7a7117ed40a25c29fa8e4db7a7117ed40a25c29fa8
No hash tool match this string.
checkHash a32dba32dbe4db7a7117ed40a25c29fa8e4db7a7117ed40a25c29fa8da921adb
String a32dba32dbe4db7a7117ed40a25c29fa8e4db7a7117ed40a25c29fa8da921adb could be sha256sum
... then ${var+return this only if $var exist}
... and ${var:-return this if $var is empty}
Further explaining #Gordon Davissons' comment and some basics for anyone who stops by
NB This answer is extremely simplified to apply only to the current question. here's my preferred guide for more regex
Basics of regex
^ - start of a line
$ - end of a line
[...] - list of possible characters
has special sauce
a-z = all lowercase (English) letters; 0-9 = all digits; etc.
also accepts character classes - e.g [:xdigit:] for hexadecimal characters
the expression is now [[:xdigit:]] - i.e [:class:] inside [...]
{...} - number of times the preceding expression should be matched
^[a]{1}$ will match a but not aa
^f[o]{2}d$ will match food but not fod, foood, fooo*d
^[a-z]{4}$ will match
ball ✔️ but not buffalo ❌
cove ✔️ but not cover ❌
basically any line ( because of the ^...$) containing a string of exactly 4 (English) alphabetic characters
{1,5} - at least 1 and at most 5
* - shorthand for {0,} meaning 0 or any number of times
+ - shorthand for {1,} meaning at least 1; but no upper limit
? - shorthand for {1}
So ${32} is looking for 32 "end of line" \n in jargon and what you need is [a-z0-9=]{32} instead
BUT as also pointed out by Andrej Podzimek in the comments you need to match only hexadecimal [0-9a-f] characters which is the same as [:xdigit:]. Either can be used.
PS
more Basics
. (fullstop/period) matches ANY character including spaces and special characters
(...) is to match patterns
[a-z ]*(chicken).*
will match anything from chicken coop to chicken soup and please pass that chicken cookbook, Alex?
[.] means period/fullstop not any character
note the space after z this is to make space (ascii 32 ) a possible character
and . is case-insensituve
PPS if it's for homework/assignment/schoolwork, please specify so in your question :)

Loop's regex conditional doesn't work using extended test

I have a loop that evaluates based on a regex conditional:
until read -p "Enter oprator: " operator
[[ $operator =~ ^[+-*\/]$ ]] #doesn't work
do...
The loop will run until the user enters an arithmetic operator (+, -, * or /). When I enter any of those four, the loop still runs.
I've tried variations of this (i.e. place regex in variable, using quotes, grep) but nothing seems to work.
^[+-*\/]$ ]]$
Here problem is placement of an unescaped - in the middle of the bracket expression which acts as a range between + and *.
You may use this regex (no need to escape / in BASH regex):
[[ $operator =~ ^[-+*/]$ ]]
Or even better without regex use glob match:
[[ $operator == [-+*/] ]]
When including the dash or minus sign - in a character class of a Regex, it must be first or last position, or it will be handled like a range marker. Also the slash / does not need escaping with a backslash:
#!/usr/bin/env bash
until
read -r -n1 -p "Enter oprator: " operator
printf \\n
[[ "$operator" =~ [+*/-] ]] #doesn't work
do
printf 'Symbol %q is not an operator!\n' "$operator" >&2
done
POSIX shell grammar implementation:
#!/usr/bin/env sh
until
printf 'Enter oprator: '
read -r operator
printf \\n
[ -n "$operator" ] && [ -z "${operator%%[-+*/]}" ]
do
printf 'Symbol %s is not an operator!\n' "$operator" >&2
done

Changing alternative character from lower to upper and upper to low - Unix shell script

How to convert the alternative character of a string passed to script, if it is lower then it should be converted to upper and if it is upper then to lower??
read -p " Enter string" str
for i in `seq 0 ${#str}`
do
#echo $i
rem=$(($i % 2 ))
if [ $rem -eq 0 ]
then
echo ${str:$i:1}
else
fr=${str:$i:1}
if [[ "$fr" =~ [A-Z] ]]
then
echo ${str:$i:1} | tr '[:upper:]' '[:lower:]'
elif [[ "$fr" =~ [a-z] ]]
then
echo ${str:$i:1} | tr '[:lower:]' '[:upper:]'
else
echo ""
fi
fi
done
Your question is a bit challenging given that it is tagged shell and not as a question pertaining to an advanced shell like bash or zsh. In POSIX shell, you have no string indexes, no C-style for loop, and no [[ .. ]] operator to use character class pattern matching.
However, with a bit of awkward creativity, the old expr and POSIX string and arithmetic operations, and limiting your character strings to ASCII characters, you can iterate over a string changing uppercase to lowercase and lowercase and uppercase while leaving all other characters unchanged.
I wouldn't recommend the approach if you have an advanced shell available, but if you are limited to POSIX shell, as your question is tagged, it will work, but don't expect it to be super-fast...
#!/bin/sh
a=${1:-"This Is My 10TH String"} ## input and output strings
b=
i=1 ## counter and string length
len=$(expr length "$a")
asciiA=$(printf "%d" "'A") ## ASCII values for A,Z,a,z
asciiZ=$(printf "%d" "'Z")
asciia=$(printf "%d" "'a")
asciiz=$(printf "%d" "'z")
echo "input : $a" ## output original string
while [ "$i" -le "$len" ]; do ## loop over each character
c=$(expr substr "$a" "$i" "1") ## extract char from string
asciic=$(printf "%d" "'$c") ## convert to ASCII value
## check if asciic is [A-Za-z]
if [ "$asciiA" -le "$asciic" -a "$asciic" -le "$asciiZ" ] ||
[ "$asciia" -le "$asciic" -a "$asciic" -le "$asciiz" ]
then ## toggle the sign bit (bit-6)
b="${b}$(printf "\x$(printf "%x" $((asciic ^ 1 << 5)))\n")"
else
b="$b$c" ## otherwise copy as is
fi
i=$(expr $i + 1)
done
echo "output: $b" ## output resluting string
The case change is affected by relying on a simple bit-toggle of the case-bit (bit-6) in the ASCII value of each upper or lower case character to change it from lower to upper or vice-versa. (and note, you can exchange the printf and bit-shift for tr of asciic as an alternative)
Example Use/Output
$ sh togglecase.sh
input : This Is My 10TH String
output: tHIS iS mY 10th sTRING
When you want to swab every second characters case, try this:
read -p " Enter string " str
for i in `seq 0 ${#str}`; do
rem=$(($i % 2 ))
if [ $rem -eq 0 ]
then
printf "%s" "${str:$i:1}"
else
fr=${str:$i:1}
printf "%s" "$(tr '[:upper:][:lower:]' '[:lower:][:upper:]' <<< "${str:$i:1}")"
fi
done
echo
EDIT: Second solution
Switch case of str and merge the old and new string.
#!/bin/bash
str="part is lowercase & PART IS UPPERCASE"
str2=$(tr '[:upper:][:lower:]' '[:lower:][:upper:]' <<< "${str}")
str_chopped=$(sed -r 's/(.)./\1\n/g' <<< "${str}");
# Will have 1 additional char for odd length str
# str2_chopped_incorrect=$(sed -r 's/.(.)/\1\n/g' <<< "${str2}");
str2_chopped=$(fold -w2 <<< "${str2}" | sed -nr 's/.(.)/\1/p' );
paste -d '\n' <(echo "${str_chopped}") <(echo "${str2_chopped}") | tr -d '\n'; echo

substring extraction in bash

iamnewbie: this code is inefficient but it should extract the substring, the problem is with last echo statement,need some insight.
function regex {
#this function gives the regular expression needed
echo -n \'
for (( i = 1 ; i <= $1 ; i++ ))
do
echo -n .
done
echo -n '\('
for (( i = 1 ; i <= $2 ; i++ ))
do
echo -n .
done
echo -n '\)'
echo -n \'
}
# regex function ends
echo "Enter the string:"
read stg
#variable stg holds the string entered
if [ -z "$stg" ] ; then
echo "Null string"
exit
else
echo "Length of the $stg is:"
z=`expr "$stg" : '.*' `
#variable z holds the length of given string
echo $z
fi
echo "Enter the number of trailing characters to be extracted from $stg:"
read n
m=`expr $z - $n `
#variable m holds an integer value which is equal to total length - length of characters to be extracted
x=$(regex $m $n)
echo ` expr "$stg" : "$x" `
#the echo statement(above) is just printing a newline!! But not the result
What I intend to do with this code is, if I enter "racecar" and give "3" , it should display "car" which are the last three characters. Instead of displaying "car" its just printing a newline. Please correct this code rather than giving a better one.
Although you didn't ask for a better solution, it's worth mentioning:
$ n=3
$ stg=racecar
$ echo "${stg: -n}"
car
Note that the space after the : in ${stg: -n} is required. Without the space, the parameter expansion is a default-value expansion rather than a substring expansion. With the space, it's a substring expansion; -n is interpreted as an arithmetic expression (which means that n is interpreted as $n) and since the result is a negative number, it specifies the number of characters from the end to start the substring. See the Bash manual for details.
Your solution is based on evaluating the equivalent of:
expr "$stg" : '......\(...\)'
with an appropriate number of dots. It's important to understand what the above bash syntax actually means. It invokes the command expr, passing it three arguments:
arg 1: the contents of the variable stg
arg 2: :
arg 3: ......\(...\)
Note that there are no quotes visible. That's because the quotes are part of bash syntax, not part of the argument values.
If the value of stg had enough characters, the result of the above expr invocation would be to print out the 7th, 8th and 9th character of the value of stg`. Otherwise, it would print a blank line, and fail.
But that's not what you are doing. You're creating the regular expression:
'......\(...\)'
which has single quotes in it. Since single-quotes are not special characters in a regex, they match themselves; in other words, that pattern will match a string which starts with a single quote, followed by nine arbitrary characters, followed by another single quote. And if the string does match, it will print the three characters prior to the second single-quote.
Of course, since the regular expression you make has a . for every character in the target string, it won't match the target even if the target started and begun with a single-quote, since there would be too many dots in the regex to match that.
If you don't put single quotes into the regex, then your program will work, but I have to say that few times have I seen such an intensely circuitous implementation of the substring function. If you're not trying to win an obfuscated bash competition (a difficult challenge since most production bash code is obfuscated by nature), I'd suggest you use normal bash features instead of trying to do everything with regexen.
One of those is the syntax to determine the length of a string:
$ stg=racecar
$ echo ${#stg}
7
(although, as shown at the beginning, you don't actually even need that.)
What about:
$ n=3
$ string="racecar"
$ [[ "$string" =~ (.{$n})$ ]]
$ echo ${BASH_REMATCH[1]}
car
This looks for the last n characters at the end of the line. In a script:
#!/bin/bash
read -p "Enter a string: " string
read -p "Enter the number of characters you want from the end: " n
[[ "$string" =~ (.{$n})$ ]]
echo "These are the last $n characters: ${BASH_REMATCH[1]}"
You may want to add some more error handling, but this'll do it.
I'm not sure you need loops for this task. I wrote some example to get two parameters from user and cut the word according to it.
#!/bin/bash
read -p "Enter some word? " -e stg
#variable stg holds the string entered
if [ -z "$stg" ] ; then
echo "Null string"
exit 1
fi
read -p "Enter some number to set word length? " -e cutNumber
# check that cutNumber is a number
if ! [ "$cutNumber" -eq "$cutNumber" ]; then
echo "Not a number!"
exit 1
fi
echo "Cut first n characters:"
echo ${stg:$cutNumber}
echo
echo "Show first n characters:"
echo ${stg:0:$cutNumber}
echo "Alternative get last n characters:"
echo -n "$stg" | tail -c $cutNumber
echo
Example:
Enter some word? TheRaceCar
Enter some number to set word length? 7
Cut first n characters:
Car
Show first n characters:
TheRace
Alternative get last n characters:
RaceCar

parse and expand interval

In my script I need to expand an interval, e.g.:
input: 1,5-7
to get something like the following:
output: 1,5,6,7
I've found other solutions here, but they involve python and I can't use it in my script.
Solution with Just Bash 4 Builtins
You can use Bash range expansions. For example, assuming you've already parsed your input you can perform a series of successive operations to transform your range into a comma-separated series. For example:
value1=1
value2='5-7'
value2=${value2/-/..}
value2=`eval echo {$value2}`
echo "input: $value1,${value2// /,}"
All the usual caveats about the dangers of eval apply, and you'd definitely be better off solving this problem in Perl, Ruby, Python, or AWK. If you can't or won't, then you should at least consider including some pipeline tools like tr or sed in your conversions to avoid the need for eval.
Try something like this:
#!/bin/bash
for f in ${1//,/ }; do
if [[ $f =~ - ]]; then
a+=( $(seq ${f%-*} 1 ${f#*-}) )
else
a+=( $f )
fi
done
a=${a[*]}
a=${a// /,}
echo $a
Edit: As #Maxim_united mentioned in the comments, appending might be preferable to re-creating the array over and over again.
This should work with multiple ranges too.
#! /bin/bash
input="1,5-7,13-18,22"
result_str=""
for num in $(tr ',' ' ' <<< "$input"); do
if [[ "$num" == *-* ]]; then
res=$(seq -s ',' $(sed -n 's#\([0-9]\+\)-\([0-9]\+\).*#\1 \2#p' <<< "$num"))
else
res="$num"
fi
result_str="$result_str,$res"
done
echo ${result_str:1}
Will produce the following output:
1,5,6,7,13,14,15,16,17,18,22
expand_commas()
{
local arg
local st en i
set -- ${1//,/ }
for arg
do
case $arg in
[0-9]*-[0-9]*)
st=${arg%-*}
en=${arg#*-}
for ((i = st; i <= en; i++))
do
echo $i
done
;;
*)
echo $arg
;;
esac
done
}
Usage:
result=$(expand_commas arg)
eg:
result=$(expand_commas 1,5-7,9-12,3)
echo $result
You'll have to turn the separated words back into commas, of course.
It's a bit fragile with bad inputs but it's entirely in bash.
Here's my stab at it:
input=1,5-7,10,17-20
IFS=, read -a chunks <<< "$input"
output=()
for chunk in "${chunks[#]}"
do
IFS=- read -a args <<< "$chunk"
if (( ${#args[#]} == 1 )) # single number
then
output+=(${args[*]})
else # range
output+=($(seq "${args[#]}"))
fi
done
joined=$(sed -e 's/ /,/g' <<< "${output[*]}")
echo $joined
Basically split on commas, then interpret each piece. Then join back together with commas at the end.
A generic bash solution using the sequence expression `{x..y}'
#!/bin/bash
function doIt() {
local inp="${#/,/ }"
declare -a args=( $(echo ${inp/-/..}) )
local item
local sep
for item in "${args[#]}"
do
case ${item} in
*..*) eval "for i in {${item}} ; do echo -n \${sep}\${i}; sep=, ; done";;
*) echo -n ${sep}${item};;
esac
sep=,
done
}
doIt "1,5-7"
Should work with any input following the sample in the question. Also with multiple occurrences of x-y
Use only bash builtins
Using ideas from both #Ansgar Wiechers and #CodeGnome:
input="1,5-7,13-18,22"
for s in ${input//,/ }
do
if [[ $f =~ - ]]
then
a+=( $(eval echo {${s//-/..}}) )
else
a+=( $s )
fi
done
oldIFS=$IFS; IFS=$','; echo "${a[*]}"; IFS=$oldIFS
Works in Bash 3
Considering all the other answers, I came up with this solution, which does not use any sub-shells (but one call to eval for brace expansion) or separate processes:
# range list is assumed to be in $1 (e.g. 1-3,5,9-13)
# convert $1 to an array of ranges ("1-3" "5" "9-13")
IFS=,
local range=($1)
unset IFS
list=() # initialize result list
local r
for r in "${range[#]}"; do
if [[ $r == *-* ]]; then
# if the range is of the form "x-y",
# * convert to a brace expression "{x..y}",
# * using eval, this gets expanded to "x" "x+1" … "y" and
# * append this to the list array
eval list+=( {${r/-/..}} )
else
# otherwise, it is a simple number and can be appended to the array
list+=($r)
fi
done
# test output
echo ${list[#]}

Resources