bash: movendo arquivos com espaços

10

Quando movo um único arquivo com espaços no nome do arquivo, ele funciona assim:

$ mv "file with spaces.txt" "new_place/file with spaces.txt"

Agora eu tenho uma lista de arquivos que podem conter espaços e quero movê-los. Por exemplo:

$ echo "file with spaces.txt" > file_list.txt
$ for file in $(cat file_list.txt); do mv "$file" "new_place/$file"; done;

mv: cannot stat 'file': No such file or directory
mv: cannot stat 'with': No such file or directory
mv: cannot stat 'spaces.txt': No such file or directory

Por que o primeiro exemplo funciona, mas o segundo não? Como posso fazer isso funcionar?

MrJingles87
fonte

Respostas:

20

Nunca, nunca usefor foo in $(cat bar) . Este é um erro clássico, conhecido como bash pitfall number 1 . Você deve usar:

while IFS= read -r file; do mv -- "$file" "new_place/$file"; done < file_list.txt

Quando você executar o forloop, o bash irá aplicar wordsplitting ao que se lê, o que significa que a strange blue cloudvai ser lido como a, strange, bluee cloud:

$ cat files 
a strange blue cloud.txt
$ for file in $(cat files); do echo "$file"; done
a
strange
blue
cloud.txt

Comparado a:

$ while IFS= read -r file; do echo "$file"; done < files 
a strange blue cloud.txt

Ou ainda, se você insistir no UUoC :

$ cat files | while IFS= read -r file; do echo "$file"; done
a strange blue cloud.txt

Portanto, o whileloop lerá sua entrada e usará o readpara atribuir cada linha a uma variável. Os IFS=conjuntos do separador de campos de entrada para NULL * , e a -ropção readdo impede de interpretar fugas de barra invertida (de modo que \té tratado como barra + te não como uma guia). O --depois dos mvmeios "trata tudo depois do - como um argumento e não uma opção", o que permite lidar com nomes de arquivos iniciados -corretamente.


* Isso não é necessário aqui, estritamente falando, o único benefício nesse cenário é evitar a readremoção de qualquer espaço em branco à esquerda ou à direita, mas é um bom hábito quando você precisa lidar com nomes de arquivos que contenham caracteres de nova linha ou em geral, quando você precisa lidar com nomes de arquivos arbitrários.

terdon
fonte
1
ok, eu estava apenas procurando maneiras de escapar da parte "$ file". não esperava o erro em outro lugar.
MrJingles87 15/17
@ MrJingles87 eu sei. Este leva todo mundo em algum momento. É muito pouco intuitivo se você não souber.
terdon
@JohnKugelman sim, de fato, bom argumento. Resposta editada, obrigado.
terdon
IFS=não ajudará com novas linhas nos nomes dos arquivos. O formato do arquivo aqui simplesmente não permite nomes de arquivos com novas linhas. IFS=é necessário para nomes de arquivos que começam com espaço ou tabulação (ou qualquer outro caractere que não seja a nova linha $ IFS contida anteriormente)
Stéphane Chazelas
Essa armadilha de lote é mais sobre a tentativa de analisar a saída de lsou findcom substituições de comando quando houver maneiras melhores e mais confiáveis ​​de fazer isso. $(cat file)ficaria bem quando bem feito. É tão difícil "fazer o certo" com um loop while de leitura quanto com um loop for + $ (...). Veja minha resposta.
Stéphane Chazelas
11

Isso $(cat file_list.txt)nos shells POSIX, como bashno contexto da lista, é o operador split + glob ( zshapenas faz a parte dividida conforme o esperado).

Ele se divide em caracteres de $IFS(por padrão, SPC , TAB e NL) e fica glob a menos que você desative as globbing por completo.

Aqui, você deseja dividir apenas a nova linha e não a parte glob, portanto deve ser:

IFS='
' # split on newline only
set -o noglob # disable globbing

for file in $(cat file_list.txt); do # split+glob
  mv -- "$file" "new_place/$file"
