Come posso ottenere la prima partita dall'espansione con caratteri jolly?


39

Conchiglie come Bash e Zsh espandono i caratteri jolly in argomenti, tanti argomenti quanti corrispondono allo schema:

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

E se volessi solo restituire la prima partita, non tutte le partite?

$ echo *.txt
1.txt

Non mi dispiace per le soluzioni specifiche della shell, ma vorrei una soluzione che funzioni con gli spazi bianchi nei nomi dei file.


ls * .txt | testa -1?
Archemar,

1
@Archemar: non funziona con le nuove righe nei nomi dei file.
Flimm,

Risposte:


26

Un modo affidabile in bash è di espandersi in un array e produrre solo il primo elemento:

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

(Puoi anche solo echo $files, un indice mancante viene trattato come [0].)

Questo gestisce in modo sicuro spazio / tab / newline e altri metacaratteri durante l'espansione dei nomi dei file. Si noti che le impostazioni locali in effetti possono modificare il "primo".

Puoi anche farlo in modo interattivo con una funzione di completamento 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

Ciò vincola la _echofunzione per completare gli argomenti al echocomando (ignorando il completamento normale). Un "*" extra viene aggiunto nel codice sopra, puoi semplicemente premere il tasto tab su un nome file parziale e speriamo che accada la cosa giusta.

Il codice è leggermente contorto, piuttosto che set o assume nullglob( shopt -s nullglob) controlliamo di compgen -Gpoter espandere il glob in alcune corrispondenze, quindi espandiamo in modo sicuro in un array e infine impostiamo COMPREPLY in modo che il preventivo sia solido.

Puoi parzialmente farlo (espandere a livello di codice un glob) con bash's compgen -G, ma non è robusto in quanto emette non quotato su stdout.

Come al solito, il completamento è piuttosto irto, questo interrompe il completamento di altre cose, comprese le variabili di ambiente (vedere la _bash_def_completion()funzione qui per i dettagli sull'emulazione del comportamento predefinito).

Puoi anche usare solo compgenal di fuori di una funzione di completamento:

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

Un punto da notare è che "~" non è un glob, è gestito da bash in una fase di espansione separata, così come lo sono le variabili $ e altre espansioni. compgen -Gfa solo il globbing del nome file, ma compgen -Wti dà tutta l'espansione predefinita di bash, anche se forse troppe espansioni (incluso ``e $()). A differenza di -G, -W viene citato in modo sicuro (non posso spiegare la disparità). Poiché lo scopo di -Wè che espande i token, ciò significa che espanderà da "a" a "a" anche se non esiste un file simile, quindi forse non è l'ideale.

Questo è più facile da capire, ma può avere effetti collaterali indesiderati:

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

Poi:

touch $'curious \n filename'

echo curious*tab

Nota l'uso di printf %qper citare in sicurezza i valori.

Un'ultima opzione è usare l'output delimitato da 0 con le utility GNU (vedi le FAQ di 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 )

Questa opzione ti dà un po 'più di controllo sull'ordinamento (l'ordine quando si espande un glob sarà soggetto al tuo locale / LC_COLLATEe potrebbe o non potrebbe piegare il caso), ma è altrimenti un martello piuttosto grande per un problema così piccolo ;-)


21

In zsh, usa il [1] qualificatore glob . Si noti che anche se questo caso speciale restituisce al massimo una corrispondenza, è comunque un elenco e le globs non sono espanse in contesti che prevedono una sola parola come assegnazioni (assegnazioni di array a parte).

echo *.txt([1])

In ksh o bash, puoi riempire l'intero elenco di corrispondenze in un array e usare il primo elemento.

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

In qualsiasi shell, è possibile impostare i parametri posizionali e utilizzare il primo.

set -- *.txt
echo "$1"

Questo blocca i parametri posizionali. Se non lo desideri, puoi utilizzare una subshell.

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

Puoi anche usare una funzione, che ha il proprio set di parametri posizionali.

set_to_first () {
  eval "$1=\"\$2\""
}
set_to_first f *.txt
echo "$f"

1
E per ottenere le prime partite da $ n $, puoi usare*.txt([1,n])
Emre

6

Provare:

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

Nota che l'espansione del nome file viene ordinata in base alla sequenza di confronto in vigore nella locale corrente.



1

Mi sono appena imbattuto in questa vecchia domanda mentre mi chiedevo la stessa cosa. Ho finito con questo:

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

Ovviamente puoi sostituire headcon taile -n1con qualsiasi altro numero.


Quanto sopra non funzionerà se stai lavorando con file che hanno una nuova riga nel loro nome. Per lavorare con le newline puoi usare uno di questi:

  • ls -b *.txt | head -n1 | sed -E 's/\\n/\n/g' (Non funziona su 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)" (Funziona con qualsiasi carattere speciale)

3
No, ciò fallirà se il nome del file ha nuove righe.
Isaac,

7
in che tipo di mondo pazzo viviamo in cui i nomi dei file contengono nuove righe?
Billynoah,

-1

Un caso d'uso che incontro spesso consiste nel definire la directory superiore / inferiore dopo l'espansione glob (ad esempio una directory piena di SDK con versione o strumenti di compilazione). In questa situazione di solito voglio salvare il nome di quella directory in una variabile da usare in alcuni punti all'interno di uno script di shell.

Questo comando di solito lo fa per me:

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

Dichiarazione di non responsabilità: l'espansione globale non ordinerà le cartelle per semestre; sei stato avvisato. Questo è fantastico se hai una Dockerfilesola versione di una directory, ma quella versione delle directory potrebbe variare da immagine a immagine 🤷


Benvenuto in U&L! Questo gestisce la maggior parte dei nomi di directory, ma non gestisce i nomi di directory con una nuova riga. Prova a creare una directory come questa mkdir "$(echo one; echo two)"e vedi cosa intendo.
Flimm,

Qual è il vantaggio rispetto alle altre alternative, in particolare la versione che utilizza tail?
RalfFriedl,

Lo standard dirnamerichiede solo un singolo percorso, quindi non puoi fare affidamento su di esso lavorando su più percorsi a meno che tu non sappia che la tua specifica implementazione lo supporta.
Kusalananda

@Flimm Ottimo punto; Penso che la maggior parte degli sviluppatori avrebbe problemi più grandi da affrontare se la loro struttura di cartelle avesse delle nuove linee ... Non ho mai avuto a che fare con quello, e non mi aspetto di avere contenitori e software decenti che sto usando
Andrew Odri,

@RalfFriedl Buona domanda; questo essenzialmente filtra tutto ciò che non è una directory valida (e non elenca / attraversa. e ..)
Andrew Odri
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.