Matrizes associativas em scripts Shell

116

Exigimos um script que simula matrizes associativas ou estrutura de dados como um mapa para Shell Scripting, qualquer corpo?

Irfan Zulfiqar
fonte

Respostas:

20

Para adicionar à resposta de Irfan , aqui está uma versão mais curta e rápida de, get()uma vez que não requer iteração sobre o conteúdo do mapa:

get() {
    mapName=$1; key=$2

    map=${!mapName}
    value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -e 's/:SP:/ /g' )"
}
Jerry Penner
fonte
16
bifurcar um subshell e sed dificilmente é o ideal. O Bash4 oferece suporte nativo e o bash3 tem alternativas melhores.
lhunath de
149

Outra opção, se a portabilidade não for sua preocupação principal, é usar matrizes associativas que são integradas ao shell. Isso deve funcionar no bash 4.0 (disponível agora na maioria das principais distros, embora não no OS X, a menos que você mesmo instale), ksh e zsh:

declare -A newmap
newmap[name]="Irfan Zulfiqar"
newmap[designation]=SSE
newmap[company]="My Own Company"

echo ${newmap[company]}
echo ${newmap[name]}

Dependendo do shell, você pode precisar fazer um em typeset -A newmapvez de declare -A newmap, ou em alguns pode nem ser necessário.

Brian Campbell
fonte
Obrigado por postar a resposta, acho que seria a melhor maneira de fazer isso para caras que estariam usando o bash 4.0 ou superior.
Irfan Zulfiqar,
Eu adicionaria um pequeno kludge para ter certeza de que BASH_VERSION está definido e> = 4. E sim, BASH 4 é muito, muito legal!
Tim Post
Estou usando algo assim. Qual é a melhor maneira de "pegar" o erro onde o índice / subscrito da matriz não existe? Por exemplo, e se eu estivesse considerando o subscrito como uma opção de linha de comando e o usuário cometesse um erro de digitação e inserisse "designatio"? Recebo um erro de "subscrito de array inválido", mas não sei como validar a entrada no momento da pesquisa de array, se isso for possível?
Jer
3
@Jer É bastante obscuro, mas para determinar se uma variável está definida no shell, você pode usar test -z ${variable+x}( xnão importa, pode ser qualquer string). Para uma matriz associativa no Bash, você pode fazer o mesmo; usar test -z ${map[key]+x}.
Brian Campbell
95

Outra forma não-bash 4.

#!/bin/bash

# A pretend Python dictionary with bash 3 
ARRAY=( "cow:moo"
        "dinosaur:roar"
        "bird:chirp"
        "bash:rock" )

