¿Cómo puedo obtener la primera coincidencia de la expansión comodín?

39

Los shells como Bash y Zsh expanden comodines en argumentos, tantos argumentos como coincidan con el patrón:

$ echo *.txt
1.txt 2.txt 3.txt

Pero, ¿qué sucede si solo quiero que se devuelva la primera coincidencia, no todas las coincidencias?

$ echo *.txt
1.txt

No me importan las soluciones específicas de shell, pero me gustaría una solución que funcione con espacios en blanco en los nombres de archivo.

Flimm
fuente
ls * .txt | cabeza -1?
Archemar
1
@Archemar: no funciona con nuevas líneas en los nombres de archivo.
Flimm

Respuestas:

26

Una forma sólida en bash es expandirse en una matriz y generar solo el primer elemento:

pattern="*.txt"
files=( $pattern )
echo "${files[0]}"  # printf is safer!

(Incluso puede simplemente echo $files, un índice faltante se trata como [0]).

Esto maneja de forma segura el espacio / tabulador / nueva línea y otros metacaracteres al expandir los nombres de archivo. Tenga en cuenta que la configuración regional vigente puede alterar lo que es "primero".

También puede hacer esto interactivamente con una función de finalización de bash :

_echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}   # string to expand

    if compgen -G "$cur*" > /dev/null; then
        local files=( ${cur:+$cur*} )   # don't expand empty input as *
        [ ${#files} -ge 1 ] && COMPREPLY=( "${files[0]}" )
    fi
}
complete -o bashdefault -F _echo echo

Esto vincula la _echofunción para completar argumentos al echocomando (anulando la finalización normal). Se agrega un "*" adicional en el código anterior, solo puede presionar la pestaña en un nombre de archivo parcial y, con suerte, sucederá lo correcto.

El código es un poco complicado, en lugar de establecer o asumir nullglob( shopt -s nullglob) comprobamos que compgen -Gpuede expandir el glob a algunas coincidencias, luego expandimos de forma segura en una matriz y finalmente establecemos COMPREPLY para que las citas sean robustas.

Puede parte hacer esto (mediante programación expandir un pegote) con fiesta de compgen -G, pero no es tan robusta que da salida a la salida estándar sin comillas.

Como de costumbre, la finalización es bastante complicada, esto interrumpe la finalización de otras cosas, incluidas las variables de entorno (consulte la _bash_def_completion()función aquí para obtener detalles sobre cómo emular el comportamiento predeterminado).

También podría usarlo compgenfuera de una función de finalización:

files=( $(compgen -W "$pattern") )

Un punto a tener en cuenta es que "~" no es un problema, se maneja con bash en una etapa separada de expansión, al igual que las variables $ y otras expansiones. compgen -Gsolo hace globing de nombre de archivo, pero compgen -Wle brinda toda la expansión predeterminada de bash, aunque posiblemente demasiadas expansiones (incluyendo ``y $()). A diferencia -G, -W se cita con seguridad (no puedo explicar la disparidad). Dado que el propósito -Wes que expande los tokens, esto significa que expandirá "a" a "a" incluso si no existe tal archivo, por lo que tal vez no sea ideal.

Esto es más fácil de entender, pero puede tener efectos secundarios no deseados:

_echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}
    local files=( $(compgen -W "$cur") ) 
    printf -v COMPREPLY %q "${files[0]}"  
}

Luego:

touch $'curious \n filename'

echo curious*tab

Tenga en cuenta el uso de printf %qpara citar de forma segura los valores.

Una última opción es usar una salida delimitada por 0 con utilidades GNU (consulte las preguntas frecuentes de bash ):

pattern="*.txt"
while IFS= read -r -d $'\0' filename; do 
    printf '%q' "$filename"; 
    break; 
done < <(find . -maxdepth 1 -name "$pattern" -printf "%f\0" | sort -z )

Esta opción le da un poco más de control sobre el orden de clasificación (el orden al expandir un globo estará sujeto a su ubicación / LC_COLLATEy puede o no doblar mayúsculas y minúsculas), pero de lo contrario es un martillo bastante grande para un problema tan pequeño ;-)

Sr. púrpura
fuente
21

