Shell Scripting - less than operation not working - shell

echo "Enter age"
read $ag
if [[ $ag -lt 30 ]]
then
echo "Age is less than 30"
else
echo "not"
fi
For this program, whatever be the value of 'ag', the output is shown as "Age is less than 30".
What could be reason? and a solution?

Your mistake is:
read $ag
You should write:
read ag
With your current syntax, read stores the user input in a variable whose name is the content of variable ag.

An echo $ag after the read would have told you. (Checking your input and your assumptions is elementary debugging, really.)
read $ag
This resolves $ag (which is undefined), giving:
read
You're reading, but you're not reading into the variable as you expected. $ag is still empty, which resolves to "less than 30".
Fix:
read ag
That's the proper syntax for read.

Related

Verifying bash script inputs

I've just started using Linux as part of my computer science degree.
I'm writing some very simple Bash scripts and I've become a tad bit stuck.
I would like the script I'm attempting to write to be able to differentiate between "non valid inputs ie letters" from "valid inputs ie numbers from a specific range"
Currently the script "works" although I'm having troubles with another echo that I would like only to "echo" when the below line is "not true", is there a simple way to write this? I'm not specifically looking for efficient code, just code that I can learn from and understand at my amateur level.
So, long story short, is it possible to obtain information from the command line below, so that I can have a simple "not true" variable that I can use in another "else" or "elif" command?
For reference line 1 is to detect alphabetical inputs, and line 2 being the line of code I would like to write as "not true" for use in another part of my script.
let xx=$a+1-1 2>/dev/null; ret=$?
if [ $a -ge 7 ] && [ $a -le 70 ] && [ $xx -eq $xx ] && [ $ret -eq 0 ]
I'm not sure I'm explaining it very well, so any help would be appreciated. :)
Welcome to Stack Overflow. :)
Start by reading the docs. I don't mean that in any way to be mean - it's just the best way to go about this.
c.f. this manual
Then read through the BashFAQs
Also, this site is really your friend. Start by familiarizing yourself with how to ask a question well.
For your question, if I read it right:
typeset -i xx # accepts only digits now.
If the input is foo, the value defaults to 0, so now just check the range.
if (( xx >= 7 && xx <= 70 )); then : value is ok
else echo "Value must be a number from 7 to 70"
exit 1
fi
Good luck. :)
One problem with the "variable with integer attribute" is that it still doesn't protect you from invalid input:
$ declare -i aNumber
$ aNumber=1234X
bash: 1234X: value too great for base (error token is "1234X")
See 6.5 Shell Arithmetic for how bash interprets values to be numbers (scroll down to the paragraph starting with "Integer constants follow the C language definition")
In my experience, the best way to check for valid numeric input is with string-oriented pattern matching.
if [[ $1 =~ ^[+-]?[0-9]+$ ]]; then
echo "input $1 is an integer"
fi
In addition to extended regular expressions, bash's advanced pattern matching can also be used within [[...]]
if [[ $1 == ?([+-])+([0-9]) ]]; then
echo "input $1 is an integer"
fi
((...)) is preferred over let. See the let builtin
command for details.
Also the shellcheck wiki entry.

why in an 'if' statement 'then' has to be in the next line in bash?

