├── myscript.bash ├── test.sh ├── testify.bash ├── README.md └── utils.bash /myscript.bash: -------------------------------------------------------------------------------- 1 | function Name() { 2 | fName="$1" 3 | lName="$2" 4 | 5 | if [[ -z "$fName" ]];then 6 | return 5 7 | fi 8 | 9 | if [[ -z "$lName" ]];then 10 | return 3 11 | fi 12 | 13 | echo "$fName $lName" 14 | return 0 15 | } 16 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | source ./myscript.bash 2 | source ./testify.bash 3 | 4 | assert expect "$(Name 'Jane' 'Doe')" "John Doe" "Test for Name Function" "should fail" 5 | assert expect "$(Name 'Jane' 'Doe')" "Jane Doe" "Test for Name Function" "should succeed" 6 | assert status "Name" "5" "Test for status code" "should return 5" 7 | assert status "Name 'Jane' 'Doe'" "0" "Test for Status Code" "should return 0" 8 | assert status "Name 'Jane' " "3" "Test for Status Code" "should return 3" 9 | assert status "Name 'Jane' 'Doe'" "12" "Test for Status Code" "it should fail" 10 | assert regex "$(Name 'Jane' 'Doe')" "Jane" "Test for Regexp" "it should match" 11 | assert regex "123victory" "\W" "Test for Regexp Non Word Character" "it should fail if match failes" 12 | assert expect "$((2+2))" "4" "Test for Simple Math Operation" "It should succeed" 13 | assert regex "What is the difference between 6 and half a dozen" "[[:digit:]]" "Match Number Regular Expression" "It should succeed" 14 | assert status "ls ." "0" "List in current dir" "it should return 0" 15 | assert done 16 | -------------------------------------------------------------------------------- /testify.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | open="\e[" 4 | close="\e[0m" 5 | bold="1;" 6 | light="0;" 7 | red="31m" 8 | green="32m" 9 | yellow="33m" 10 | lightGrey="37m" 11 | 12 | declare -i fails=0 13 | declare -i succ=0 14 | 15 | function failure() { 16 | 17 | printf "\t\t${open}${bold}${red}%b${close}" "\u2716" 18 | printf "${open}${light}${lightGrey} %s${close}\n" "Failure: ${message} " 19 | fails=$((fails + 1)) 20 | 21 | } 22 | function success() { 23 | 24 | printf "\t\t${open}${bold}${green}%b${close}" "\u2714" 25 | printf "${open}${light}${lightGrey} %s${close}\n" "Success: ${message} " 26 | succ=$((succ + 1)) 27 | } 28 | function assert() { 29 | local subcom="${1}" 30 | 31 | # take out the first argument which is subcom from ${@} 32 | shift 33 | 34 | if [[ "$subcom" != "done" ]];then 35 | 36 | (( ${#@} < 2 )) && { 37 | printf "%s\n" "Need at least 2 argument but got ${#@}" 38 | exit 0; 39 | } 40 | 41 | fi 42 | 43 | 44 | local describe="${3}" 45 | local message="${4}" 46 | 47 | printf "\n" 48 | [[ ! -z "${describe}" ]] && { 49 | printf "\t${open}${light}${yellow}%s${close}\n\n" "${describe}" 50 | 51 | } 52 | 53 | case $subcom in 54 | expect) 55 | local actual="${1}" 56 | local expected="${2}" 57 | 58 | [[ "${actual}" != "${expected}" ]] && { 59 | failure "${message}" 60 | } || { 61 | success "${message}" 62 | } 63 | 64 | ;; 65 | regex) 66 | 67 | local actual="${1}" 68 | local regexp="${2}" 69 | 70 | 71 | [[ ! "${actual}" =~ ${regexp} ]] && { 72 | failure "${message}" 73 | } || { 74 | success "${message}" 75 | } 76 | 77 | 78 | ;; 79 | status) 80 | 81 | local com="${1}" 82 | local expectedStatus="${2}" 83 | 84 | source ./utils.bash 85 | 86 | expectedStatus=$(int "${expectedStatus}") 87 | status=$? 88 | 89 | (( status == 0 )) && { 90 | 91 | #declare -F ${com} &>/dev/null 92 | 93 | #[[ $? == 0 ]] && { 94 | 95 | ${com} &>/dev/null 96 | status=$? 97 | 98 | [[ "${status}" != "${expectedStatus}" ]] && { 99 | failure "${message}" 100 | } || { 101 | success "${message}" 102 | } 103 | 104 | #} || { 105 | 106 | # printf "\t\t\t${open}${light}${red}%s${close}\n" "Cannot run this test" 107 | #} 108 | 109 | } || { 110 | printf "\t\t\t${open}${light}${red}%s${close}\n" "Cannot run this test" 111 | } 112 | 113 | ;; 114 | \done) 115 | printf "\n\n" 116 | printf "\t${open}${light}${red}%s %d${close}\n" "Fails: " "${fails}" 117 | printf "\t${open}${light}${green}%s %d${close}\n" "Success: " "${succ}" 118 | exit 0 119 | ;; 120 | *) 121 | printf "${open}${bold}${red}%s${close}|\n" "invalid subcommand" 122 | exit 2 123 | esac 124 | } 125 | 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bash-assert 2 | 3 | testify is a lightweight unit testing framework for bash 4 | 5 | # Usage 6 | 7 | clone this repository `git clone https://github.com/zombieleet/testify.git` 8 | 9 | create a test file then source testify.bash and the script you want to test in the test file 10 | 11 | 12 | ```bash 13 | # myscript.bash 14 | function Name() { 15 | fName="$1" 16 | lName="$2" 17 | 18 | if [[ -z "$fName" ]];then 19 | return 5 20 | fi 21 | 22 | if [[ -z "$lName" ]];then 23 | return 3 24 | fi 25 | 26 | echo "$fName $lName" 27 | return 0 28 | } 29 | 30 | 31 | ``` 32 | 33 | 34 | ```bash 35 | # test.bash 36 | source ./myscript.bash 37 | source ./testify.bash 38 | assert expect "$(Name 'Jane' 'Doe')" "John Doe" "Test for Name Function" "should fail" 39 | assert expect "$(Name 'Jane' 'Doe')" "Jane Doe" "Test for Name Function" "should succeed" 40 | assert status "Name" "5" "Test for status code" "should return 5" 41 | assert status "Name 'Jane' 'Doe'" "0" "Test for Status Code" "should return 0" 42 | assert status "Name 'Jane' " "3" "Test for Status Code" "should return 3" 43 | assert status "Name 'Jane' 'Doe'" "12" "Test for Status Code" "it should fail" 44 | assert regex "$(Name 'Jane' 'Doe')" "Jane" "Test for Regexp" "it should match" 45 | assert regex "123victory" "\W" "Test for Regexp Non Word Character" "it should fail if match failes" 46 | assert done 47 | 48 | 49 | 50 | ``` 51 | 52 | # Commands 53 | 54 | all subcommands to the assert functions requres 4 arguments, the first argument is the actual value to test for, while the second argument 55 | is the expected value, the thrid argument is a description of the test , while the fourth argument is a short description of what the test output should be 56 | 57 | **expect** Compares two values 58 | 59 | `assert expect "$(Name 'Jane' 'Doe')" "John Doe" "Test for Name Function" "should fail"` 60 | 61 | To test the output of a function you have to use command substitution 62 | 63 | You can also test single values 64 | 65 | `assert expect "victory" "favour" "Test for Name comparison" "This should fail"` 66 | 67 | testing for mathematical expressions 68 | 69 | `assert expect "$((2+2))" "4" "Test for Simple Math Operation" "It should succeed"` 70 | 71 | 72 | **regex** Does a regular expression match. The second argument to this subcommand should be a regular expression 73 | 74 | `assert regex "What is the difference between 6 and half a dozen" "[[:digit:]]" "Match Number Regular Expression" "It should succeed"` 75 | 76 | 77 | **status** Test for any status code. The second argument should be the expected status code. The first argument to this subcommand should be a command name, and it should not be passed as a command substitution but it should be passed as just a string wrapped in double quotes. 78 | The arguments to the function should also be in the double quotes. Arguments with spaced should be wrapped in single quotes 79 | 80 | `assert status "ls ." "0" "List in current dir" "it should return 0"` 81 | 82 | **done** This should be last subcommand to call, it does not require any argument 83 | 84 | 85 | # LICENSE 86 | 87 | GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 88 | 89 | 90 | -------------------------------------------------------------------------------- /utils.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | true=0 3 | false=1 4 | 5 | repeatString() { 6 | local stringToRepeat="${1}" 7 | declare -i depth="${2}" 8 | 9 | if [[ -z "${stringToRepeat}" ]];then 10 | printf "%s\n" "Usage:${FUNCNAME} string ?depth" 11 | return $false 12 | fi 13 | 14 | (( depth == 0 )) && depth=1 15 | 16 | ( 17 | # depthIndex will loose it value after been executed in this subshell 18 | for ((depthIndex=0;depthIndex<${depth};depthIndex+=1)) { 19 | 20 | printf "%s" "${stringToRepeat}" 21 | 22 | } 23 | 24 | printf "\n" 25 | ) 26 | } 27 | charAt() { 28 | local char="${1}" 29 | declare -i charPosition=${2} 30 | 31 | [[ -z "${char}" ]] && \ 32 | printf "%s\n" "Usage:${FUNCNAME} string (position to extract string)" && return $false 33 | 34 | { 35 | [[ ${charPosition} -eq 0 ]] && printf "%c\n" "${char}" && return $true 36 | } || { 37 | # if the position specified is greater than the length of the string print out an empty string 38 | [[ ${charPosition} -gt ${#char} ]] && printf "%s\n" "" && return $true 39 | } 40 | 41 | 42 | 43 | ( 44 | # All the variables delcared here will get lost after this subshell finsih executing 45 | 46 | local temp=${char} 47 | local cutFirstString 48 | declare -i i=0 49 | while [[ -n "${temp}" ]];do 50 | #if [[ $charPosition == $i ]];then 51 | # printf "%c" 52 | #fi 53 | : $((i++)) 54 | cutFirstString=$(printf "%c" "${temp}") 55 | temp=${temp#*$cutFirstString} 56 | (( i == charPosition )) && printf "%s\n" "${cutFirstString}" 57 | 58 | done 59 | ) 60 | } 61 | includes() { 62 | local char="${1}" 63 | local includes="${2}" 64 | declare -i depth="${3}" 65 | { 66 | [[ -z "$char" ]] || [[ -z "$includes" ]] 67 | } && printf "%s\n" "Usage:${FUNCNAME} string includesToCheck ?depth" && return $false; 68 | if [[ $depth -gt ${#char} ]];then 69 | depth=0 70 | elif [[ $depth != 0 ]];then 71 | while [[ -n $char ]];do 72 | if [[ ! $depth -eq ${#char} ]];then 73 | char=${char#*?} 74 | continue ; 75 | fi 76 | break ; 77 | done 78 | fi 79 | 80 | for ((i=$depth;i<=${#char};)) { 81 | while [[ -n $char ]] || [[ -n $includes ]];do 82 | printChar=$(printf "%c\n" "$char") 83 | printIncludes=$(printf "%c\n" "$includes" ) 84 | 85 | [[ -z $printIncludes ]] && { 86 | printf "%s\n" "true" 87 | return $true 88 | 89 | } # did this to fix a bug, if the string can be cut to the ending and printInlcudes become null that means all other test was true 90 | 91 | 92 | 93 | if [[ $printChar != $printIncludes ]];then 94 | printf "%s\n" "false" && return $false 95 | fi 96 | char=${char#*?} 97 | includes=${includes#*?} 98 | : $(( i++ )) 99 | done 100 | } 101 | } 102 | 103 | endsWith() { 104 | local char="${1}" 105 | local endswith="${2}" 106 | declare -i depth="${3}" 107 | 108 | { 109 | [[ -z "$char" ]] || [[ -z "$endswith" ]] 110 | } && printf "%s\n" "Usage:${FUNCNAME} string endToCheck ?depth" && return $false 111 | 112 | (( depth == 0 )) && depth=${#char} 113 | 114 | 115 | ( 116 | character="${char}" 117 | for ((i=1;i<=$depth;i++)) { 118 | while [ -n "$character" ];do 119 | 120 | printOne=$(printf "%c" "$character") 121 | character=${character#*"${printOne}"} 122 | 123 | (( i == depth )) && { 124 | 125 | [[ "${printOne}" == "${endswith}" ]] && { 126 | printf "%s\n" "true" && return $true\ 127 | 128 | } || { 129 | printf "%s\n" "false" 130 | return $false 131 | } 132 | 133 | 134 | } || { 135 | 136 | continue 2; 137 | } 138 | 139 | done 140 | 141 | } 142 | ) 143 | } 144 | offset() { 145 | # Bug: It does not deal with negative numbers 146 | # better still use ${var:position:length} to get the offset of a value 147 | local string=${1} 148 | local position=${2} 149 | local length=${3} 150 | 151 | [[ -z "${string}" ]] && printf "%s\n" "Error: String to work with was not specified" && \ 152 | printf "%s\n" "Usage:${FUNCNAME} string ?postion ?length" && return $false 153 | if [[ -z "${position}" ]] && [[ -z "${length}" ]];then 154 | printf "%s\n" "${string}" 155 | return $true 156 | fi 157 | 158 | [[ "${position}" =~ [A-Za-z] ]] && \ 159 | printf "%s\n" "Error: Required an integer for postion but got a string" && return $false 160 | [[ "${length}" =~ [A-Za-z] ]] && \ 161 | printf "%s\n" "Error: Required an integer for length but got a string" && return $false 162 | if [[ ${position} -gt ${#string} ]] || [[ ${length} -gt ${#string} ]] ;then 163 | printf "%s\n" "Error: index is greater than string length" 164 | return $false 165 | fi 166 | 167 | ( 168 | # Kill all the variables declared inside this subshell when done 169 | # Using index++ inside the for (()) introduced an unwanted feature 170 | # i had to take it to the body of the while loop 171 | for ((index=0;index<=${#string};)) { 172 | 173 | while [ -n "${string}" ];do 174 | 175 | (( index == position )) && { 176 | # If the value of index equals to the position specified run this block of code 177 | # if length is null print the string and return from this function ${FUNCNAME} 178 | [[ -z "${length}" ]] && printf "%s\n" "${string}" && return $true 179 | 180 | # if length is not null get the offset specified by the user 181 | for ((ind=0;ind<=${#string};)) { 182 | 183 | while [ -n "${string}" ];do 184 | 185 | (( ${#string} == length )) && { 186 | echo "$string" && return $true; 187 | } 188 | string=${string%$(printf "%c" "$(rev <<<${string})")*} 189 | # : >> don't run the result of $(( ind++ )) 190 | # better still ind=$(( ind++ )) 191 | : $(( ind++ )) 192 | done 193 | } 194 | } 195 | 196 | printOneChar=$(printf "%c" "${string}" ) 197 | string=${string#*$printOneChar} 198 | : $((index++)) 199 | done 200 | } 201 | ) 202 | } 203 | 204 | isInteger() { 205 | local number="${1}" 206 | 207 | [[ -z "${number}" ]] && { 208 | printf "%s\n" "Usage: ${FUNCNAME} number" 209 | return $false 210 | } 211 | 212 | # check if the content of $number is an alphabet or any punctuation mark 213 | 214 | ( 215 | for ((i=0;i<=${#number};)) { 216 | while [ -n "$number" ];do 217 | printNumber=$(printf "%c" "$number") 218 | [[ ! $printNumber == [0-9] ]] && return $false 219 | number=${number#*?} 220 | : $(( i++ )) 221 | done 222 | } 223 | ) 224 | 225 | [[ $? == 1 ]] && return $false 226 | 227 | #if egrep -q "([[:alpha:]])|([[:punct:]])" <<<"${number}";then 228 | #return $false 229 | #fi 230 | 231 | return $true 232 | } 233 | 234 | int() { 235 | # get all the integers before the decimal point 236 | # non integers values will cause an error 237 | local integer="${1}" 238 | 239 | [[ -z "${integer}" ]] && { 240 | printf "%s\n" "Usage: ${FUNCNAME} number" 241 | return $false 242 | } 243 | 244 | isInteger $integer 245 | 246 | # if the exit status of "isInteger $integer" greater than 0 enter the below block of code 247 | [[ $? != 0 ]] && { 248 | # setting integer to another variable 249 | local privInteger=$integer 250 | local ind; 251 | for ((ind=0;ind<=${#privInteger};)) { 252 | 253 | # while privInteger is non-zero i.e if there is still text in privInteger 254 | 255 | while [ -n "$privInteger" ];do 256 | # save the first character of privInteger in printchar variable 257 | local printchar=$(printf "%c" "${privInteger}" ) 258 | # cut the first character in privInteger until there is nothing in privInteger 259 | privInteger=${privInteger#*$printchar} 260 | # incase printchar variable does not contain 0-9 or . 261 | [[ ! $printchar =~ ([0-9\.]) ]] && { 262 | # declare a variable space 263 | local space="" 264 | # save integer again on another variable 265 | local int=$integer 266 | local err; 267 | for ((err=0;err<=${#int};)) { 268 | # this block of code , will add a single space to the space variable 269 | # aslong as int is non-zero and $pchar(see the next while loop ) does not equal printchar 270 | # Note:- $printchar is the single value that does not equal 0-9 or . 271 | # if a match is find return from this function with return code of 1 272 | while [ -n "${int}" ];do 273 | local pchar=$(printf "%c" "${int}") 274 | [[ $pchar == $printchar ]] && { 275 | printf "%s\n" "${integer}" 276 | printf "%s\n" "$space^Invalid character" 277 | return $false 278 | } 279 | space+=" " 280 | : $(( err++ )) 281 | # cut a single value from int until there is nothing inside 282 | int=${int#*$pchar} 283 | done 284 | 285 | } ; #end of $err 286 | 287 | 288 | } ; # End of $printchar 289 | 290 | #for ((period=0;period<=${#integer};period++)) { 291 | # echo $printchar 292 | # } 293 | 294 | : $(( ind++ )) 295 | done 296 | # printchar does not equal any punct value 297 | # cut any leading . forward 298 | printf "%s\n" "${integer%%.*}" 299 | return $true 300 | } 301 | } 302 | printf "%s\n" "${integer}" 303 | return $true 304 | } 305 | raw() { 306 | # you might not need this 307 | local str="${1}" 308 | [[ -z "${@}" ]] && { 309 | printf "%s\n" "Usage: raw string" 310 | } 311 | sed 's|\\|\\\\|g' <<<"${str}" 312 | } 313 | destructure() { 314 | # do not quote the array argument ( first agument ) 315 | # it is important you quote the second argument to this function 316 | # associative arrays work in alphabetical order 317 | # use "," to separate the variables to assign each array element to 318 | # for example 319 | # array=( bash ksh zsh ) 320 | # destructure ${array[@]} "var1,var2,var3" 321 | # echo $var1 322 | # echo $var2 323 | # echo $var3 324 | [[ -z "${@}" ]] && { 325 | 326 | printf "%s\n" "Usage:${FUNCNAME} array values" 327 | printf "%s\n" "destructure \${array[@]} \"var1,var2,,var3\"" 328 | printf "%s\n" "The array should not be quoted but the variables to assign the array element should be quoted" 329 | return $false 330 | } 331 | 332 | # Substract 1 from the total number of arguments 333 | local arrayLength=$(( ${#@} - 1)) 334 | # get the location of the last argument 335 | local str=$(( arrayLength + 1 )) 336 | # get the value of the last argument using indirect reference ( ! ) 337 | local strToDestruct="${!str}," 338 | declare -i y=0; 339 | local varList; 340 | # loop through the length of arrayLength 341 | for ((i=0;i<=$arrayLength;)) { 342 | # for j in the total number of arguments 343 | for j ; do 344 | # if the value of i equals the length of our arrayLength variable, break from the 2 loops 345 | (( i == arrayLength )) && break 2; 346 | while [ -n "$strToDestruct" ] ;do 347 | (( y == arrayLength )) && break 3; 348 | local destruct=${strToDestruct%%,*} 349 | strToDestruct=${strToDestruct#*,} 350 | { 351 | [[ -z "${destruct}" ]] || [[ "${destruct}" == +( ) ]] 352 | } && { 353 | declare -x null="null" 354 | varList+=${!destruct}, # ${null} >> ignore this comment 355 | : $(( y++ )) 356 | continue 2 357 | } 358 | declare -g $destruct=$j 359 | varList+=${!destruct}, 360 | : $(( y++ )) 361 | continue 2; 362 | 363 | done 364 | : $(( i++ )) 365 | done 366 | } 367 | varList=${varList%,*} 368 | } 369 | 370 | ...() { 371 | # Spread a bunch of string inside an array 372 | # for example:- 373 | # str=bash 374 | # array=( $(... $str) ) 375 | # echo ${str[@]} 376 | # b a s h 377 | 378 | local stringToSpread="$@" 379 | 380 | [[ -z "${stringToSpread}" ]] && { 381 | 382 | printf "%s\n" "Usage: ${FUNCNAME} string" 383 | return $false 384 | } 385 | 386 | [[ ${#@} -eq 1 ]] && { 387 | for ((i=0;i<=${#stringToSpread};i++)) { 388 | while [[ -n "${stringToSpread}" ]];do 389 | printf "%c\n" "${stringToSpread}" 390 | stringToSpread=${stringToSpread#*?} 391 | done 392 | } 393 | } 394 | } 395 | 396 | foreach() { 397 | # dont'quote the array arugment ( i.e the first agument ) 398 | # If you pass in a function as the callback using the function command you should wrap it in single quotes 399 | local array=$(( ${#@} - 1 )) 400 | local callback=$(( array + 1 )) 401 | declare -ga newArray 402 | [[ -z ${#@} ]] && { 403 | printf "%s\n" "Usage: ${FUNCNAME} array callback" 404 | return $false 405 | } 406 | # stupid hack to test if argument 1 is an array 407 | [[ ${array} -le 1 ]] && { 408 | printf "%s\n" "Error: first argument is not an Array" 409 | return $false 410 | } 411 | 412 | [[ -z "${callback}" ]] && { 413 | printf "%s\n" "Error: No Callback argument was provided" 414 | return $false 415 | } 416 | declare -F ${!callback} >/dev/null 417 | 418 | [[ $? -ge 1 ]] && { 419 | #Evaluate the callback 420 | eval ${!callback} &>/dev/null 421 | #If the previous command exit status is greater or equal to 1 422 | [[ $? -ge 1 ]] && { 423 | printf "%s\n" "Error: bad array callback" 424 | return $false 425 | } 426 | 427 | local command=$(egrep -o "\w+\(\)" <<<${!callback}) 428 | command=${command/()/} 429 | for ((i=0;i<=${#array};)) { 430 | for j; do 431 | (( i == array )) && break 2; 432 | newArray+=( $( $command $j ) ) 433 | : $(( i++ )) 434 | done 435 | } 436 | echo "${newArray[@]}" 437 | return $true 438 | } 439 | 440 | for ((i=0;i<=${#array};)) { 441 | for j;do 442 | (( i == array )) && break 2; 443 | newArray+=( $( ${!callback} $j) ) 444 | 445 | : $(( i++ )) 446 | done 447 | } 448 | echo "${newArray[@]}" 449 | } 450 | 451 | copyWithin() { 452 | local array=$1 453 | declare -i indexToCopyFrom=$2 454 | declare -i indexToCopyTo=$3 455 | read -a array <<<"$array" 456 | local valueOfIndexToCopyFrom=${array[$indexToCopyFrom]} 457 | local valueOfIndexToCopyTo=${array[$indexToCopyTo]} 458 | { 459 | [[ -z ${@} ]] || [[ -z "$array" ]] 460 | } && { 461 | printf "%s\n" "Usage: copyWithin arrayArgument indexToCopyFrom indexToCopyto" 462 | return $false 463 | } 464 | array[$indexToCopyTo]=$valueOfIndexToCopyFrom 465 | echo ${array[@]} 466 | return $true; 467 | } 468 | <<'EOF' 469 | keys() { 470 | local array=$1 471 | read -a array <<<"$array" 472 | local getInfo=$(declare -p array) 473 | [[ -z "$array" ]] && { 474 | printf "%s\n" "Usage: keys arrayArgument" 475 | return $false 476 | } 477 | a=( ["theif"]="victory" ["theif1"]="favour" ["theif2"]="johnson" ) 478 | local getInfo=$(declare -p a) 479 | arrKeys=$(egrep -o '(\[[[:alnum:]]+\])' <<<"$getInfo") 480 | echo \'${arrKeys}\' 481 | } 482 | 483 | declare -A a=( ["theif"]="victory" ["theif1"]="favour" ["theif2"]="johnson" ) 484 | 485 | keys "${a[*]}" 486 | EOF 487 | --------------------------------------------------------------------------------