This is my code for a bubble sort on n numbers:
#!/bin/bash
echo -n "Input n, the number of numbers"
read N
declare -a array[N]
echo -e "Input the elements, press enter after each element"
for i in seq 1 $N
do
read array[$i]
done
swap1()
{ # for swapping two numbers, we are employing bubble sort
local temp = ${array[$1]}
array[$1] = ${array[$2]}
array[$2]=$temp
return
}
numb_elements=${#array[#]}
let "comparisons = $numb_elements - 1"
count=1
while [ "$comparisons" -gt 0]
do
index =0
while[ "$index" -lt "$comparisons" ];do
if [ ${array[$index]} \> ${array[ 'expr $index + 1']} ]
then
swap1 $index 'expr $index + 1'
fi
let "index += 1" # Or, index+=1 on Bash, ver. 2.1 or newer
done
let "comparisons -=1"
echo
echo "$count: ${array[#]}
echo
let "count +=1"
done
exit 0
I have two problems with this code:
the input array just takes 3 numbers
and then i get an error on line 42 saying syntax error for the command while do
I have tried while [] ; do, but it doesn't work.
Its just been a day that i have been trying bash syntax.
Moreover do not write
for i in seq 1 $N
which iterate i over the set {"seq","1",$N}, but type
for i in $(seq 1 $N)
to insert the result of the command as part of code.
You forgot the closing quote in this line :
echo "$count: ${array[#]}
Also the code of the nested loops is badly indented, so it is a bit hard to read and debug.
So far I have found the following errors:
while [ "$comparisons" -gt 0 ]
^ missing space here
while [ "$index" -lt "$comparisons" ];do
^ missing space
echo "$count: ${array[#]}"
^ missing quote
Note that in bash [ is equivalent to test command, so a space is required around [ and ] unlike many other programming languages.
You made a series of errors:
correct spaces are fundamental to shell scripting
missing `` apices to execute code and get the output
logic error (starting inserting from the second array element and using it from the first one)
iterating the wrong number of time for the bubblesort alg
This is your code corrected.
#!/bin/bash
swap1() { # for swapping two numbers, we are employing bubble sort
local temp=${array[$1]}
array[$1]=${array[$2]}
array[$2]=$temp
return
}
echo -n "Input n, the number of numbers: "
read N
declare -a array[$N]
echo -e "Input the elements, press enter after each element"
for i in `seq 1 $N`
do
read array[$i]
done
numb_elements=${#array[#]}
#let "comparisons = $numb_elements - 1"
comparisons=$numb_elements
count=1
while [ "$comparisons" -gt 0 ]
do
index=1
while [ "$index" -lt "$comparisons" ]
do
tmp=`expr $index + 1`
if [ ${array[$index]} -gt ${array[$tmp]} ]
then
swap1 $index $tmp
fi
let "index += 1" # Or, index+=1 on Bash, ver. 2.1 or newer
done
let "comparisons -= 1"
echo
echo "$count: ${array[#]}"
echo
let "count += 1"
done
exit 0
Try this:
while [ "$comparisons" -gt 0]
should be (notice space before the closing bracket ]):
while [ "$comparisons" -gt 0 ]
Related
Is it possible to write a script that reads the file containing numbers (one per line) and writes their maximum, minimum and sum. If the file is empty, it will print an appropriate message. The name of the file is to be given as the parameter of the script. I mange to create below script, but there are 2 errors:
./4.3: line 20: syntax error near unexpected token `done'
./4.3: line 20: `done echo "Max: $max" '
Is it possible to add multiple files as parameter?
lines=`cat "$1" | wc -l`
if [ $lines -eq 0 ];
then echo "File $1 is empty!"
exit fi min=`cat "$1" | head -n 1`
max=$min sum=0
while [ $lines -gt 0 ];
do num=`cat "$1" |
tail -n $lines`
if [ $num -gt $max ];
then max=$num
elif [ $num -lt $min ];
then min=$num fiS
sum=$[ $sum + $num] lines=$[ $lines - 1 ]
done echo "Max: $max"
echo "Min: number $min"
echo "Sum: $sum"
Pretty compelling use of GNU datamash here:
read sum min max < <( datamash sum 1 min 1 max 1 < "$1" )
[[ -z $sum ]] && echo "file is empty"
echo "sum=$sum; min=$min; max=$max"
Or, sort and awk:
sort -n "$1" | awk '
NR == 1 { min = $1 }
{ sum += $1 }
END {
if (NR == 0) {
print "file is empty"
} else {
print "min=" min
print "max=" $1
print "sum=" sum
}
}
'
Here's how I'd fix your original attempt, preserving as much of the intent as possible:
#!/usr/bin/env bash
lines=$(wc -l "$1")
if [ "$lines" -eq 0 ]; then
echo "File $1 is empty!"
exit
fi
min=$(head -n 1 "$1")
max=$min
sum=0
while [ "$lines" -gt 0 ]; do
num=$(tail -n "$lines" "$1")
if [ "$num" -gt "$max" ]; then
max=$num
elif [ "$num" -lt "$min" ]; then
min=$num
fi
sum=$(( sum + num ))
lines=$(( lines - 1 ))
done
echo "Max: $max"
echo "Min: number $min"
echo "Sum: $sum"
The dealbreakers were missing linebreaks (can't use exit fi on a single line without ;); other changes are good practice (quoting expansions, useless use of cat), but wouldn't have prevented your script from working; and others are cosmetic (indentation, no backticks).
The overall approach is a massive antipattern, though: you read the whole file for each line being processed.
Here's how I would do it instead:
#!/usr/bin/env bash
for fname in "$#"; do
[[ -s $fname ]] || { echo "file $fname is empty" >&2; continue; }
IFS= read -r min < "$fname"
max=$min
sum=0
while IFS= read -r num; do
(( sum += num ))
(( max = num > max ? num : max ))
(( min = num < min ? num : min ))
done < "$fname"
printf '%s\n' "$fname:" " min: $min" " max: $max" " sum: $sum"
done
This uses the proper way to loop over an input file and utilizes the ternary operator in the arithmetic context.
The outermost for loop loops over all arguments.
You can do the whole thing in one while loop inside a shell script. Here's the bash version:
s=0
while read x; do
if [ ! $mi ]; then
mi=$x
elif [ $mi -gt $x ]; then
mi=$x
fi
if [ ! $ma ]; then
ma=$x
elif [ $ma -lt $x ]; then
ma=$x
fi
s=$((s+x))
done
if [ ! $ma ]; then
echo "File is empty."
else
echo "s=$s, mi=$mi, ma=$ma"
fi
Save that script into a file, and then you can use pipes to send as many input files into it as you wish, like so (assuming the script is called "mysum"):
cat file1 file2 file3 | mysum
or for a single file
mysum < file1
(Make sure, the script is executable and on the $PATH, otherwise use "./mysum" for the script in the current directory or indeed "bash mysum" if it isn't executable.)
The script assumes that the numbers are one per line and that there's nothing else on the line. It gives a message if the input is empty.
How does it work? The "read x" will take input from stdin line-by-line. If the file is empty, the while loop will never be run, and thus variables mi and ma won't be set. So we use this at the end to trigger the appropriate message. Otherwise the loop checks first if the mi and ma variables exist. If they don't, they are initialised with the first x. Otherwise it is checked if the next x requires updating the mi and ma found thus far.
Note that this trick ensures that you can feed-in any sequence of numbers. Otherwise you have to initialise mi with something that's definitely too large and ma with something that's definitely too small - which works until you encounter a strange number list.
Note further, that this works for integers only. If you need to work with floats, then you need to use some other tool than the shell, e.g. awk.
Just for fun, here's the awk version, a one-liner, use as-is or in a script, and it will work with floats, too:
cat file1 file2 file3 | awk 'BEGIN{s=0}; {s+=$1; if(length(mi)==0)mi=$1; if(length(ma)==0)ma=$1; if(mi>$1)mi=$1; if(ma<$1)ma=$1} END{print s, mi, ma}'
or for one file:
awk 'BEGIN{s=0}; {s+=$1; if(length(mi)==0)mi=$1; if(length(ma)==0)ma=$1; if(mi>$1)mi=$1; if(ma<$1)ma=$1} END{print s, mi, ma}' < file1
Downside: if doesn't give a decent error message for an empty file.
a script that reads the file containing numbers (one per line) and writes their maximum, minimum and sum
Bash solution using sort:
<file sort -n | {
read -r sum
echo "Min is $sum"
while read -r num; do
sum=$((sum+num));
done
echo "Max is $num"
echo "Sum is $sum"
}
Let's speed up by using some smart parsing using tee, tr and calculating with bc and if we don't mind using stderr for output. But we could do a little fifo and synchronize tee output. Anyway:
{
<file sort -n |
tee >(echo "Min is $(head -n1)" >&2) >(echo "Max is $(tail -n1)" >&2) |
tr '\n' '+';
echo 0;
} | bc | sed 's/^/Sum is /'
And there is always datamash. The following willl output 3 numbers, being sum, min and max:
<file datamash sum 1 min 1 max 1
You can try with a shell loop and dc
while [ $# -gt 0 ] ; do
dc -f - -e '
['"$1"' is empty]sa
[la p q ]sZ
z 0 =Z
# if file is empty
dd sb sc
# populate max and min with the first value
[d sb]sY
[d lb <Y ]sM
# if max keep it
[d sc]sX
[d lc >X ]sN
# if min keep it
[lM x lN x ld + sd z 0 <B]sB
lB x
# on each line look for max, min and keep the sum
[max for '"$1"' = ] n lb p
[min for '"$1"' = ] n lc p
[sum for '"$1"' = ] n ld p
# print summary at end of each file
' <"$1"
shift
done
I have a script to write that is able to take user input of however many numbers they want until they press "q". Now I have it working, but it takes q and uses it in the math that the other numbers should be being used in. It also uses it to show what is the highest or lowest number.
total=0
count=0
largest=num
smallest=num
while [ "$num" != "q"
do
echo "Enter your numbers when you are done enter q"
read num
total=`expr $total + $sum`
count=`expr $count + 1`
if [ "$num" > "$largest" ]
then
largest=$num
fi
if [ "$num" < "$smallest" ]
then
smallest=$num
fi
done
avg=`expr $total / $count`
echo "The largest number is: $largest"
echo "The smallest number is: $smallest"
echo "The sum of the numbers is: $total"
echo "The average of the numbers is: $avg"
You aren't checking if the first value of num is "q" before attempting to use it as a number. The easiest thing to do is write an "infinite" loop with an explicit break; this avoids needing two separate read commands.
> and < are for string comparisons (and need to be escaped when used with [); use -gt and -lt instead.
You also do not need to use expr for integer arithmetic. For the average, you'll need to use bc (or some other program that can do floating-point arithmetic).
total=0
count=0
largest=
smallest=
while : ; do
echo "Enter your numbers when you are done enter q"
read num
[ "$num" = q ] && break
total=$(($total + $sum))
count=$(($count + 1))
if [ -z "$largest" ] || [ "$num" -gt "$largest" ]; then
largest=$num
fi
if [ -z "$smallest" ] || [ "$num" < "$smallest" ]; then
smallest=$num
fi
done
# Avoid division by 0 and meaningless statistics if
# no numbers are entered.
if [ "$count" -gt 0 ]; then
avg=$( echo "$total / $count" | bc )
echo "The largest number is: $largest"
echo "The smallest number is: $smallest"
echo "The sum of the numbers is: $total"
echo "The average of the numbers is: $avg"
fi
total=0
count=0
largest=num
smallest=num
echo "Enter your numbers when you are done enter q"
read num
while [ "$num" != "q"
do
total=`expr $total + $sum`
count=`expr $count + 1`
if [ "$num" > "$largest" ]
then
largest=$num
fi
if [ "$num" < "$smallest" ]
then
smallest=$num
fi
echo "Enter your numbers when you are done enter q"
read num
done
avg=`expr $total / $count`
echo "The largest number is: $largest"
echo "The smallest number is: $smallest"
echo "The sum of the numbers is: $total"
echo "The average of the numbers is: $avg"
I have a simple script in Bash to read a number in a file and then compare it with a different threshold. The output is this:
: integer expression expected
: integer expression expected
OK: 3
My code is this:
#!/bin/bash
wget=$(wget http://10.228.28.8/ -O /tmp/wget.txt 2>/dev/null)
output=$(cat /tmp/wget.txt | awk 'NR==6')
#output=7
echo $output
if [ $output -ge 11 ];then
echo "CRITICAL: $output"
exit 2
elif [ $output -ge 6 ] && [ $output -lt 11 ];then
echo "WARNING: $output"
exit 1
else
echo "OK: $output"
exit 0
fi
rm /tmp/wget.txt
I know what is the problem, I know that I'm reading a string and I try to compare a int. But I don't know how can I do to read this file and convert the number to read in a int var..
Any ideas?
The problem occurs when $output is the empty string; whether or not you quote the expansion (and you should), you'll get the integer expression required error. You need to handle the empty string explictly, with a default value of zero (or whatever default makes sense).
wget=$(wget http://10.228.28.8/ -O /tmp/wget.txt 2>/dev/null)
output=$(awk 'NR==6' < /tmp/get.txt)
output=${output:-0}
if [ "$output" -ge 11 ];then
echo "CRITICAL: $output"
exit 2
elif [ "$output" -ge 6 ];then
echo "WARNING: $output"
exit 1
else
echo "OK: $output"
exit 0
fi
(If you reach the elif, you already know the value of $output is less than 11; there's no need to check again.)
The problem also occurs, and is consistent with the error message, if output ends with a carriage return. You can remove that with
output=${output%$'\r'}
There are a couple of suggestions from my side regarding your code.
You could explicitly tell bash the output is an integer
declare -i output # See [1]
Change
output=$(cat /tmp/wget.txt | awk 'NR==6') # See [2]
may be better written as
output=$(awk 'NR==6' /tmp/wget.txt )
Change
if [ $output -ge 11 ]
to
if [ "0$output" -ge 11 ] # See [4]
or
if (( output >= 11 )) # Better See [3]
References
Check bash [ declare ].
Useless use of cat. Check [ this ]
Quoting [ this ] answer :
((...)) enable you to omit the dollar signs on integer and array variables and include spaces around operators for readability. Also empty variable automatically defaults to 0 in such a statement.
The zero in the beginning of "0$output" help you deal with empty $output
Interesting
Useless use of cat is a phrase that has been resounding in SO for long. Check [ this ]
[ #chepner ] has dealt with the empty output fiasco using [ bash parameter expansion ] in his [ answer ], worth having a look at.
A simplified script:
#!/bin/bash
wget=$(wget http://10.228.28.8/ -O /tmp/wget.txt 2>/dev/null)
output=$(awk 'NR==6' </tmp/wget.txt )
output="$(( 10#${output//[^0-9]} + 0 ))"
(( output >= 11 )) && { echo "CRITICAL: $output"; exit 2; }
(( output >= 6 )) && { echo "WARNING: $output"; exit 1; }
echo "OK: $output"
The key line to cleanup any input is:
output="$(( 10#${output//[^0-9]} + 0 ))"
${output//[^0-9]} Will leave only digits from 0 to 9 (will remove all non-numeric chars).
10#${output//[^0-9]} Will convert output to a base 10 number.
That will correctly convert numbers like 0019
"$(( 10#${output//[^0-9]} + 0 ))" Will produce a zero for a missing value.
Then the resulting number stored in output will be compared to limits and the corresponding output will be printed.
In BASH, It is a good idea to use double brackets for strings:
if [[ testing strings ]]; then
<whatever>
else
<whatever>
fi
Or double parenthesis for integers:
if (( testing ints )); then
<whatever>
else
<whatever>
fi
For example try this:
var1="foo bar"
if [ $var1 == 'foo bar' ]; then
echo "ok"
fi
Result:
$ bash: [: too many arguments
Now, this:
var2="foo bar"
if [[ $a == "foo bar" ]]; then
echo "ok"
fi
Result:
ok
For that, your code in BASH:
if [[ $output -ge 11 ]]; then
echo "CRITICAL: $output"
exit 2
elif [[ $output -ge 6 ]]; then
echo "WARNING: $output"
exit 1
else
echo "OK: $output"
exit 0
fi
I am trying to perform a variable swap where the user enters 2 different numbers and a check is performed to make sure that the second number "end" is bigger then the first number "start". If the starting number is bigger then the end number it will perform a swap so that the later calculations will work.
The error that I am getting is:
./Boot.sh: line 94: [: -ne: unary operator expected
if [ $end -gt $start ]
then
start=$swapper
end=$start
swapper=$end
fi
This is the full code as I seem to have made a few mistakes by the comments, I have just tried double brackets and am still having the same error.
{
counter=0
while :
do
if [ $counter -gt '0' ]
then
printf "\nWould you like to continue terminating processes?\n"
echo "Type 1 to contiue or 0 to exit"
read endProgram
if [ $endProgram -ne '1' ]
then
break
fi
fi
echo "Welcome to Max process killer"
while :
do
echo "Enter the first number of the process range"
read start
echo "Enter the last number of the process range"
read end
if [ $start -le '3' -o $end -le '3' ]
then
echo "Your first and last pid IDs must be geater then 3"
read -n 1
break
if [[ $end -gt $start ]]
then
start=$swapper
end=$start
swapper=$end
fi
fi
printf "\nAre you sure you would like to terminate these processes?\n"
echo "Enter 1 to confirm or 0 to cancel"
read confirm
if [ $read -ne '1' ]
then
break
fi
while [ $start -le $end ]
do
kill -9 "$start"
echo "Process ID:"$start "has been terminated"
start=$(( $start + 1 ))
done
break
done
counter=$(($counter + 1))
done
}
Your code:
read confirm
if [ $read -ne '1' ]
You read input to variable confirm and not to variable read. Variable read is empty and you get the error:
./Boot.sh: line 94: [: -ne: unary operator expected
Hint: Use a default value (e.g. 0) to avoid an empty variable:
if [ "${confirm:-0}" -ne '1' ]
From man bash:
${parameter:-word}: Use Default Values. If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted.
${parameter:=word}: Assign Default Values. If parameter is unset or null, the expansion of word is assigned to parameter. The value of parameter is then substituted. Positional parameters and special parameters may not be assigned to in this way.
Try using double brackets. They are generally safer. Here's a good wiki page comparing [[]] to [].
if [[ $end -gt $start ]]
then
swapper=$start
start=$end
end=$swapper
fi
My sample code is here
#!/bin/bash
file="output2.txt"
numbers="$(cut -d',' -f2 output2.txt)"
lines="$(cut -f2 output2.txt)"
hours="$(cut -d',' -f1 output2.txt)"
array_numbers=( $numbers )
lines_array=( $lines )
hours_array=( $hours )
difference=$1
let range=$1-1000
for (( i = 0 ; i < ${#array_numbers[#]} ; i++ ))
do
let num=$(( 10#${array_numbers[$i+1]} - 10#${array_numbers[$i]} ))
if [ $num -gt $1 ]
then
echo ${lines_array[$i+1]} "and" ${lines_array[$i]} "has a difference more than $1"
elif [ $num -ge 0 ] && [ $num -lt $range ]
then
echo ${lines_array[$i+1]} "and" ${lines_array[$i]} "has a difference more than $1"
elif [ $num -le $1 ]
then
if [${hours_array[$i+1]} != ${hours_array[$i]}]
then
echo ${lines_array[$i+1]} "and" ${lines_array[$i]} "has a difference more than one second"
fi
fi
done
I'm working with the same output2.txt again:
12:43:40,317
12:43:40,318
12:43:40,332
12:43:40,333
12:43:40,334
12:43:40,335
12:43:40,336
12:43:40,337
12:43:40,338
12:43:40,339
12:43:40,353
12:43:40,354
12:43:40,356
12:43:40,358
12:43:40,360
12:43:40,361
12:43:40,362
12:43:40,363
12:43:40,364
12:43:40,365
12:43:40,382
12:43:40,384
12:43:40,385
12:43:40,387
12:43:40,388
12:43:40,389
12:43:40,390
12:43:40,391
12:43:40,404
12:43:40,405
12:43:40,406
12:43:40,407
12:43:40,408
12:43:40,409
12:43:40,410
12:43:40,412
12:43:40,413
12:43:40,414
12:43:40,415
12:43:40,428
12:43:40,429
12:43:40,431
12:43:40,432
12:43:40,433
12:43:40,434
12:43:40,435
12:43:40,436
12:43:40,437
12:43:40,438
12:43:40,440
12:43:40,443
12:43:40,458
12:43:40,459
12:43:40,460
12:43:40,461
12:43:40,462
12:43:40,463
12:43:40,464
12:43:40,465
12:43:40,466
12:43:40,479
12:43:40,480
12:43:40,481
12:43:40,482
12:43:40,483
12:43:40,484
12:43:40,485
12:43:40,486
12:43:40,487
12:43:40,501
12:43:40,503
12:43:40,504
12:43:40,505
12:43:40,506
12:43:40,509
12:43:40,510
12:43:40,511
12:43:40,512
12:43:40,513
12:43:40,514
12:43:40,515
12:43:40,517
12:44:40,518
What I want to do is take the difference as parameter and if there is a value difference more than 100 miliseconds than I'm wanna print output. The parts
for (( i = 0 ; i < ${#array_numbers[#]} ; i++ ))
do
let num=$(( 10#${array_numbers[$i+1]} - 10#${array_numbers[$i]} ))
if [ $num -gt $1 ]
then
echo ${lines_array[$i+1]} "and" ${lines_array[$i]} "has a difference more than $1"
elif [ $num -ge 0 ] && [ $num -lt $range ]
then
echo ${lines_array[$i+1]} "and" ${lines_array[$i]} "has a difference more than $1"
are actually working well , but i realized that if input has such a columns in order like the last part
12:43:40,517
12:44:40,518
it won't print anything so i put the last elif statement to my code but even it prints hours_array good, it doesn't work with while i'm comparing them. The output is always :
script.sh: line 22: [12:43:00: command not found
Why doesn't it accept this compare or is the problem is about my bash version ?
Thank you in advance for your help.
Add space before and after [. It is an 'alias' to the test buitin command.
You should also add double quote " around your variable. Because if they are empty, bash won't recognize them as a empty word.
And I generally use double brackets [[ for test condition which is more safer and has more features.
Example:
if [[ "${hours_array[$i+1]}" != "${hours_array[$i]}" ]]
You need a space here (the [ is a command)
if [ ${hours_array[$i+1]} != ${hours_array[$i]} ]
Missing space after [. [ is a command, so it needs to be separated from its arguments.
if [ ${hours_array[$i+1]} != ${hours_array[$i]} ]
I find few things that can be changed in this code.
Add space after [ and before ].
Add double quotes so that in case if variable is empty, script does not throw error.
if [ "${hours_array[$i+1]}" != "${hours_array[$i]}" ]
Also when you reach the last line, $i + 1 will fail. Hence, following would be better.
for (( i = 0 ; i < ${#array_numbers[#]} - 1 ; i++ ))