Comment compter POSIX-ly le nombre de lignes dans une variable chaîne?

10

Je sais que je peux le faire dans Bash:

wc -l <<< "${string_variable}"

Fondamentalement, tout ce que j'ai trouvé impliquait un <<<opérateur Bash.

Mais dans le shell POSIX, <<<n'est pas défini, et je n'ai pas pu trouver une approche alternative pendant des heures. Je suis sûr qu'il existe une solution simple à cela, mais malheureusement, je ne l'ai pas trouvée jusqu'à présent.

LinuxSecurityFreak
la source

Réponses:

11

La réponse simple est qu'il wc -l <<< "${string_variable}"s'agit d'un raccourci ksh / bash / zsh pour printf "%s\n" "${string_variable}" | wc -l.

Il y a en fait des différences dans la manière <<<et le fonctionnement d'un tube: <<<crée un fichier temporaire qui est passé en entrée à la commande, tandis que |crée un tube. Dans bash et pdksh / mksh (mais pas dans ksh93 ou zsh), la commande sur le côté droit du tuyau s'exécute dans un sous-shell. Mais ces différences n'ont pas d'importance dans ce cas particulier.

Notez qu'en termes de comptage des lignes, cela suppose que la variable n'est pas vide et ne se termine pas par une nouvelle ligne. Ne pas se terminer par une nouvelle ligne est le cas lorsque la variable est le résultat d'une substitution de commande, vous obtiendrez donc le bon résultat dans la plupart des cas, mais vous obtiendrez 1 pour la chaîne vide.

