¿Dividir la salida del comando por columnas usando Bash?

87

Quiero hacer esto:

  1. ejecutar un comando
  2. capturar la salida
  3. seleccionar una línea
  4. seleccione una columna de esa línea

Solo como ejemplo, digamos que quiero obtener el nombre de comando de un $PID(tenga en cuenta que esto es solo un ejemplo, no estoy sugiriendo que esta sea la forma más fácil de obtener un nombre de comando de un ID de proceso; mi problema real es con otro comando cuyo formato de salida no puedo controlar).

Si corro psobtengo:


  PID TTY          TIME CMD
11383 pts/1    00:00:00 bash
11771 pts/1    00:00:00 ps

Ahora lo hago ps | egrep 11383y consigo

11383 pts/1    00:00:00 bash

Paso siguiente: ps | egrep 11383 | cut -d" " -f 4. La salida es:

<absolutely nothing/>

El problema es que cutcorta la salida por espacios simples, y como psagrega algunos espacios entre la 2da y 3ra columna para mantener cierta semejanza con una tabla, cutelige una cadena vacía. Por supuesto, podría usar cutpara seleccionar el séptimo y no el cuarto campo, pero cómo puedo saberlo, especialmente cuando la salida es variable y desconocida de antemano.

flybywire
fuente
2
Utilice awk (y 25 caracteres más).
Michael Foukarakis

Respuestas:

178

Una forma fácil es agregar un pase de trpara exprimir cualquier separador de campo repetido:

$ ps | egrep 11383 | tr -s ' ' | cut -d ' ' -f 4
relajarse
fuente
1
Me gusta este, parece que tres más ligero queawk
flybywire
3
Tiendo a estar de acuerdo, pero eso también podría deberse a que no he aprendido awk. :)
relájese el
No funcionará si tiene un proceso con PID que contiene el PID que le interesa como una subcadena.
David Grayson
1
Y también, los nunbers de campo estarán desactivados si algunos PID: s se rellenan con un espacio a la izquierda mientras que otros no.
tripleee
68

Creo que la forma más sencilla es usar awk . Ejemplo:

$ echo "11383 pts/1    00:00:00 bash" | awk '{ print $4; }'
bash
Brianegge
fuente
4
Por compatibilidad con la pregunta original, ps | awk "\$1==$PID{print\$4}"o (mejor) ps | awk -v"PID=$PID" '$1=PID{print$4}'. Por supuesto, en Linux simplemente podría hacer xargs -0n1 </proc/$PID/cmdline | head -n1o readlink /proc/$PID/exe, pero de todos modos ...
ephemient
Es la ;de { print $4; }necesaria? Eliminarlo parece no tener ningún efecto para mí en Linux, solo tengo curiosidad por su propósito
igniteflow
@igniteflow, ¿no indicaría el final del comando si quisiera continuar agregando más allá de la declaración de impresión?
joshmcode
16

Tenga en cuenta que la tr -s ' 'opción no eliminará ningún espacio inicial. Si su columna está alineada a la derecha (como con pspid) ...

$ ps h -o pid,user -C ssh,sshd | tr -s " "
 1543 root
19645 root
19731 root

Luego, cortar dará como resultado una línea en blanco para algunos de esos campos si es la primera columna:

$ <previous command> | cut -d ' ' -f1

19645
19731

A menos que lo preceda con un espacio, obviamente

$ <command> | sed -e "s/.*/ &/" | tr -s " "

Ahora, para este caso particular de números pid (no nombres), hay una función llamada pgrep:

$ pgrep ssh


Funciones de shell

Sin embargo, en general, todavía es posible usar funciones de shell de manera concisa, porque hay algo interesante sobre el readcomando:

$ <command> | while read a b; do echo $a; done

El primer parámetro a leer, aselecciona la primera columna, y si hay más, se incluirá todo lo demásb . Como resultado, nunca necesita más variables que el número de su columna +1 .

Entonces,

while read a b c d; do echo $c; done

luego generará la tercera columna. Como se indica en mi comentario ...

Una lectura canalizada se ejecutará en un entorno que no pasa variables al script de llamada.

out=$(ps whatever | { read a b c d; echo $c; })

arr=($(ps whatever | { read a b c d; echo $c $b; }))
echo ${arr[1]}     # will output 'b'`


La solución de matriz

Entonces, terminamos con la respuesta de @frayser, que es usar la variable de shell IFS, que por defecto es un espacio, para dividir la cadena en una matriz. Sin embargo, solo funciona en Bash. Dash y Ash no lo apoyan. Me ha costado mucho dividir una cadena en componentes en un Busybox. Es bastante fácil obtener un solo componente (por ejemplo, usando awk) y luego repetirlo para cada parámetro que necesite. Pero luego terminas llamando repetidamente a awk en la misma línea, o usando repetidamente un bloque de lectura con eco en la misma línea. Lo cual no es eficiente ni bonito. Entonces terminas dividiéndote usando ${name%% *}y así. Te hace anhelar algunas habilidades de Python porque, de hecho, el script de shell ya no es muy divertido si la mitad o más de las funciones a las que estás acostumbrado se han ido. Pero puede asumir que incluso Python no se instalaría en un sistema de este tipo, y no lo fue ;-).

