¿Es posible que un programa obtenga el número de espacios entre los argumentos de la línea de comandos en POSIX?

23

Digamos si escribí un programa con la siguiente línea:

int main(int argc, char** argv)

Ahora sabe qué argumentos de la línea de comando se le pasan comprobando el contenido de argv.

¿Puede el programa detectar cuántos espacios entre argumentos? Como cuando escribo estos en bash:

ibug@linux:~ $ ./myprog aaa bbb
ibug@linux:~ $ ./myprog       aaa      bbb

El entorno es un Linux moderno (como Ubuntu 16.04), pero supongo que la respuesta debería aplicarse a cualquier sistema compatible con POSIX.

iBug
fuente
22
Solo por curiosidad, ¿por qué su programa necesitaría saber eso?
nxnev
2
@nxnev Solía ​​escribir algunos programas de Windows y sé que es posible allí, así que me pregunto si hay algo similar en Linux (o Unix).
iBug
99
Recuerdo vagamente en CP / M que los programas tenían que analizar sus propias líneas de comando; esto significaba que cada tiempo de ejecución de C tenía que implementar un analizador de shell. Y todos lo hicieron ligeramente diferente.
Toby Speight
3
@iBug Hay, pero debe citar los argumentos al invocar el comando. Así es como se hace en los shells POSIX (y similares).
Konrad Rudolph
3
@iBug, ... Windows tiene el mismo diseño que Toby menciona de CP / M arriba. UNIX no hace eso: desde la perspectiva del proceso llamado, no hay una línea de comando involucrada en su ejecución.
Charles Duffy

Respuestas:

39

No tiene sentido hablar de "espacios entre argumentos"; Ese es un concepto de concha.

El trabajo de un shell es tomar líneas enteras de entrada y formarlas en matrices de argumentos para iniciar comandos. Esto puede implicar analizar cadenas entre comillas, expandir variables, comodines de archivo y expresiones tilde, y más. El comando se inicia con una execllamada al sistema estándar , que acepta un vector de cadenas.

Existen otras formas de crear un vector de cadenas. Muchos programas bifurcan y ejecutan sus propios subprocesos con invocaciones de comando predeterminadas, en cuyo caso, nunca existe una "línea de comando". De manera similar, un shell gráfico (de escritorio) puede iniciar un proceso cuando un usuario arrastra un icono de archivo y lo coloca en un widget de comando; nuevamente, no hay una línea de texto para tener caracteres "entre" argumentos.

En lo que respecta al comando invocado, lo que sucede en un shell u otro proceso padre / precursor es privado y oculto: solo vemos la matriz de cadenas que el estándar C especifica que main()puede aceptar.

Toby Speight
fuente
Buena respuesta: es importante señalar esto para los novatos de Unix, que a menudo asumen que, si se ejecutan tar cf texts.tar *.txt, el programa tar obtiene dos argumentos y tiene que expandir el segundo ( *.txt). Muchas personas no se dan cuenta de cómo funciona realmente hasta que comienzan a escribir sus propios scripts / programas que manejan argumentos.
Laurence Renshaw
58

En general, no. El análisis de la línea de comando lo realiza el shell que no hace que la línea no analizada esté disponible para el programa llamado. De hecho, su programa podría ejecutarse desde otro programa que creó el argumento no analizando una cadena sino construyendo una matriz de argumentos mediante programación.

Hans-Martin Mosner
fuente
99
Es posible que desee mencionar execve(2).
iBug
3
Tienes razón, como una excusa poco convincente, puedo decir que actualmente estoy usando un teléfono y buscar páginas de manual es un poco tedioso :-)
Hans-Martin Mosner
1
Esta es la sección relevante de POSIX.
Stephen Kitt
1
@ Hans-MartinMosner: ¿Termux ...? ;-)
DevSolar
99
"en general" se entiende como una protección contra la citación de un caso enrevesado especial donde es posible; por ejemplo, un proceso raíz suid podría inspeccionar la memoria del shell de llamada y encontrar la cadena de línea de comando no analizada.
Hans-Martin Mosner
16

