Nesting If Condition Inside A While Loop - bash

I'm reading the contents of a file and storing them in 2 variables then simultaneously want to compare it with an array using if statement. Code is given below
#!/bin/bash
# Define File
datafile=./regions-with-keys
# Create Nodes File
cat << EOF > $datafile
region1 key1
region2 key2
region3 key3
EOF
# User Input
clear;
echo -ne "PLEASE SELECT REGIONS(s) :\n\033[37;40m[Minimum 1 Region Required]\033[0m"
read -ra a
echo "${a[#]}"
# Reading Regions & Keys
for i in "${a[#]}"
do
while read -r $b $c; do
if [ "${a[#]}" -eq "$b" ]; then
echo "$b" "$c"
fi
done < $datafile
done;
it gives command not found for if statement when executed..
Aim of the code is to match the array indexes of userinput with $a from $datafile, if match is successful print
$b and $c

Try this Shellcheck-clean code:
#!/bin/bash -p
# Define File
datafile=./regions-with-keys
# Create Nodes File
cat <<EOF >"$datafile"
region1 key1
region2 key2
region3 key3
EOF
# User Input
clear
echo 'PLEASE SELECT REGIONS(s) :'
echo -ne '\e[37;40m[Minimum 1 Region Required]\e[0m'
read -ra input_regions
declare -p input_regions
# Reading Regions & Keys
for input_rgn in "${input_regions[#]}" ; do
while read -r data_rgn key ; do
if [[ $data_rgn == "$input_rgn" ]] ; then
printf '%s %s\n' "$data_rgn" "$key"
fi
done <"$datafile"
done
Significant changes from the code in the question are:
Use meaningful variable names.
Use declare -p input_regions to print the contents of the array in an unambiguous way.
Use varname instead of $varname as arguments to read. That fixes a serious bug in the original code.
Use printf instead of echo for printing variable values. See Why is printf better than echo?.
Used [[ ... == ...]] instead of [ ... -eq ... ] for comparing the region names. [[ ... ]] is more powerful than [ ... ]. See Is double square brackets [[ ]] preferable over single square brackets [ ] in Bash?. Also, -eq is for comparing integers and == (or, equivalently, =) is for comparing strings.
Did various cleanups (removed some blank lines, removed unnecessary semicolons, ...).
The new code is Shellcheck-clean. Shellcheck identified several problems with the original code.
If you want to report incorrect input regions, try replacing the "Reading Regions & Keys" code with this:
for input_rgn in "${input_regions[#]}" ; do
# Find the key corresponding to $input_rgn
key=
while read -r data_rgn data_key ; do
[[ $data_rgn == "$input_rgn" ]] && key=$data_key && break
done <"$datafile"
if [[ -n $key ]] ; then
printf '%s %s\n' "$input_rgn" "$key"
else
printf "error: region '%s' not found\\n" "$input_rgn" >&2
fi
done

Related

Access associative arrays with variables

Let's say we declared two associative arrays:
#!/bin/bash
declare -A first
declare -A second
first=([ele]=value [elem]=valuee [element]=valueee)
second=([ele]=foo [elem]=fooo [element]=foooo)
# echo ${$1[$2]}
I want to echo the given hashmap and element from script inputs. For example, if I run sh.sh second elem, the script should echo fooo.
An inelegant but bullet-proof solution would be to white-list $1 with the allowed values:
#!/bin/bash
# ...
[[ $2 ]] || exit 1
unset result
case $1 in
first) [[ ${first["$2"]+X} ]] && result=${first["$2"]} ;;
second) [[ ${second["$2"]+X} ]] && result=${second["$2"]} ;;
*) exit 1 ;;
esac
[[ ${result+X} ]] && printf '%s\n' "$result"
notes:
[[ $2 ]] || exit 1 because bash doesn't allow empty keys
[[ ${var+X} ]] checks that the variable var is defined; with this expansion you can also check that an index or key is defined in an array.
A couple ideas come to mind:
Variable indirection expansion
Per this answer:
arr="$1[$2]" # build array reference from input fields
echo "${!arr}" # indirect reference via the ! character
For the sample call sh.sh second elem this generates:
fooo
Nameref (declare -n) (requires bash 4.3+)
declare -n arr="$1"
echo "${arr[$2]}"
For the sample call sh.sh second elem this generates:
fooo

Updating a config file using shell script

