bash: uso procedurale sicuro degli spazi di find in select


12

Dati questi nomi di file:

$ ls -1
file
file name
otherfile

bash si adatta perfettamente agli spazi bianchi incorporati:

$ for file in *; do echo "$file"; done
file
file name
otherfile
$ select file in *; do echo "$file"; done
1) file
2) file name
3) otherfile
#?

Tuttavia, a volte potrei non voler lavorare con tutti i file, o anche rigorosamente in $PWD, che è dove findarriva. Che gestisce anche gli spazi nominalmente:

$ find -type f -name file\*
./file
./file name
./directory/file
./directory/file name

Sto cercando di inventare una versione sicura di questo scriptlet che prenderà l'output finde lo presenterà in select:

$ select file in $(find -type f -name file); do echo $file; break; done
1) ./file
2) ./directory/file

Tuttavia, questo esplode con spazi bianchi nei nomi dei file:

$ select file in $(find -type f -name file\*); do echo $file; break; done
1) ./file        3) name          5) ./directory/file
2) ./file        4) ./directory/file  6) name

Di solito, vorrei aggirare questo problema IFS. Tuttavia:

$ IFS=$'\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'
$ IFS='\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'

Qual è la soluzione a questo?



1
Se stai usando solofind per la sua capacità di abbinare un determinato nome di file, puoi semplicemente usare select file in **/file*(dopo l'impostazione shopt -s globstar) in bash4 o versioni successive.
Chepner,

Risposte:


14

Se hai solo bisogno di gestire spazi e tabulazioni (non newline incorporate), puoi usare mapfile(o il suo sinonimo readarray) per leggere in un array, ad es. Dato

$ ls -1
file
other file
somefile

poi

$ IFS= mapfile -t files < <(find . -type f)
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
#? 3
./other file

Se fai necessità di nuove righe manico, e la vostra bashversione fornisce un null delimitato mapfile1 , quindi è possibile modificare quello a IFS= mapfile -t -d '' files < <(find . -type f -print0). Altrimenti, assemblare un array equivalente finddall'output delimitato da null usando un readloop:

$ touch $'filename\nwith\nnewlines'
$ 
$ files=()
$ while IFS= read -r -d '' f; do files+=("$f"); done < <(find . -type f -print0)
$ 
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
4) ./filename
with
newlines
#? 4
./filename?with?newlines

1 l' -dopzione è stata aggiunta mapfilenella bashversione 4.4 IIRC


2
+1 per un altro verbo che non ho mai usato prima
roaima il

Anzi, mapfileè nuovo anche per me. Complimenti.
DopeGhoti,

La while IFS= readversione funziona in Bash v3 (che è importante per quelli di noi che usano macOS).
Gordon Davisson,

3
+1 per la find -print0variante; brontolare per averlo inserito dopo una versione nota non corretta e per averlo descritto solo se si sa che devono gestire le nuove righe. Se uno gestisce solo l'imprevisto nei luoghi in cui è previsto, non gestirà mai l'imprevisto.
Charles Duffy,

8

Questa risposta ha soluzioni per qualsiasi tipo di file. Con newline o spazi.
Ci sono soluzioni per bash recenti, nonché bash antichi e persino conchiglie posix vecchie.

L'albero elencato di seguito in questa risposta [1] viene utilizzato per i test.

Selezionare

È facile arrivare selecta lavorare con un array:

$ dir='deep/inside/a/dir'
$ arr=( "$dir"/* )
$ select var in "${arr[@]}"; do echo "$var"; break; done

O con i parametri posizionali:

$ set -- "$dir"/*
$ select var; do echo "$var"; break; done

Quindi, l'unico vero problema è ottenere la "lista di file" (correttamente delimitata) all'interno di un array o all'interno dei parametri posizionali. Continua a leggere.

bash

Non vedo il problema segnalato con bash. Bash è in grado di cercare all'interno di una determinata directory:

$ dir='deep/inside/a/dir'
$ printf '<%s>\n' "$dir"/*
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Oppure, se ti piace un loop:

$ set -- "$dir"/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Nota che la sintassi sopra funzionerà correttamente con qualsiasi shell (ragionevole) (non almeno csh).

L'unico limite che ha la sintassi sopra è di scendere in altre directory.
Ma bash potrebbe farlo:

$ shopt -s globstar
$ set -- "$dir"/**/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Per selezionare solo alcuni file (come quelli che finiscono nel file) è sufficiente sostituire *:

$ set -- "$dir"/**/*file
$ printf '<%s>\n' "$@"
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/zz last file>

robusto

Quando si inserisce uno "spazio- sicuro " nel titolo, suppongo che ciò che intendevi fosse " robusto ".

Il modo più semplice per essere robusti riguardo agli spazi (o newline) è rifiutare l'elaborazione di input che ha spazi (o newline). Un modo molto semplice per farlo nella shell è uscire con un errore se un nome di file si espande con uno spazio. Esistono diversi modi per farlo, ma il più compatto (e posix) (ma limitato ai contenuti di una directory, inclusi i nomi delle suddirectory ed evitando i file di punti) è:

$ set -- "$dir"/file*                            # read the directory
$ a="$(printf '%s' "$@" x)"                      # make it a long string
$ [ "$a" = "${a%% *}" ] || echo "exit on space"  # if $a has an space.
$ nl='
'                    # define a new line in the usual posix way.  

$ [ "$a" = "${a%%"$nl"*}" ] || echo "exit on newline"  # if $a has a newline.

Se la soluzione utilizzata è robusta in uno di questi elementi, rimuovere il test.

In bash, le sottodirectory potevano essere testate contemporaneamente con il ** spiegato sopra.

Esistono un paio di modi per includere file dot, la soluzione Posix è:

set -- "$dir"/* "$dir"/.[!.]* "$dir"/..?*

trova

Se find deve essere utilizzato per qualche motivo, sostituire il delimitatore con un NUL (0x00).

bash 4.4+

$ readarray -t -d '' arr < <(find "$dir" -type f -name file\* -print0)
$ printf '<%s>\n' "${arr[@]}"
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/file>

bash 2.05+

i=1  # lets start on 1 so it works also in zsh.
while IFS='' read -d '' val; do 
    arr[i++]="$val";
done < <(find "$dir" -type f -name \*file -print0)
printf '<%s>\n' "${arr[@]}"

POSIXLY

Per creare una soluzione POSIX valida in cui find non ha un delimitatore NUL e non esiste -d(né -a) per la lettura, è necessario un approccio completamente diverso.

Dobbiamo usare un complesso -execda find con una chiamata a una shell:

find "$dir" -type f -exec sh -c '
    for f do
        echo "<$f>"
    done
    ' sh {} +

Oppure, se ciò che serve è una selezione (select fa parte di bash, non sh):

$ find "$dir" -type f -exec bash -c '
      select f; do echo "<$f>"; break; done ' bash {} +

1) deep/inside/a/dir/file name
2) deep/inside/a/dir/zz last file
3) deep/inside/a/dir/file with a
newline
4) deep/inside/a/dir/directory/file name
5) deep/inside/a/dir/directory/zz last file
6) deep/inside/a/dir/directory/file with a
newline
7) deep/inside/a/dir/directory/file
8) deep/inside/a/dir/file
#? 3
<deep/inside/a/dir/file with a
newline>

[1] Questo albero (i \ 012 sono newline):

$ tree
.
└── deep
    └── inside
        └── a
            └── dir
                ├── directory
                   ├── file
                   ├── file name
                   └── file with a \012newline
                ├── file
                ├── file name
                ├── otherfile
                ├── with a\012newline
                └── zz last file

Potrebbe essere costruito con questi due comandi:

$ mkdir -p deep/inside/a/dir/directory/
$ touch deep/inside/a/dir/{,directory/}{file{,\ {name,with\ a$'\n'newline}},zz\ last\ file}

6

Non è possibile impostare una variabile di fronte a un costrutto di ciclo, ma è possibile impostarla di fronte alla condizione. Ecco il segmento dalla pagina man:

L'ambiente per qualsiasi comando o funzione semplice può essere temporaneamente aumentato aggiungendolo con assegnazioni di parametri, come descritto sopra in PARAMETRI.

(Un ciclo non è un semplice comando .)

Ecco un costrutto comunemente usato che dimostra gli scenari di fallimento e successo:

IFS=$'\n' while read -r x; do ...; done </tmp/file     # Failure
while IFS=$'\n' read -r x; do ...; done </tmp/file     # Success

Sfortunatamente non riesco a vedere un modo per incorporare una modifica IFSnel selectcostrutto mentre influisce sull'elaborazione di un associato $(...). Tuttavia, non c'è nulla che impedisca di IFSessere impostato al di fuori del ciclo:

IFS=$'\n'; while read -r x; do ...; done </tmp/file    # Also success

ed è questo costrutto che posso vedere funziona con select:

IFS=$'\n'; select file in $(find -type f -name 'file*'); do echo "$file"; break; done

Quando si scrive il codice difensiva mi consiglia che la clausola sia essere eseguito in una subshell, o IFSe SHELLOPTSsalvato e ripristinato intorno al blocco:

OIFS="$IFS" IFS=$'\n'                     # Split on newline only
OSHELLOPTS="$SHELLOPTS"; set -o noglob    # Wildcards must not expand twice

select file in $(find -type f -name 'file*'); do echo $file; break; done

IFS="$OIFS"
[[ "$OSHELLOPTS" !~ noglob ]] && set +o noglob

5
Supponendo che IFS=$'\n'sia sicuro è infondato. I nomi dei file sono perfettamente in grado di contenere letterali newline.
Charles Duffy,

4
Sono francamente titubante nell'accettare tali asserzioni sul possibile set di dati al valore nominale, anche quando presente. Il peggior evento di perdita di dati per cui sono stato presente è stato un caso in cui uno script di manutenzione responsabile della pulizia di vecchi backup ha cercato di rimuovere un file che era stato creato da uno script Python utilizzando un modulo C con una cattiva dereferenza del puntatore che ha scaricato immondizia casuale - incluso un carattere jolly separato da spazi bianchi - nel nome.
Charles Duffy,

2
Le persone che costruiscono lo script della shell mentre puliscono quei file non si sono preoccupate di citare perché i nomi "non possono" fallire [0-9a-f]{24}. Sono stati persi TB di backup dei dati utilizzati per supportare la fatturazione dei clienti.
Charles Duffy,

4
Concordo con @CharlesDuffy completamente. Non gestire i casi limite va bene solo quando lavori in modo interattivo e puoi vedere cosa stai facendo. selectdal suo stesso design è per soluzioni con script , quindi dovrebbe sempre essere progettato per gestire casi limite.
Wildcard il

2
@ilkkachu, ovviamente - non chiameresti mai selectda una shell in cui stai digitando i comandi da eseguire, ma solo in uno script, in cui stai rispondendo a un prompt fornito da quello script e dove si trova lo script eseguire la logica predefinita (costruita senza la conoscenza dei nomi dei file su cui si opera) in base a tale input.
Charles Duffy,

4

Potrei essere fuori dalla mia giurisdizione qui, ma forse puoi iniziare con qualcosa del genere, almeno non ha alcun problema con lo spazio bianco:

find -maxdepth 1 -type f -printf '%f\000' | {
    while read -d $'\000'; do
            echo "$REPLY"
            echo
    done
}

Per evitare possibili ipotesi false, come indicato nei commenti, tenere presente che il codice sopra è equivalente a:

   find -maxdepth 1 -type f -printf '%f\0' | {
        while read -d ''; do
                echo "$REPLY"
                echo
        done
    }

read -dè una soluzione intelligente; grazie per questo.
DopeGhoti,

2
read -d $'\000'è esattamente identico a read -d '', ma per gente fuorviante sulle capacità di bash (implicando, erroneamente, che è in grado di rappresentare letteralmente NUL all'interno delle stringhe). Esegui s1=$'foo\000bar'; s2='foo', quindi prova a trovare un modo per distinguere tra i due valori. (Una versione futura può normalizzarsi con il comportamento di sostituzione dei comandi rendendo equivalente il valore memorizzato foobar, ma oggi non è così).
Charles Duffy,
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.