No, esto no es posible, a menos que los espacios sean parte de un argumento.

El comando accede a los argumentos individuales desde una matriz (de una forma u otra dependiendo del lenguaje de programación) y la línea de comando real puede guardarse en un archivo de historial (si se escribe en un mensaje interactivo en un shell que tiene archivos de historial), pero es nunca pasó al comando de ninguna forma.

Todos los comandos en Unix son finalmente ejecutados por una de las exec()familias de funciones. Estos toman el nombre del comando y una lista o matriz de argumentos. Ninguno de ellos toma una línea de comando como se escribe en el indicador de comandos de la shell. La system()función lo hace, pero su argumento de cadena se ejecuta más tarde execve(), lo que, nuevamente, toma una matriz de argumentos en lugar de una cadena de línea de comando.

Kusalananda
fuente
2
@LightnessRacesinOrbit Puse eso allí solo en caso de que hubiera alguna confusión sobre "espacios entre argumentos". Poner espacios entre comillas helloy worldes literalmente espacios entre los dos argumentos.
Kusalananda
55
@Kusalananda - Bueno, no ... Introducción de espacios entre comillas helloy worldestá literalmente suministrar el segundo de los tres argumentos.
Jeremy
@Jeremy Como dije, en caso de que hubiera alguna confusión sobre lo que se entiende por "entre los argumentos". Sí, como un segundo argumento entre los otros dos si quieres.
Kusalananda
Sus ejemplos fueron buenos e instructivos.
Jeremy
1
Bueno, muchachos, los ejemplos fueron una fuente obvia de confusión y malentendidos. Los eliminé porque no agregaron al valor de respuesta.
Kusalananda
9

En general, no es posible, como se explicaron varias otras respuestas.

Sin embargo, los shells de Unix son programas ordinarios (y están interpretando la línea de comando y englobándola , es decir, expandiendo el comando antes de hacerlo forky execvepara ello). Vea esta explicación sobre bashlas operaciones de shell . Puede escribir su propio shell (o puede parchear un shell de software libre existente , por ejemplo, GNU bash ) y usarlo como su shell (o incluso su shell de inicio de sesión, vea passwd (5) y shells (5) ).

Por ejemplo, puede hacer que su propio programa de shell coloque la línea de comando completa en alguna variable de entorno (imagine MY_COMMAND_LINEpor ejemplo) -o use cualquier otro tipo de comunicación entre procesos para transmitir la línea de comando de shell a proceso hijo-.

No entiendo por qué querrías hacer eso, pero podrías codificar un shell que se comporte de esa manera (pero recomiendo no hacerlo).

Por cierto, un programa podría ser iniciado por algún programa que no sea un shell (pero que hace fork (2) luego execve (2) , o simplemente execvepara iniciar un programa en su proceso actual). En ese caso, no hay línea de comando en absoluto, y su programa podría iniciarse sin un comando ...

Tenga en cuenta que puede tener algún sistema Linux (especializado) sin ningún shell instalado. Esto es extraño e inusual, pero posible. A continuación, tendrá que escribir una especializada init programa a partir de otros programas según sea necesario - sin utilizar ningún cáscara pero al hacerlo forky execvelas llamadas al sistema.

Lea también Sistemas operativos: tres piezas fáciles y no olvide que execveprácticamente siempre es una llamada al sistema (en Linux, se enumeran en syscalls (2) , consulte también la introducción (2) ) que reinicializa el espacio de direcciones virtuales (y algunos otros cosas) del proceso que lo hace.

