Find longest and shortest strings in stdin - bash

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).

Related

Problem with comparison for checking that a string contains only digits in a bash shell script

I am having to modify a bash shell script, but I don't have much experience with bash shell scripting (I have worked with many other languages, but just not much with shell scripting).
The script displays a menu with "X" items, and then it prompts the user to enter a number.
After the user enters a number, I think that the script checks the number/string that was entered to make sure it is a digit (I think) and then also if the number is within a range of values.
Part of what I want to change is the number of items in the menu and there were originally 9 items, but I need to add another 10 or so. It looks like the check is failing if the number entered is more than 1 digit.
Here's the snippet where it is doing the check:
while menu && read -rp "$prompt" num && [[ "$num" ]]; do
if [[ "$num" == [[:digit:]] ]] && [[ $num -gt 0 ]] && [[ $num -le ${#service_accounts[#]} ]]; then
I think the part of the "if" that is failing is:
[[ "$num" == [[:digit:]] ]]
Can someone tell me if that comparison would fail if the string in $num is more than 1 digit?
And, also, if that is the case, how can I change that comparison to work correctly even if the $num has more than 1 digit?
Sorry if this is a kind of odd type of question :(!! And, thanks in advance!
Jim
[[ "$num" == [[:digit:]] ]] fails with everything but one digit.
This enables your script to accept numbers with one to two digits:
[[ "$num" =~ ^[[:digit:]]{1,2}$ ]]
=~ compares $num with regex ^[[:digit:]]{1,2}$.
See: The Stack Overflow Regular Expressions FAQ

Most efficient way to perform exception handling in bash?

I'm new to bash scripting & more familiar with python, but lets say I have this script for example.
Problem:
Entire script terminates if null value is inputed, such as accidental pressing enter twice or not putting "n" or "y" when prompted.
Solution Goal:
Instead of terminating, I would like to add exception/error message & perform prompt the user again every time there's a null value or a not an y/n.
read -r -p "Check the test results below.. do they look good enough to continue? [y/N]" response
if pp $response =~ ^([yY[eE][sS]|[yY])$
then
echo "Continuing"
elif [[ $response =~ ^([nN][oO]|[nN])$ ]]
exit
else
# want to use read -r -p prompt again. Perform recursion if possible
exit
fi
I am trying to perform recursion with this builtin type read. I am wondering if there's a simple solution to achieve my goal.
Enclose it inside a while loop
#!/usr/bin/env bash
while read -r -p "Check the test results below.. do they look good enough to continue? [y/N] " response; do
if [[ $response =~ ^([yY[eE][sS]|[yY])$ ]]; then
echo "Continuing"
elif [[ $response =~ ^([nN][oO]|[nN])$ ]]; then
exit
else
printf '%s\n' "Illegal argument ${response:-empty}" >&2
fi
done
the while read loop should be enough.
The "${response:-empty}" is form of P.E. parameter expansion.

how to match a specific file extension in shellscript

I looked some other posts and learnt to match file extension in the following way but why my code is not working? Thanks.
1 #!/bin/sh
2
3 for i in `ls`
4 do
5 if [[ "$i" == *.txt ]]
6 then
7 echo "$i is .txt file"
8 else
9 echo "$i is NOT .txt file"
10 fi
11 done
eidt:
I realized #!/bin/sh and #!/bin/bash are different, if you are looking at this post later, remember to check which one you are using.
The [[ ]] expression is only available in some shells, like bash and zsh. Some more basic shells, like dash, do no support it. I'm guessing you're running this on a recent version of Ubuntu or Debian, where /bin/sh is actually dash, and hence doesn't recognize [[. And actually, you shouldn't use [[ ]] with a #!/bin/sh shebang anyway, since it's unsafe to depend on a feature that the shebang doesn't request.
So, what to do about it? You'll have the [ ] type of test expression available, but it doesn't do pattern matching (like *.txt). There are a number of alternate ways to do it:
The case statement is available in even basic shells, and has the same pattern matching capability as [[ = ]]. This is the most common way to do this type of thing, especially when you have a list of different patterns to check against.
More indirectly, you can use ${var%pattern} to try remove .txt from the end of the end of the value (see "Remove Smallest Suffix Pattern" here), and then check to see if that changed the value:
if [ "$i" != "${i%.txt}" ]
More explanation: suppose $i is "file.txt"; then this expands to [ "file.txt" != "file" ], so they're not equal, and the test (for !=) succeeds. On the other hand, if $i is "file.pdf", then it expands to [ "file.pdf" != "file.pdf" ], which fails because the strings are the same.
Other notes: when using [ ], use a single equal sign for string comparison, and be sure to properly double-quote all variable references to avoid confusion. Also, if you use anything that has special meaning to the shell (like < or >), you need to quote or escape them.
You could use the expr command's : operator to do regular expression matching. (Regular expressions are a different type of pattern from the basic wildcard or "glob" expression.) You could do this, but don't.
#!/bin/sh
for i in `ls`
do
if [[ "$i" = *".txt" ]] ; then
echo "$i is .txt file"
else
echo "$i is NOT .txt file"
fi
done
You don't have to loop in ls output, and sh implementation might vary among OS distributions.
Consider:
#! /bin/sh
for i in *
do
if [[ "$i" == *.txt ]]
then
echo "$i is txt file"
else
echo "$i is NOT txt file"
fi
done

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

Is it possible to mark and save a particular bash script line number and restart the script at that saved position?

I need to know if it is possible to mark a bash script line number and then restart that script at the saved line number.
Code:
#!/bin/bash
while read -r line; do #I'm reading from a big wordlist
command1 using $line
command2 using $line
done
Specifically, is there a way to write the current $line number of the script automatically into a separate text file in order for the script to start from the line number specified, so that I won't have to start everything from scratch in case I have to stop the script?
Does it make sense?
Thank you very much !
This may help:
#!/bin/bash
TMP_FILE="/tmp/currentLineNumber" # a constant
current_line_count=0 # track the current line number
processed_lines_count=0
# Verify if we have already processed some stuff.
if [ -r "${TMP_FILE}" ]; then
processed_lines_count=$(cat ${TMP_FILE})
fi
while read -r line; do # I 'm reading from a big wordlist
# Skip processing till we reach the line that needs to be processed.
if [ $current_line_count -le $processed_line_count ]; then
# do nothing as this line has already been processed
current_line_count=$((current_line_count+1)) # increment the counter
continue
fi
current_line_count=$((current_line_count+1))
echo $current_line_count > ${TMP_FILE} # cache the line number
# perform your operations
command1 using $line
command2 using $line
done
This should work:
#!/bin/bash
I=`cat lastline`;
A=0;
while read -r line; do
if [$A>=$I]; then
command1 using $line
command2 using $line
(( I++ ))
echo "$I" > "lastline";
fi;
(( A++ ))
done
Remember you will have to delete lastline if you want to restart. :-)
The bash-only solutions are nice, but you may get better performance by using other tools to streamline your restart. Like the script in your question, the following takes the wordlist on stdin.
#!/bin/sh
# Get the current position, or 0 if we haven't run before
if [ -f /tmp/processed ]; then
read processed < /tmp/processed
else
processed=0
fi
# Skip up to the current position
awk -v processed="$processed" 'NR > processed' | while read -r line; do
# Run your commands
command1 using $line
command2 using $line
# Record our new position
processed=$((processed + 1))
echo $processed > /tmp/processed
done
Oh, and the way I wrote this, it's Bourne shell compatible, so it doesn't require bash.

Resources