Asking for user input in a loop until match found in array of command line arguments - bash

Sometimes I need to find a specific serial in a box with many items, so I wrote a simple Bash script that allows me to use a barcode scanner to scan hundreds of barcodes until a match is found, at which point the screen flashes (so I can see it from the corner of my eyes while looking at the box).
The script works great, but it only checks against one specific serial number provided by the user. Here's the code:
#!/bin/bash
INPUT=''
SCAN=''
SN=''
I='0'
clear
printf "Enter serial\n"
read INPUT
SN=`printf "${INPUT}" | tr '[:lower:]' '[:upper:]'`
# Keep comparing scans to needed serial until a match is found
while [[ "${SCAN}" != *"${SN}"* ]];
do
clear
printf "Looking for [ "${SN}" ]\n"
printf "Please scan barcode\n"
read INPUT
SCAN=`printf "${INPUT}" | tr '[:lower:]' '[:upper:]'`
done
# Flash screen when match is found
while [[ "${I}" -lt 3 ]];
do
printf '\e[?5h' && sleep 0.3
printf '\e[?5l' && sleep 0.3
I=$[${I}+1]
done
printf "FOUND\n"
Today I spent hours trying to implement a way to pass multiple possible serial numbers as command line arguments, but I can't seem to get it working. I would like to be able to pass a small, manageable number of possible serials, like this:
$ ./script.sh sn1 sn2 sn3 sn4 sn5
And for the script continue asking for input until I come across the item I am looking for.
I've studied the handling of shell arguments, but I can't seem to "massage" the above while loop to get it to check if the scanned serial exists in the array (created from the command line arguments passed):
#!/bin/bash
snList=( "$#" )
INPUT=''
SCAN=''
SN=''
I='0'
clear
#displaying "things" so I can see what each variable contains (debugging)
printf "$#\n"
printf "$0\n"
printf "$*\n"
printf "$0\n"
printf "$1\n"
printf "$2\n"
printf "$3\n"
printf "snList: $snList\n"
printf "snList[#]: ${snList[#]}\n"
printf "snList[*]: ${snList[*]}\n"
# Keep comparing scans to needed serial until a match is found
while [[ ! " ${snList[*]} " =~ "${SCAN}" ]];
do
clear
printf "Looking for [ "$*" ]\n"
printf "Please scan barcode\n"
read INPUT
SCAN=`printf "${INPUT}" | tr '[:lower:]' '[:upper:]'`
done
I've tried using ${snList[#]} in the loop as well, same result, it behaves like a match was found immediately, without even asking for a scan (indicating that the content of the while loop is not being executed).
Any help will be immensely appreciated, I think I am close, but I can't figure out what I am doing wrong.
Thanks in advance!