done

Isso também tem a vantagem (sobre um while readloop) de descartar linhas vazias, preservar uma linha não terminada à direita e preservar mvo stdin (necessário em caso de solicitações, por exemplo).

Ele tem a desvantagem de que todo o conteúdo do arquivo deve ser armazenado na memória (várias vezes com shells como bashe zsh).

Com algumas conchas ( ksh, zshe em menor grau bash), você pode otimizá-lo com em $(<file_list.txt)vez de $(cat file_list.txt).

Para fazer o equivalente com um while readloop, você precisa:

while IFS= read <&3 -r file || [ -n "$file" ]; do
  {
    [ -n "$file" ] || mv -- "$file" "new_place/$file"
  } 3<&-
done 3< file_list.txt

Ou com bash:

readarray -t files < file_list.txt &&
for file in "${files[@]}"
  [ -n "$file" ] || mv -- "$file" "new_place/$file"
done

Ou com zsh:

for file in ${(f)"$(<file_list.txt)"}
  mv -- "$file" "new_place/$file"
done

Ou com GNU mve zsh:

mv -t -- new_place ${(f)"$(<file_list.txt)"}

Ou com GNU mve GNU xargse ksh / zsh / bash:

xargs -rd '\n' -a <(grep . file_list.txt) mv -t -- new_place

Mais informações sobre o que significa deixar expansões sem citação em Implicações de segurança de esquecer de citar uma variável em shells bash / POSIX

Stéphane Chazelas
fonte
Eu acho que você deve reconhecer, no entanto, que $(cat file_list.txt)lê o arquivo inteiro na memória antes de iterar, enquanto que while read ...apenas lê uma linha por vez. Isso pode ser um problema para arquivos grandes.
chepner 16/09
@chepner. Bom ponto. Adicionado.
Stéphane Chazelas
1

Em vez de escrever um script, você pode usar o find ..

 find -type f -iname \*.txt -print0 | xargs -IF -0 mv F /folder/to/destination/

para o caso em que os arquivos estão localizados no arquivo, você pode fazer o seguinte:

cat file_with_spaces.txt | xargs -IF -0 mv F /folder/to/destination

o segundo não tenho certeza ..

Boa sorte

Noel Alex Makumuli
fonte
3
Se você estiver usando find, também poderá usar findpara tudo. Por que trazer xargspara isso? find -type f -iname '*.txt' -exec mv {} /folder/to/destination/.
terdon
O xargs simplesmente manipula os resultados da localização aqui: -I atribui todos os resultados a uma variável F enquanto 0 e print0 simplesmente garante que arquivos com espaços não sejam escapados. e -0 evita que os xargs escapem dos resultados. não sei como -exec se comporta com espaços ..
Noel Alex Makumuli
2
-execse comporta perfeitamente com nomes de arquivos arbitrários (incluindo aqueles que contêm espaços, novas linhas ou qualquer outra estranheza). Eu sei como xargsfunciona, obrigado :) Eu simplesmente não vejo o ponto de chamar um comando separado quando findjá pode fazer isso por você. Nada de errado com isso, apenas ineficiente.
terdon
@terdonyou tem razão .. -exec {} / path / destination funciona sem problemas .. prefiro xargs por causa de mais coisas extras que posso fazer com ele.
Noel Alex Makumuli
xargstambém tem a desvantagem de derrotar stdin (que mvprecisa de seus prompts). Também é um problema com a solução do @ terdon. Com GNU xargse shells com substituição de processo, pode ser atenuado comxargs -0IF -a <(find...) mv F /dest
Stéphane Chazelas
-1
FIRST INSTALL 

apt-get install chromium-browser

apt-get install omxplayer

apt-get install terminator

apt-get install nano (if not already installed)

apt-get install zenity (if not already installed)

THEN CREATE A BASH SCRIPT OF ALL OF THESE PERSONALLY WRITTEN SCRIPTS. 

