Bash Script Compare and Run a Command - bash

I have a shell script that gives me a txt file containing certain numbers.
For instance, " 48 347 345 221 1029 3943 1245 7899 " .
It only contains one line.
I want to trigger another shell script if one of those numbers exceeds 500.
How can and compare the numbers and run the shell script?
Thanks in advance
cat text.txt | if [ awk '{print $1}' -ge 500 ] then command.sh fi

Using awk you can try this:
awk '{for(i=1;i<=NF;i++)if($i > 500)exit 1}' text.txt || ./command.sh
command.sh will be executed if the awk command exits with a non-zero code because of the || operator.

Throw the file contents into an array -
$: declare -a lst=( $(<text.txt) )
Then run a quick loop. If you want to run the command for each hit,
$: for n in "${lst[#]}"
do (( n > 500 )) && echo "command.sh # $n > 500"
done
command.sh # 1029 > 500
command.sh # 3943 > 500
command.sh # 1245 > 500
command.sh # 7899 > 500
If you just want a quick and dirty version,
$: for n in $(<text.txt); do (( n > 500 )) && command.sh; done
But I recommend you take the time to do the horribly complicated 2 steps, lol
If you want to run it just once if any number is over 500,
$: for n in "${lst[#]}"
do (( n > 500 )) && { echo "command.sh # $n > 500"; break; }
done
command.sh # 1029 > 500
And don't use cat like that. Try to never use cat like that.
If you *REALLY needed the file as input to an if, then do this:
if condition
then action
fi < text.txt

You could use a simple for loop like this:
for n in $(cat test.txt)
do
if [ $n -gt 500 ]; then
something.sh
fi
done

You can simply :
[[ "48 347 345 221 500" =~ ([ ][5-9][0-9][0-9]|[ ][1-9][0-9]{3,}) ]] && echo "Hi"
Hi
[[ "48 347 345 221" =~ ([ ][5-9][0-9][0-9]|[ ][1-9][0-9]{3,}) ]] && echo "Hi"
Hope this helps!

When you don't want to convert the sring to numbers, it will become nasty.
Look for a number with at least 4 digits (first not 0), or a number with 3 digits, first 5 or higher will give
# incorrect, includes 500
grep -Eq "([1-9][0-9]|[5-9])[0-9][0-9]" text.txt && command.sh
This will also perform command.sh with number 500.
# incorrect, will fail for 5000 and 1500
grep -E "([1-9][0-9]|[5-9])[0-9][0-9]" text.txt | grep -qv 500 && command.sh
Fixing it becomes too complex. awk seems to be the most natural way.
An artificial way is converted the input to lines with one number and add an extra line with the border:
(echo "500 border"; tr ' ' '\n' < text.txt) |
sort -n |
tail -1 |
grep -qv "border" && command.sh

Related

Arithmetic operation fails in Shell script

