Misterio de expansión de llaves anidadas en Bash


19

Esta:

$ echo {{a..c},{1..3}}

produce esto:

a b c 1 2 3

Lo cual es bueno, pero difícil de explicar dado que

$ echo {a..c},{1..3}

da

a,1 a,2 a,3 b,1 b,2 b,3 c,1 c,2 c,3

¿Está esto documentado en alguna parte? La Referencia de Bash no lo menciona (aunque tiene un ejemplo usándolo).

Respuestas:


18

Bueno, se desenreda una capa a la vez:

X{{a..c},{1..3}}Y

está documentado como expandido a X{a..c}Y X{1..3}Y(que se X{A,B}Yexpande XA XBcon Aser {a..c}y Bser {1..3}), ellos mismos documentados como expandido a XaY XbY XcY X1Y X2Y X3Y.

Lo que puede valer la pena documentar es que pueden anidarse ( por ejemplo, que el primero }no cierra el primero {).

Supongo que los proyectiles podrían haber elegido resolver primero los frenos internos , como si actuaran sobre cada cierre }por turno:

  1. X{{a..c},{1..3}}
  2. X{a,{1..3}}Y X{b,{1..3}}Y X{c,{1..3}}Y

    (que se A{a..c}Bexpande a AaB AbB AcB, dónde Aestá X{y Bestá ,{1..3}Y)

  3. X{a,1}Y X{a,2}Y X{a,3}Y X{b,1}Y X{b,2}Y X{b,3}Y X{c,1}Y X{c,2}Y X{c,3}Y

  4. XaY X1Y XaY Xa2...

Pero no creo que sea particularmente más intuitivo ni útil (ver el ejemplo de Kevin en los comentarios, por ejemplo), todavía habría cierta ambigüedad en cuanto al orden en que se realizarían las expansiones, y no es así csh(el caparazón que introdujo la llave) expansión a finales de los años 70, mientras que la {1..3}forma llegó más tarde (1995) zshy {a..c}aún más tarde (2004) de bash) lo hizo.

Tenga en cuenta que csh(desde el principio, consulte la página de manual de 2BSD (1979) ) documentó el hecho de que las expansiones de llaves podrían anidarse, aunque no dijo explícitamente cómo se expandirían las expansiones de llaves anidadas. Pero puede ver el cshcódigo de 1979 para ver cómo se hizo entonces. Vea cómo maneja explícitamente el anidamiento y cómo se resuelve a partir de las llaves externas.

En cualquier caso, realmente no veo cómo la expansión de {a..c},{1..3}podría tener alguna relación. Allí, ,no es un operador de una expansión de llaves (ya que no está dentro de las llaves), por lo que se trata como cualquier personaje ordinario.


Me parece extraño que se suponga que los brackets externos deben resolverse antes que los brackets internos.
Hauke ​​Laging

@ stéphane-chazelas Hay dos formas obvias de analizar esta expresión. ¿Por qué se analiza de una manera y no de la otra? Su comentario no parece dar una explicación.
igal

Entonces, esa explicación tiene sentido, pero si esto "está documentado como expandido a ..." ¿hay una URL?
xenoid el

@xenoid Vea mi solución actualizada.
Igual

1
@ (todos): considere la expansión /dev/{h,s}d{a..d}{1..4,}. Ahora suponga que desea extenderlo para incluir también /dev/nully /dev/zero. Si la expansión del aparato ortopédico funcionara de adentro hacia afuera, esa expansión sería realmente molesta de construir. Pero, ya que funciona de afuera hacia adentro, es bastante trivial:/dev/{null,zero,{h,s}d{a..d}{1..4,}}
Kevin

7

Aquí está la respuesta corta. En la primera expresión, la coma se usa como separador, por lo que la expansión de llaves es solo la concatenación de las dos subexpresiones anidadas. En la segunda expresión, la coma se trata como una subexpresión de un solo carácter, por lo que se forman expresiones de producto .

Lo que te faltaba era la definición de cómo se realizan las expansiones de llaves. Aquí hay tres referencias:

Sigue una explicación más detallada.


Comparaste el resultado de esta expresión:

$ echo {{a..c},{1..3}}
a b c 1 2 3

al resultado de esta expresión:

$ echo {a..c},{1..3}
a,1 a,2 a,3 b,1 b,2 b,3 c,1 c,2 c,3

Usted dice que esto es difícil de explicar, es decir, que esto es contra-intuitivo. Lo que falta es una definición formal de cómo se procesan las expansiones de llaves. Usted nota que el Manual Bash no da una definición completa.

Busqué un poco pero tampoco pude encontrar la definición faltante (completa, formal). Entonces fui al código fuente:

La fuente contiene un par de comentarios útiles. Primero es una descripción general de alto nivel del algoritmo de expansión de llaves:

Basic idea:

Segregate the text into 3 sections: preamble (stuff before an open brace),
postamble (stuff after the matching close brace) and amble (stuff after
preamble, and before postamble).  Expand amble, and then tack on the
expansions to preamble.  Expand postamble, and tack on the expansions to
the result so far.

Entonces, el formato de un token de expansión de llaves es el siguiente:

<PREAMBLE><AMBLE><POSTAMBLE>

El principal punto de entrada a la expansión es una función llamada brace_expandque se describe a continuación:

Return an array of strings; the brace expansion of TEXT.

Entonces, la brace_expandfunción toma una cadena que representa una expresión de expansión de llaves y devuelve la matriz de cadenas expandidas.

Combinando estas dos observaciones, vemos que el amble se expande a una lista de cadenas, cada una de las cuales se concatena en el preámbulo. Luego, el postámbulo se expande en una lista de cadenas, y cada cadena de la lista posterior se concatena en cada cadena de la lista de preámbulos / ambles (es decir, se forma el producto de las dos listas). Pero esto no describe cómo se procesan el amble y el postámbulo. Afortunadamente hay un comentario que describe eso también. El amble es procesado por una función llamada expand_amblecuya definición está precedida por el siguiente comentario:

Expand the text found inside of braces.  We simply try to split the
text at BRACE_ARG_SEPARATORs into separate strings.  We then brace
expand each slot which needs it, until there are no more slots which
need it.

En otra parte del código, vemos que BRACE_ARG_SEPARATOR se define como una coma. Esto deja en claro que el amble es una lista de cadenas separadas por comas, algunas de las cuales también pueden ser expresiones de expansión de llaves. Estas cadenas luego forman una sola matriz. Finalmente, también podemos ver que después de que expand_amblese llama, la brace_expandfunción se llama recursivamente en el postámbulo. Esto nos da una descripción completa del algoritmo.

Hay algunas otras referencias (no oficiales) que corroboran este hallazgo.

Para una referencia, echa un vistazo a Bash Hackers Wiki . La sección sobre combinación y anidamiento no aborda su problema, pero la página proporciona la sintaxis / gramática de la expansión de llaves, lo que creo que responde a su pregunta. La sintaxis viene dada por los siguientes patrones:

{string1,string2,...,stringN}

{<START>..<END>}

<PREAMBLE>{........}

{........}<POSTSCRIPT>

<PREAMBLE>{........}<POSTSCRIPT>

Y el análisis se describe de la siguiente manera:

La expansión de llaves se usa para generar cadenas arbitrarias. Las cadenas especificadas se utilizan para generar todas las combinaciones posibles con los preámbulos y postscripts opcionales que lo rodean.

Para otra referencia, eche un vistazo a la Guía para principiantes de Bash , que tiene lo siguiente para decir:

Brace expansion is a mechanism by which arbitrary strings may be generated. Patterns to be brace-expanded take the form of an optional PREAMBLE, followed by a series of comma-separated strings between a pair of braces, followed by an optional POSTSCRIPT. The preamble is prefixed to each string contained within the braces, and the postscript is then appended to each resulting string, expanding left to right.

Entonces, para analizar las expresiones de expansión de llaves, vamos de izquierda a derecha, expandiendo cada expresión y formando productos sucesivos (con respecto a la operación de concatenación de cadenas).

Ahora consideremos tu primera expresión:

{{a..c},{1..3}}

En el lenguaje de Bash Hacker's Wiki, esto coincide con la primera forma:

{string1,string2,...,stringN}

Dónde N=2, string1={a..c}y string2={1..3}- las expansiones abrazadera interior se realizan primero y cada uno de ellos es de la forma {<START>..<END>}. Alternativamente, podemos decir que esta es una expresión de expansión de llaves que consiste solo en un deambular (sin preámbulos ni posámbulos). El amble es una lista separada por comas, por lo que revisamos la lista un espacio a la vez y realizamos expansiones adicionales donde sea necesario. No se forma ningún producto porque no hay expresiones adyacentes (la coma se usa como separador).

A continuación, veamos tu segunda expresión:

{a..c},{1..3}

En el lenguaje de Bash Hacker's Wiki, esta expresión coincide con la forma:

{........}<POSTSCRIPT>

donde la posdata es la subexpresión ,{1..3}. Alternativamente, podemos decir que esta expresión tiene un amble ( {a..c}) y un postamble ( ,{1..3}). El amble se expande a la lista a b cy luego cada uno de estos se concatena con cada una de las cadenas en la expansión del postámbulo. El postámbulo se procesa de forma recursiva: tiene un preámbulo ,y una ambla de {1..3}. Esto se expande a la lista ,1 ,2 ,3. Las dos listas a b cy ,1 ,2 ,3luego se combinan para formar la lista de productos a,1 a,2 a,3 b,1 b,2 b,3 c,1 c,2 c,3.

Puede ser útil dar una descripción psuedoalgebraica de cómo se analizan estas expresiones, donde los corchetes "[]" denotan matrices, "+" denota la concatenación de matrices y "*" denota el producto cartesiano (con respecto a la concatenación).

Así es como se expande la primera expresión (un paso por línea):

{{a..c},{1..3}}
{a..c} + {1..3}
[a b c] + [1 2 3]
a b c 1 2 3

Y así es como se expande la segunda expresión:

{a..c},{1..3}
{a..c} * ,{1..3}
[a b c] * [,1 ,2 ,3]
a,1 a,2 a,3 b,1 b,2 b,3 c,1 c,2 c,3

2

Mi entendimiento es este:

Los tirantes internos se resuelven primero (como siempre), lo que convierte

{{a..c},{1..3}}

dentro

{a,b,c,1,2,3}

Debido a que ,está entre llaves, solo separa los elementos de las llaves.

Pero en el caso de

{a..c},{1..3}

el ,no está entre llaves, es decir, es un carácter ordinario que causa permutaciones de llaves en ambos lados.


Entonces {a..c}, ¿se resuelve a,b,co a b cdepende de la humedad y Dow Jones? Ordenado.
kubanczyk

Esto parece un poco confuso. Si {{a..c},{1..3}}es lo mismo que {a,b,c,1,2,3}, ¿no debería {{a..c}.{1..3}}ser lo mismo que {a,b,c.1,2,3}? Por supuesto, este no es el caso.
ilkkachu

@ilkkachu ¿Por qué debería ser lo mismo? ,es el carácter de separación de expansión de llaves, .no lo es. ¿Por qué un personaje ordinario debería conducir a los mismos resultados que uno especial? c.1Es un elemento ortopédico. Pero en {a..c}.{1..3}el .está el ancla para las expansiones de aparatos a la izquierda y a la derecha. Con ,las llaves externas se usan para la expansión de llaves porque su contenido tiene formato de expansión de llaves, y .no lo son porque su contenido no tiene ese formato.
Hauke ​​Laging

@HaukeLaging, bueno, si se {{a..c},{1..3}}convierte en {a,b,c,1,2,3}comas, simplemente aparecieron entre a, by c. ¿Por qué no aparecerían de la misma manera {a..c}.{1..3}? El comentario de @kubanczyk es casi lo mismo, si las comas aparecen allí así, ¿cómo sabemos cuándo la expansión genera comas y cuándo no? La respuesta, por supuesto, es que nunca genera comas por sí mismo, genera una lista de palabras. Entonces nada se convierte en {a,b,c,1,2,3}o {a,b,c.1,2,3}.
ilkkachu

@kubanczyk No debes burlarte de las respuestas que no entiendes.
Hauke ​​Laging
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.