Using line count as a counter to break a while loop in Bash - bash

Is it possible to use a line count to break a while loop in bash?
I have the following code but it doesn't break and infinitely runs:
wc -l Database.faa > counter
perl -p -e 's/ /\t/g' counter | cut -f6 > temp; mv temp counter
while
($counter > '0')
do
# some commands
wc -l Database.faa > counter
perl -p -e 's/ /\t/g' counter | cut -f6 > temp; mv temp counter
done
My program reduces Database.faa with each run but when Database.faa is empty it continues to run. Can anyone help?
Thanks.

In your conditional $counter does not look at a file counter which is seemingly what you're trying to use in the condition. Also, ( isn't a traditional way to test values, though you can use the exit code of the subshell you're creating. You're probably more interested in [ which is the same as the test command. Finally, for this part, when using test > is not a comparison operator, it's a redirection operator. man test shows the comparison you can do, and for int comparisons you'd use -gt instead. So putting that together we can do your loop like
counter=$(wc -l Database.faa | cut -f1 -d" ")
while [ $counter -gt 0 ]; do
# some commands
counter=$(wc -l Database.faa | cut -f1 -d" ")
done
or, you could just test if the file is empty like
while [ -s Database.faa ]; do
# some commands
done

Related

Counting all the 5 from a specific range in Bash

I want to count how many times the digit "5" appears from the range 1 to 4321. For example, the number 5 appears 1 or the number 555, 5 would appear 3 times etc.
Here is my code so far, however, the results are 0, and they are supposed to be 1262.
#!/bin/bash
typeset -i count5=0
for n in {1..4321}; do
echo ${n}
done | \
while read -n1 digit ; do
if [ `echo "${digit}" | grep 5` ] ; then
count5=count5+1
fi
done | echo "${count5}"
P.s. I am looking to fix my code so it can print the right output. I do not want a completely different solution or a shortcut.
What about something like this
seq 4321 | tr -Cd 5 | wc -c
1262
Creates the sequence, delete everything but 5's and count the chars
The main problem here is http://mywiki.wooledge.org/BashFAQ/024. With minimal changes, your code could be refactored to
#!/bin/bash
typeset -i count5=0
for n in {1..4321}; do
echo $n # braces around ${n} provide no benefit
done | # no backslash required here; fix weird indentation
while read -n1 digit ; do
# prefer modern command substitution syntax over backticks
if [ $(echo "${digit}" | grep 5) ] ; then
count5=count5+1
fi
echo "${count5}" # variable will not persist outside subprocess
done | head -n 1 # so instead just print the last one after the loop
With some common antipatterns removed, this reduces to
#!/bin/bash
printf '%s\n' {1..4321} |
grep 5 |
wc -l
A more efficient and elegant way to do the same is simply
printf '%s\n' {1..4321} | grep -c 5
One primary issue:
each time results are sent to a pipe said pipe starts a new subshell; in bash any variables set in the subshell are 'lost' when the subshell exits; net result is even if you're correctly incrementing count5 within a subshell you'll still end up with 0 (the starting value) when you exit from the subshell
Making minimal changes to OP's current code:
while read -n1 digit ; do
if [ `echo "${digit}" | grep 5` ]; then
count5=count5+1
fi
done < <(for n in {1..4321}; do echo ${n}; done)
echo "${count5}"
NOTE: there are a couple performance related issues with this method of coding but since OP has explicitly asked to a) 'fix' the current code and b) not provide any shortcuts ... we'll leave the performance fixes for another day ...
A simpler way to get the number for a certain n would be
nx=${n//[^5]/} # Remove all non-5 characters
count5=${#nx} # Calculate the length of what is left
A simpler method in pure bash could be:
printf -v seq '%s' {1..4321} # print the sequence into the variable seq
fives=${seq//[!5]} # delete all characters but 5s
count5=${#fives} # length of the string is the count of 5s
echo $count5 # print it
Or, using standard utilities tr and wc
printf '%s' {1..4321} | tr -dc 5 | wc -c
Or using awk:
awk 'BEGIN { for(i=1;i<=4321;i++) {$0=i; x=x+gsub("5",""); } print x} '

Running `rg` in a while loop breaks after the first iteration

My script is simple:
while read -r key; do
rg --glob='!some_dir' --fixed-strings --quiet "$key" || echo "$key"
done < <(grep 'some_pattern' some/file | cut -d'"' -f2)
I hoped to use this bash script to print keys that aren't used. This loop, however, breaks after the first iteration at every run. Why and how to fix? Thank you :D
This looks like a classic signature of cases when the command run over a while..read loop starts from the standard input also. You expected the output of grep will be read over by the while loop in an iterative way, but for some reason your command rg is also reading from the same.
Close it as
rg --glob='!some_dir' --fixed-strings --quiet "$key" < /dev/null || echo "$key"
or use a different file descriptor
while read -r -u 3 key; do
rg --glob='!some_dir' --fixed-strings --quiet "$key" || echo "$key"
done 3< <(grep 'some_pattern' some/file | cut -d'"' -f2)

Bash script - how to count processes that have most parents

we were given a task to write a script in a course. We have to make the script find out which proccess is "deepest" in process hierarchy, something like "pstree" command, but the output will be "depth_of_process : processes_with_the_depth".
I have started something, but I can't make it work. Could you please look at it and help me ? I haven't even started producing the output, I am working on the algorithm now - trying to make it into something like reverse depth-first search. In case the code is not self-explanatory enough, please let me know, I will do my best to describe it.
#!/bin/bash
PROCS=$(ps -eo "%p %P" | tail -n +2 | sort -nr)
declare -a array
while read -r line; do
counter=1
read kid parent
while read -r otherline; do
read kid2 parent2
if [ "$parent" = "$kid2" ]; then
counter=$((counter+1))
parent="$parent2"
fi
done <<< "$PROCS"
test=2
array["$kid"]="$counter"
done <<< "$PROCS"
#for value in "${!array[#]}"; do
# echo "$value ${array[value]}"
#done
echo "$PROCS"
If pstree is allowed I could offer this (thanks #tripleee for optimizing):
for processid in $(ps -ax | awk 'NR>1 {print $1}' ); do
depth=$(pstree -sA $processid | head -n1 | sed -e 's#-+-.*#---foobar#' -e 's#---*#\n#g' -eq | wc -l)
echo "$depth: $processid"
done
It might have issues if your processes contain two or more dashes in a row.
Of course you can add " | sort" after "done" to get the deepest processes.

How to process values from for loop in shell script

I have below for loop in shell script
#!/bin/bash
#Get the year
curr_year=$(date +"%Y")
FILE_NAME=/test/codebase/wt.properties
key=wt.cache.master.slaveHosts=
prop_value=""
getproperty(){
prop_key=$1
prop_value=`cat ${FILE_NAME} | grep ${prop_key} | cut -d'=' -f2`
}
#echo ${prop_value}
getproperty ${key}
#echo "Key = ${key}; Value="${prop_value}
arr=( $prop_value )
for i in "${arr[#]}"; do
echo $i | head -n1 | cut -d "." -f1
done
The output I am getting is as below.
test1
test2
test3
I want to process the test2 from above results to below script in place of 'ABCD'
grep test12345 /home/ptc/storage/**'ABCD'**/apache/$curr_year/logs/access.log* | grep GET > /tmp/test.access.txt
I tried all the options but could not able to succeed as I am new to shell scripting.
Ignoring the many bugs elsewhere and focusing on the one piece of code you say you want to change:
for i in "${arr[#]}"; do
val=$(echo "$i" | head -n1 | cut -d "." -f1)
grep test12345 /dev/null "/home/ptc/storage/$val/apache/$curr_year/logs/access.log"* \
| grep GET
done > /tmp/test.access.txt
Notes:
Always quote your expansions. "$i", "/path/with/$val/"*, etc. (The * should not be quoted on the assumption that you want it to be expanded).
for i in $prop_value would have the exact same (buggy) behavior; using arr buys you nothing. If you want using arr to increase correctness, populate it correctly: read -r -a arr <<<"$prop_value"
The redirection is moved outside the loop -- that way the second iteration through the loop doesn't overwrite the file written by the first one.
The extra /dev/null passed to grep ensures that its behavior is consistent regardless of the number of matches; otherwise, it would display filenames only if more than one matching log file existed, and not otherwise.

Bash script to remove text from each line of a txt before a :

I have written this script to remove text from each line before ::
#!/bin/bash
txt=test.txt
COUNT=$(cat $txt | wc -l)
while [ $COUNT -gt 0 ]; do
data=$(sed -n ${count}p $txt)
sed '$count \c
"${data#*:}"' $txt
let COUNT=COUNT-1
done
I think I have an issue with using variables in commands without spaces. Can anyone tell me what I have done wrong?
I think you are over complicating it. To do this you just need cut:
cut -d':' -f2- file
-d sets the field separator.
-f indicates what fields to use. By saying 2- we indicate "all from the 2nd one on".
Test
$ cat a
hello
hello:man i am here:or there
and:you are here
$ cut -d':' -f2- a
hello
man i am here:or there
you are here
Some comments regarding your code:
#!/bin/bash
txt=test.txt
COUNT=$(cat $txt | wc -l) # you can directly say 'wc -l < "$txt"'
while [ $COUNT -gt 0 ]; do
data=$(sed -n ${count}p $txt) # you are using "count", not "COUNT"
sed '$count \c # same here. And I don't know what
"${data#*:}"' $txt # this sed is supposed to work like
let COUNT=COUNT-1 # you have to say let "COUNT=COUNT-1"
done
Also, it is good to indent the code, so that it shows like:
while ...
do
... things ...
done
All together, I would do:
#!/bin/bash
txt=a
count=$(wc -l < "$txt")
while (( count-- > 0 )); do
data=$(sed -n "${count}p" "$txt")
#sed '$COUNT \c "${data#*:}"' $txt # not using it
echo "${data#*:}"
done
Since you are reading the file from the bottom and done some conditions around it, you could just drop it and just use tac to print the file on reverse:
while IFS= read -r data do
echo "${data#*:}"
done < <(tac file)

Resources