Número de caracteres en la salida de un comando de shell

12

Estoy escribiendo un script que necesita calcular el número de caracteres en la salida de un comando en un solo paso .

Por ejemplo, el uso del comando readlink -f /etc/fstabdebería regresar 10porque la salida de ese comando tiene 10 caracteres de longitud.

Esto ya es posible con variables almacenadas utilizando el siguiente código:

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

Desafortunadamente, usar la misma fórmula con una cadena generada por comando no funciona:

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

Entiendo que es posible hacer esto primero guardando el resultado en una variable:

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

Pero me gustaría eliminar el paso adicional.

es posible? Es preferible la compatibilidad con el shell Almquist (sh) utilizando solo utilidades incorporadas o estándar.

usuario339676
fuente
1
La salida de readlink -f /etc/fstabes de 11 caracteres. No olvides la nueva línea. De lo contrario, verías /etc/fstabluser@cern:~$ cuándo lo ejecutaste desde un caparazón.
Phil Frost
@PhilFrost parece que tienes un mensaje divertido, ¿trabajas en el CERN?
Dmitry Grigoryev

Respuestas:

9

Con GNU expr :

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

El +no es una característica especial de GNU exprpara asegurarse de que el siguiente argumento es tratado como una cadena, incluso si pasa a ser un exproperador como match, length, +...

Lo anterior eliminará cualquier nueva línea final de salida. Para solucionarlo:

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

El resultado fue restado a 2 porque la nueva línea final de readlinky el personaje .que agregamos.

Con la cadena Unicode, exprno parece funcionar, porque devuelve la longitud de la cadena en bytes en lugar del recuento de caracteres (consulte la línea 654 )

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

Entonces, puedes usar:

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

POSIXLY:

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

El espacio antes de la sustitución del comando evita que el comando se bloquee con el inicio de la cadena -, por lo que debemos restar 3.

Cuonglm
fuente
¡Gracias! Parece que su tercer ejemplo funciona incluso sin el LC_ALL=C.UTF-8, lo que simplifica significativamente las cosas si no se conoce de antemano la codificación de la cadena.
user339676
2
expr length $(echo "*")- no Al menos utilizar comillas dobles: expr length "$(…)". Pero esto elimina las nuevas líneas del comando, es una característica ineludible de la sustitución de comandos. (Puede trabajar alrededor de ella, pero entonces la respuesta se vuelve aún más complejo.)
Gilles 'SO siendo parada del mal'
6

No estoy seguro de cómo hacer esto con shell incorporado ( aunque Gnouc sí lo es ), pero las herramientas estándar pueden ayudar:

  1. Puedes usar lo wc -mque cuenta caracteres. Desafortunadamente, también cuenta la nueva línea final, por lo que primero debería deshacerse de eso:

    readlink -f /etc/fstab | tr -d '\n' | wc -m
  2. Por supuesto, puedes usar awk

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

    readlink -f /etc/fstab | perl -lne 'print length'
terdon
fuente
¿Quieres decir que expres un incorporado? ¿En que caparazón?
mikeserv
5

Usualmente lo hago así:

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

Para hacer comandos, lo adaptaría así:

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

Este enfoque es similar a lo que estaba haciendo en sus 2 pasos, excepto que los estamos combinando en un solo revestimiento.

