¿Hay algo como "split ()" de JavaScript en el shell?


18

Es muy fácil de usar split()en JavaScript para dividir una cadena en una matriz.

¿Qué pasa con el script de shell?

Digamos que quiero hacer esto:

$ script.sh var1_var2_var3

Cuando el usuario le da dicha cadena var1_var2_var3al script.sh, dentro del script convertirá la cadena en una matriz como

array=( var1 var2 var3 )
for name in ${array[@]}; do
    # some code
done

1
qué shellestás usando, con lo bashque puedes hacerIFS='_' read -a array <<< "${string}"
gwillie

perlpuede hacer eso también No es un caparazón "puro", pero es bastante común.
Sobrique

@Sobrique Tampoco conozco la definición técnica de shell "puro", pero hay node.js.
emory

Tiendo a trabajar en 'es probable que esté instalado en mi caja de Linux de forma predeterminada' y no me preocupe las minucias :)
Sobrique

Respuestas:


24

Los shells tipo Bourne / POSIX tienen un operador split + glob y se invoca cada vez que deja una expansión de parámetro ( $var, $-...), una sustitución de comando ( $(...)) o una expansión aritmética ( $((...))) sin comillas en el contexto de la lista.

En realidad, lo invocaste por error cuando lo hiciste en for name in ${array[@]}lugar de hacerlo for name in "${array[@]}". (En realidad, debe tener cuidado de que invocar a ese operador por error es fuente de muchos errores y vulnerabilidades de seguridad ).

Ese operador está configurado con el $IFSparámetro especial (para indicar en qué caracteres dividir (aunque tenga cuidado con que el espacio, la pestaña y la nueva línea reciban un tratamiento especial allí)) y la -fopción de deshabilitar ( set -f) o habilitar ( set +f) la globparte.

También tenga en cuenta que si bien el Sin $IFSera originalmente (en el shell Bourne de donde $IFSproviene) para el Sseparador, en los shells POSIX, los caracteres en $IFSdeberían verse como delimitadores o terminadores (ver a continuación un ejemplo).

Entonces para dividir _:

string='var1_var2_var3'
IFS=_ # delimit on _
set -f # disable the glob part
array=($string) # invoke the split+glob operator

for i in "${array[@]}"; do # loop over the array elements.

Para ver la distinción entre separador y delimitador , pruebe:

string='var1_var2_'

Eso lo dividirá en var1y var2solo (sin elemento vacío adicional).

Entonces, para que sea similar a JavaScript split(), necesitarías un paso adicional:

string='var1_var2_var3'
IFS=_ # delimit on _
set -f # disable the glob part
temp=${string}_ # add an extra delimiter
array=($temp) # invoke the split+glob operator

(tenga en cuenta que dividiría un elemento vacío $stringen 1 (no 0 ), como JavaScript split()).

Para ver la pestaña de tratamientos especiales, recibir espacio y nueva línea, compare:

IFS=' '; string=' var1  var2  '

(donde consigues var1y var2) con

IFS='_'; string='_var1__var2__'

donde se obtiene: '', var1, '', var2, ''.

Tenga en cuenta que el zshshell no invoca ese operador split + glob implícitamente así, a menos que esté en sho kshemulación. Allí, debes invocarlo explícitamente. $=stringpara la parte dividida, $~stringpara la parte glob ( $=~stringpara ambos), y también tiene un operador dividido donde puede especificar el separador:

array=(${(s:_:)string})

o para preservar los elementos vacíos:

array=("${(@s:_:)string}")

Tenga en cuenta que existe spara dividir , no delimitar (también con $IFSuna no conformidad POSIX conocida de zsh). Es diferente de JavaScript split()en que una cadena vacía se divide en 0 (no 1) elemento.

Una diferencia notable con $IFS-splitting es que se ${(s:abc:)string}divide en la abccadena, mientras que con IFS=abc, se dividiría en a, bo c.

Con zshy ksh93, el tratamiento especial que recibe el espacio, la pestaña o la nueva línea se puede eliminar al duplicarlos $IFS.

Como nota histórica, el shell Bourne (el ancestro o los shell POSIX modernos) siempre despojó a los elementos vacíos. También tenía una serie de errores relacionados con la división y expansión de $ @ con valores no predeterminados de $IFS. Por ejemplo IFS=_; set -f; set -- $@, no sería equivalente a IFS=_; set -f; set -- $1 $2 $3....

División en expresiones regulares

Ahora, para algo más cercano a JavaScript split()que puede dividirse en expresiones regulares, necesitaría confiar en utilidades externas.