Basile Starynkevitch
fuente
Esta es la mejor respuesta. Supongo (sin haberlo buscado) que argv[0] para el nombre del programa y los elementos restantes para los argumentos son especificaciones POSIX y no se pueden cambiar. argv[-1]Supongo que un entorno de tiempo de ejecución podría especificar para la línea de comando ...
Peter - Restablecer Monica
No, no pudo. Lea más cuidadosamente la execvedocumentación. No se puede usar argv[-1], es un comportamiento indefinido usarlo.
Basile Starynkevitch
Sí, buen punto (también la pista de que tenemos una llamada al sistema): la idea es un poco artificial. Los tres componentes del tiempo de ejecución (shell, stdlib y OS) tendrían que colaborar. El shell necesita llamar a una función especial que no sea POSIX execvepluscmdcon un parámetro adicional (o convención argv), el syscall construye un vector de argumento para main que contiene un puntero a la línea de comando antes del puntero al nombre del programa, y ​​luego pasa la dirección del puntero al nombre del programa como argvcuando se llama al programa main...
Peter - Restablecer Monica
No es necesario volver a escribir el shell, solo use las comillas. Esta característica estaba disponible desde el shell Bourn sh. Entonces no es nuevo.
ctrl-alt-delor
El uso de comillas requiere cambiar la línea de comando. Y OP no quiere eso
Basile Starynkevitch
3

Siempre puede decirle a su shell que diga a las aplicaciones qué código de shell condujo a su ejecución. Por ejemplo, con zsh, al pasar esa información en la $SHELL_CODEvariable de entorno usando el preexec()gancho ( printenvusado como ejemplo, usaría getenv("SHELL_CODE")en su programa):

$ preexec() export SHELL_CODE=$1
$ printenv SHELL_CODE
printenv SHELL_CODE
$ printenv  SHELL_CODE
printenv  CODE
$ $(echo printenv SHELL_CODE)
$(echo printenv SHELL_CODE)
$ for i in SHELL_CODE; do printenv "$i"; done
for i in SHELL_CODE; do printenv "$i"; done
$ printenv SHELL_CODE; : other command
printenv SHELL_CODE; : other command
$ f() printenv SHELL_CODE
$ f
f

Todos esos se ejecutarían printenvcomo:

execve("/usr/bin/printenv", ["printenv", "SHELL_CODE"], 
       ["PATH=...", ..., "SHELL_CODE=..."]);

Permitiendo printenvrecuperar el código zsh que condujo a la ejecución de printenvesos argumentos. Lo que desearía hacer con esa información no me queda claro.

Con bash, la característica más cercana a zsh's preexec()sería usarla $BASH_COMMANDen una DEBUGtrampa, pero tenga en cuenta que bashtiene cierto nivel de reescritura en eso (y en particular refactoriza parte del espacio en blanco utilizado como delimitador) y eso se aplica a cada comando (bueno, algunos) ejecutar, no toda la línea de comando como se ingresó en el indicador (ver también la functraceopción).

$ trap 'export SHELL_CODE="$BASH_COMMAND"' DEBUG
$ printenv SHELL_CODE
printenv SHELL_CODE
$ printenv $(echo 'SHELL_CODE')
printenv $(echo 'SHELL_CODE')
$ for i in SHELL_CODE; do printenv "$i"; done; : other command
printenv "$i"
$ printf '%s\n' "$(printenv "SHELL_CODE")"
printf '%s\n' "$(printenv "SHELL_CODE")"
$ set -o functrace
$ printf '%s\n' "$(printenv "SHELL_CODE")"
printenv "SHELL_CODE"
$ print${-+env  }    $(echo     'SHELL_CODE')
print${-+env  } $(echo     'SHELL_CODE')

Vea cómo algunos de los espacios que son delimitadores en la sintaxis del lenguaje de shell se han comprimido en 1 y cómo no la línea de comando completa no siempre se pasa al comando. Probablemente no sea útil en su caso.

Tenga en cuenta que no recomendaría hacer este tipo de cosas, ya que potencialmente está filtrando información confidencial a cada comando como en:

echo very_secret | wc -c | untrustedcmd

filtraría ese secreto a ambos wcy untrustedcmd.

Por supuesto, podría hacer ese tipo de cosas para otros idiomas además del shell. Por ejemplo, en C, podría usar algunas macros que exportan el código C que ejecuta un comando al entorno:

#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#define WRAP(x) (setenv("C_CODE", #x, 1), x)