slm
fuente
2
Debe usar en -mlugar de -c. Con los caracteres Unicode, su enfoque se romperá.
Cuonglm
1
¿Por qué no simplemente readlink -f /etc/fstab | wc -m?
Phil Frost
1
¿Por qué usas este método poco confiable en lugar de ${#variable}? Al menos use comillas dobles echo -n "$variable", pero esto todavía falla si, por ejemplo, el valor de variablees -e. Cuando lo use en combinación con una sustitución de comando, tenga en cuenta que las nuevas líneas finales se eliminan.
Gilles 'SO- deja de ser malvado'
@philfrost b / c lo que mostré se basó en lo que el operador ya estaba pensando. También funciona para cualquier cmds que pueda haber configurado previamente en vars y quiera sus longitudes posteriores. También terdon tiene ese ejemplo ya.
slm
1

Puede llamar a utilidades externas (vea otras respuestas), pero harán que su secuencia de comandos sea más lenta y es difícil hacer la instalación correcta.

Zsh

En zsh, puede escribir ${#$(readlink -f /etc/fstab)}para obtener la duración de la sustitución del comando. Tenga en cuenta que esta no es la longitud de la salida del comando, es la longitud de la salida sin ninguna nueva línea final.

Si desea la longitud exacta de la salida, muestre un carácter adicional que no sea de nueva línea al final y reste uno.

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

Si lo que desea es la carga útil en la salida del comando, entonces debe restar dos aquí, porque la salida de readlink -fes la ruta canónica más una nueva línea.

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

Esto difiere del ${#$(readlink -f /etc/fstab)}caso raro pero posible en el que el camino canónico termina en una nueva línea.

Para este ejemplo específico, no necesita una utilidad externa en absoluto, porque zsh tiene una construcción incorporada que es equivalente a readlink -ftravés del modificador de historial A.

echo /etc/fstab(:A)

Para obtener la longitud, use el modificador de historial en una expansión de parámetros:

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

Si tiene el nombre del archivo en una variable filename, sería ${#filename:A}.

Conchas de estilo Bourne / POSIX

Ninguno de los shells Bourne / POSIX puros (Bourne, ash, mksh, ksh93, bash, yash ...) tiene una extensión similar que yo sepa. Si necesita aplicar una sustitución de parámetros a la salida de una sustitución de comando o anidar sustituciones de parámetros, utilice etapas sucesivas.

Puede rellenar el procesamiento en una función si lo desea.

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

o

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

pero generalmente no hay beneficio; excepto con ksh93, eso hace que una bifurcación adicional pueda usar la salida de la función, por lo que hace que su script sea más lento y rara vez hay algún beneficio de legibilidad.

Una vez más, la salida de readlink -fes la ruta canónica más una nueva línea; si desea la longitud del camino canónico, reste 2 en lugar de 1 pulg command_output_length. El uso command_output_length_sans_trailing_newlinesda el resultado correcto solo cuando la ruta canónica en sí misma no termina en una nueva línea.

Bytes vs caracteres

${#…}se supone que es la longitud en caracteres, no en bytes, lo que hace la diferencia en configuraciones regionales multibyte. Las versiones razonablemente actualizadas de ksh93, bash y zsh calculan la longitud en caracteres de acuerdo con el valor de LC_CTYPEen el momento en que ${#…}se expande la construcción. Muchos otros shells comunes realmente no admiten configuraciones regionales multibyte: a partir del guión 0.5.7, mksh 46 y posh 0.12.3, ${#…}devuelve la longitud en bytes. Si desea la longitud en caracteres de manera confiable, use la wcutilidad:

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

Siempre que $LC_CTYPEdesigne una configuración regional válida, puede estar seguro de que esto se eliminará por error (en una plataforma antigua o restringida que no admite configuraciones regionales de varios bytes) o devolverá la longitud correcta en caracteres. (Para Unicode, "longitud en caracteres" significa la cantidad de puntos de código; la cantidad de glifos es otra historia, debido a complicaciones como la combinación de caracteres).

Si desea la longitud en bytes, establezca LC_CTYPE=Ctemporalmente o use en wc -clugar de wc -m.

El conteo de bytes o caracteres wcincluye cualquier nueva línea final del comando. Si desea la longitud de la ruta canónica en bytes, es

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

Para obtenerlo en caracteres, resta 2.

Gilles 'SO- deja de ser malvado'
fuente
@cuonglm No, debe restar 1. echo .agrega dos caracteres, pero el segundo carácter es una nueva línea final que es eliminada por la sustitución del comando.
Gilles 'SO- deja de ser malvado'
La nueva línea es de readlinksalida, más el .by echo. Ambos estamos de acuerdo en que echo .agreguemos dos caracteres, pero la nueva línea final se eliminó. Pruebe printf .o vea mi respuesta unix.stackexchange.com/a/160499/38906 .
Cuonglm
@cuonglm La pregunta preguntaba el número de caracteres en la salida del comando. La salida de readlinkes el objetivo del enlace más una nueva línea.
Gilles 'SO- deja de ser malvado'
0

Esto funciona dashpero requiere que la var objetivo esté definitivamente vacía o desarmada. Es por eso que en realidad son dos comandos: explícitamente vacío $len el primero:

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

SALIDA

len is 10 and result is /etc/fstab

Eso es todo integrado en el shell, sin incluir el readlinksupuesto, pero evaluarlo en el shell actual de esa manera implica que debe hacer la asignación antes de obtener el len, razón por la cual hago %.silegibilidad al primer argumento en la printfcadena de formato y solo lo agrego nuevamente para El valor literal en la cola de printfla lista de argumentos.

Con eval:

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

SALIDA

10:/etc/fstab

Puede acercarse a esa misma cosa, pero en lugar de la salida en una variable en el primer comando, lo obtiene en stdout:

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

... que escribe ...

10:/etc/fstab

... al archivo descriptor 1 sin asignar ningún valor a ningún vars en el shell actual.

mikeserv
fuente
1
¿No es eso exactamente lo que el OP quería evitar? "Entiendo que es posible hacer esto primero guardando la salida en una variable: variable=$(readlink -f /etc/fstab); echo ${#variable};pero me gustaría eliminar el paso adicional".
terdon
@terdon, probablemente entendí mal, pero tuve la impresión de que el punto y coma era el problema y no la variable. Es por eso que estos obtienen la len y la salida en un solo comando simple usando solo shell incorporado. El shell no ejecuta readlink y luego exec expr, por ejemplo. Probablemente solo importa si de alguna manera obtener el len ocluye el valor, lo cual admito, estoy teniendo dificultades para entender por qué puede ser, pero sospecho que podría haber un caso en el que importara.
mikeserv
1
La evalforma, por cierto, es probablemente la más limpia aquí: asigna la salida y el len al mismo nombre var en una sola ejecución, muy cerca de hacerlo l=length(l):out(l). Haciendo expr length $(command) hace ocluir el valor a favor de la len, por cierto.
mikeserv