how will I fix the echo when counting chars in a string - bash

I have an issue with the echo in the for loop, as I want to count string chars especailly for "*" it but it prints all the files in current directory.
clearvar() {
int=0
str=0
uniqchar=0
}
countstring(){
for c in $(echo "${1}" | fold -w1); do
echo "$c"
if [[ $c == [0-9] ]];then
int=$(( $int + 1 ))
elif [[ $c == [a-Z] ]];then
str=$(( $str + 1 ))
else
uniqchar=$(( $uniqchar + 1 ))
fi
done
}
while [ $# -gt 0 ];do
echo "Argument input: $1"
read -p "Input: " string
rmws=$(echo $string | tr -d " ")
mashed=$rmws$1
countstring $mashed
echo -e "int: $int\nstr: $str\nuniquechar: $uniqchar\nWhole string: $mashed"
clearvar
shift
done
Example output:
Argument input: io1
Input: fj^*23
f
j
^
file1
file2
file3
2
3
i
o
1
int: 3
str: 4
uniquechar: 4
Whole string: fj^*2wio1
it interprets as echo * instead of echo "*".
so I expect it to not print out the file names.

rmws=$(echo $string | tr -d " ")
If string=* this just executes echo * and expands the *.
The same happens in:
countstring $mashed
Both these expansions are unquoted. Quote them in double quotes. As a rule of a thumb - always use double quotes.
Also the same happens in the for loop:
for c in $(echo "${1}" | fold -w1)
the expansion, as elsewhere, is unquoted, so * expands. You have to quote. That's why the for i in $(..) is considered bad style - because such bugs happen. You can't do for i in "$(...)" because then you would iterate over one element. To iterate over lines or elements in a stream use a while IFS= read -r loop. You can print every character on each separate line with ex. sed 's/./&\n/g' and iterate over lines, or use bash extension read -n1 to read one character.
while IFS= read -r -n1 c; do
..
done <<<"$1"
The <<<"$1" is a bash's "here string".
You don't need $ in arithmetic expansion. Just:
int=$(( int + 1 ))
str=$(( str + 1 ))
uniqchar=$(( uniqchar + 1 ))
or in bash you can even do:
(( int++ ))
# and so on
Your script could become:
clearvar() {
int=0
str=0
uniqchar=0
}
countstring(){
while IFS= read -r -n1 c; do
echo "$c"
if [[ $c == [0-9] ]];then
(( int++ ))
elif [[ $c == [a-Z] ]];then
(( str++ ))
else
(( uniqchar++ ))
fi
done <<<"$1"
}
while [ $# -gt 0 ]; do
echo "Argument input: $1"
read -p "Input: " string
rmws="$(echo "$string" | tr -d " ")"
mashed="$rmws$1"
countstring "$mashed"
echo "int: $int"
echo "str: $str"
echo "uniquechar: $uniqchar"
echo "Whole string: $mashed"
clearvar
shift
done
Notes:
echo has portability issues. Prefer to use printf instead.
I prefer while (($#)); do in place of while [ $# -eq 0 ]; do.
PS. I would use tr:
countstring() {
printf "%s" "$1" | tr -cd '[0-9]' | wc -c
printf "%s" "$1" | tr -cd '[a-zA-Z]' | wc -c
printf "%s" "$1" | tr -d '[0-9a-zA-Z]' | wc -c
}

Related

extract a numeric substring and add value to it

I have a string like 1001.2001.3001.5001.6001 or 1001-2001-3001-5001-6001. How to extract the 4th string i.e., 5001, add a value like 121 to it and put it back in the same string. The output should be like 1001.2001.3001.5122.6001 or 1001-2001-3001-5122-6001. I have to achieve this in Linux bash scripting.
Try this
#!/bin/bash
str=$1
if [[ $(echo $str | grep '\.' | wc -l) == 1 ]]
then
str1=$(echo $str | cut -d '.' -f 1,2,3)
str2=$(echo $str | cut -d '.' -f 4 | awk {'print $1+121'})
str3=$(echo $str | cut -d '.' -f 5)
echo $str1.$str2.$str3
elif [[ $(echo $str | grep - | wc -l) == 1 ]]
then
str1=$(echo $str | cut -d '-' -f 1,2,3)
str2=$(echo $str | cut -d '-' -f 4 | awk {'print $1+121'})
str3=$(echo $str | cut -d '-' -f 5)
echo $str1-$str2-$str3
else
echo "do nothing"
fi
Pass a string as parameter
No pipes, no forks, no cutting, no awking, just plain POSIX shell:
$ s=1001.2001.3001.5001.6001
$ oldIFS=$IFS
$ IFS=.-
$ set -- $s
$ case $s in
> (*.*) echo "$1.$2.$3.$(($4 + 121)).$5";;
> (*-*) echo "$1-$2-$3-$(($4 + 121))-$5";;
> esac
1001.2001.3001.5122.6001
$ IFS=$oldIFS
One liner
value=121 ; str='1001.2001.3001.5001.6001' ; token="$(echo "$str" | cut -f 4 -d '.')" ; newtoken=$(( $token + $value )) ; newstr="$(echo "$str" | sed -e "s/$token/$newtoken/g" | tr '.' '-')" ; echo "$newstr"
Breakdown:
value=121 # <- Increment
str='1001.2001.3001.5001.6001' # <- Initial String
token="$(echo "$str" | cut -f 4 -d '.')" # <- Extract the 4th field with . sep
newtoken=$(( $token + $value )) # <- Add value and save to $newtoken
newstr="$(echo "$str" \
| sed -e "s/$token/$newtoken/g" \
| tr '.' '-')" # <- Replace 4th field with $newtoken
# and translate "." to "-"
echo "$newstr" # <- Echo new string
Works in:
Bash
sh
FreeBSD
Busybox
Using out of the box tools
If the field separator can either be . or -, then do something like
echo "1001.2001.3001.5001.6001" | awk 'BEGIN{FS="[.-]";OFS="-"}{$4+=121}1'
1001-2001-3001-5122-6001
However, if you need to match the regex FS or field separator with OFS then you need to have gawk installed
echo "1001.2001.3001.5001.6001" |
gawk 'BEGIN{FS="[.-]"}{split($0,a,FS,seps)}{$4+=121;OFS=seps[1]}1'
1001.2001.3001.5122.6001
Though resetting the argument list with the values is probably the preferred way, or by setting IFS to the delimiter and reading the values into an array and adding the desired value to the array index at issue, you can also do it with a simple loop to look for the delimiters and continually skipping characters until the desired segment is found (4 in you case -- when the delimiter count is 3). Then simply appending the digit at each array index until your next delimiter is found will give you the base value. Simply adding your desired 121 to the completed number completes the script, e.g.
#!/bin/bash
str=${1:-"1001.2001.3001.5001.6001"} ## string
ele=${2:-4} ## element to add value to [1, 2, 3, ...]
add=${3:-121} ## value to add to element
cnt=0 ## flag to track delimiters found
num=
## for each character in str
for ((i = 0; i < ${#str}; i++))
do
if [ "${str:$i:1}" = '.' -o "${str:$i:1}" = '-' ] ## is it '.' or '-'
then
(( cnt++ )) ## increment count
(( cnt == ele )) && break ## if equal to ele, break
## check each char is a valid digit 0-9
elif [ "0" -le "${str:$i:1}" -a "${str:$4i:1}" -le "9" ]
then
(( cnt == (ele - 1) )) || continue ## it not one of interest, continue
num="$num${str:$i:1}" ## append digit to num
fi
done
((num += add)) ## add the amount to num
printf "num: %d\n" $num ## print results
Example Use/Output
$ bash parsenum.sh
num: 5122
$ bash parsenum.sh "1001.2001.3001.5001.6001" 2
num: 2122
$ bash parsenum.sh "1001.2001.3001.5001.6001" 2 221
num: 2222
Look things over and let me know if you have any questions.

Grep command in array

For a homework assignment I have to Take the results from the grep command, and write out up to the first 5 of them, numbering them from 1 to 5. (Print the number, then a space, then the line from grep.) If there are no lines, print a message saying so. So far I managed to store the grep command in an array but this is where I've gotten stuck: Can anyone provide guidance as to how to proceed in printing this as stated above
pattern="*.c"
fileList=$(grep -l "main" $pattern)
IFS=$"\n"
declare -a array
array=$fileList
for x in "${array[#]}"; do
echo "$x"
done
you can grep options -c and -l
pattern="*.c"
searchPattern="main"
counter=1
while read -r line ; do
IFS=':' read -r -a lineInfo <<< "$line"
if [[ $counter > 5 ]]; then
exit 1
fi
if [[ ${lineInfo[1]} > 0 ]]; then
numsOfLine=""
while read -r fileline ; do
IFS=':' read -r -a fileLineInfo <<< "$fileline"
numsOfLine="$numsOfLine ${fileLineInfo[0]} "
done < <(grep -n $searchPattern ${lineInfo[0]})
echo "$counter ${lineInfo[0]} match on lines: $numsOfLine"
let "counter += 1"
else
echo "${lineInfo[0]} no match lines"
fi
done < <(grep -c $searchPattern $pattern)
If you're only allowed to use grep and bash(?):
pattern="*.c"
fileList=($(grep -l "main" $pattern))
if test ${#fileList[#]} = 0 ; then
echo "No results"
else
n=0
while test $n -lt ${#fileList[#]} -a $n -lt 5 ; do
i=$n
n=$(( n + 1 ))
echo "$n ${fileList[$i]}"
done
fi
If you are allowed to use commands in addition to grep, you can pipe the results through nl to add line numbers, then head to limit the results to the first 5 lines, then a second grep to test if there were any lines. For example:
if ! grep -l "main" $pattern | \
nl -s ' ' | sed -e 's/^ *//' | \
head -n 5 | grep '' ; then
echo "No results"
fi

Check if a string contains "-" and "]" at the same time

I have the next two regex in Bash:
1.^[-a-zA-Z0-9\,\.\;\:]*$
2.^[]a-zA-Z0-9\,\.\;\:]*$
The first matches when the string contains a "-" and the other values.
The second when contains a "]".
I put this values at the beginning of my regex because I can't scape them.
How I can get match the two values at the same time?
You can also place the - at the end of the bracket expression, since a range must be closed on both ends.
^[]a-zA-Z0-9,.;:-]*$
You don't have to escape any of the other characters, either. Colons, semicolons, and commas have no special meaning in any part of a regular expression, and while a period loses its special meaning inside a bracket expression.
Basically you can use this:
grep -E '^.*\-.*\[|\[.*\-.*$'
It matches either a - followed by zero or more arbitrary chars and a [ or a [ followed by zero or more chars and a -
However since you don't accept arbitrary chars, you need to change it to:
grep -E '^[a-zA-Z0-9,.;:]*\-[a-zA-Z0-9,.;:]*\[|\[[a-zA-Z0-9,.;:]*\-[a-zA-Z0-9,.;:]*$'
Maybe, this can help you
#!/bin/bash
while read p; do
echo $p | grep -E '\-.*\]|\].*\-' | grep "^[]a-zA-Z0-9,.;:-]*$"
done <$1
user-host:/tmp$ cat test
-i]string
]adfadfa-
string-
]string
str]ing
]123string
123string-
?????
++++++
user-host:/tmp$ ./test.sh test
-i]string
]adfadfa-
There are two questions in your post.
One is in the description:
How I can get match the two values at the same time?
That is an OR match, which could be done with a range that mix your two ranges:
pattern='^[]a-zA-Z0-9,.;:-]*$'
That will match a line that either contains one (or several) -…OR…]…OR any of the included characters. That would be all the lines (except ?????, ++++++ and as df gh) in the test script below.
Two is in the title:
… a string contains “-” and “]” at the same time
That is an AND match. The simplest (and slowest) way to do it is:
echo "$line" | grep '-' | grep ']' | grep '^[-a-zA-Z0-9,.;:]*$'
The first two calls to grep select only the lines that:
contain both (one or several) - and (one or several) ]
Test script:
#!/bin/bash
printlines(){
cat <<-\_test_lines_
asdfgh
asdfgh-
asdfgh]
as]df
as,df
as.df
as;df
as:df
as-df
as]]]df
as---df
asAS]]]DFdf
as123--456DF
as,.;:-df
as-dfg]h
as]dfg-h
a]s]d]f]g]h
a]s]d]f]g]h-
s-t-r-i-n-g]
as]df-gh
123]asdefgh
123asd-fgh-
?????
++++++
as df gh
_test_lines_
}
pattern='^[]a-zA-Z0-9,.;:-]*$'
printf '%s\n' "Testing the simple pattern of $pattern"
while read line; do
resultgrep="$( echo "$line" | grep "$pattern" )"
printf '%13s %-13s\n' "$line" "$resultgrep"
done < <(printlines)
echo "#############################################################"
echo
p1='-'; p2=']'; p3='^[]a-zA-Z0-9,.;:-]*$'
printf '%s\n' "Testing a 'grep AND' of '$p1', '$p2' and '$p3'."
while read line; do
resultgrep="$( echo "$line" | grep "$p1" | grep "$p2" | grep "$p3" )"
[[ $resultgrep ]] && printf '%13s %-13s\n' "$line" "$resultgrep"
done < <(printlines)
echo "#############################################################"
echo
printf '%s\n' "Testing an 'AWK AND' of '$p1', '$p2' and '$p3'."
while read line; do
resultawk="$( echo "$line" |
awk -v p1="$p1" -v p2="$p2" -v p3="$p3" '$0~p1 && $0~p2 && $0~p3' )"
[[ $resultawk ]] && printf '%13s %-13s\n' "$line" "$resultawk"
done < <(printlines)
echo "#############################################################"
echo
printf '%s\n' "Testing a 'bash AND' of '$p1', '$p2' and '$p3'."
while read line; do
rgrep="$( echo "$line" | grep "$p1" | grep "$p2" | grep "$p3" )"
[[ ( $line =~ $p1 ) && ( $line =~ $p2 ) && ( $line =~ $p3 ) ]]
rbash=${BASH_REMATCH[0]}
[[ $rbash ]] && printf '%13s %-13s %-13s\n' "$line" "$rgrep" "$rbash"
done < <(printlines)
echo "#############################################################"
echo

