4

I have two mappings file like this as shown below:

primary_mapping.txt

{1=[343, 0, 686, 1372, 882, 196], 2=[687, 1, 1373, 883, 197, 736, 1030, 1569], 3=[1374, 2, 884, 737, 198, 1570], 4=[1375, 1032, 1424, 3, 885, 1228], 5=[1033, 1425, 4, 200, 886]}

secondary_mapping.txt

{1=[1152, 816, 1488, 336, 1008], 2=[1153, 0, 817, 337, 1489, 1009, 1297], 3=[1, 1154, 1490, 338], 4=[1155, 2, 339, 1491, 819, 1299, 1635], 5=[820, 1492, 340, 3, 1156]}

In the above mapping files, each clientId has primary and secondary mapping. For example: clientId 1 has 343, 0, 686, 1372, 882, 196 primary mapping and 1152, 816, 1488, 336, 1008 secondary mapping. Similarly for other clientIds as well.

Below is my shell script in which it prints primary and secondary mapping for a particular clientid:

#!/bin/bash
mapfiles=(primary-mappings.txt secondary-mappings.txt)

declare -a arr

mappingsByClientID () {
  id=$1 # 1 to 5 
  file=${mapfiles[$2]} # 0 to 1
  arr=($(sed -r "s/.*\b${id}=\[([^]\]+).*/\1/; s/,/ /g" $file))
  echo "${arr[@]}"
}

# assign output of function to an array
# this prints mappings for clientid 3. In general I will take this parameter from command line.
pri=($(mappingsByClientID 3 0))
snd=($(mappingsByClientID 3 1))

Now let's say if we can't find primary or secondary mapping for a particular clientid then I want to exit from the shell script with nonzero status code by logging message. I tried exiting from subshell and it didn't worked for me. Is this possible to do?

codeforester
  • 39,467
  • 16
  • 112
  • 140
user1950349
  • 4,738
  • 19
  • 67
  • 119
  • It is, research how to get exit code from a subshell. – LMC Apr 21 '18 at 03:40
  • 3
    The ideal practice here is indirect assignment. That is, make your function's usage `mappingsByClientID pri 3 0` or `mappingsByClientID snd 3 1`; then it doesn't need to run in a subshell, so its `exit` exits the real interpreter. – Charles Duffy Apr 21 '18 at 04:15
  • 2
    `declare -n arr="$1"` will make the variable `arr` an alias for whatever variable was named in your first parameter, making the above easy. Have fun. – Charles Duffy Apr 21 '18 at 04:16

1 Answers1

2

You can do this (incorporated all the great suggestions of our guru, Charles Duffy):

mappingsByClientID () {
  (($# != 3)) && { echo "Insufficient arguments" >&2; exit 1; }
  declare -n arr=$1    # for indirect assignment (need **Bash 4.3 or above**)
  id=$2                # 1 to 5 
  file=${mapfiles[$3]} # 0 to 1
  [[ $file ]]    || { echo "No mapping file found for id '$id', type '$2'" >&2; exit 1; }
  [[ -f $file ]] || { echo "File '$file' does not exist" >&2; exit 1; }
  # Note: the word boundary `\b` is not supported in ERE
  # See post: https://stackoverflow.com/q/27476347/6862601
  if ! grep -q "[{ ]$id=" "$file"; then
      echo "Couldn't find mapping for id '$id' in file '$file'" >&2
      exit 1
  fi
  mapfile -t arr < <(sed -r "s/.*[{ ]$id=\[([^]\]+).*/\1/" "$file" | tr -s '[ ,]' '\n')
  if ((${#arr[@]} == 0)); then
      echo "Couldn't find mapping for id '$id' in file '$file'" >&2
      exit 1
  fi
  echo "${arr[@]}"
}

Now call the function without the subshell $() so that the exit inside the function will actually exit the script:

mappingsByClientID pri 3 0
mappingsByClientID sec 3 1

It is a better practice to do the error checks in the function.

If you don't want the function to exit, you can check the array size in the caller code, after calling the function.


If you are on a version of Bash that doesn't support namerefs, you can use global variables for arrays, let's says arr is the global, then:

arr=()                   # initialize the global
mappingsByClientID 3 0
pri=("${arr[@]}")        # make a copy of the global array into pri
mappingsByClientID 3 1
sec=("${arr[@]}")        # make a copy of the global array into sec

Modify the mappingsByClientID accordingly to use the global variable instead of the nameref.


Related:

codeforester
  • 39,467
  • 16
  • 112
  • 140
  • yeah but it will exit the function. I also need to exit from shell script in that case. – user1950349 Apr 21 '18 at 04:04
  • Your function has its body inside `{ ... }`, not `( ... )`. So, it does run in the current shell and exit would ensure that we exit the script. – codeforester Apr 21 '18 at 04:08
  • @user1950349 `exit` exits the whole script, `return` exits just the function. – Barmar Apr 21 '18 at 04:09
  • 2
    *grumble* re: copying several of OP's bad practices (unquoted expansion for string-splitting rather than `read -a`, mising quotes, etc) rather than demonstrating how to repair them. – Charles Duffy Apr 21 '18 at 04:13
  • 2
    @codeforester, ...you're only assigning to `arr`, not assigning to an array of the OP's choice. If you want your output to be assigned elsewhere, you need to either (1) use an indirect assignment, or (2) rely on the OP to call this in a subshell and read its output (in which case the `exit` *doesn't* without the parent process's cooperation... of course, such cooperation *is* a feasible option). And btw, the error message should go to stderr to prevent its capture. – Charles Duffy Apr 21 '18 at 04:17
  • @CharlesDuffy: I put my answer in a hurry. Thanks for pointing out the mistakes. I have updated my answer. – codeforester Apr 21 '18 at 06:22
  • 1
    @codeforester sorry I was away for a while. I tried with your suggestion and looks like it prints out everything if we don't have any mapping for a particular `clientid` but instead it should print this line `Couldn't find mapping for id '$id' in file '$file'`. Any thoughts what could be wrong? – user1950349 Apr 22 '18 at 02:41
  • 1
    That was an issue in your original code. We needed an explicit check to make sure the desired mapping is indeed present in the file before running `sed` to extract the matched portion. I added the necessary code. We should be good now. – codeforester Apr 22 '18 at 07:12
  • works fine now. just that your arguments numbering is messed up with your latest edit but apart from that it works fine. also do we need last check which has this `Couldn't find mapping` now? – user1950349 Apr 22 '18 at 08:06
  • Corrected the arguments ordering issue. – codeforester Apr 22 '18 at 15:33
  • 1
    @codeforester I have one last question. I found something very strange today. If I try to iterate this `pri` array then it doesn't work at all: `for item in "${pri[@]}"; do echo "$item" done`. I mean it prints all the elements of that array instead of iterating them? – user1950349 Apr 22 '18 at 20:22
  • `mapfile` was not interpreting the space delimited stream correctly. Please see the updated answer. – codeforester Apr 22 '18 at 21:58
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/169548/discussion-between-codeforester-and-user1950349). – codeforester Apr 22 '18 at 21:59
  • @codeforester I found one more problem with this. Sorry about that, just noticed now. It looks like when I print out the whole `pri` and `sec` array then each element has two spaces between them instead of one. For example: `primary files: 1220 1374 415`. So now when I iterate this array in a for loop and print out each individual element then each element has extra space infront of it. I am not sure why it is coming like this. Any idea? – user1950349 Apr 23 '18 at 20:27