I have a config file which looks like this:
define hostgroup {
hostgroup_name NA-servers ; The name of the hostgroup
alias NA region ; Long name of the group
members sample.com ; hosts belonging to this group
}
define hostgroup{
hostgroup_name FTP-server ; The name of the hostgroup
alias FTP NA region ; Long name of the group
members example.com
}
I need to update the members value conditionally depending on the hostgroup_name.
How do I parse the above file?
This format is amenable to regex-based parsing:
#!/usr/bin/env bash
case $BASH_VERSION in ''|[1-3].*) echo "ERROR: Bash 4.0 or newer required" >&2; exit 1;; esac
PS4=':$LINENO+'; set -x # enable trace logging w/ line numbers
start_hostgroup_re='^define[[:space:]]+hostgroup[[:space:]]*[{]'
kv_re='^[[:space:]]*([^[:space:];]+)[[:space:]]+([^;]+)(;.*)?'
end_re='^[[:space:]]*}'
declare -A keys=( ) comments=( )
build_new_members() { # Replace this with your own code for generating a new member list
local hostgroup_name=$1 old_members=$2
echo "New member list for $hostgroup_name"
}
in_hostgroup=0
while IFS= read -r line; do : "line=$line"
if (( in_hostgroup )); then
if [[ $line =~ $kv_re ]]; then
keys[${BASH_REMATCH[1]}]=${BASH_REMATCH[2]}
comments[${BASH_REMATCH[1]}]=${BASH_REMATCH[3]}
elif [[ $line =~ $end_re ]]; then
keys["members"]=$(build_new_members "${keys["hostgroup_name"]}" "${keys["members"]}")
printf '%s\n' 'define hostgroup {'
for key in "${!keys[#]}"; do : key="$key"
value=${keys[$key]}
comment=${comments[$key]}
printf ' %-16s %s %s\n' "$key" "$value" "$comment"
done
printf '%s\n' '}'
keys=( ); comments=( ); in_hostgroup=0
elif [[ $line ]]; then # warn about non-empty non-assignment lines
printf 'WARNING: Unrecognized line in hostgroup: %s\n' "$line" >&2
fi
else
if [[ $line =~ $start_hostgroup_re ]]; then
in_hostgroup=1
else
printf '%s\n' "$line"
fi
fi
done
See this code running at https://ideone.com/Z6kvcf

bash select multiple answers at once