for animal in "${ARRAY[@]}" ; do
    KEY=${animal%%:*}
    VALUE=${animal#*:}
    printf "%s likes to %s.\n" "$KEY" "$VALUE"
done

echo -e "${ARRAY[1]%%:*} is an extinct animal which likes to ${ARRAY[1]#*:}\n"

Você também pode lançar uma instrução if para pesquisar lá. if [[$ var = ~ / blah /]]. como queiras.

Bubnoff
fonte
2
Esse método é bom quando você não tem o Bash 4 de fato. Mas acho que a linha que busca o VALUE seria mais segura desta forma: VALUE = $ {animal # *:}. Com apenas um caractere #, a correspondência será interrompida no primeiro ":". Isso permite que os valores contenham ":" também.
Ced-le-pingouin de
@ Ced-le-pingouin ~ Esse é um ponto excelente! Eu não entendi isso. Eu editei minha postagem para refletir suas melhorias sugeridas.
Bubnoff de
1
É uma emulação bastante hack de matrizes associativas usando a substituição de parâmetro BASH. A "chave" param-sub substitui tudo antes dos dois pontos e o padrão de valor substitui tudo depois dos dois pontos. Semelhante a uma correspondência de curinga regex. Portanto, NÃO é um verdadeiro array associativo. Não recomendado, a menos que você precise de uma maneira fácil de entender para fazer funcionalidade semelhante a hash / array associativo no BASH 3 ou inferior. Mas funciona! Mais aqui: tldp.org/LDP/abs/html/parameter-substitution.html#PSOREX2
Bubnoff
1
Isso não implementa uma matriz associativa porque não fornece uma maneira de procurar um item pela chave. Ele fornece apenas uma maneira de encontrar cada chave (e valor) de um índice numérico. (Um item pode ser encontrado por chave iterando através da matriz, mas isso não é o que é desejado para uma matriz associativa.)
Eric Postpischil
@EricPostpischil True. É apenas um hack. Ele permite que uma pessoa use uma sintaxe familiar na configuração, mas ainda requer a iteração por meio do array conforme você disser. Tentei deixar claro em meu comentário anterior que definitivamente não é um array associativo e nem mesmo o recomendo se você tiver alternativas. O único ponto a seu favor, na minha opinião, é que é fácil de escrever e usar para aqueles familiarizados com outras linguagens como Python. Se você estiver em um ponto em que realmente deseja implementar matrizes associativas no BASH 3, talvez seja necessário refazer um pouco seus passos.
Bubnoff
34

Eu acho que você precisa dar um passo atrás e pensar sobre o que um mapa, ou array associativo, realmente é. Tudo isso é uma maneira de armazenar um valor para uma determinada chave e obter esse valor de volta com rapidez e eficiência. Você também pode querer iterar as chaves para recuperar cada par de valores-chave ou excluir chaves e seus valores associados.

Agora, pense em uma estrutura de dados que você usa o tempo todo em scripts de shell, e mesmo apenas no shell sem escrever um script, que tenha essas propriedades. Perplexo? É o sistema de arquivos.

Na verdade, tudo o que você precisa para ter uma matriz associativa na programação do shell é um diretório temporário. mktemp -dé o seu construtor de matriz associativa:

prefix=$(basename -- "$0")
map=$(mktemp -dt ${prefix})
echo >${map}/key somevalue
value=$(cat ${map}/key)

Se você não quiser usar echoe cat, você pode escrever alguns pequenos invólucros; estes são modelados a partir do Irfan, embora eles apenas gerem o valor em vez de definir variáveis ​​arbitrárias como $value:

#!/bin/sh

prefix=$(basename -- "$0")
mapdir=$(mktemp -dt ${prefix})
trap 'rm -r ${mapdir}' EXIT

put() {
  [ "$#" != 3 ] && exit 1
  mapname=$1; key=$2; value=$3
  [ -d "${mapdir}/${mapname}" ] || mkdir "${mapdir}/${mapname}"
  echo $value >"${mapdir}/${mapname}/${key}"
}

get() {
  [ "$#" != 2 ] && exit 1
  mapname=$1; key=$2
  cat "${mapdir}/${mapname}/${key}"
}

put "newMap" "name" "Irfan Zulfiqar"
put "newMap" "designation" "SSE"
put "newMap" "company" "My Own Company"

value=$(get "newMap" "company")
echo $value

value=$(get "newMap" "name")
echo $value

editar : Esta abordagem é na verdade um pouco mais rápida do que a pesquisa linear usando sed sugerida pelo questionador, bem como mais robusta (permite que chaves e valores contenham -, =, espaço, qnd ": SP:"). O fato de usar o sistema de arquivos não o torna lento; esses arquivos nunca têm garantia de serem gravados no disco, a menos que você chamesync ; para arquivos temporários como esse com uma vida útil curta, não é improvável que muitos deles nunca sejam gravados no disco.

Fiz alguns benchmarks do código de Irfan, a modificação de Jerry do código de Irfan e meu código, usando o seguinte programa de driver:

#!/bin/sh

mapimpl=$1
numkeys=$2
numvals=$3

. ./${mapimpl}.sh    #/ <- fix broken stack overflow syntax highlighting

for (( i = 0 ; $i < $numkeys ; i += 1 ))
do
    for (( j = 0 ; $j < $numvals ; j += 1 ))
    do
        put "newMap" "key$i" "value$j"
        get "newMap" "key$i"
    done
done

Os resultados:

    $ time ./driver.sh irfan 10 5

    0m0.975s reais
    usuário 0m0.280s
    sys 0m0.691s

    $ time ./driver.sh brian 10 5

    0m0.226s reais
    usuário 0m0.057s
    sys 0m0.123s

    $ time ./driver.sh jerry 10 5

    0m0.706s reais
    usuário 0m0.228s
    sys 0m0.530s

    $ time ./driver.sh irfan 100 5

    0m10.633s reais
    usuário 0m4.366s
    sys 0m7.127s

    $ time ./driver.sh brian 100 5

    0m1.682s reais
    usuário 0m0.546s
    sys 0m1.082s

    $ time ./driver.sh jerry 100 5

    0m9.315s reais
    usuário 0m4.565s
    sys 0m5.446s

    $ time ./driver.sh irfan 10 500

    real 1m46.197s
    usuário 0m44.869s
    sys 1m12.282s

    $ time ./driver.sh brian 10 500

    0m16.003s reais
    usuário 0m5.135s
    sys 0m10.396s

    $ time ./driver.sh jerry 10 500

    real 1m24.414s
    usuário 0m39.696s
    sys 0m54.834s

    $ time ./driver.sh irfan 1000 5

    4m25.145s reais
    usuário 3m17.286s
    sys 1m21.490s

    $ time ./driver.sh brian 1000 5

    0m19.442s reais
    usuário 0m5.287s
    sys 0m10.751s

    $ time ./driver.sh jerry 1000 5

    5m29.136s reais
    usuário 4m48.926s
    sys 0m59.336s

Brian Campbell
fonte
1
Não acho que você deva usar o sistema de arquivos para mapas, que basicamente usar IO para algo que pode ser feito com bastante rapidez na memória.
Irfan Zulfiqar,
9
Os arquivos não serão necessariamente gravados no disco; a menos que você chame a sincronização, o sistema operacional pode apenas deixá-los na memória. Seu código está chamando sed e fazendo várias pesquisas lineares, todas muito lentas. Fiz alguns benchmarks rápidos e minha versão é 5-35 vezes mais rápida.
Brian Campbell,
por outro lado, os arrays nativos do bash4 são significativamente melhores uma abordagem e no bash3 você ainda pode manter tudo fora do disco sem bifurcação pelo uso de declaração e indireção.
lhunath de
7
"fast" e "shell" realmente não combinam de qualquer maneira: certamente não para o tipo de problemas de velocidade de que estamos falando no nível "evitar IO minúsculo". Você pode pesquisar e usar / dev / shm para garantir que não haja IO.
jmtd 01 de
2
Esta solução me surpreendeu e é simplesmente incrível. Ainda é válido em 2016. Realmente deveria ser a resposta aceita.
Gordon
7

Bash4 oferece suporte nativo. Não use grepou eval, eles são os mais feios dos hacks.

Para uma resposta detalhada e detalhada com código de exemplo, consulte: /programming/3467959

lhunath
fonte
7
####################################################################
# Bash v3 does not support associative arrays
# and we cannot use ksh since all generic scripts are on bash
# Usage: map_put map_name key value
#
function map_put
{
    alias "${1}$2"="$3"
}

# map_get map_name key
# @return value
#
function map_get
{
    alias "${1}$2" | awk -F"'" '{ print $2; }'
}

# map_keys map_name 
# @return map keys
#
function map_keys
{
    alias -p | grep $1 | cut -d'=' -f1 | awk -F"$1" '{print $2; }'
}

Exemplo:

mapName=$(basename $0)_map_
map_put $mapName "name" "Irfan Zulfiqar"
map_put $mapName "designation" "SSE"

for key in $(map_keys $mapName)
do
    echo "$key = $(map_get $mapName $key)
done
Vadim
fonte
4

Agora respondendo a esta pergunta.

Os scripts a seguir simulam arrays associativos em scripts de shell. É simples e muito fácil de entender.

Mapa nada mais é do que uma string sem fim que tem keyValuePair salvo como --name = Irfan --designation = SSE --company = Meu: SP: Próprio: SP: Empresa

espaços são substituídos por ': SP:' para valores

put() {
    if [ "$#" != 3 ]; then exit 1; fi
    mapName=$1; key=$2; value=`echo $3 | sed -e "s/ /:SP:/g"`
    eval map="\"\$$mapName\""
    map="`echo "$map" | sed -e "s/--$key=[^ ]*//g"` --$key=$value"
    eval $mapName="\"$map\""
}

get() {
    mapName=$1; key=$2; valueFound="false"

    eval map=\$$mapName

    for keyValuePair in ${map};
    do
        case "$keyValuePair" in
            --$key=*) value=`echo "$keyValuePair" | sed -e 's/^[^=]*=//'`
                      valueFound="true"
        esac
        if [ "$valueFound" == "true" ]; then break; fi
    done
    value=`echo $value | sed -e "s/:SP:/ /g"`
}

put "newMap" "name" "Irfan Zulfiqar"
put "newMap" "designation" "SSE"
put "newMap" "company" "My Own Company"

get "newMap" "company"
echo $value

get "newMap" "name"
echo $value

editar: acabou de adicionar outro método para buscar todas as chaves.

getKeySet() {
    if [ "$#" != 1 ]; 
    then 
        exit 1; 
    fi

    mapName=$1; 

    eval map="\"\$$mapName\""

    keySet=`
           echo $map | 
           sed -e "s/=[^ ]*//g" -e "s/\([ ]*\)--/\1/g"
          `
}
Irfan Zulfiqar
fonte
1
Você está evalinserindo dados como se fossem código bash, e mais: você falha ao citá-los corretamente. Ambos causam muitos bugs e injeção arbitrária de código.
lhunath de
3

Para o Bash 3, há um caso particular que tem uma solução simples e agradável:

Se você não quiser lidar com muitas variáveis ​​ou se as chaves forem simplesmente identificadores de variáveis ​​inválidos, e se sua matriz tiver a garantia de ter menos de 256 itens , você pode abusar dos valores de retorno da função. Esta solução não requer nenhum subshell, pois o valor está prontamente disponível como uma variável, nem qualquer iteração de modo que o desempenho grite. Também é muito legível, quase como a versão Bash 4.

Esta é a versão mais básica:

hash_index() {
    case $1 in
        'foo') return 0;;
        'bar') return 1;;
        'baz') return 2;;
    esac
}