int main(int argc, char *argv[])
{
  if (!fork()) WRAP(execlp("printenv", "printenv", "C_CODE", NULL));
  wait(NULL);
  if (!fork()) WRAP(0 + execlp("printenv",   "printenv", "C_CODE", NULL));
  wait(NULL);
  if (argc > 1 && !fork()) WRAP(execvp(argv[1], &argv[1]));
  wait(NULL);
  return 0;
}

Ejemplo:

$ ./a.out printenv C_CODE
execlp("printenv", "printenv", "C_CODE", NULL)
0 + execlp("printenv", "printenv", "C_CODE", NULL)
execvp(argv[1], &argv[1])

Vea cómo el preprocesador C condensó algunos espacios como en el caso bash. En la mayoría de los idiomas, si no en todos, la cantidad de espacio utilizado en los delimitadores no hace ninguna diferencia, por lo que no es sorprendente que el compilador / intérprete se tome algo de libertad aquí.

Stéphane Chazelas
fuente
Cuando estaba probando esto, BASH_COMMANDno contenía los argumentos de separación de espacios en blanco originales, por lo que esto no era utilizable para la solicitud literal del OP. ¿Esta respuesta incluye alguna demostración en ambos sentidos para ese caso de uso en particular?
Charles Duffy
@CharlesDuffy, solo quería indicar el equivalente más cercano del preexec () de zsh en bash (ya que ese era el shell al que se refería el OP) y señalar que no podía usarse para ese caso de uso específico, pero estoy de acuerdo en que no era muy claro. Ver editar. Esta respuesta pretende ser más genérica sobre cómo pasar el código fuente (aquí en zsh / bash / C) que causó la ejecución del comando que se está ejecutando (no es algo útil, pero espero que al hacerlo, y especialmente con los ejemplos, demuestro que no es muy útil)
Stéphane Chazelas
0

Solo agregaré lo que falta en las otras respuestas.

No

Ver otras respuestas

Tal vez

No se puede hacer nada en el programa, pero hay algo que se puede hacer en el shell cuando ejecuta el programa.

Necesitas usar comillas. Entonces en lugar de

./myprog      aaa      bbb

necesitas hacer uno de estos

./myprog "     aaa      bbb"
./myprog '     aaa      bbb'

Esto pasará un único argumento al programa, con todos los espacios. Hay una diferencia entre los dos, el segundo es literal, exactamente la cadena como aparece (excepto que 'debe escribirse como \'). El primero interpretará algunos caracteres, pero se dividirá en varios argumentos. Ver cotización de shell para más información. Así que no hay necesidad de reescribir el shell, los diseñadores del shell ya lo han pensado. Sin embargo, como ahora es un argumento, tendrá que pasar más dentro del programa.

opcion 2

Pase los datos a través de stdin. Esta es la forma normal de obtener grandes cantidades de datos en un comando. p.ej

./myprog << EOF
    aaa      bbb
EOF

o

./myprog
Tell me what you want to tell me:
aaaa bbb
ctrl-d

(Las cursivas son salida del programa)

ctrl-alt-delor
fuente
Técnicamente, el código de shell: ./myprog␣"␣␣␣␣␣aaa␣␣␣␣␣␣bbb"ejecuta (generalmente en un proceso secundario) el archivo almacenado ./myprogy le pasa dos argumentos: ./myprogy ␣␣␣␣␣aaa␣␣␣␣␣␣bbb( argv[0]y argc[1], argcsiendo 2) y, como en los OP, el espacio que separa esos dos argumentos no se pasa de ninguna manera a myprog.
Stéphane Chazelas
Pero está cambiando el comando, y OP no quiere cambiarlo
Basile Starynkevitch
@BasileStarynkevitch Después de tu comentario, leí la pregunta nuevamente. Estás haciendo una suposición. En ninguna parte el OP dice que no quieren cambiar la forma en que se ejecuta el programa. Tal vez esto es cierto, pero no tenían nada que decir al respecto. Por lo tanto, esta respuesta puede ser lo que necesitan.
ctrl-alt-delor
OP pregunta explícitamente sobre espacios entre argumentos, no sobre un solo argumento que contiene espacios
Basile Starynkevitch