MAKE SURE THAT EVERY SHELL SCRIPT IS A .sh FILE ENDING EACH ONE OF THESE IS SCRIPTED TO OPEN UP A DIRECTORY FOR YOU TO FIND AND PICK WHAT YOU WANT. 

IT RUNS A BACKGROUND TERMINAL & ALLOWS YOU TO CONTROL THE SONG OR MOVIE.  

MOVIE KEYS ARE AS FOLLOWS; p or space bar for pause, q for quit, - & + for sound up and down, left arrow and right arrow for skipping forward and back. 

MUSIC PLAYER REALLY ONLY HAS A SKIP SONG AND THAT IS CTRL+C AND IF THAT IS THE LAST SONG OR ONLY SONG THEN IT SHUTS DOWN AND THE TERMINAL GOES AWAY.


****INSTRUCTIONS TO MAKE THE SCRIPTS****
- OPEN UP A TERMINAL 

- CD TO THE DIRECTORY YOU WANT THE SCRIPTS TO BE
cd /home/pi/Desktop/

- OPEN UP THE NANO EDITOR WITH THE TITLE OF SHELL YOU WANT
sudo nano Movie_Player.sh

- INSIDE NANO, TYPE OR COPY/PAST (REMEMBER THAT IN TERMINAL YOU NEED TO CTRL+SHIFT+V) THE SCRITP

- SAVE THE DATA WITH CTRL+O
ctrl+o

- HIT ENTER TO SAVE AS THAT FILE NAME OR DELETE THE FILE NAME THEN TYPE NEW ONE JUST MAKE SURE IT ENDS IN .sh THEN HIT ENTER

- NEXT YOU NEED TO SUDO CHMOD IT WITH +X TO MAKE IT CLICKABLE AS A BASH
sudo chmod +x Movie_Player.sh

- FINALLY RUN IT TO TEST EITHER BY DOUBLE CLICKING IT AND CHOOSING "EXECUTE IN TERMINAL" OR BY ./ IT IN TERMINAL
./Movie_Player.sh

YOU ARE NOW GOOD TO PICK A MOVIE OR SELECT A SONG OR ALBUM AND ENJOY!  




**** ALL OF THESE SCRIPTS ACCOUNT FOR SPACES IN THE FILENAME 
SO YOU CAN ACCESS "TOM PETTY" OR "SIXTEEN STONE" WITHOUT NEEDING THE " _ " BETWEEN THE WORDS.




-------WATCH A MOVIE SCRIPT ------- (

#! / bin / bash

FILE =zenity --title "Pick a Movie" --file-selection

para FILE em "$ {FILE [0]}"

omxplayer "$ {FILE [0]}" concluído

--------LISTEN TO A SONG -------

! / bin / bash

FILE =zenity --title "Pick a Song" --file-selection

para FILE em "$ {FILE [0]}"

jogar "$ {FILE [0]}" feito

--------LISTEN TO AN ALBUM ---------- SONGS IN ORDER

! / bin / bash

DIR =zenity --title "Pick a Album" --file-selection --directory

para DIR em "$ {DIR [0]}"

faça o cd "$ {DIR [0]}" && find. -type f -name ' .ogg' -o -name ' .mp3' | sort --version-sort | enquanto lê DIR; jogar "$ DIR" feito pronto

--------LISTEN TO AN ALBUM WITH SONGS IN RANDOM ORDER --------

! / bin / bash

DIR =zenity --title "Pick a Album" --file-selection --directory

para DIR em "$ {DIR [0]}"

faça o cd "$ {DIR [0]}" && find. -type f -name ' .ogg' -o -name ' .mp3' | enquanto lê DIR; jogar "$ DIR" feito pronto

Alfred Gauf
fonte
Leia sobre descontos e aplique um pouco de maquiagem na sua resposta. E por favor, explique por que isso responde à pergunta. E NÃO GRITO !!!
Tenho a sensação de que essa resposta não pertence a esta pergunta. Mas se assim for, vai muito longe, instalar tanto software para um problema tão simples.
precisa saber é o seguinte