hash_vals=("foo_val"
           "bar_val"
           "baz_val");

hash_index "foo"
echo ${hash_vals[$?]}

Lembre-se, use aspas simples case, caso contrário, está sujeito a globbing. Muito útil para hashes estáticos / congelados desde o início, mas pode-se escrever um gerador de índice a partir de um hash_keys=()array.

Cuidado, o padrão é o primeiro, então você pode querer deixar de lado o elemento zero:

hash_index() {
    case $1 in
        'foo') return 1;;
        'bar') return 2;;
        'baz') return 3;;
    esac
}

hash_vals=("",           # sort of like returning null/nil for a non existent key
           "foo_val"
           "bar_val"
           "baz_val");

hash_index "foo" || echo ${hash_vals[$?]}  # It can't get more readable than this

Advertência: o comprimento agora está incorreto.

Como alternativa, se você deseja manter a indexação baseada em zero, pode reservar outro valor de índice e se proteger contra uma chave inexistente, mas é menos legível:

hash_index() {
    case $1 in
        'foo') return 0;;
        'bar') return 1;;
        'baz') return 2;;
        *)   return 255;;
    esac
}

hash_vals=("foo_val"
           "bar_val"
           "baz_val");

hash_index "foo"
[[ $? -ne 255 ]] && echo ${hash_vals[$?]}