Basically I'm trying to check if there are any 200 http responses in the log, in last 3 line. but I'm getting the below error. Because of this the head command is failing..Please help
LINES=`cat http_access.log |wc -l`
for i in $LINES $LINES-1 $LINES-2
do
echo "VALUE $i"
head -$i http_access.log | tail -1 > holy.txt
temp=`cat holy.txt| awk '{print $9}'`
if [[ $temp == 200 ]]
then
echo "line $i has 200 code at "
cat holy.txt | awk '{print $4}'
fi
done
Output:
VALUE 18
line 18 has 200 code at [21/Jan/2018:15:34:23
VALUE 18-1
head: invalid trailing option -- - Try `head --help' for more information.
Use $((...)) to perform arithmetic.
for i in $((LINES)) $((LINES-1)) $((LINES-2))
Without it, it's attempting to run the commands:
head -18 http_access.log
head -18-1 http_access.log
head -18-2 http_access.log
The latter two are errors.
A more flexible way to write the for loop would be using C-style syntax:
for ((i = LINES - 2; i <= LINES; ++i)); do
...
done
You got the why from JohnKugelman's answer, I will just propose a simplified code that might work for you:
while read -ra fields; do
[[ ${fields[9]} = 200 ]] && echo "Line ${fields[0]} has 200 code: ${fields[4]}"
done < <(cat -n http_access.log | tail -n 3 | tac)
cat -n: Numbers lines of the file
tail -n 3: Prints 3 last lines. You can just change this number for more lines
tac: Prints the lines outputted by tail in reversed order
read -ra fields: Reads the fields into an array $fields
${fields[0]}: The line number
${fields[num_of_field]}: Individual fields
You can also use wc instead of numbering using cat -n. For larger inputs, this will be slightly faster:
lines=$(wc -l < http_access.log)
while read -ra fields; do
[[ ${fields[8]} = 200 ]] && echo "Line $lines has 200 code: ${fields[3]}"
((lines--))
done < <(tail -n 3 http_access.log | tac)

Output a file in two columns in BASH

I'd like to rearrange a file in two columns after the nth line.
For example, say I have a file like this here:
This is a bunch
of text
that I'd like to print
as two
columns starting
at line number 7
and separated by four spaces.
Here are some
more lines so I can
demonstrate
what I'm talking about.
And I'd like to print it out like this:
This is a bunch and separated by four spaces.
of text Here are some
that I'd like to print more lines so I can
as two demonstrate
columns starting what I'm talking about.
at line number 7
How could I do that with a bash command or function?
Actually, pr can do almost exactly this:
pr --output-tabs=' 1' -2 -t tmp1
↓
This is a bunch and separated by four spaces.
of text Here are some
that I'd like to print more lines so I can
as two demonstrate
columns starting what I'm talking about.
at line number 7
-2 for two columns; -t to omit page headers; and without the --output-tabs=' 1', it'll insert a tab for every 8 spaces it added. You can also set the page width and length (if your actual files are much longer than 100 lines); check out man pr for some options.
If you're fixed upon “four spaces more than the longest line on the left,” then perhaps you might have to use something a bit more complex;
The following works with your test input, but is getting to the point where the correct answer would be, “just use Perl, already;”
#!/bin/sh
infile=${1:-tmp1}
longest=$(longest=0;
head -n $(( $( wc -l $infile | cut -d ' ' -f 1 ) / 2 )) $infile | \
while read line
do
current="$( echo $line | wc -c | cut -d ' ' -f 1 )"
if [ $current -gt $longest ]
then
echo $current
longest=$current
fi
done | tail -n 1 )
pr -t -2 -w$(( $longest * 2 + 6 )) --output-tabs=' 1' $infile
↓
This is a bunch and separated by four spa
of text Here are some
that I'd like to print more lines so I can
as two demonstrate
columns starting what I'm talking about.
at line number 7
… re-reading your question, I wonder if you meant that you were going to literally specify the nth line to the program, in which case, neither of the above will work unless that line happens to be halfway down.
Thank you chatraed and BRPocock (and your colleague). Your answers helped me think up this solution, which answers my need.
function make_cols
{
file=$1 # input file
line=$2 # line to break at
pad=$(($3-1)) # spaces between cols - 1
len=$( wc -l < $file )
max=$(( $( wc -L < <(head -$(( line - 1 )) $file ) ) + $pad ))
SAVEIFS=$IFS;IFS=$(echo -en "\n\b")
paste -d" " <( for l in $( cat <(head -$(( line - 1 )) $file ) )
do
printf "%-""$max""s\n" $l
done ) \
<(tail -$(( len - line + 1 )) $file )
IFS=$SAVEIFS
}
make_cols tmp1 7 4
Could be optimized in many ways, but does its job as requested.
Input data (configurable):
file
num of rows borrowed from file for the first column
num of spaces between columns
format.sh:
#!/bin/bash
file=$1
if [[ ! -f $file ]]; then
echo "File not found!"
exit 1
fi
spaces_col1_col2=4
rows_col1=6
rows_col2=$(($(cat $file | wc -l) - $rows_col1))
IFS=$'\n'
ar1=($(head -$rows_col1 $file))
ar2=($(tail -$rows_col2 $file))
maxlen_col1=0
for i in "${ar1[#]}"; do
if [[ $maxlen_col1 -lt ${#i} ]]; then
maxlen_col1=${#i}
fi
done
maxlen_col1=$(($maxlen_col1+$spaces_col1_col2))
if [[ $rows_col1 -lt $rows_col2 ]]; then
rows=$rows_col2
else
rows=$rows_col1
fi
ar=()
for i in $(seq 0 $(($rows-1))); do
line=$(printf "%-${maxlen_col1}s\n" ${ar1[$i]})
line="$line${ar2[$i]}"
ar+=("$line")
done
printf '%s\n' "${ar[#]}"
Output:
$ > bash format.sh myfile
This is a bunch and separated by four spaces.
of text Here are some
that I'd like to print more lines so I can
as two demonstrate
columns starting what I'm talking about.
at line number 7
$ >

How to simplify the syntax needed for 10 >= x >= 1 in BASH conditionals?

I use this BASH script to check if file.txt has between 1 and 10 lines:
if [[ `wc -l file.txt | awk '{ print $1 }'` -le "10" && `wc -l file.txt | awk '{ print $1}'` -ge "2" ]]
echo "It has between 1 and 10 lines."
fi
This code is too verbose. If I make a change to one part, it is easy to forget to make a change to the repeated part.
Is there a way to simplify the syntax?
One option would be to do the whole thing using awk:
awk 'END{if(1<=NR&&NR<=10) print "It has between 1 and 10 lines."}' file.txt
As pointed out in the comments (thanks rici), you might want to prevent awk from processing the rest of your file once it has read 10 lines:
awk 'NR>10{exit}END{if(1<=NR&&NR<=10) print "It has between 1 and 10 lines."}' file.txt
The END block is still processed if exit is called, so it is still necessary to have both checks in the if.
Alternatively, you could store the result of wc -l to a variable in bash:
lines=$(wc -l < file.txt)
(( 1 <= lines && lines <= 10)) && echo "It has between 1 and 10 lines."
Note that redirecting the file into wc means that you just get the number without the filename.
Get the line count, then check it against the range bounds:
lc=$(wc -l < file.txt)
if (( 1 <= lc && lc <= 10 )); then
echo "It has between 1 and 10 lines"
fi

Replacing numbers with SED

I'm trying to replace numbers from -20 to 30 using sed, but it adds "v" character. What's wrong?
For example: SINR=-18, output must be "c", but output is "vc".
I tryed to delete 1st character, but it returns 1 instead of j.
SINR=`curl -s http://10.0.0.1/status | awk '/3GPP.SINR=/ {print $0}' | awk -F "3GPP.SINR=" '{print $2}'` # returns number
echo $SINR | sed "s/-20/a/;s/-19/b/;s/-18/c/;s/-17/d/;s/-16/e/;s/-15/f/;s/-14/g/;s/-13/h/;s/-12/i/;s/-11/j/;s/-10/k/;s/-9/l/;s/-8/m/;s/-7/n/;s/-6/o/;s/-5/p/;s/-4/q/;s/-3/r/;s/-2/s/;s/-1/t/;s/0/u/;s/1/v/;s/2/w/;s/3/x/;s/4/y/;s/5/z/;s/6/A/;s/7/B/;s/8/C/;s/9/D/;s/10/E/;s/11/F/;s/12/G/;s/13/H/;s/14/I/;s/15/J/;s/16/K/;s/17/L/;s/18/M/;s/19/N/;s/20/O/;s/21/P/;s/22/Q/;s/23/R/;s/24/S/;s/25/T/;s/26/U/;s/27/V/;s/28/W/;s/29/X/;s/30/Y/"
This way would be more elegant and less error-prone:
echo $SINR | awk 'BEGIN { chars="abcdefg" } { print substr(chars, $1 + 21, 1) }'
Of course, chars should contain all the letters you need for the mapping. That is, all the way until ...VWXY as in your example, I just wrote until g to keep it short and sweet.
With this solution your problem disappears.
You don't really need sed or awk if you have bash like you say you do. You can use arrays, which is maybe even less error-prone ;-)
map=({a..z} {A..Z}) # Create map of your characters
SINR=-18 # Set your SINR number to something
SINR=$(($SINR+20)) # Add an offset to get to right place
result=${map[$SINR]} # Lookup your result
echo $result # Print it
c
If you have a mapping process, you're surely better off building a switch statement, a couple of if's, or even using bash associative arrays (bash >= 4.0). For example, you could tackle your problem with the following snippet:
function mapper() {
if [[ $1 -ge -20 && $1 -le 5 ]]; then
printf \\$(printf '%03o' $(( $1 + 117 )) )
elif [[ $1 -ge 6 && $1 -le 30 ]]; then
printf \\$(printf '%03o' $(( $1 + 59 )) )
else
echo ""; return 1
fi
return 0
}
And use like below:
$ mapper -20
a
$ mapper 5
z
$ mapper 6
A
$ mapper 30
Y
$ mapper $SINR
c
echo "${SINR}" | sed 's/-20/a/;t;s/-19/b/;t;s/-18/c/;t;s/-17/d/;t;s/-16/e/;t;s/-15/f/;t;s/-14/g/;t;s/-13/h/;t;s/-12/i/;t;s/-11/j/;t;s/-10/k/;t;s/-9/l/;t;s/-8/m/;t;s/-7/n/;t;s/-6/o/;t;s/-5/p/;t;s/-4/q/;t;s/-3/r/;t;s/-2/s/;t;s/-1/t/;t;s/0/u/;t;s/1/v/;t;s/2/w/;t;s/3/x/;t;s/4/y/;t;s/5/z/;t;s/6/A/;t;s/7/B/;t;s/8/C/;t;s/9/D/;t;s/10/E/;t;s/11/F/;t;s/12/G/;t;s/13/H/;t;s/14/I/;t;s/15/J/;t;s/16/K/;t;s/17/L/;t;s/18/M/;t;s/19/N/;t;s/20/O/;t;s/21/P/;t;s/22/Q/;t;s/23/R/;t;s/24/S/;t;s/25/T/;t;s/26/U/;t;s/27/V/;t;s/28/W/;t;s/29/X/;t;s/30/Y/'
Use the t after s// to accelerate a bit.
vc is normaly not occuring if SINR is just a number like specified

Ping hundreds in one script

I want to ping some servers on a game, they are all in the same format, only there are possibly hundreds of them. This is what I currently use:
ping server1.servername.com -n 20 | grep Minimum | awk '{print $3}' | sed s/,// >> Output.txt
That will ping the server 20 times, and chop off everything but the minimum ping amount. If I wanted to ping 300 of these servers, I would have to paste that same line 300 times... Is it possible to have it specify just something like 1-300 in one line without needing 300 lines of the same thing?
rojo#aspire:~$ help for
<snip...>
for ((: for (( exp1; exp2; exp3 )); do COMMANDS; done
Arithmetic for loop.
Equivalent to
(( EXP1 ))
while (( EXP2 )); do
COMMANDS
(( EXP3 ))
done
EXP1, EXP2, and EXP3 are arithmetic expressions. If any expression is
omitted, it behaves as if it evaluates to 1.
Try something like this:
for (( x=1; $x<=300; x++ )); do ( ping server$x.servername.com -n 20 | grep Minimum | awk '{print $3}' | sed s/,// >> Output.txt ); done
Update:
Here's the hackish idea I mentioned in my comments to this answer below. Caveat: I think my ping command must be different from yours. I'm composing this idea on a Debian machine.
Instead of -n count my ping syntax is -c count, and instead of a line containing "Minimum" I have "min/avg/max/mdev". So you might need to play with the grep syntax and so on. Anyway, with that in mind, modify the following as needed to perform a ping of each server in sequence from 1 to whatever until error.
#!/bin/bash
i=0
while [ $? -eq 0 ] && i=$(( i + 1 )); do (
echo -n "server$i min: "
ping server$i.servername.com -c 20 -i 0.2 | grep -P -o -e '(?<=\= )\d\.\d+'
); done
echo "n/a"
Basically in plain English, that means while exit code = 0 and increment i, echo the server name without a line break and ping it 20 times at 200ms interval, completing the echoed line with (scraping from the ping results) a decimal number preceded by an equal-space. (That pattern matches the minimum ping time result in the summary for Linux iputils ping.) If the ping fails, exit code will not equal 0 and the loop will break.
You can use loops:
while read line
do
ping $line.servername.com -n 20 | grep Minimum | awk '{print $3}' | sed s/,// >> Output.txt
done < servers_list
Sounds like a job for xargs, e.g.,
$ cat server-list | xargs -I% ping % -n 20 ...

Resources