if is followed by then in bash but I don't understand why then cannot be used in the same line like if [...] then it has to be used in the next line. Does that remove some ambiguity from the code? or bash is designed like that? what is the underlying reason for it?
I tried to write if and then in the same line but it gave the error below:
./test: line 6: syntax error near unexpected token \`fi'
./test: line 6: \`fi'
the code is:
#!/bin/bash
if [ $1 -gt 0 ] then
echo "$1 is positive"
fi
It has to be preceded by a separator of some description, not necessarily on the next line(a). In other words, to achieve what you want, you can simply use:
if [[ $1 -gt 0 ]] ; then
echo "$1 is positive"
fi
As an aside, for one-liners like that, I tend to prefer:
[[ $1 -gt 0 ]] && echo "$1 is positive"
But that's simply because I prefer to see as much code on screen as possible. It's really just a style thing which you can freely ignore.
(a) The reason for this can be found in the Bash manpage (my emphasis):
RESERVED WORDS: Reserved words are words that have a special meaning to the shell. The following words are recognized as reserved when unquoted and either the first word of a simple command (see SHELL GRAMMAR below) or the third word of a case or for command:
! case coproc do done elif else esac fi for function if in select then until while { } time [[ ]]
Note that, though that section states it's the "first word of a simple command", the manpage seems to contradict itself in the referenced SHELL GRAMMAR section:
A simple command is a sequence of optional variable assignments followed by blank-separated words and redirections, and terminated by a control operator. The first word specifies the command to be executed, and is passed as argument zero.
So, whether you consider it part of the next command or a separator of some sort is arguable. What is not arguable is that it needs a separator of some sort (newline or semicolon, for example) before the then keyword.
The manpage doesn't go into why it was designed that way but it's probably to make the parsing of commands a little simpler.
Here's another way to explain the need for a line break or semicolon before then: the thing that goes between if and then is a command (or sequence of commands); if the then just came directly after the command without a delimiter, it'd be ambiguous whether it should be treated as a shell keyword or just an argument to the command.
For instance, this is a perfectly valid command:
echo This prints a phrase ending with then
...which prints "This prints a phrase ending with then". Now, consider this one:
if echo This prints a phrase ending with then
should that print "This prints a phrase ending with then" and look for a then keyword later on, or should it just print "This prints a phrase ending with" and treat the then as a keyword?
In order to settle this ambiguity, shell syntax says it should treat "then" as an argument to echo, and in order to get it treated as a keyword you need a command delimiter (line break or semicolon) to mark the end of the command.
Now, you might think that your if condition [ $1 -gt 0 ], already has a perfectly good delimiter, namely the ]. But in shell syntax, that's really just an argument to the [ command (yes, that's a command). Try this command:
[ 1 -gt 0 ] then
...and you'll probably get an error like "-bash: [: missing ']'", because the [ command checked its last argument to make sure it was "]", found that it was "then" instead, and panicked.
Perhaps it helps to understand why this is so by way of a few examples. The argument to if is a sequence of commands; so you can say e.g.
if read -r -p "What is your name?" name
[ "$name" -eq "tripleee" ]
then
echo "I kneel before thee"
fi
or even a complex compound like
while read -r -p "Favorite number?" number
case $number in
42) true; break;;
*) false;;
esac
do
echo "Review your preferences, then try again"
done
This extremely powerful but potentially confusing feature of the shell is probably one of its most misunderstood constructs. The ability to pass a sequence of commands to the flow control statements can make for very elegant scripts, but is often missed entirely (see e.g. Why is testing "$?" to see if a command succeeded or not, an anti-pattern?)
If it helps, you can use semi-colons
if [ $1 -gt 0 ]; then
echo "$1 is positive"
fi
# or even
if [ $1 -gt 0 ]; then echo "$1 is positive"; fi
As for why, it helps me to think of if, then, else, and fi as bash commands, and just like all other commands, they need to be at the start of a line (or after a semi-colon).

Find longest and shortest strings in stdin

This program is intended to read a file from stdin and return the longest and shortest lines. Right now it's in an infinite loop.Could someone tell me what I'm doing wrong?
Update: So after reading more about what read does, it's actually right (yay me) but I want to set the delimiter to a newline character while currently the read command is taking a string at every whitespace character. Does anyone know what I can do?
read T
short=""
long=""
for i in $T; do
if [[ $short = "" ]]; then
short=$i
elif [[ ${#i} -lt ${#short} ]]; then
short=$i
elif [[ ${#i} -gt ${#long} ]];then
long=$i
fi
done
echo $short
echo $long
It can't possibly reach an infinite loop, since you are looping over a
necessarily finite variable ($T).
I'm going to assume that your script is "hanging" and assume
one of the possible reasons (to provide you one possible
solution to your classroom problem): that the script is sleeping
waiting for data from stdin and no data is being sent to him.
Here's what you could do:
read short
long=$short
while read line; do
if [[ ${#line} -lt ${#short} ]]; then
short=$line
fi
if [[ ${#line} -gt ${#long} ]]; then
long=$line
fi
done
echo $short
echo $long
Notice that I've first initialized the short and long with the
first line of input, or empty string on stdin's EOF. Then I attempt
to read more lines in a while loop to then check for conditions to update short and
long; it is important to not exclude the line size checks if one
of them applies (like you didn't in your script using elifs).

grep, else print message for no matches

In a bash script, I have a list of lines in a file I wish to grep and then display on standard out, which is easiest done with a while read:
grep "regex" "filepath" | while read line; do
printf "$line\n"
done
However, I would like to inform the user if no lines were matched by the grep. I know that one can do this by updating a variable inside the loop but it seems like a much more elegant approach (if possible) would be to try to read a line in an until loop, and if there were no output, an error message could be displayed.
This was my first attempt:
grep "regex" "filepath" | until [[ -z ${read line} ]]; do
if [[ -z $input ]]; then
printf "No matches found\n"
break
fi
printf "$line\n"
done
But in this instance the read command is malformed, and I wasn't sure of another way the phrase the query. Is this approach possible, and if not, is there a more suitable solution to the problem?
You don't need a loop at all if you simply want to display a message when there's no match. Instead you can use grep's return code. A simple if statement will suffice:
if ! grep "regex" "filepath"; then
echo "no match" >&2
fi
This will display the results of grep matches (since that's grep's default behavior), and will display the error message if it doesn't.
A popular alternative to if ! is to use the || operator. foo || bar can be read as "do foo or else do bar", or "if not foo then bar".
grep "regex" "filepath" || echo "no match" >&2
John Kugelman's answer is the correct and succinct one and you should accept it. I am addressing your question about syntax here just for completeness.
You cannot use ${read line} to execute read -- the brace syntax actually means (vaguely) that you want the value of a variable whose name contains a space. Perhaps you were shooting for $(read line) but really, the proper way to write your until loop would be more along the lines of
grep "regex" "filepath" | until read line; [[ -z "$line" ]]; do
... but of course, when there is no output, the pipeline will receive no lines, so while and until are both wrong here.
It is worth amphasizing that the reason you need a separate do is that you can have multiple commands in there. Even something like
while output=$(grep "regex filepath"); echo "grep done, please wait ...";
count=$(echo "$output" | wc -l); [[ $count -gt 0 ]]
do ...
although again, that is much more arcane than you would ever really need. (And in this particular case, you would want probably actually want if , not while.)
As others already noted, there is no reason to use a loop like that here, but I wanted to sort out the question about how to write a loop like this for whenever you actually do want one.
As mentioned by #jordanm, there is no need for a loop in the use case you mentioned.
output=$(grep "regex" "file")
if [[ -n $output ]]; then
echo "$output"
else
echo "Sorry, no results..."
fi
If you need to iterate over the results for processing (rather than just displaying to stdout) then you can do something like this:
output=$(grep "regex" "file")
if [[ -n $output ]]; then
while IFS= read -r line; do
# do something with $line
done <<< "$output"
else
echo "Sorry, no results..."
fi
This method avoids using a pipeline or subshell so that any variable assignments made within the loop will be available to the rest of the script.
Also, i'm not sure if this relates to what you are trying to do at all, but grep does have the ability to load patterns from a file (one per line). It is invoked as follows:
grep search_target -f pattern_file.txt

Trying to check if information written in a text file is equal to a variable within shell script

I'm still a newbie to Linux scripting. My goal is to determine if the information written in a .txt file is equal to a variable within the script. This is the code that I have written so far:
#!/bin/sh
a=-5
if [$(cat < top.txt) -eq $a ]; then
echo "YES!"
else
echo "NNOOO!"
fi
Please let me know what I should be doing. To reiterate, I want top.txt to equal -5. I currently have -5 written in the text but can't seem to get it to work.
Use More Quotes™:
[ "$(cat top.txt)" = "$a" ]
However, beware that this doesn't handle the case where top.txt ends with a newline - $() will chop that right off. So you might be better served with this:
[ "$(cat top.txt; printf x)" = "$a"x ]
Another issue is that you can't check files containing the NUL character this way; variables can not contain NUL characters.
At this point I'd be tempted to ask what you are trying to achieve.
You can use:
[ "$(<top.txt)" -eq "$a" ] && echo "YES!" || echo "NOOOS!"

Resources