bash: uso processual seguro de espaço de localização em select

13

Dados estes nomes de arquivos:

$ ls -1
file
file name
otherfile

bash se adapta perfeitamente ao espaço em branco incorporado:

$ 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
#?

No entanto, às vezes, talvez eu não queira trabalhar com todos os arquivos, ou mesmo estritamente em $PWD , que é onde find entra. O que também lida com os espaços em branco nominalmente:

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

Estou tentando inventar uma versão segura do este scriptlet , que terá a saída de find e apresentá-lo em select :

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

No entanto, isso explode com espaços em branco nos nomes dos arquivos:

$ 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

Normalmente, eu contornaria isso mexendo com IFS . No entanto:

$ 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 é a solução para isso?

    
por DopeGhoti 13.07.2017 / 18:05

4 respostas

14

Se você só precisa manipular espaços e tabulações (não novas linhas incorporadas), então você pode usar mapfile (ou seu sinônimo, readarray ) para ler em uma matriz, por exemplo, dado

$ ls -1
file
other file
somefile

então

$ 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 você fizer precisar lidar com novas linhas, e sua bash versão fornecer mapfile 1 delimitada por nulo, você poderá modificar isso para IFS= mapfile -t -d '' files < <(find . -type f -print0) . Caso contrário, monte uma matriz equivalente da saída find delimitada por nulo usando um loop read :

$ 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 a opção -d foi adicionada a mapfile em bash versão 4.4 iirc

    
por 13.07.2017 / 19:32
8

Esta resposta tem soluções para qualquer tipo de arquivos. Com novas linhas ou espaços.
Existem soluções para o bash recente, bem como para o bash antigo e até mesmo para o velho posix.

A árvore listada abaixo nesta resposta [1] é usada para os testes.

selecione

É fácil obter select para trabalhar com uma matriz:

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

Ou com os parâmetros posicionais:

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

Assim, o único problema real é obter a "lista de arquivos" (delimitada corretamente) dentro de uma matriz ou dentro dos Parâmetros posicionais. Continue lendo.

bash

Eu não vejo o problema que você relata com o bash. O Bash pode pesquisar dentro de um determinado diretório:

$ 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>

Ou, se você gosta de um 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>

Note que a sintaxe acima funcionará corretamente com qualquer shell (razoável) (não pelo menos csh).

O único limite que a sintaxe acima tem é descer em outros diretórios.
Mas o bash poderia fazer isso:

$ 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>

Para selecionar apenas alguns arquivos (como aqueles que terminam no arquivo), basta substituir o *:

$ 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 você coloca um "espaço- seguro " no título, vou assumir que o que você quis dizer foi " robust ".

A maneira mais simples de ser robusto sobre espaços (ou novas linhas) é rejeitar o processamento de entrada que possui espaços (ou novas linhas). Uma maneira muito simples de fazer isso no shell é sair com um erro se algum nome de arquivo se expandir com um espaço. Existem várias maneiras de fazer isso, mas o mais compacto (e posix) (mas limitado a um conteúdo de diretório, incluindo nomes de diretórios e evitando arquivos de pontos) é:

$ 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 a solução usada for robusta em qualquer um desses itens, remova o teste.

No bash, os subdiretórios podem ser testados imediatamente com o ** explicado acima.

Existem algumas maneiras de incluir arquivos de ponto, a solução Posix é:

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

encontrar

Se for preciso encontrar algum motivo, substitua o delimitador por um 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

Para fazer uma solução POSIX válida em que o find não tenha um delimitador NUL e não exista -d (nem -a ) para leitura, precisamos de uma abordagem completamente diferente.

Precisamos usar um -exec complexo para encontrar com uma chamada para um shell:

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

Ou, se o que é necessário é um select (select faz parte do bash, não 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] Esta árvore (o \ 012 são novas linhas):

$ tree
.
└── deep
    └── inside
        └── a
            └── dir
                ├── directory
                │   ├── file
                │   ├── file name
                │   └── file with a 2newline
                ├── file
                ├── file name
                ├── otherfile
                ├── with a2newline
                └── zz last file

Poderia ser criado com estes dois comandos:

$ mkdir -p deep/inside/a/dir/directory/
$ touch deep/inside/a/dir/{,directory/}{file{,\ {name,with\ a$'\n'newline}},zz\ last\ file}
    
por 14.07.2017 / 07:53
6

Você não pode definir uma variável na frente de uma construção em loop, mas pode configurá-la na frente da condição. Aqui está o segmento da página man:

The environment for any simple command or function may be augmented temporarily by prefixing it with parameter assignments, as described above in PARAMETERS.

(Um loop não é um comando simples .)

Aqui está uma construção comumente usada que demonstra os cenários de falha e sucesso:

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

Infelizmente, não consigo encontrar uma maneira de incorporar um IFS alterado na construção select , embora isso afete o processamento de um $(...) associado. No entanto, não há nada que impeça que IFS seja definido fora do loop:

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

e é este construto que eu posso ver funciona com select :

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

Ao escrever um código defensivo, recomendo que a cláusula seja executada em uma subcamada ou IFS e SHELLOPTS sejam salvos e restaurados em torno do bloco:

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
    
por 13.07.2017 / 18:17
4

Eu posso estar fora da minha jurisdição aqui, mas talvez você possa começar com algo assim, pelo menos não tem nenhum problema com o espaço em branco:

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

Para evitar possíveis falsas suposições, conforme observado nos comentários, esteja ciente de que o código acima é equivalente a:

%pre%     
por 13.07.2017 / 18:43