Y a-t-il quelque chose comme le «split () de JavaScript dans le shell?

18

Il est très facile à utiliser split()en JavaScript pour diviser une chaîne en un tableau.

Et le script shell?

Dis que je veux faire ceci:

$ script.sh var1_var2_var3

Lorsque l'utilisateur donne une telle chaîne var1_var2_var3au script.sh, à l'intérieur du script, il convertira la chaîne en un tableau comme

array=( var1 var2 var3 )
for name in ${array[@]}; do
    # some code
done
AGamePlayer
la source
1
qu'est shell- ce que vous utilisez, avec bashvous pouvez faireIFS='_' read -a array <<< "${string}"
gwillie
perlpeut le faire aussi. Ce n'est pas un shell "pur", mais c'est assez courant.
Sobrique
@Sobrique Je ne connais pas non plus la définition technique du shell "pur", mais il y a node.js.
emory
J'ai tendance à travailler sur 'est-ce qu'il est probablement installé sur ma box Linux par défaut' et ne vous inquiétez pas des détails :)
Sobrique

Réponses:

24

Les shells de type Bourne / POSIX ont un opérateur split + glob et il est invoqué chaque fois que vous laissez une expansion de paramètre ( $var, $-...), une substitution de commande ( $(...)) ou une expansion arithmétique ( $((...))) sans guillemets dans le contexte d'une liste.