Ou, para manter o comprimento correto, desloque o índice em um:

hash_index() {
    case $1 in
        'foo') return 1;;
        'bar') return 2;;
        'baz') return 3;;
    esac
}

hash_vals=("foo_val"
           "bar_val"
           "baz_val");

hash_index "foo" || echo ${hash_vals[$(($? - 1))]}
Lloeki
fonte
2

Você pode usar nomes de variáveis ​​dinâmicas e permitir que os nomes das variáveis ​​funcionem como as chaves de um hashmap.

Por exemplo, se você tem um arquivo de entrada com duas colunas, nome, crédito, como no exemplo abaixo, e deseja somar a renda de cada usuário:

Mary 100
John 200
Mary 50
John 300
Paul 100
Paul 400
David 100

O comando abaixo somará tudo, usando variáveis ​​dinâmicas como chaves, na forma de mapa _ $ {pessoa} :

while read -r person money; ((map_$person+=$money)); done < <(cat INCOME_REPORT.log)

Para ler os resultados:

set | grep map

O resultado será:

map_David=100
map_John=500
map_Mary=150
map_Paul=500

Elaborando essas técnicas, estou desenvolvendo no GitHub uma função que funciona exatamente como um objeto HashMap , shell_map .

Para criar " instâncias de HashMap ", a função shell_map é capaz de criar cópias de si mesma com nomes diferentes. Cada nova cópia de função terá uma variável $ FUNCNAME diferente. $ FUNCNAME então é usado para criar um namespace para cada instância do Mapa.

As chaves do mapa são variáveis ​​globais, no formato $ FUNCNAME_DATA_ $ KEY, onde $ KEY é a chave adicionada ao Mapa. Essas variáveis ​​são variáveis ​​dinâmicas .

Abaixo colocarei uma versão simplificada para que você possa usar como exemplo.

#!/bin/bash