Il existe deux différences entre var=$(somecommand); wc -l <<<"$var"et somecommand | wc -l: l'utilisation d'une substitution de commande et d'une variable temporaire supprime les lignes vides à la fin, oublie si la dernière ligne de sortie s'est terminée par une nouvelle ligne ou non (c'est toujours le cas si la commande génère un fichier texte non vide valide) et dépasse le nombre d'un si la sortie est vide. Si vous souhaitez à la fois conserver le résultat et compter les lignes, vous pouvez le faire en ajoutant du texte connu et en le supprimant à la fin:

output=$(somecommand; echo .)
line_count=$(($(printf "%s\n" "$output" | wc -l) - 1))
printf "The exact output is:\n%s" "${output%.}"
Gilles 'SO- arrête d'être méchant'
la source
1
@Inian Keeping wc -lest exactement équivalent à l'original: <<<$fooajoute une nouvelle ligne à la valeur de $foo(même si elle $fooétait vide). J'explique dans ma réponse pourquoi ce n'était peut-être pas ce que l'on voulait, mais c'est ce qui a été demandé.
Gilles 'SO- arrête d'être méchant'
2

Non conforme aux fonctions intégrées du shell, utilisant des utilitaires externes tels que grep et awkavec des options compatibles POSIX,

string_variable="one
two
three
four"

Faire avec grep pour faire correspondre le début des lignes

printf '%s' "${string_variable}" | grep -c '^'
4

Et avec awk

printf '%s' "${string_variable}" | awk 'BEGIN { count=0 } NF { count++ } END { print count }'

Notez que certains des outils GNU, en particulier, GNU grepne respectent pas l' POSIXLY_CORRECT=1option pour exécuter la version POSIX de l'outil. Dans greple seul comportement affecté par la définition de la variable, il y aura la différence de traitement de l'ordre des drapeaux de ligne de commande. D'après la documentation ( grepmanuel GNU ), il semble que

POSIXLY_CORRECT

S'il est défini, grep se comporte comme POSIX l'exige; sinon, grepse comporte plus comme les autres programmes GNU. POSIX requiert que les options qui suivent les noms de fichiers soient traitées comme des noms de fichiers; par défaut, ces options sont permutées au début de la liste des opérandes et sont traitées comme des options.

Voir Comment utiliser POSIXLY_CORRECT dans grep?

Inian
la source
2
Sûrement wc -lencore viable ici?
Michael Homer
@MichaelHomer: D'après ce que j'ai observé, a wc -lbesoin d'un flux délimité par une nouvelle ligne (avec un '\ n' à la fin pour compter correctement). On ne peut pas utiliser un FIFO simple à utiliser printf, par exemple printf '%s' "${string_variable}" | wc -lpourrait ne pas fonctionner comme prévu mais le <<<ferait à cause de la fin \najoutée par l'héritage
Inian
1
C'est ce qui se printf '%s\n'passait avant de le retirer ...
Michael Homer
1

La chaîne here-string <<<est à peu près une version à une ligne du document here <<. Le premier n'est pas une fonctionnalité standard, mais le second l'est. Vous pouvez <<également utiliser dans ce cas. Ceux-ci devraient être équivalents:

wc -l <<< "$somevar"

wc -l << EOF
$somevar
EOF

Cependant, notez que les deux ajoutent une nouvelle ligne supplémentaire à la fin de $somevar, par exemple, cela s'imprime 6, même si la variable n'a que cinq lignes:

s=$'foo\n\n\nbar\n\n'
wc -l <<< "$s"

Avec printf, vous pouvez décider si vous souhaitez ou non la nouvelle ligne supplémentaire:

printf "%s\n" "$s" | wc -l         # 6
printf "%s"   "$s" | wc -l         # 5

Mais alors, notez que wcne compte que les lignes complètes (ou le nombre de caractères de nouvelle ligne dans la chaîne). grep -c ^devrait également compter le dernier fragment de ligne.

s='foo'
printf "%s" "$s" | wc -l           # 0 !

printf "%s" "$s" | grep -c ^       # 1

(Bien sûr, vous pouvez également compter les lignes entièrement dans le shell en utilisant l' ${var%...}extension pour les supprimer une à une dans une boucle ...)

ilkkachu
la source
0

Dans ces cas étonnamment fréquents où ce que vous devez réellement faire est de traiter toutes les lignes non vides à l'intérieur d'une variable d'une certaine manière (y compris en les comptant), vous pouvez définir IFS sur une nouvelle ligne seulement, puis utiliser le mécanisme de séparation des mots du shell pour rompre les lignes non vides à part.

Par exemple, voici une petite fonction shell qui totalise les lignes non vides à l'intérieur de tous les arguments fournis:

lines() (
IFS='
'
set -f #disable pathname expansion
set -- $*
echo $#
)

Les parenthèses, plutôt que les accolades, sont utilisées ici pour former la commande composée pour le corps de la fonction. Cela fait que la fonction s'exécute dans un sous-shell afin qu'elle ne pollue pas la variable IFS du monde extérieur et le paramètre d'extension de nom de chemin à chaque appel.

Si vous souhaitez parcourir les lignes non vides, vous pouvez le faire de la même manière:

IFS='
'
set -f
for line in $lines
do
    printf '[%s]\n' $line
done

La manipulation d'IFS de cette manière est une technique souvent négligée, également pratique pour effectuer des tâches telles que l'analyse des noms de chemin d'accès qui pourraient contenir des espaces à partir d'une entrée en colonnes délimitée par des tabulations. Cependant, vous devez être conscient que la suppression délibérée du caractère espace généralement inclus dans le paramètre par défaut IFS de space-tab-newline peut finir par désactiver la division des mots aux endroits où vous vous attendez normalement à le voir.

Par exemple, si vous utilisez des variables pour créer une ligne de commande compliquée pour quelque chose comme ffmpeg, vous souhaiterez peut-être inclure -vf scale=$scaleuniquement lorsque la variable scaleest définie sur quelque chose de non vide. Normalement, vous pouvez y parvenir avec ${scale:+-vf scale=$scale}mais si IFS n'inclut pas son caractère d'espace habituel au moment où cette expansion de paramètre est effectuée, l'espace entre -vfet scale=ne sera pas utilisé comme séparateur de mots et ffmpegsera transmis en -vf scale=$scaletant qu'argument unique, qu'il ne comprendra pas.

Pour remédier à cela, vous avez besoin soit de se assurer IFS a été mis plus normalement avant de faire l' ${scale}extension, ou faire deux extensions: ${scale:+-vf} ${scale:+scale=$scale}. Le mot fractionnement que le shell effectue dans le processus d'analyse initiale des lignes de commande, par opposition au fractionnement qu'il fait pendant la phase d'expansion du traitement de ces lignes de commande, ne dépend pas d'IFS.

Quelque chose d'autre qui pourrait valoir la peine si vous voulez faire ce genre de chose serait de créer deux variables shell globales pour contenir juste un onglet et juste une nouvelle ligne:

t=' '
n='
'

De cette façon, vous pouvez simplement inclure $tet $ndans les extensions où vous avez besoin d'onglets et de nouvelles lignes, plutôt que de jeter tout votre code avec des espaces entre guillemets. Si vous préférez éviter les espaces entre guillemets dans un shell POSIX qui n'a aucun autre mécanisme pour le faire, vous printfpouvez vous aider, même si vous avez besoin d'un peu de bidouillage pour contourner la suppression des sauts de ligne dans les extensions de commande:

nt=$(printf '\n\t')
n=${nt%?}
t=${nt#?}

Parfois, définir IFS comme s'il s'agissait d'une variable d'environnement par commande fonctionne bien. Par exemple, voici une boucle qui lit un chemin d'accès autorisé à contenir des espaces et un facteur d'échelle à partir de chaque ligne d'un fichier d'entrée délimité par des tabulations:

while IFS=$t read -r path scale
do
    ffmpeg -i "$path" ${scale:+-vf scale=$scale} "${path%.*}.out.mkv"
done <recode-queue.txt

Dans ce cas, le readmodule intégré voit IFS défini sur un simple onglet, donc il ne divisera pas la ligne d'entrée qu'il lit également sur les espaces. Mais IFS=$t set -- $lines cela ne fonctionne pas : le shell se développe au $linesfur et à mesure qu'il construit les setarguments du module intégré avant d' exécuter la commande, de sorte que le paramètre temporaire d'IFS d'une manière qui ne s'applique que pendant l'exécution du module intégré lui-même arrive trop tard. C'est pourquoi les extraits de code que j'ai donnés avant tout définissent IFS dans une étape distincte, et pourquoi ils doivent gérer le problème de sa conservation.

flabdablet
la source