Xennex81
fuente
Debe usar comillas alrededor de la variable en echo "$a"y echo "$c"aunque.
tripleee
¿Parece que cada bloque canalizado se ejecuta en su propia subcapa o proceso y no puede devolver ninguna variable al bloque adjunto? Aunque puede obtener el resultado de eso después de repetirlo. var=$(....... | { read a b c d; echo $c; }). Eso solo funciona para una sola (cadena), aunque en Bash puede dividirla en una matriz usandoar=($var)
Xennex81
@tripleee No creo que eso sea un problema en esta etapa del proceso. Pronto descubrirá si lo necesita o no, y si eso se rompe en algún momento, es una lección de aprendizaje. Y luego sabes por qué has tenido que usar esas comillas dobles ;-). Y luego ya no es algo que hayas escuchado decir a otros. ¡Jugar con fuego! :RE. :pag.
Xennex81
respuesta elaborada: D
ncomputers
Esta fue una respuesta demasiado útil para no decirlo.
Ivan X
4

tratar

ps |&
while read -p first second third fourth etc ; do
   if [[ $first == '11383' ]]
   then
       echo got: $fourth
   fi       
done
James Anderson
fuente
1
@flybywire: posiblemente exagerado para este ejemplo simple, pero este idioma es excelente si necesita hacer un procesamiento más complejo en los datos seleccionados.
James Anderson
Además, tenga en cuenta que en estos días el shell de secuencias de comandos predeterminado no suele ser bash.
David dado
2

Usando variables de matriz

set $(ps | egrep "^11383 "); echo $4

o

A=( $(ps | egrep "^11383 ") ) ; echo ${A[3]}
deshilachador
fuente
2

Similar a la solución awk de brianegge, aquí está el equivalente de Perl:

ps | egrep 11383 | perl -lane 'print $F[3]'

-ahabilita el modo de división automática, que llena la @Fmatriz con los datos de la columna.
Úselo -F,si sus datos están delimitados por comas, en lugar de por espacios.

El campo 3 se imprime ya que Perl comienza a contar desde 0 en lugar de 1

Chris Koknat
fuente
1
Gracias por su solución de perl - no sabía acerca de la división automática y aún creo que perl es la herramienta para terminar con las otras herramientas ..;).
Gerard ONeill
1

Obtener la línea correcta (ejemplo para la línea n. ° 6) se hace con head y tail y la palabra correcta (palabra n. ° 4) se puede capturar con awk:

command|head -n 6|tail -n 1|awk '{print $4}'
fusión de almas
fuente
Solo señalar para los futuros lectores que awk también puede seleccionar por línea: awk NR=6 {print $4}sería un poco más eficiente
David Z
1
y con eso, por supuesto, quise decir awk NR==6 {print $4}* doh *
David Z
1

Tu mando

ps | egrep 11383 | cut -d" " -f 4

echa de menos un tr -spara apretar espacios, como lo explica relajarse en su respuesta .

Sin embargo, es posible que desee utilizar awk, ya que maneja todas estas acciones en un solo comando:

ps | awk '/11383/ {print $4}'

Esto imprime la cuarta columna en las líneas que contienen 11383. Si desea que esto coincida 11383si aparece al principio de la línea, puede decir ps | awk '/^11383/ {print $4}'.

fedorqui 'ASÍ que deja de hacer daño'
fuente
0

En lugar de hacer todos estos greps y esas cosas, le aconsejo que utilice las capacidades ps para cambiar el formato de salida.

ps -o cmd= -p 12345

Obtiene la línea cmmand de un proceso con el pid especificado y nada más.

Esto es compatible con POSIX y, por lo tanto, puede considerarse portátil.

P Shved
fuente
1
flybywire afirma que solo está usando ps como ejemplo, la pregunta es más general que eso.
Ogre Psalm33
0

Bash setanalizará toda la salida en parámetros de posición.

Por ejemplo, con el set $(free -h)comando, echo $7mostrará "Mem:"

dman
fuente
Este método es útil solo cuando el comando tiene una sola línea de salida. No es lo suficientemente genérico.
codeforester
Eso no es cierto, toda la salida se coloca en parámetros posicionales independientemente de las líneas. ex set $(sar -r 1 1); echo "${23}"
dman
Mi punto fue que es difícil determinar la posición del argumento cuando la salida es voluminosa y tiene muchos campos. awkes la mejor manera de hacerlo.
codeforester
Esta es solo otra solución. Es posible que el OP no desee aprender el lenguaje awk para este caso de uso único. Las etiquetas indican bashy no awk.
dman