shell_map () {
    local METHOD="$1"

    case $METHOD in
    new)
        local NEW_MAP="$2"

        # loads shell_map function declaration
        test -n "$(declare -f shell_map)" || return

        # declares in the Global Scope a copy of shell_map, under a new name.
        eval "${_/shell_map/$2}"
    ;;
    put)
        local KEY="$2"  
        local VALUE="$3"

        # declares a variable in the global scope
        eval ${FUNCNAME}_DATA_${KEY}='$VALUE'
    ;;
    get)
        local KEY="$2"
        local VALUE="${FUNCNAME}_DATA_${KEY}"
        echo "${!VALUE}"
    ;;
    keys)
        declare | grep -Po "(?<=${FUNCNAME}_DATA_)\w+((?=\=))"
    ;;
    name)
        echo $FUNCNAME
    ;;
    contains_key)
        local KEY="$2"
        compgen -v ${FUNCNAME}_DATA_${KEY} > /dev/null && return 0 || return 1
    ;;
    clear_all)
        while read var; do  
            unset $var
        done < <(compgen -v ${FUNCNAME}_DATA_)
    ;;
    remove)
        local KEY="$2"
        unset ${FUNCNAME}_DATA_${KEY}
    ;;
    size)
        compgen -v ${FUNCNAME}_DATA_${KEY} | wc -l
    ;;
    *)
        echo "unsupported operation '$1'."
        return 1
    ;;
    esac
}

Uso:

shell_map new credit
credit put Mary 100
credit put John 200
for customer in `credit keys`; do 
    value=`credit get $customer`       
    echo "customer $customer has $value"
done
credit contains_key "Mary" && echo "Mary has credit!"
Bruno Negrão Zica
fonte
2

Ainda outra forma não-bash-4 (ou seja, bash 3, compatível com Mac):

val_of_key() {
    case $1 in
        'A1') echo 'aaa';;
        'B2') echo 'bbb';;
        'C3') echo 'ccc';;
        *) echo 'zzz';;
    esac
}

for x in 'A1' 'B2' 'C3' 'D4'; do
    y=$(val_of_key "$x")
    echo "$x => $y"
done

Impressões:

A1 => aaa
B2 => bbb
C3 => ccc
D4 => zzz

A função com o caseatua como uma matriz associativa. Infelizmente, ele não pode usar return, por isso tem de echoproduzir, mas isso não é um problema, a menos que você seja um purista que evita bifurcar subshells.

Walter Tross
fonte
1

É uma pena que não tenha visto a pergunta antes - escrevi biblioteca shell-framework que contém, entre outros, os mapas (matrizes associativas). A última versão pode ser encontrada aqui .

Exemplo:

#!/bin/bash 
#include map library
shF_PATH_TO_LIB="/usr/lib/shell-framework"
source "${shF_PATH_TO_LIB}/map"

#simple example get/put
putMapValue "mapName" "mapKey1" "map Value 2"
echo "mapName[mapKey1]: $(getMapValue "mapName" "mapKey1")"

#redefine old value to new
putMapValue "mapName" "mapKey1" "map Value 1"
echo "after change mapName[mapKey1]: $(getMapValue "mapName" "mapKey1")"

#add two new pairs key/values and print all keys
putMapValue "mapName" "mapKey2" "map Value 2"
putMapValue "mapName" "mapKey3" "map Value 3"
echo -e "mapName keys are \n$(getMapKeys "mapName")"

#create new map
putMapValue "subMapName" "subMapKey1" "sub map Value 1"
putMapValue "subMapName" "subMapKey2" "sub map Value 2"

#and put it in mapName under key "mapKey4"
putMapValue "mapName" "mapKey4" "subMapName"

#check if under two key were placed maps
echo "is map mapName[mapKey3]? - $(if isMap "$(getMapValue "mapName" "mapKey3")" ; then echo Yes; else echo No; fi)"
echo "is map mapName[mapKey4]? - $(if isMap "$(getMapValue "mapName" "mapKey4")" ; then echo Yes; else echo No; fi)"

#print map with sub maps
printf "%s\n" "$(mapToString "mapName")"
Beggy
fonte
1

Adicionar outra opção, se jq estiver disponível:

export NAMES="{
  \"Mary\":\"100\",
  \"John\":\"200\",
  \"Mary\":\"50\",
  \"John\":\"300\",
  \"Paul\":\"100\",
  \"Paul\":\"400\",
  \"David\":\"100\"
}"
export NAME=David
echo $NAMES | jq --arg v "$NAME" '.[$v]' | tr -d '"' 
critium
fonte
0