I have a flat file called items that I want to populate a select but I want to be able to choose multiple items at one time.
contents of items file:
cat 1
dog 1
pig 1
cherry 2
apple 2
Basic script:
#!/bin/bash
PS3=$'\n\nSelect the animals you like: '
options=$(grep '1' items|grep -v '^#' |awk '{ print $1 }')
select choice in $options
do
echo "you selected: $choice"
done
exit 0
The way it flows now is I can only select one option at at time. I'd like to be able to answer 1,3 or 1 3 and have it respond "you selected: cat pig"
Thank you,
Tazmarine
I can offer a somewhat different approach that uses a different selection prompt style. Here's a bash function that allows user to select multiple options with arrow keys and Space, and confirm with Enter. It has a nice menu-like feel. I wrote it with the help of https://unix.stackexchange.com/a/415155. It can be called like this:
multiselect result "Option 1;Option 2;Option 3" "true;;true"
The result is stored as an array in a variable with the name supplied as the first argument. Last argument is optional and is used for making some options selected by default. It looks like this:
function prompt_for_multiselect {
# little helpers for terminal print control and key input
ESC=$( printf "\033")
cursor_blink_on() { printf "$ESC[?25h"; }
cursor_blink_off() { printf "$ESC[?25l"; }
cursor_to() { printf "$ESC[$1;${2:-1}H"; }
print_inactive() { printf "$2 $1 "; }
print_active() { printf "$2 $ESC[7m $1 $ESC[27m"; }
get_cursor_row() { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; }
key_input() {
local key
IFS= read -rsn1 key 2>/dev/null >&2
if [[ $key = "" ]]; then echo enter; fi;
if [[ $key = $'\x20' ]]; then echo space; fi;
if [[ $key = $'\x1b' ]]; then
read -rsn2 key
if [[ $key = [A ]]; then echo up; fi;
if [[ $key = [B ]]; then echo down; fi;
fi
}
toggle_option() {
local arr_name=$1
eval "local arr=(\"\${${arr_name}[#]}\")"
local option=$2
if [[ ${arr[option]} == true ]]; then
arr[option]=
else
arr[option]=true
fi
eval $arr_name='("${arr[#]}")'
}
local retval=$1
local options
local defaults
IFS=';' read -r -a options <<< "$2"
if [[ -z $3 ]]; then
defaults=()
else
IFS=';' read -r -a defaults <<< "$3"
fi
local selected=()
for ((i=0; i<${#options[#]}; i++)); do
selected+=("${defaults[i]}")
printf "\n"
done
# determine current screen position for overwriting the options
local lastrow=`get_cursor_row`
local startrow=$(($lastrow - ${#options[#]}))
# ensure cursor and input echoing back on upon a ctrl+c during read -s
trap "cursor_blink_on; stty echo; printf '\n'; exit" 2
cursor_blink_off
local active=0
while true; do
# print options by overwriting the last lines
local idx=0
for option in "${options[#]}"; do
local prefix="[ ]"
if [[ ${selected[idx]} == true ]]; then
prefix="[x]"
fi
cursor_to $(($startrow + $idx))
if [ $idx -eq $active ]; then
print_active "$option" "$prefix"
else
print_inactive "$option" "$prefix"
fi
((idx++))
done
# user key control
case `key_input` in
space) toggle_option selected $active;;
enter) break;;
up) ((active--));
if [ $active -lt 0 ]; then active=$((${#options[#]} - 1)); fi;;
down) ((active++));
if [ $active -ge ${#options[#]} ]; then active=0; fi;;
esac
done
# cursor position back to normal
cursor_to $lastrow
printf "\n"
cursor_blink_on
eval $retval='("${selected[#]}")'
}
You can not do that as such, but you can always record each individual selection:
#!/bin/bash
PS3=$'\n\nSelect the animals you like: '
options=$(grep '1' items|grep -v '^#' |awk '{ print $1 }')
# Array for storing the user's choices
choices=()
select choice in $options Finished
do
# Stop choosing on this option
[[ $choice = Finished ]] && break
# Append the choice to the array
choices+=( "$choice" )
echo "$choice, got it. Any others?"
done
# Write out each choice
printf "You selected the following: "
for choice in "${choices[#]}"
do
printf "%s " "$choice"
done
printf '\n'
exit 0
Here's an example interaction:
$ ./myscript
1) cat
2) dog
3) pig
4) Finished
Select the animals you like: 3
pig, got it. Any others?
Select the animals you like: 1
cat, got it. Any others?
Select the animals you like: 4
You selected the following: pig cat
If you instead want to be able to write 3 1 on the same line, you'll have to make your own menu loop with echo and read
This is what I came up with. This seems to works as I want. I want the final output to be comma separated:
#!/bin/bash
newarray=(all $(grep '1' items|grep -v '^#' |awk '{ print $1 }'))
options() {
num=0
for i in ${newarray[#]}; do
echo "$num) $i"
((num++))
done
}
getanimal() {
while [[ "$show_clean" =~ [A-Za-z] || -z "$show_clean" ]]; do
echo "What animal do you like: "
options
read -p 'Enter number: ' show
echo
show_clean=$(echo $show|sed 's/[,.:;]/ /g')
selected=$(\
for s in $show_clean; do
echo -n "\${newarray[${s}]},"
done)
selected_clean=$(echo $selected|sed 's/,$//')
done
eval echo "You selected $selected_clean"
}
getanimal
exit 0

Bash print the number of incorrect file line

A beginner asking for help (:
So, I have a script that checks brackets in a text file and tells whether they are closed correctly. However, I also want to make my script print out the number of the incorrect line (where brackets are closed incorrectly). I have tried counting file lines and then making a nested while loop, however, it doesn't work for me at all ): Are there any simple solutions for this? I would like to leave the LINE counter if that's possible o:
INPUT="$1"
count=0
LINE=0
# Check if file exists
[ ! -f $INPUT ] && { echo "file $INPUT do not exist."; exit ; }
# Count file lines and read every char
while IFS= read -r LINE
do
LINE=$(( LINE + 1 ))
while read -n1 char
do
[ "$char" == "(" ] && (( count++ ))
[ "$char" == ")" ] && (( count-- ))
if [ "$count" -lt 0 ]
then
break
fi
done
done < "$INPUT"
if [ "$count" -lt 0 ]
then
echo "Found a mistake in $LINE line "
else
echo "Everything's correct"
fi
You have a couple of problems:
Your read in the inner loop consumes the input from the file, not from LINE.
The line
LINE=$(( LINE + 1 ))
is really wrong: LINE is the content of the line of your file, and your trying to add 1 to it. Weird.
Your break only breaks the inner loop (it should break two loops). Use break 2 for this.
Here's a working version of your script:
input=$1
count=0
linenb=0
# Check if file exists
[[ -f $input ]] || { echo "Error: file $input do not exist."; exit 1; }
# Count file lines and read every char
while IFS= read -r line; do
((++linenb))
while read -n1 char; do
[[ $char == '(' ]] && ((++count))
[[ $char == ')' ]] && ((--count))
((count>=0)) || break 2
done <<< "$line"
done < "$input"
if ((count<0)); then
echo "Found a mistake in line #$linenb:"
printf '%s\n' "$line"
else
echo "Everything's correct"
fi
Note that I used more ((...)) and [[...]].
I also used lowercase variable names, as your computer isn't deaf: you don't need to shout the name of the variable. (And it's nicer to the eye). And it's good practice to use lowercase variable names, as there's no chance that they clash with Bash's own variables.

In xcode is there a way to verify all NSLocalizedStrings' keys?

Aside from running every code path that has an NSLocalizedString in it, is there a way to verify that all NSLocalizedStrings have a key that actually exists in all your Localizable.strings files of all your bundles?
E.g. there wasn't a typo in one key such that NSLocalizedString won't find the key it's looking for?
OK I wrote a bash script to accomplish the above. Here it is. It took me hours so don't forget to up-vote me if you like. Feel free to make improvements, etc. I added a few comments suggesting potential improvements.
#!/bin/sh
# VerNSLocalizedStrings
while getopts "vsl:" arg; do
case $arg in
v)
verbose="yes"
;;
s)
stopOnMissing="yes"
;;
l)
lang=$OPTARG
;;
esac
done
if [[ -z $lang ]]
then
lang="en"
fi
searchDir=$lang.lproj
fileFound=`ls . | grep $searchDir`
if [[ -z $fileFound ]]
then
echo "dir "$searchDir" not found."
exit
fi
fileFound=`ls $searchDir/ | grep strings`
if [[ -z $fileFound ]]
then
echo "No .strings files found in dir "$searchDir"."
exit
fi
echo "Verifying NSLocalizationStrings in "$searchDir
# Get all the NSLocalizedString Commands
output=$(grep -R NSLocalizedString . --include="*.m")
# Go thru the NSLocalizedString commands line for line
count=$(( 0 ))
missing=$(( 0 ))
unusable=$(( 0 ))
OIFS="${IFS}"
NIFS=$'\n'
IFS="${NIFS}"
for LINE in ${output} ; do
IFS="${OIFS}"
# Now extract the key from it
# admittedly this only works if there are no line breaks between
# NSLocalizedStrings and the entire key,
# but it accounts for the keys it couldn't identify.
quotes=`echo $LINE | awk -F\" '{ for(i=2; i<=NF; i=i+2){ a = a"\""$i"\"""^";} {print a; a="";}}'`
key=`echo $quotes | cut -f1 -d"^"`
# If we couldn't find the key then flag problem
if [[ -z $key ]]
then
(( unusable += 1 ))
echo "Couldn't extract key: " $LINE
if [ -n "$stopOnMissing" ]
then
break
else
continue
fi
fi
# I don't know how grep works regarding length of string, only that
# if the string is too long then it doesn't find it in the file
keyLength=$(echo ${#key})
if [ $keyLength -gt 79 ]
then
(( unusable += 1 ))
echo "Key too long ("$keyLength"): " $key
if [ -n "$stopOnMissing" ]
then
break
else
continue
fi
fi
# It would be nice if this were a regular expression that allowed as many
# spaces as you want, even a line break then forced the quotes on the
# other side of the equal sign.
keyString=$key" ="
# Search for the key
found=$(iconv -sc -f utf-16 -t utf8 $searchDir/*.strings | grep "$keyString")
# damned if I know why some strings files are utf-16 and others are utf8
if [[ -z $found ]]
then
found=$(grep -r "$keyString" $searchDir/ --include=*.strings)
fi
# analyze the result
if [[ -z $found ]]
then
(( missing += 1 ))
echo "Missing: " $key "\n from: " $LINE
if [ -n "$stopOnMissing" ]
then
break
fi
else
if [ -n "$verbose" ]
then
echo "found: " $key
fi
fi
(( count += 1 ))
IFS="${NIFS}"
done
IFS="${OIFS}"
# It would also be nice if it went the other way and identified
# extraneous unused items in the strings files. But
# I've spent enough time on this for now
echo $count " keys analyzed"
echo $unusable " keys could not be determined"
echo $missing " keys missing"
To verify that all NSLocalizedStrings have a key that actually exists in all your Localizable.strings files or you missed localised you can enable the Localization enable "Show non-localized strings" option in the your project scheme editor.
Now run application you will see console logs for the missing localised string.

Resources