En fait, vous l'avez invoqué par erreur lorsque vous l'avez fait à la for name in ${array[@]}place de for name in "${array[@]}". (En fait, vous devez être conscient que l' invocation de cet opérateur par erreur est source de nombreux bugs et failles de sécurité ).

Cet opérateur est configuré avec le $IFSparamètre spécial (pour indiquer sur quels caractères se diviser (mais attention à ce que l'espace, la tabulation et la nouvelle ligne y reçoivent un traitement spécial)) et l' -foption pour désactiver ( set -f) ou activer ( set +f) la globpartie.

Notez également que si Sin $IFSétait à l'origine (dans le shell Bourne d'où $IFSvient) pour Separator, dans les shells POSIX, les caractères dans $IFSdevraient plutôt être considérés comme des délimiteurs ou des terminateurs (voir ci-dessous pour un exemple).

Donc, pour partager _:

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.

Pour voir la distinction entre séparateur et délimiteur , essayez:

string='var1_var2_'

Cela le divisera en var1et var2seulement (pas d'élément vide supplémentaire).

Donc, pour le rendre similaire à JavaScript split(), vous auriez besoin d'une étape supplémentaire:

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

(notez qu'il diviserait un élément vide $stringen 1 (et non 0 ), comme JavaScript split()).

Pour voir l'onglet des traitements spéciaux, l'espace et la nouvelle ligne reçoivent, comparez:

IFS=' '; string=' var1  var2  '

(où vous obtenez var1et var2) avec

IFS='_'; string='_var1__var2__'

où vous obtenez: '', var1, '', var2, ''.

Notez que le zshshell n'invoque pas cet opérateur split + glob implicitement comme ça sauf dans shou en kshémulation. Là, vous devez l'invoquer explicitement. $=stringpour la partie scindée, $~stringpour la partie glob ( $=~stringpour les deux), et il a également un opérateur de scission où vous pouvez spécifier le séparateur:

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

ou pour conserver les éléments vides:

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

Notez qu'il existe spour le fractionnement , pas la délimitation (également avec $IFS, une non-conformité POSIX connue de zsh). Il est différent de JavaScript split()en ce sens qu'une chaîne vide est divisée en élément 0 (et non 1).

Une différence notable avec $IFS-splitting est que ${(s:abc:)string}se divise sur la abcchaîne, tandis qu'avec IFS=abc, cela se diviserait sur a, bou c.

Avec zshet ksh93, le traitement spécial que l'espace, la tabulation ou la nouvelle ligne reçoit peut être supprimé en les doublant $IFS.

Comme note historique, le shell Bourne (l'ancêtre ou les shell POSIX modernes) a toujours dépouillé les éléments vides. Il avait également un certain nombre de bogues liés au fractionnement et à l'expansion de $ @ avec des valeurs non par défaut de $IFS. Par exemple IFS=_; set -f; set -- $@, ne serait pas équivalent à IFS=_; set -f; set -- $1 $2 $3....

Fractionnement sur regexps

Maintenant, pour quelque chose de plus proche de JavaScript split()qui peut se diviser en expressions régulières, vous devez vous fier à des utilitaires externes.

Dans la boîte à outils POSIX, awkpossède un splitopérateur qui peut se diviser en expressions régulières étendues (ce sont plus ou moins un sous-ensemble des expressions régulières de type Perl prises en charge par 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" '[_+]+'))"

Le zshshell a un support intégré pour les expressions régulières compatibles Perl (dans son zsh/pcremodule), mais l'utiliser pour diviser une chaîne, bien que possible, est relativement lourd.

Stéphane Chazelas
la source
Y a-t-il une raison pour des traitements spéciaux avec tabulation, espace et nouvelle ligne?
cuonglm
1
@cuonglm, généralement vous voulez partager sur les mots lorsque les délimiteurs sont blancs, dans le cas de non-délimiteurs blanc (comme Split $PATHsur :) au contraire, vous voulez généralement conserver des éléments vides. Notez que dans le shell Bourne, tous les personnages recevaient le traitement spécial, kshchangé cela pour que seuls les blancs (seulement l'espace, la tabulation et la nouvelle ligne) soient traités spécialement.
Stéphane Chazelas
Eh bien, la récente note ajoutée du shell Bourne m'a surpris. Et pour terminer, devez-vous ajouter la note pour le zshtraitement avec une chaîne contenant 2 caractères ou plus ${(s:string:)var}? Si ajouté, je peux supprimer ma réponse :)
cuonglm
1
Que voulez-vous dire par "Notez également que le S dans $ IFS est pour le délimiteur, pas le séparateur."? Je comprends la mécanique et qu'il ignore les séparateurs de fin, mais le Ssigle pour Séparateur , pas délimiteur . C'est du moins ce que dit le manuel de mon bash.
terdon
@terdon, $IFSvient du shell Bourne où il était séparateur , ksh a changé le comportement sans changer le nom. Je mentionne cela pour souligner que split+glob(sauf en zsh ou pdksh) ne se divise plus simplement.
Stéphane Chazelas
7

Oui, utilisez-le IFSet réglez-le sur _. Ensuite, utilisez read -apour stocker dans un tableau ( -rdésactive l'expansion de la barre oblique inverse). Notez que ceci est spécifique à bash; ksh et zsh ont des fonctionnalités similaires avec une syntaxe légèrement différente, et plain sh n'a pas du tout de variables de tableau.

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

De man bash:

lis

-un aname

Les mots sont attribués à des indices séquentiels de la variable de tableau aname, commençant à 0. aname n'est pas défini avant que de nouvelles valeurs ne soient attribuées. Les autres arguments de nom sont ignorés.

IFS

Séparateur de champ interne utilisé pour le fractionnement de mots après expansion et pour diviser des lignes en mots avec la commande intégrée read. La valeur par défaut est `` ''.

Notez que cela reads'arrête à la première nouvelle ligne. Passez -d ''à readpour éviter cela, mais dans ce cas, il y aura une nouvelle ligne supplémentaire à la fin en raison de l' <<<opérateur. Vous pouvez le supprimer manuellement:

IFS='_' read -r -d '' -a array <<< "$r"
array[$((${#array[@]}-1))]=${array[$((${#array[@]}-1))]%?}
fedorqui
la source
Cela suppose $rqu'il ne contient pas de caractères de nouvelle ligne ni de barres obliques inverses. Notez également que cela ne fonctionnera que dans les versions récentes du bashshell.
Stéphane Chazelas
@ StéphaneChazelas bon point. Oui, c'est le cas "basique" d'une chaîne. Pour le reste, tout le monde devrait aller chercher votre réponse complète. En ce qui concerne les versions de bash, a read -aété introduit dans bash 4, non?
fedorqui
1
désolé ma mauvaise, je pensais que cela <<<n'a été ajouté que récemment, bashmais il semble qu'il existe depuis 2.05b (2002). read -aest encore plus vieux que ça. <<<provient zshet est également supporté par ksh93(et mksh et yash) mais read -aest spécifique à bash (il est -Aen ksh93, yash et zsh).
Stéphane Chazelas
@ StéphaneChazelas existe-t-il un moyen "facile" de savoir quand ces changements se sont produits? Je dis "facile" de ne pas fouiller dans les fichiers de sortie, peut-être une page les montrant tous.
fedorqui
1
Je regarde les journaux des modifications pour cela. zsh possède également un référentiel git avec un historique remontant à 3.1.5 et sa liste de diffusion est également utilisée pour suivre les modifications.
Stéphane Chazelas