Something like this maybe?
#!/usr/bin/env bash
to_compare_input=("$#")
exglob_pattern_input=$(IFS='|'; printf '%s' "#(${to_compare_input[*]})")
until [[ $user_input == $exglob_pattern_input ]]; do
read -r user_input
done
Run the script with the the following arguments.
bash -x ./myscript foo bar baz more
Output
+ to_compare_input=("$#")
++ IFS='|'
++ printf %s '#(foo|bar|baz|more)'
+ exglob_pattern_input='#(foo|bar|baz|more)'
+ [[ '' == #(foo|bar|baz|more) ]]
+ read -r user_input
papa
+ [[ papa == #(foo|bar|baz|more) ]]
+ read -r user_input
mama
+ [[ mama == #(foo|bar|baz|more) ]]
+ read -r user_input
baz
+ [[ baz == #(foo|bar|baz|more) ]]
The first user input is empty since the builtinread has not been executed to ask for the user's input. As shown at the debug message.
+ [[ '' == #(foo|bar|baz|more) ]]
The second (assuming the user has entered papa) is papa
The third (assuming the user has entered mama) is mama
The last is baz which breaks out of off the until loop, because it belongs to the $extglob_pattern_input, which is an extglob feature.
A regex is also an alternative using the =~ operator.
#!/usr/bin/env bash
to_compare_input=("$#")
regex_pattern_input=$(IFS='|'; printf '%s' "^(${to_compare_input[*]})$")
until [[ $user_input =~ $regex_pattern_input ]]; do
read -r user_input
done
Run the script same as before.
Using two loops which was suggested in the comments section.
#!/usr/bin/env bash
to_compare_input=("$#")
inarray() {
local n=$1 h
shift
for h; do
[[ $n == "$h" ]] && return
done
return 1
}
until inarray "$user_input" "${to_compare_input[#]}"; do
read -r user_input
done
As for the tr if your version of bash supports the ^^ and ,, for uppercase and lowercase parameter expansion. use ${user_input^^}
until [[ ${user_input^^} == $exglob_pattern_input ]]; do
until [[ ${user_input^^} =~ $regex_pattern_input ]]; do
until inarray "${user_input^^}" "${to_compare_input[#]}"; do

Assuming no spaces in the bar code texts. You can do something like this
while read -r INPUT
do
#Append spaces to prevent substring matching
if [[ $(echo " $# " | grep -i " ${INPUT} " | wc -l) -eq 1 ]]
then
break
fi
done

Related

Get first character of each string with BASH_REMATCH

I'am trying to get the first character of each string using regex and BASH_REMATCH in shell script.
My input text file contain :
config_text = STACK OVER FLOW
The strings STACK OVER FLOW must be uppercase like that.
My output should be something like this :
SOF
My code for now is :
var = config_text
values=$(grep $var test_file.txt | tr -s ' ' '\n' | cut -c 1)
if [[ $values =~ [=(.*)]]; then
echo $values
fi
As you can see I'am using tr and cut but I'am looking to replace them with only BASH_REMATCH because these two commands have been reported in many links as not functional on MacOs.
I tried something like this :
var = config_text
values=$(grep $var test_file.txt)
if [[ $values =~ [=(.*)(\b[a-zA-Z])]]; then
echo $values
fi
VALUES as I explained should be :
S O F
But it seems \b does not work on shell script.
Anyone have an idea how to get my desired output with BASH_REMATCH ONLY.
Thanks in advance for any help.
A generic BASH_REMATCH solution handling any number of words and any separator.
local input="STACK OVER FLOW" pattern='([[:upper:]]+)([^[:upper:]]*)' result=""
while [[ $input =~ $pattern ]]; do
result+="${BASH_REMATCH[1]::1}${BASH_REMATCH[2]}"
input="${input:${#BASH_REMATCH[0]}}"
done
echo "$result"
# Output: "S O F"
Bash's regexes are kind of cumbersome if you don't know how many words there are in the input string. How's this instead?
config_text="STACK OVER FLOW"
sed 's/\([^[:space:]]\)[^[:space:]]*/\1/g' <<<"$config_text"
First Put a valid shebang and paste your script at https://shellcheck.net for validation/recommendation.
With the assumption that the line starts with config and ends with FLOW e.g.
config_text = STACK OVER FLOW
Now the script.
#!/usr/bin/env bash
values="config_text = STACK OVER FLOW"
regexp="config_text = ([[:upper:]]{1})[^ ]+ ([[:upper:]]{1})[^ ]+ ([[:upper:]]{1}).+$"
while IFS= read -r line; do
[[ "$line" = "$values" && "$values" =~ $regexp ]] &&
printf '%s %s %s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"
done < test_file.txt
If there is Only one line or the target string/pattern is at the first line of the test_file.txt, the while loop is not needed.
#!/usr/bin/env bash
values="config_text = STACK OVER FLOW"
regexp="config_text = ([[:upper:]]{1})[^ ]+ ([[:upper:]]{1})[^ ]+ ([[:upper:]]{1}).+$"
IFS= read -r line < test_file.txt
[[ "$line" = "$values" && "$values" =~ $regexp ]] &&
printf '%s %s %s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"
Make sure you have and running/using Bashv4+ since MacOS, defaults to Bashv3
See How can I read a file (data stream, variable) line-by-line (and/or field-by-field)?
Another option rather than bash regex would be to utilize bash parameter expansion substring ${parameter:offset:length} to extract the desired characters:
$ read -ra arr <text.file ; printf "%s%s%s\n" "${arr[2]:0:1}" "${arr[3]:0:1}" "${arr[4]:0:1}"
SOF

Why will it not echo "$d" anwser

I need the script to output the result, but echo "$d" does not output anything. I made the ciphertext.gz earlier in the script and the $fil is ciphertext.gz. Bash script:
echo "Fil: ciphertext.gz"
a="ABCDEFGHIJKLMNOPQRSTUVXYZ"
[[ "${*/-d/}" != "" ]] &&
echo "Usage: $0 [-d]" && exit 1
m=${1:+-}
m=-
t=$fil
printf "Nøgle 'eks. ABCDE': "
read -r k
k=$(echo "$k" | tr [a-vx-z] [A-VX-Z] )
printf "\n"
for ((i=0;i<${#t};i++)); do
p1=${a%%${t:$i:1}*}
p2=${a%%${k:$((i%${#k})):1}*}
d="${d}${a:$(((${#p1}${m:-+}${#p2})%${#a})):1}"
done
echo "$d"
Extended comments, (not really an answer, since it's unclear what the code should do):
This is wrong:
m=${1:+-}
m=-
...since it has the same effect as:
m=-
This reads one line from standard input:
read -r k
...which, unless ciphertext is only one line long, probably
defeats the purpose of the next eight lines of code. Even if
standard input was unzip < ciphertext.gz |, it would only decode
the first line of ciphertext.
Wrap the for in an appropriate while read k loop.

Bash problems with string comparison

I have a problem with writing bash script. The problem is in comparison of strings. When I launch it, there's no errors. However in result, it is always changing the variable client.
So if for an example we have two lines in file
apple A
orange D
and if I give the who=A I expect to see in result apple, or if at D - orange
But no matter of what I choose A or D it is always giving me the result - orange
No matter of the strings, it always change the variable client, like ignoring the comparison. Please help.
while read line
do
IFS=" "
set -- $line
echo $2" "$who":"$1
if [[ "$2"="$who" ]]
then
echo "change"
client=$1
fi
done < $file
echo $client
So now I changed the code as in one of the comment below, but now the caparison always false therefore the variable client is always empty
while read -r line
do
#IFS=" "
#set -- $line
#echo $2" "$who":"$1
#if [[ "$2" = "$who" ]]
a="${line% *}"
l="${line#* }"
if [[ "$l" == "$who" ]]
then
echo "hi"
client="$a"
fi
done < $file
If you have data in a file with each line like apple D and you want to read the file and separate then items, the parameter expansion/substring extraction is the correct way to process the line. For example (note $who is taken from your problem statement):
while read -r line
do
fruit="${line% *}" # remove from end to space
letter="${line#* }" # remove from start to space
if [[ "$letter" == "$who" ]]
then
echo "change"
client="$fruit"
fi
done < $file
Short Example
Here is a quick example of splitting the words with parameter expansion/substring extraction:
#!/bin/bash
while read -r line
do
fruit="${line% *}"
letter="${line#* }"
echo "fruit: $fruit letter: $letter"
done
exit 0
input
$ cat dat/apple.txt
Apple A
Orange D
output
$ bash apple.sh <dat/apple.txt
fruit: Apple letter: A
fruit: Orange letter: D
Change if [[ "$2"="$who" ]] to
if [[ "$2" = "$who" ]]
spaces around =
Example (for clarification):
who=A
while read line
do
IFS=" "
set -- $line
echo $2" "$who":"$1
if [[ "$2" = "$who" ]]
then
echo "change"
client=$1
fi
done < file #this is the file I used for testing
echo $client
Output:
A A:apple
change
D A:orange
apple
For who=D:
A D:apple
D D:orange
change
orange
You do need spaces around that = operator.
However, I think you're facing yet another issue as you're trying to change the value of the client variable from inside the while loop (which executes in a subshell). I don't think that will work; see this quesion for details.

What's the best practice in capitalization and punctuation when outputting program progress to STDERR?

I'm making some command line tools that output progress information as it runs to STDERR, like
found document
using cached version
analyzing
etc.
Should I output full sentences with capitalized first letters and periods at the end, or is this kind of terse uncapitalized output OK? What's the expert consensus on this?
My favorite method to denote progress is a 'spinner'. Here is one I implemented using bash. The first parameter is the PID of the process you want to track and the second parameter is an optional message. The PID is most easily passed via $(pgrep <some_process_name>)
#!/bin/bash
spinner() {
[[ -n "$2" ]] && echo -n "$2 "
if [[ ! $1 =~ ^[[:digit:]]+$ ]]; then
return
fi
while [[ -d /proc/$1 ]]; do
for c in '/' '-' '\' '|'; do
printf "%c\b" "$c"
sleep 0.1
done
done
printf " \n"
}
du /usr > /dev/null 2>&1 & # Example program to monitor
spinner $(pgrep du) "Optional Message Here"

Bash Shell Scripting - detect the Enter key

I need to compare my input with Enter/Return key...
read -n1 key
if [ $key == "\n" ]
echo "###"
fi
But this is not working.. What is wrong with this code
Several issues with the posted code. Inline comments detail what to fix:
#!/bin/bash
# ^^ Bash, not sh, must be used for read options
read -s -n 1 key # -s: do not echo input character. -n 1: read only 1 character (separate with space)
# double brackets to test, single equals sign, empty string for just 'enter' in this case...
# if [[ ... ]] is followed by semicolon and 'then' keyword
if [[ $key = "" ]]; then
echo 'You pressed enter!'
else
echo "You pressed '$key'"
fi
Also it is good idea to define empty $IFS (internal field separator) before making comparisons, because otherwise you can end up with " " and "\n" being equal.
So the code should look like this:
# for distinguishing " ", "\t" from "\n"
IFS=
read -n 1 key
if [ "$key" = "" ]; then
echo "This was really Enter, not space, tab or something else"
fi
I'm adding below code just for reference if someone will want to use such solution containing countdown loop.
IFS=''
echo -e "Press [ENTER] to start Configuration..."
for (( i=10; i>0; i--)); do
printf "\rStarting in $i seconds..."
read -s -N 1 -t 1 key
if [ "$key" = $'\e' ]; then
echo -e "\n [ESC] Pressed"
break
elif [ "$key" == $'\x0a' ] ;then
echo -e "\n [Enter] Pressed"
break
fi
done
read reads a line from standard input, up to but not including the new line at the end of the line. -n specifies the maximum number of characters, forcing read to return early if you reach that number of characters. It will still end earlier however, when the Return key is pressed. In this case, its returning an empty string - everything up to but not including the Return key.
You need to compare against the empty string to tell if the user immediately pressed Return.
read -n1 KEY
if [[ "$KEY" == "" ]]
then
echo "###";
fi
None of these conditions worked for me and so I've came up with this one:
${key} = $'\0A'
Tested on CentOS with Bash 4.2.46.

Resources