Nombre de caractères dans la sortie d'une commande shell

12

J'écris un script qui doit calculer le nombre de caractères dans la sortie d'une commande en une seule étape .

Par exemple, l'utilisation de la commande readlink -f /etc/fstabdoit renvoyer 10car la sortie de cette commande comporte 10 caractères.

C'est déjà possible avec des variables stockées à l'aide du code suivant:

variable="somestring";
echo ${#variable};
# 10

Malheureusement, l'utilisation de la même formule avec une chaîne générée par commande ne fonctionne pas:

${#(readlink -f /etc/fstab)};
# bash: ${#(readlink -f /etc/fstab)}: bad substitution

Je comprends qu'il est possible de le faire en enregistrant d'abord la sortie dans une variable:

variable=$(readlink -f /etc/fstab);
echo ${#variable};

Mais je voudrais supprimer l'étape supplémentaire.

Est-ce possible? La compatibilité avec le shell Almquist (sh) en utilisant uniquement des utilitaires intégrés ou standard est préférable.

user339676
la source
1
La sortie de readlink -f /etc/fstabest de 11 caractères. N'oubliez pas la nouvelle ligne. Sinon, vous verriez /etc/fstabluser@cern:~$ quand vous l'avez exécuté à partir d'un shell.
Phil Frost
@PhilFrost vous semblez avoir une drôle d'invite, travaillez-vous au CERN?
Dmitry Grigoryev du

Réponses:

9

Avec GNU expr :

$ expr length + "$(readlink -f /etc/fstab)"
10

L' +il est une caractéristique particulière de GNU exprpour vous assurer que le prochain argument est traité comme une chaîne , même si elle se trouve être un expropérateur comme match, length, +...

Ce qui précède supprimera toute nouvelle ligne de sortie. Pour contourner ce problème:

$ expr length + "$(readlink -f /etc/fstab; printf .)" - 2
10

Le résultat a été soustrait à 2, car la dernière ligne de readlinket le caractère que .nous avons ajouté.

Avec la chaîne Unicode, exprne semble pas fonctionner, car elle renvoie la longueur de la chaîne en octets au lieu du nombre de caractères (voir ligne 654 )

$ LC_ALL=C.UTF-8 expr length ăaa
4

Vous pouvez donc utiliser:

$ printf "ăaa" | LC_ALL=C.UTF-8 wc -m
3

POSIX:

$ expr " $(readlink -f /etc/fstab; printf .)" : ".*" - 3
10

L'espace avant la substitution de commande empêche la commande de planter avec la chaîne commençant par -, nous devons donc soustraire 3.

cuonglm
la source
Merci! Il semble que votre troisième exemple fonctionne même sans le LC_ALL=C.UTF-8, ce qui simplifie considérablement les choses si l'encodage de la chaîne n'est pas connu à l'avance.
user339676
2
expr length $(echo "*")- Nan. Au moins utiliser des guillemets doubles: expr length "$(…)". Mais cela supprime les nouvelles lignes de la commande, c'est une caractéristique incontournable de la substitution de commandes. (Vous pouvez contourner cela, mais la réponse devient alors encore plus complexe.)
Gilles 'SO- arrête d'être méchant'
6

Je ne sais pas comment faire cela avec les commandes internes de shell (cependant Gnouc ), mais les outils standard peuvent aider:

  1. Vous pouvez utiliser wc -mqui compte les caractères. Malheureusement, il compte également la nouvelle ligne finale, vous devez donc vous en débarrasser en premier:

    readlink -f /etc/fstab | tr -d '\n' | wc -m
  2. Vous pouvez bien sûr utiliser awk

    readlink -f /etc/fstab | awk '{print length($0)}'
  3. Ou Perl

    readlink -f /etc/fstab | perl -lne 'print length'
terdon
la source
Voulez-vous dire exprest un intégré? Dans quelle coquille?
mikeserv
5

Je le fais habituellement comme ceci:

$ echo -n "$variable" | wc -m
10

Pour faire des commandes, je l'adapterais ainsi:

$ echo -n "$(readlink -f /etc/fstab)" | wc -m
10

Cette approche est similaire à ce que vous faisiez dans vos 2 étapes, sauf que nous les combinons en une seule doublure.

slm
la source
2
Vous devez utiliser à la -mplace de -c. Avec les caractères Unicode, votre approche sera rompue.
cuonglm
1
Pourquoi pas simplement readlink -f /etc/fstab | wc -m?
Phil Frost,
1
Pourquoi utilisez-vous plutôt cette méthode peu fiable ${#variable}? Utilisez au moins des guillemets doubles echo -n "$variable", mais cela échoue toujours si, par exemple, la valeur de variableest -e. Lorsque vous l'utilisez en combinaison avec une substitution de commande, gardez à l'esprit que les retours à la ligne de fin sont supprimés.
Gilles 'SO- arrête d'être méchant'
@philfrost b / c ce que j'ai montré est basé sur ce que l'op pensait déjà. Cela fonctionne également pour tous les cmds qu'il peut avoir configurés auparavant dans vars et qui veulent leurs longueurs postérieures. Terdon a également cet exemple déjà.
slm
1

Vous pouvez appeler des utilitaires externes (voir les autres réponses), mais ils ralentiront votre script et il est difficile de bien faire la plomberie.

Zsh

Dans zsh, vous pouvez écrire ${#$(readlink -f /etc/fstab)}pour obtenir la longueur de la substitution de commande. Notez que ce n'est pas la longueur de la sortie de la commande, c'est la longueur de la sortie sans retour à la ligne.

Si vous voulez la longueur exacte de la sortie, sortez un caractère supplémentaire non-retour à la fin et soustrayez-en un.

$((${#$(readlink -f /etc/fstab; echo .)} - 1))

Si ce que vous voulez est la charge utile dans la sortie de la commande, vous devez soustraire deux ici, car la sortie de readlink -fest le chemin canonique plus une nouvelle ligne.

$((${#$(readlink -f /etc/fstab; echo .)} - 2))

Cela diffère du ${#$(readlink -f /etc/fstab)}cas rare mais possible où le chemin canonique lui-même se termine par une nouvelle ligne.

Pour cet exemple spécifique, vous n'avez absolument pas besoin d'un utilitaire externe, car zsh a une construction intégrée équivalente à readlink -f, via le modificateur d'historique A.

echo /etc/fstab(:A)

Pour obtenir la longueur, utilisez le modificateur d'historique dans une extension de paramètre:

${#${:-/etc/fstab}:A}

Si vous avez le nom de fichier dans une variable filename, ce serait ${#filename:A}.

Coquilles de style Bourne / POSIX

Aucun des shells Bourne / POSIX purs (Bourne, ash, mksh, ksh93, bash, yash…) n'a une extension similaire à ma connaissance. Si vous devez appliquer une substitution de paramètres à la sortie d'une substitution de commandes ou imbriquer des substitutions de paramètres, utilisez des étapes successives.

Vous pouvez bourrer le traitement dans une fonction si vous le souhaitez.

command_output_length_sans_trailing_newlines () {
  set -- "$("$@")"
  echo "${#1}"
}

ou

command_output_length () {
  set -- "$("$@"; echo .)"
  echo "$((${#1} - 1))"
}

mais il n'y a généralement aucun avantage; sauf avec ksh93, cela permet à un fork supplémentaire de pouvoir utiliser la sortie de la fonction, donc cela rend votre script plus lent, et il y a rarement un avantage de lisibilité.

Encore une fois, la sortie de readlink -fest le chemin canonique plus une nouvelle ligne; si vous voulez la longueur du chemin canonique, soustrayez 2 au lieu de 1 po command_output_length. L'utilisation command_output_length_sans_trailing_newlinesdonne le bon résultat uniquement lorsque le chemin canonique lui-même ne se termine pas par une nouvelle ligne.

Octets vs caractères

${#…}est censé être la longueur en caractères, pas en octets, ce qui fait la différence dans les paramètres régionaux multi-octets. Des versions raisonnablement à jour de ksh93, bash et zsh calculent la longueur en caractères selon la valeur de LC_CTYPEau moment où la ${#…}construction est développée. De nombreux autres shells courants ne prennent pas vraiment en charge les paramètres régionaux multioctets: à partir du tiret 0.5.7, mksh 46 et posh 0.12.3, ${#…}renvoie la longueur en octets. Si vous voulez que la longueur en caractères soit fiable, utilisez l' wcutilitaire:

$(readlink -f /etc/fstab | wc -m)

Tant que $LC_CTYPEdésigne un paramètre régional valide, vous pouvez être sûr que cela entraînera une erreur (sur une plate-forme ancienne ou restreinte qui ne prend pas en charge les paramètres régionaux à plusieurs octets) ou renverra la longueur correcte en caractères. (Pour Unicode, «longueur en caractères» signifie le nombre de points de code - le nombre de glyphes est encore une autre histoire, en raison de complications telles que la combinaison de caractères.)

Si vous voulez la longueur en octets, définissez LC_CTYPE=Ctemporairement ou utilisez à la wc -cplace de wc -m.

Le comptage d'octets ou de caractères avec wcinclut tous les retours à la ligne de fin de la commande. Si vous voulez la longueur du chemin canonique en octets, c'est

$(($(readlink -f /etc/fstab | wc -c) - 1))

Pour l'obtenir en caractères, soustrayez 2.

Gilles 'SO- arrête d'être méchant'
la source
@cuonglm Non, vous devez soustraire 1. echo .ajoute deux caractères, mais le deuxième caractère est une nouvelle ligne de fin qui est supprimée par la substitution de commande.
Gilles 'SO- arrête d'être méchant'
La nouvelle ligne provient de la readlinksortie, plus le .by echo. Nous sommes tous deux d'accord pour echo .ajouter deux caractères, mais la nouvelle ligne de fin a été supprimée. Essayez avec printf .ou voyez ma réponse unix.stackexchange.com/a/160499/38906 .
cuonglm
@cuonglm La question demandait le nombre de caractères dans la sortie de la commande. La sortie de readlinkest la cible du lien plus une nouvelle ligne.
Gilles 'SO- arrête d'être méchant'
0

Cela fonctionne, dashmais cela nécessite que la variable cible soit définitivement vide ou non définie. C'est pourquoi il s'agit en fait de deux commandes - je vide explicitement $ldans la première:

l=;printf '%.slen is %d and result is %s\n' \
    "${l:=$(readlink -f /etc/fstab)}" "${#l}" "$l"

PRODUCTION

len is 10 and result is /etc/fstab

C'est tout ce qui est intégré au shell - sans inclure le readlinkcours bien sûr - mais l'évaluer de cette façon dans le shell actuel implique que vous devez effectuer l'affectation avant d'obtenir le len, c'est pourquoi %.sj'ilence le premier argument dans la printfchaîne de format et l'ajoute à nouveau pour la valeur littérale à la fin de printfla liste arg.

Avec eval:

l=$(readlink -f /etc/fstab) eval 'l=${#l}:$l'
printf %s\\n "$l"

PRODUCTION

10:/etc/fstab

Vous pouvez vous rapprocher de la même chose, mais au lieu de la sortie dans une variable dans la première commande, vous l'obtenez sur stdout:

PS4='${#0}:$0' dash -cx '2>&1' "$(readlink -f /etc/fstab)"

... qui écrit ...

10:/etc/fstab

... au fichier descripteur 1 sans affecter de valeur à aucun vars dans le shell courant.

mikeserv
la source
1
N'est-ce pas exactement ce que le PO voulait éviter? "Je comprends qu'il est possible de le faire en enregistrant d'abord la sortie dans une variable: variable=$(readlink -f /etc/fstab); echo ${#variable};mais je voudrais supprimer l'étape supplémentaire."
terdon
@terdon, j'ai probablement mal compris, mais j'avais l'impression que le point-virgule était le problème et non la variable. C'est pourquoi ceux-ci obtiennent le len et la sortie dans une seule commande simple en utilisant uniquement les commandes internes du shell. Le shell n'exécute pas readlink puis exec expr, par exemple. Cela n'a probablement d' importance que si, d'une manière ou d'une autre, le len obstrue la valeur, j'avoue que j'ai du mal à comprendre pourquoi cela peut être, mais je soupçonne qu'il pourrait y avoir un cas dans lequel cela importerait.
mikeserv
1
Au evalfait, le chemin est probablement le plus propre ici - il affecte la sortie et le len au même nom de var en une seule exécution - très proche de faire l=length(l):out(l). Faire - expr length $(command) t occlure la valeur en faveur de la len, par la voie.
mikeserv