En zsh, usa el [1] calificador glob . Tenga en cuenta que, aunque este caso especial devuelve como máximo una coincidencia, sigue siendo una lista, y los globos no se expanden en contextos que esperan una sola palabra, como asignaciones (asignaciones de matriz a un lado).

echo *.txt([1])

En ksh o bash, puede rellenar toda la lista de coincidencias en una matriz y usar el primer elemento.

tmp=(*.txt)
echo "${tmp[0]}"

En cualquier shell, puede establecer los parámetros posicionales y usar el primero.

set -- *.txt
echo "$1"

Esto cambia los parámetros posicionales. Si no quieres eso, puedes usar una subshell.

echo "$(set -- *.txt; echo "$1")"

También puede usar una función, que tiene su propio conjunto de parámetros posicionales.

set_to_first () {
  eval "$1=\"\$2\""
}
set_to_first f *.txt
echo "$f"
Gilles 'SO- deja de ser malvado'
fuente
1
Y para obtener las primeras coincidencias de $ n $, puede usar*.txt([1,n])
Emre
6

Tratar:

for i in *.txt; do printf '%s\n' "$i"; break; done
1.txt

Una nota de que la expansión del nombre de archivo se ordena de acuerdo con la secuencia de clasificación vigente en la configuración regional actual.

Cuonglm
fuente
3

Una solución simple:

sh -c 'echo "$1"' sh *.txt

O úsalo printfsi lo prefieres.

G-Man dice 'restablecer a Monica'
fuente
1

Me topé con esta vieja pregunta mientras me preguntaba lo mismo. Terminé con esto:

echo $(ls *.txt | head -n1)

Puede, por supuesto, reemplazar headcon taily -n1con cualquier otro número.


Lo anterior no funcionará si está trabajando con archivos que tienen nuevas líneas en su nombre. Para trabajar con líneas nuevas puede usar cualquiera de estos:

  • ls -b *.txt | head -n1 | sed -E 's/\\n/\n/g' (No funciona en BSD)
  • ls -b *.txt | head -n1 | sed -e 's/\\n/\'$'\n/g'
  • ls -b *.txt | head -n1 | perl -pe 's/\\n/\n/g'
  • echo -e "$(ls -b *.txt | head -n1)" (Funciona con cualquier personaje especial)
usuario149485
fuente
3
No, eso fallará si el nombre de archivo tiene nuevas líneas.
Isaac
77
¿en qué tipo de mundo loco vivimos donde los nombres de archivo contienen nuevas líneas?
billynoah
-1

Un caso de uso que a menudo encuentro es definir el directorio superior / inferior después de la expansión global (por ejemplo, un directorio lleno de SDK versionados o herramientas de compilación). En esta situación, generalmente querré guardar ese nombre de directorio en una variable para usarlo en algunos lugares dentro de un script de shell.

Este comando generalmente lo hace por mí:

export SDK_DIR=$(dirname /path/to/versioned/sdks/*/. | tail -n1)

Descargo de responsabilidad: la expansión Glob no va a ordenar sus carpetas por semver; has sido advertido. Esto es genial si tiene una Dockerfileúnica versión de un directorio, pero esa versión de los directorios puede variar de una imagen a otra 🤷

Andrew Odri
fuente
¡Bienvenido a U&L! Eso maneja la mayoría de los nombres de directorio, pero no maneja los nombres de directorio con una nueva línea. Intente crear un directorio como este mkdir "$(echo one; echo two)"y vea a qué me refiero.
Flimm
¿Cuál es la ventaja en comparación con las otras alternativas, especialmente la versión que utiliza tail?
RalfFriedl
Standard dirnamesolo toma un solo nombre de ruta, por lo que no puede confiar en que funcione en varios nombres de ruta a menos que sepa que su implementación particular lo admite.
Kusalananda
@Flimm Buen punto; Creo que la mayoría de los desarrolladores tendrían problemas más grandes con los que lidiar si su estructura de carpetas tiene nuevas líneas ... Nunca he tenido que lidiar con eso, y no espero tener ningún contenedor y software medio decente que estoy usando
Andrew Odri
@RalfFriedl Buena pregunta; esto esencialmente filtra cualquier cosa que no sea un directorio válido (y no enumere / recorra. y ...)
Andrew Odri