Descobri que é verdade, como já mencionei, que o método de melhor desempenho é gravar key / vals em um arquivo e, em seguida, usar grep / awk para recuperá-los. Parece todo tipo de IO desnecessário, mas o cache de disco entra em ação e o torna extremamente eficiente - muito mais rápido do que tentar armazená-los na memória usando um dos métodos acima (como mostram os benchmarks).

Este é um método rápido e limpo de que gosto:

hinit() {
    rm -f /tmp/hashmap.$1
}

hput() {
    echo "$2 $3" >> /tmp/hashmap.$1
}

hget() {
    grep "^$2 " /tmp/hashmap.$1 | awk '{ print $2 };'
}

hinit capitols
hput capitols France Paris
hput capitols Netherlands Amsterdam
hput capitols Spain Madrid

echo `hget capitols France` and `hget capitols Netherlands` and `hget capitols Spain`

Se você quiser impor um valor único por chave, também pode fazer uma pequena ação grep / sed em hput ().

Al P.
fonte
0

vários anos atrás, escrevi uma biblioteca de scripts para o bash que suportava matrizes associativas entre outros recursos (registro, arquivos de configuração, suporte estendido para argumento de linha de comando, geração de ajuda, teste de unidade, etc). A biblioteca contém um wrapper para matrizes associativas e alterna automaticamente para o modelo apropriado (interno para bash4 e emular para versões anteriores). Era chamado de shell-framework e hospedado em origo.ethz.ch, mas hoje o recurso está fechado. Se alguém ainda precisar, posso compartilhar com você.

Beggy
fonte
Pode valer a pena insistir no github
Mark K Cowan
0

Shell não tem um mapa embutido como estrutura de dados, eu uso string bruta para descrever itens como este:

ARRAY=(
    "item_A|attr1|attr2|attr3"
    "item_B|attr1|attr2|attr3"
    "..."
)

ao extrair itens e seus atributos:

for item in "${ARRAY[@]}"
do
    item_name=$(echo "${item}"|awk -F "|" '{print $1}')
    item_attr1=$(echo "${item}"|awk -F "|" '{print $2}')
    item_attr2=$(echo "${item}"|awk -F "|" '{print $3}')

    echo "${item_name}"
    echo "${item_attr1}"
    echo "${item_attr2}"
done

Isso não parece inteligente do que a resposta de outras pessoas, mas fácil de entender para que novas pessoas descubram.

Coanor
fonte
-1

Modifiquei a solução de Vadim com o seguinte:

####################################################################
# Bash v3 does not support associative arrays
# and we cannot use ksh since all generic scripts are on bash
# Usage: map_put map_name key value
#
function map_put
{
    alias "${1}$2"="$3"
}

# map_get map_name key
# @return value
#
function map_get {
    if type -p "${1}$2"
        then
            alias "${1}$2" | awk -F "'" '{ print $2; }';
    fi
}

# map_keys map_name 
# @return map keys
#
function map_keys
{
    alias -p | grep $1 | cut -d'=' -f1 | awk -F"$1" '{print $2; }'
}

A mudança é para map_get a fim de evitar que ele retorne erros se você solicitar uma chave que não existe, embora o efeito colateral seja que ele também irá ignorar silenciosamente os mapas ausentes, mas se adequou melhor ao meu caso de uso, já que eu apenas queria verificar se há uma chave para pular itens em um loop.

Haravikk
fonte
-1

Resposta tardia, mas considere resolver o problema desta forma, usando o bash builtin lido conforme ilustrado no fragmento de código de um script de firewall ufw a seguir. Essa abordagem tem a vantagem de usar tantos conjuntos de campos delimitados (não apenas 2) quantos forem desejados. Usamos o | delimitador porque os especificadores de intervalo de porta podem exigir dois pontos, ou seja, 6001: 6010 .

#!/usr/bin/env bash

readonly connections=(       
                            '192.168.1.4/24|tcp|22'
                            '192.168.1.4/24|tcp|53'
                            '192.168.1.4/24|tcp|80'
                            '192.168.1.4/24|tcp|139'
                            '192.168.1.4/24|tcp|443'
                            '192.168.1.4/24|tcp|445'
                            '192.168.1.4/24|tcp|631'
                            '192.168.1.4/24|tcp|5901'
                            '192.168.1.4/24|tcp|6566'
)

function set_connections(){
    local range proto port
    for fields in ${connections[@]}
    do
            IFS=$'|' read -r range proto port <<< "$fields"
            ufw allow from "$range" proto "$proto" to any port "$port"
    done
}

set_connections
AsymLabs
fonte