How to check if string contains more than one special character

I have this
if [[ ! $newstring == *['!'##\$%^\&*()_+]* ]]
then
echo Error - Does not contain One Special Character - $newstring
i=$((i+1))
fi
Which checks if the string only has one single character from the bank, i want to check if it has more than one?
What would be the best way?
Either add a second class
if [[ "$newstring" != *['!'##\$%^\&*\(\)_+]*['!'##\$%^\&*\(\)_+]* ]]
or strip anything else out and check length
t="${newstring//[^!##\$%^\&*()_+]}"
if [ ${#t} -lt 2 ]
We can use tr to solve it.
$ string='Hello-World_12#$##*&%)(!####'
$ number=$(( $(tr -d '[[:alnum:]]' <<< "$string"|wc -m) - 1 ))
$ echo "We have $number of special characters"
$ 16
This should be short and faster.
#!/bin/bash
a='!*#%6789';
if [[ `echo $a | sed "s/\(.\)/\1\n/g"|grep -c "[[:punct:]]"` -gt 1 ]]; then echo shenzi; else echo koba; fi
grep can be useful to provide the match
grep -oP "^[^'\!'##\$%^\&*()_+]*['\!'##\$%^\&*()_+][^'\!'##\$%^\&*()_+]+$"
test
$ echo "#asdfasdf234" | grep -oP "^[^'\!'##\$%^\&*()_+]*['\!'##\$%^\&*()_+][^'\!'##\$%^\&*()_+]+$"
will match the string as
#asdfasdf234
$ echo "#asdf#asdf234" | grep -oP "^[^'\!'##\$%^\&*()_+]*['\!'##\$%^\&*()_+][^'\!'##\$%^\&*()_+]+$"
will not match the string
The if construct can be
echo $newstring| grep -oP "^[^'\!'##\$%^\&*()_+]*['\!'##\$%^\&*()_+][^'\!'##\$%^\&*()_+]+$"
if [[ $? -eq 0 ]] > /dev/null
then
echo Error - Does not contain One Special Character - $newstring
i=$((i+1))
fi
Here the regex
^[^'\!'##\$%^\&*()_+]*['\!'##\$%^\&*()_+][^'\!'##\$%^\&*()_+]+$
matches all strings with exact one occurence of the special character

count words in a file without using wc

Working in a shell script here, trying to count the number of words/characters/lines in a file without using the wc command. I can get the file broken into lines and count those easy enough, but I'm struggling here to get the words and the characters.
#define word_count function
count_stuff(){
c=0
w=0
l=0
local f="$1"
while read Line
do
l=`expr $line + 1`
# now that I have a line I want to break it into words and characters???
done < "$f"
echo "Number characters: $chars"
echo "Number words: $words"
echo "Number lines: $line"
}
As for characters, try this (adjust echo "test" to where you get your output from):
expr `echo "test" | sed "s/./ + 1/g;s/^/0/"`
As for lines, try this:
expr `echo -e "test\ntest\ntest" | sed "s/^.*$/./" | tr -d "\n" | sed "s/./ + 1/g;s/^/0/"`
===
As for your code, you want something like this to count words (if you want to go at it completely raw):
while read line ; do
set $line ;
while true ; do
[ -z $1 ] && break
l=`expr $l + 1`
shift ;
done ;
done
You can do this with the following Bash shell script:
count=0
for var in `cat $1`
do
count=`echo $count+1 | bc`
done
echo $count

Resources