Compare two decimals and print similarities in Bash? - bash

I'm working on a script that compares two very similar decimal numbers and I want the script to print the portion that the two numbers share. For example, suppose I have the numbers 42.86579 and 42.84578. Since both numbers have the 42.8 portion in common, I would like the script to output 42.8. How should I go about implementing this?

You could search for the longest common prefix in two strings with sed:
$ x=42.86579
$ y=42.84578
$ sed "s/\(.*\).* \1.*/\1/" <<< "$x $y"
42.8
Or slightly more concisely using GNU grep:
$ grep -Po '(.*).* \K\1' <<< "$x $y"
42.8

a=42.86579
b=42.84578
[[ ${a%.*} != ${b%.*} ]] && exit
for ((i=0;i<${#a};i++)); do
if [[ ${a:$i:1} == ${b:$i:1} ]]; then
echo -n ${a:$i:1}
else
break
fi
done
Output:
42.8
See: Bash's parameter expansion

Related

Compare length of sound file in bash

I want to have a shell script, that checks the length of a sound file and check if the length is shorter, than a specified length. But I keep getting an error message "command not found" at the if-statement.
#!/bin/bash
soundlength=$(soxi -D $1)
enter code here
if [$soundlength < $2]
then
# do something
fi
I am guessing the $soundlength is a string and it's failing to compare string to int, but I can't find a solution to this.
Thanks in advance for all answers.
The problem is that soxi returns 0 or a float - example 27.741995, therefore you will need bc or awk to check if the output is bigger than N, this because bash don't support floats.
Here is an example with bc:
#!/bin/bash
soundlength=$(soxi -D $1)
if [ 1 -eq "$(echo "${soundlength} > ${2}" | bc)" ]; then
echo "${soundlength} is > than ${2}"
fi
And here is an example with AWK:
#!/bin/bash
soundlength=$(soxi -D $1)
if awk 'BEGIN{exit ARGV[1]>ARGV[2]}' "$z" "$y"; then
echo "${soundlength} is > than ${2}"
fi
If you don't want to use either bc of awk you could give a try to zsh shell, it supports floats.

how to remove leading zeros from negative numbers in shell

Is there any simple way to remove leading zeros from a negative number in shell?
For example : for a number like -02, the output will be -2
There a multiply ways to do this:
a="-02"
echo "$((a+0))"
Another with regex:
a="-02"
echo "${a//-0/-}"
Or
a="-02"
[[ "$a" =~ ^(-*|\+*)0*(.*)$ ]]
echo "${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
And bc:
a="-02"
bc <<< "$a + 0"
What about using the builtin printf?
$ num=-02
$ printf "%d\n" "$num"
-2
One solution as I know is the following :
echo -02 | awk '{$0=int($0)}1'
but it only works with integer number. For floating is there any way?

bash longest common part of two string

I have the following strings: "abcdefx", "zzdefghij"
I would like to extract the common part of the two strings, i.e. here "def".
I tried with sed but I couldn't do that unless the common part was a prefix like this:
fprint "%s\n%\n" | sed -e 'N;s:\(.*\).*\n\1.*:\1:'
This pure bash script will find the first longest substring of its two arguments, in a fairly efficient way:
#!/bin/bash
if ((${#1}>${#2})); then
long=$1 short=$2
else
long=$2 short=$1
fi
lshort=${#short}
score=0
for ((i=0;i<lshort-score;++i)); do
for ((l=score+1;l<=lshort-i;++l)); do
sub=${short:i:l}
[[ $long != *$sub* ]] && break
subfound=$sub score=$l
done
done
if ((score)); then
echo "$subfound"
fi
Demo (I called the script banana):
$ ./banana abcdefx zzdefghij
def
$ ./banana "I have the following strings: abcdefx, zzdefghij I would like to extract the common part of the two strings, i.e. here def." "I tried with sed but I couldn't do that unless the common part was a prefix like this"
the common part
I thought this sounded interesting, here is my solution:
first="abcdefx"
second="zzdefghij"
for i in $(seq ${#first} -1 1); do
for j in $(seq 0 $((${#first}-i))); do
grep -q "${first:$j:$i}" <<< "$second" && match="${first:$j:$i}" && break 2
done
done
echo "Longest common substring: ${match:-None found}"
Output:
Longest common substring: def

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[#]}

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