Reverse Triangle using shell - bash

OK so Ive been at this for a couple days,im new to this whole bash UNIX system thing i just got into it but I am trying to write a script where the user inputs an integer and the script will take that integer and print out a triangle using the integer that was inputted as a base and decreasing until it reaches zero. An example would be:
reverse_triangle.bash 4
****
***
**
*
so this is what I have so far but when I run it nothing happens I have no idea what is wrong
#!/bin/bash
input=$1
count=1
for (( i=$input; i>=$count;i-- ))
do
for (( j=1; j>=i; j++ ))
do
echo -n "*"
done
echo
done
exit 0
when I try to run it nothing happens it just goes to the next line. help would be greatly appreciated :)

As I said in a comment, your test is wrong: you need
for (( j=1; j<=i; j++ ))
instead of
for (( j=1; j>=i; j++ ))
Otherwise, this loop is only executed when i=1, and it becomes an infinite loop.
Now if you want another way to solve that, in a much better way:
#!/bin/bash
[[ $1 = +([[:digit:]]) ]] || { printf >&2 'Argument must be a number\n'; exit 1; }
number=$((10#$1))
for ((;number>=1;--number)); do
printf -v spn '%*s' "$number"
printf '%s\n' "${spn// /*}"
done
Why is it better? first off, we check that the argument is really a number. Without this, your code is subject to arbitrary code injection. Also, we make sure that the number is understood in radix 10 with 10#$1. Otherwise, an argument like 09 would raise an error.
We don't really need an extra variable for the loop, the provided argument is good enough. Now the trick: to print n times a pattern, a cool method is to store n spaces in a variable with printf: %*s will expand to n spaces, where n is the corresponding argument found by printf.
For example:
printf '%s%*s%s\n' hello 42 world
would print:
hello world
(with 42 spaces).
Editor's note: %*s will NOT generally expand to n spaces, as evidenced by above output, which contains 37 spaces.
Instead, the argument that * is mapped to,42, is the field width for the sfield, which maps to the following argument,world, causing string world to be left-space-padded to a length of 42; since world has a character count of 5, 37 spaces are used for padding.
To make the example work as intended, use printf '%s%*s%s\n' hello 42 '' world - note the empty string argument following 42, which ensures that the entire field is made up of padding, i.e., spaces (you'd get the same effect if no arguments followed 42).
With printf's -v option, we can store any string formatted by printf into a variable; here we're storing $number spaces in spn. Finally, we replace all spaces by the character *, using the expansion ${spn// /*}.
Yet another possibility:
#!/bin/bash
[[ $1 = +([[:digit:]]) ]] || { printf >&2 'Argument must be a number\n'; exit 1; }
printf -v s '%*s' $((10#1))
s=${s// /*}
while [[ $s ]]; do
printf '%s\n' "$s"
s=${s%?}
done
This time we construct the variable s that contains a bunch of * (number given by user), using the previous technique. Then we have a while loop that loops while s is non empty. At each iteration we print the content of s and we remove a character with the expansion ${s%?} that removes the last character of s.

Building on gniourf_gniourf's helpful answer:
The following is simpler and performs significantly better:
#!/bin/bash
count=$1 # (... number-validation code omitted for brevity)
# Create the 1st line, composed of $count '*' chars, and store in var. $line.
printf -v line '%.s*' $(seq $count)
# Count from $count down to 1.
while (( count-- )); do
# Print a *substring* of the 1st line based on the current value of $count.
printf "%.${count}s\n" "$line"
done
printf -v line '*%.s' $(seq $count) is a trick that prints * $count times, thanks to %.s* resulting in * for each argument supplied, irrespective of the arguments' values (thanks to %.s, which effectively ignores its argument). $(seq $count) expands to $count arguments, resulting in a string composed of $count * chars. overall, which - thanks to -v line, is stored in variable $line.
printf "%.${count}s\n" "$line" prints a substring from the beginning of $line that is $count chars. long.

Related

continue <n> not skipping <n> iterations forward in shell script

I have created a hex to ASCII converter for strings in bash. The application I'm on changes characters (anything but [0-9],[A-Z],[a-z]) , in a string to its corresponding %hexadecimal. Eg: / changes to %2F in a string
I want to retain the ASCII characters as it is. Below is my code:
NAME=%2fhome%40%21%23
C_NAME=""
for (( i=0; i<${#NAME}; i++ )); do
CHK=$(echo "{NAME:$i:1}" | grep -v "\%" &> /dev/null;echo $?)
if [[ ${CHK} -eq 0 ]]; then
C_NAME=`echo "$C_NAME${NAME:$i:1}"`
else
HEX=`echo "${NAME:$i:3}" | sed "s/%//"`
C_NAME=`echo -n "$C_NAME";printf "\x$HEX"`
continue 2
fi
done
echo "$C_NAME"
OUTPUT:
/2fhome#40!21#23
EXPECTED:
/home#!#
So basically the conversion is happening, but not in place. Its retaining the hex values as well, which tells me the continue 2 statement is probably not working as I expect in my code. Any workarounds please.
You only have one loop so I assume you expected that continue 2 skips the current and next iteration of the current loop, however, the documentation help continue clearly states
continue [n]
[...]
If N is specified, resumes the Nth enclosing loop.
There is no built-in to skip the current and also the next iteration of the current loop, but in your case you can use (( i += 2 )) instead of continue 2.
Using the structure of your script with some simplifications and corrections:
#!/bin/bash
name=%2fhome%40%21%23
c_name=""
for (( i=0; i<${#name}; i++ )); do
c=${name:i:1}
if [[ $c != % ]]; then
c_name=$c_name$c
else
hex=${name:i+1:2}
printf -v c_name "%s\x$hex" "$c_name"
(( i += 2 )) # stolen from Dudi Boy's answer
fi
done
echo "$c_name"
Always use lower case or mixed case variables to avoid the chance of name collisions with shell or environment variables
Always use $() instead of backticks
Most of the echo commands you use aren't necessary
You can avoid using sed and grep
Variables should never be included in the format string of printf but it can't be avoided easily here (you could use echo -e "\x$hex" instead though)
You can do math inside parameter expansions
% doesn't need to be escaped in your grep command
You could eliminate the $hex variable if you used its value directly:
printf -v c_name "%s\x${name:i+1:2}" "$c_name"
I really enjoyed your exercise and decided to solve it with awk (my current study).
Hope you like it as well.
cat script.awk
BEGIN {RS = "%[[:xdigit:]]+"} { # redefine record separtor to RegEx (gawk specific)
decNum = strtonum("0x"substr(RT, 2)); # remove prefix # from record separator, convert hex num to dec
outputStr = outputStr""$0""sprintf("%c", decNum); # reconstruct output string
}
END {print outputStr}
The output
echo %2fhome%40%21%23 |awk -f script.awk
/home#!#

bash getting numbers from a file and make the average of them

Write a script that expects a file as its first argument. Some lines of the
file will consist of integers 0 - 1000.
The script should select the lines matching the previous criteria and print out their average to stdout (average of n integers is their sum divided by n).
And the file given looks like this:
22
78907
77 88 99 0000
need 11 gallons of water
0
roses are red
11
Example output:
11
Explanation: (22 + 11 + 0) / 3 = 11
I have tried already with this code:
#!/bin/bash
sum=0
ind=0
while IFS='' read -r line || [[ -n "$line" ]]; do
if [[ $line =~ ^[a-zA-Z\ ]+$ ]]
then
${sum}=${sum}+${#line}
${ind}=${ind}+1
echo ${sum}
fi
done < "$1"
value=${sum}/${ind}
echo ${value}
the print of this code is always 0/0 and some errors like:
./test1: line 9: 0=0+13: command not found
./test1: line 10: 0=0+1: command not found
Any ideas?
Part of the issue with your script is answered here.. Your variable assignments are incorrect. You only use the $ to refer to a variable that has already been assigned. The assignment process drops the dollar sign.
The other issue you're having is that your arithmetic is not being expressed within an arithmetic expression.
Note that you can use use arithmetic expansion to handle your variables:
if [[ $line =~ ^[a-zA-Z\ ]+$ ]]; then
(( sum += ${#line} ))
(( ind++ ))
printf '%s\n' "$sum"
fi
and later ...
value="$(( sum / ind ))"
printf '%s\n' "$value"
Beware that bash can only deal with integer math, floats are truncated. For more advanced math, consider using bc or dc (which are not built in to bash, they are separate tools that may need to be installed on your system) or another language like awk or perl which can do the same thing with better performance and more precise math.
That said, you can "fake" a couple of decimal places with a few extra lines of code and string manipulation, if you really need to:
$ sum=100; ind=7
$ printf -v x '%d' "$((${sum}00/${ind}))"
$ printf '%d.%d\n' "${x%??}" "${x:$((${#x}-2))}"
14.28
The first printf has division which multiplies the dividend by 100 (by adding two zeroes after it). The resultant quotient is then split with the second printf to insert the decimal point. This is a hack. Use tools that support real math.

Bash while loop with done < $1

I'm a bit confused by the done < $1 notation.
I'm trying to write a program "sumnums" that reads in a file called "nums" that has a couple rows of numbers. Then it should print out the rows of the numbers followed by a sum of all the numbers.
Currently I have:
#!/bin/bash
sum=0;
while read myline
do
echo "Before for; Current line: \"$myline\""
done
for i in $myline; do
sum=$(expr $sum + $i)
done < $1
echo "Total sum is: $sum"
and it outputs the list of the numbers from nums correctly then says
./sumnums: line 10: $1: ambiguous redirect, then outputs Total sum is: 0.
So somehow it isn't adding. How do I rearrange these lines to fix the program and get rid of the "ambiguous redirect"?
Assuming your filename is in $1 (that is, that your script was called with ./yourscript nums):
#!/bin/bash
[[ $1 ]] || set -- nums ## use $1 if already set; otherwise, override with "nums"
sum=0
while read -r i; do ## read from stdin (which is redirected by < for this loop)
sum=$(( sum + i )) ## ...treat what we read as a number, and add it to our sum
done <"$1" ## with stdin reading from $1 for this loop
echo "Total sum is: $sum"
If $1 doesn't contain your filename, then use something that does contain your filename in its place, or just hardcode the actual filename itself.
Notes:
<"$1" is applied to a while read loop. This is essential, because read is (in this context) the command that actually consumes content from the file. It can make sense to redirect stdin to a for loop, but only if something inside that loop is reading from stdin.
$(( )) is modern POSIX sh arithmetic syntax. expr is legacy syntax; don't use it.
awk to the rescue!
awk '{for(i=1;i<=NF;i++) sum+=$i} END{print "Total sum is: " sum}' file
bash is not the right tool for this task.

How come my code is still printing out numbers when I specify only letters to be printed?

#!/bin/bash
# this is a sample value
hash=d7dd933b5bb968b6ba9ee40548b1b27a
# retrieve all letters from this hash
count=0
for (( i=0; i<${#hash}; i++)); do
if [[ ${hash:i:1} == [a-f] ]] ; then
code[$count]=${hash:i:1}
count=$((count + 1))
echo ${code[i]}
#echo ${hash:i:1}
fi
done
Instead of printing all characters in the hash (as I expect), this is printing only the first two characters, followed by newlines. (Eventually, I intend to take only the first two characters extracted from hash, but this is not an immediate goal).
What's wrong here?
In order to filter out all the letters from the hash string, you can modify your for loop as follows:
code=()
count=0
for (( i = 0; i < ${#hash}; i++ )); do
if [[ ${hash:i:1} == [a-zA-Z] ]]; then
code[$count]="${hash:i:1}"
((count++))
fi
done
Here, ${hash:i:1} selects each character in hash individually. If the character is in the range of letters a-zA-Z, meaning it is a letter, then it is stored in the array code, and count is accumulated.
Consider the following full code as an example (assume it is contained in a file called script):
#!/bin/bash
hash="$1" # $1 means first argument given to this script file
printf "hash = %s\n" "$hash"
code=() # initialize code array
count=0
for (( i = 0; i < ${#hash}; i++ )); do
if [[ ${hash:i:1} == [a-zA-Z] ]]; then
code[$count]="${hash:i:1}"
((count++))
fi
done
printf "All letters are: "
printf "%s" "${code[#]}"
printf "\n"
printf "First two letters are: %c%c \n" "${code[0]}" "${code[1]}"
Tests:
$ ./script dd7d933b5bb968b6ba9ee40548b1b27a
hash = dd7d933b5bb968b6ba9ee40548b1b27a
All letters are: dddbbbbbaeebba
First two letters are: dd
$ ./script slfj2948347slddkshfsl2348sldfjsf
hash = slfj2948347slddkshfsl2348sldfjsf
All letters are: slfjslddkshfslsldfjsf
First two letters are: sl
Parameter expansion provides numerous string-manipulation primitives, including search-and-replace, which can be readily used for this purpose:
s=1a2b3c4d5e6f7g
code=${s//[![:alpha:]]/}
echo "$code"
...should emit:
abcdefg
...if you wanted to emit only the first two characters in code, that would be:
echo "${code:0:2}"
Why this works
[:alpha:] is a POSIX character class which includes characters consider alphabet members in the current locale.
[![:alpha:]] is a glob expression which matches any single character not in that class
${var//value/replacement} expands the variable named var, changing all instances of value to replacement
Thus, ${s//[![:alpha:]]/} expands the variable s, changing all characters which are not alphabetic in nature to the empty string.

Read user given file character by character in bash

I have a file which is kind of unformatted, I want to place a new-line after every 100th character and remove any other new lines in it so that file may look with consistent width and readable
This code snippet helps read all the lines
while read LINE
do
len=${#LINE}
echo "Line length is : $len"
done < $file
but how do i do same for characters
Idea is to have something like this : (just an example, it may have syntax errors, not implemented yet)
while read ch #read character
do
chcount++ # increment character count
if [ "$chcount" -eq "100" && "$ch"!="\n" ] #if 100th character and is not a new line
then
echo -e "\n" #echo new line
elif [ "$ch"=="\n" ] #if character is not 100th but new line
then
ch=" " $replace it with space
fi
done < $file
I am learning bash, so please go easy!!
I want to place a new-line after every 100th character and remove any
other new lines in it so that file may look with consistent width and
readable
Unless you have a good reason to write a script, go ahead but you don't need one.
Remove the newline from the input and fold it. Saying:
tr -d '\n' < inputfile | fold -w 100
should achieve the desired result.
bash adds a -n flag to the standard read command to specify a number of characters to read, rather than a full line:
while read -n1 c; do
echo "$c"
done < $file
You can call the function below in any of the following ways:
line_length=100
wrap $line_length <<< "$string"
wrap $line_length < file_name
wrap $line_length < <(command)
command | wrap $line_length
The function reads the input line by line (more efficiently than by character) which essentially eliminates the existing newlines (which are replaced by spaces). The remainder of the previous line is prefixed to the current one and the result is split at the desired line length. The remainder after the split is kept for the next iteration. If the output buffer is full, it is output and cleared otherwise it's kept for the next iteration so more can be added. Once the input has been consumed, there may be additional text in the remainder. The function is called recursively until that is also consumed and output.
wrap () {
local remainder rest part out_buffer line len=$1
while IFS= read -r line
do
line="$remainder$line "
(( part = $len - ${#out_buffer} ))
out_buffer+=${line::$part}
remainder=${line:$part}
if (( ${#out_buffer} >= $len ))
then
printf '%s\n' "$out_buffer"
out_buffer=
fi
done
rest=$remainder
while [[ $rest ]]
do
wrap $len <<< "$rest"
done
if [[ $out_buffer ]]
then
printf '%s\n' "$out_buffer"
out_buffer=
fi
}
#!/bin/bash
w=~/testFile.txt
chcount=0
while read -r word ; do
len=${#word}
for (( i = 0 ; i <= $len - 1 ; ++i )) ; do
let chcount+=1
if [ $chcount -eq 100 ] ; then
printf "\n${word:$i:1}"
let chcount=0
else
printf "${word:$i:1}"
fi
done
done < $w
Are you looking for something like this?

Resources