En el cofre de herramientas POSIX, awktiene un splitoperador que puede dividirse en expresiones regulares extendidas (que son más o menos un subconjunto de las expresiones regulares similares a Perl compatibles con JavaScript).

split() {
  awk -v q="'" '
    function quote(s) {
      gsub(q, q "\\" q q, s)
      return q s q
    }
    BEGIN {
      n = split(ARGV[1], a, ARGV[2])
      for (i = 1; i <= n; i++) printf " %s", quote(a[i])
      exit
    }' "$@"
}
string=a__b_+c
eval "array=($(split "$string" '[_+]+'))"

El zshshell tiene soporte incorporado para expresiones regulares compatibles con Perl (en su zsh/pcremódulo), pero usarlo para dividir una cadena, aunque es posible, es relativamente engorroso.


¿Hay alguna razón para tratamientos especiales con tabulación, espacio y nueva línea?
Cuonglm

1
@cuonglm, generalmente desea dividir en palabras cuando los delimitadores son espacios en blanco, en el caso de delimitadores no están en blanco (como para dividir $PATHen :) por el contrario, generalmente se desea conservar los elementos vacíos. Tenga en cuenta que en el shell Bourne, todos los personajes estaban recibiendo el tratamiento especial, lo kshcambiaron para que solo los en blanco (solo el espacio, la pestaña y la nueva línea) se trataran especialmente.
Stéphane Chazelas

Bueno, la reciente nota agregada de Bourne me sorprendió. Y para completar, ¿debería agregar la nota para el zshtratamiento con cadena que contiene 2 o más caracteres ${(s:string:)var}? Si se agrega, puedo eliminar mi respuesta :)
cuonglm

1
¿Qué quiere decir con "también tenga en cuenta que la S en $ IFS es para delimitador, no separador"? Entiendo la mecánica y que ignora los separadores finales, pero Ssignifica Separador , no delimitador . Al menos, eso es lo que dice el manual de mi bash.
terdon

@terdon, $IFSproviene del shell Bourne donde estaba el separador , ksh cambió el comportamiento sin cambiar el nombre. Menciono eso para enfatizar que split+glob(excepto en zsh o pdksh) ya no se divide simplemente.
Stéphane Chazelas

7

Sí, utilícelo IFSy configúrelo en _. Luego, use read -apara almacenar en una matriz ( -rdesactiva la expansión de barra invertida). Tenga en cuenta que esto es específico de bash; ksh y zsh tienen características similares con una sintaxis ligeramente diferente, y sh simple no tiene variables de matriz en absoluto.

$ r="var1_var2_var3"
$ IFS='_' read -r -a array <<< "$r"
$ for name in "${array[@]}"; do echo "+ $name"; done
+ var1
+ var2
+ var3

De man bash:

leer

-un nombre

Las palabras se asignan a índices secuenciales de la variable de matriz aname, comenzando en 0. aname se desarma antes de que se asignen nuevos valores. Otros argumentos de nombre son ignorados.

IFS

El separador de campo interno que se usa para dividir palabras después de la expansión y para dividir líneas en palabras con el comando de lectura incorporado. El valor predeterminado es `` ''.

Tenga en cuenta que se readdetiene en la primera línea nueva. Pase -d ''a readpara evitar eso, pero en ese caso, habrá una nueva línea adicional al final debido al <<<operador. Puedes eliminarlo manualmente:

IFS='_' read -r -d '' -a array <<< "$r"
array[$((${#array[@]}-1))]=${array[$((${#array[@]}-1))]%?}

Eso supone $rque no contiene caracteres de nueva línea o barras invertidas. También tenga en cuenta que solo funcionará en versiones recientes del bashshell.
Stéphane Chazelas

@ StéphaneChazelas buen punto. Sí, este es el caso "básico" de una cadena. Por lo demás, todos deberían buscar su respuesta integral. En cuanto a las versiones de bash, read -ase introdujo en bash 4, ¿verdad?
fedorqui

1
Lo siento, lo malo, pensé que <<<se agregó recientemente, bashpero parece que ha estado allí desde 2.05b (2002). read -aes incluso más viejo que eso. <<<proviene zshy es compatible con ksh93(y mksh y yash) también, pero read -aes específico de bash (está -Aen ksh93, yash y zsh).
Stéphane Chazelas

@ StéphaneChazelas ¿hay alguna forma "fácil" de encontrar cuándo ocurrieron estos cambios? Digo "fácil" para no profundizar en los archivos de publicación, tal vez una página que los muestre a todos.
fedorqui

1
Miro los registros de cambios para eso. zsh también tiene un repositorio git con historial desde 3.1.5 y su lista de correo también se usa para rastrear cambios.
Stéphane Chazelas
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.