bash: перемещение файлов с пробелами

10

Когда я перемещаю один файл с пробелами в имени файла, он работает так:

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

Теперь у меня есть список файлов, которые могут содержать пробелы, и я хочу переместить их. Например:

$ 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

Почему первый пример работает, а второй нет? Как я могу заставить это работать?

MrJingles87
источник

Ответы:

20

Никогда, никогда не используйтеfor foo in $(cat bar) . Это классическая ошибка, обычно известная как ловушка bash № 1 . Вместо этого вы должны использовать:

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

При запуске forцикла, баш будет применяться wordsplitting к тому , что он читает, а это означает , что a strange blue cloudбудет читать a, strange, blueи cloud:

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

По сравнению с:

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

Или даже, если вы настаиваете на UUoC :

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

Таким образом, whileцикл будет считывать входные данные и использовать их readдля назначения каждой строки переменной. В IFS=устанавливает поле ввода разделитель NULL * , а также -rвариант из readостанавливает его от интерпретации обратный слеш (так , что \tрассматривается как слэш + , tа не в качестве вкладки). Значение --после mvозначает «обрабатывать все после - как аргумент, а не как параметр», что позволяет вам иметь дело с именами файлов, начинающимися с -корректно.


* Здесь нет необходимости, строго говоря, единственное преимущество в этом сценарии заключается в том, что он удерживает readот удаления любых начальных или конечных пробелов, но это хорошая привычка, когда вам нужно иметь дело с именами файлов, содержащими символы новой строки, или в общем, когда вам нужно иметь возможность иметь дело с произвольными именами файлов.

Тердон
источник
1
хорошо, я только искал способы избежать части "$ file". не ожидал ошибки где-то еще.
MrJingles87
@ MrJingles87 Я знаю. Этот получает всех в какой-то момент. Это очень не интуитивно понятно, если вы не знаете об этом.
Тердон
@JohnKugelman да, действительно, хорошая мысль. Ответ отредактирован, спасибо.
Тердон
IFS=не поможет с переводом строк в именах файлов. Формат файла здесь просто не допускает имен файлов с символами новой строки. IFS=необходим для имен файлов, начинающихся с пробела или табуляции (или любых других символов, кроме новой строки $ IFS, содержащейся ранее).
Стефан
Эта групповая ошибка больше связана с попыткой проанализировать выходные данные lsили findс подстановками команд, когда есть лучшие, более надежные способы сделать это. $(cat file)было бы хорошо, когда все сделано правильно. С «циклом чтения» так же трудно «сделать все правильно», как и с циклом for + $ (...). Смотри мой ответ.
Стефан Шазелас
11

То, что $(cat file_list.txt)в оболочках POSIX, как bashв контексте списка, является оператором split + glob ( zshтолько делит часть, как вы ожидаете).

Он разделяется на символы $IFS(по умолчанию SPC , TAB и NL) и выполняет глобирование, если вы не отключите глобализацию полностью.

Здесь вы хотите разделить только на новую строку и не хотите глобальную часть, так и должно быть:

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

Это также имеет преимущество (по сравнению с while readциклом), чтобы отбросить пустые строки, сохранить конечную неопределенную строку и сохранитьmv ввод (необходим, например, в случае подсказок).

Недостатком является то, что полное содержимое файла должно храниться в памяти (несколько раз с такими оболочками, как bashиzsh ).

С некоторыми оболочек ( ksh, zshи в меньшей степени bash), вы можете оптимизировать его $(<file_list.txt)вместо $(cat file_list.txt).

Чтобы сделать эквивалент с while readциклом, вам нужно:

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

Или с bash:

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

Или с zsh:

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

Или с GNU mvи zsh:

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

Или с GNU mvи GNU xargsи ksh / zsh / bash:

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

Подробнее о том, что значит оставлять расширения без кавычек, о последствиях для безопасности, связанных с забыванием заключать в кавычки переменную в оболочках bash / POSIX

Стефан Шазелас
источник
Я думаю, что вы должны признать, однако, что $(cat file_list.txt)считывает весь файл в память перед итерацией, тогда как while read ...читает только одну строку за раз. Это может быть проблемой для больших файлов.
chepner
@chepner. Хорошая точка зрения. Добавлен.
Стефан
1

Вместо написания скрипта вы можете использовать find ..

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

для случая, когда файлы находятся в файле, вы можете сделать следующее:

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

второй не уверен, хотя ..

Удачи

Ноэль Алекс Макумули
источник
3
Если вы используете find, вы можете также использовать findдля всего. Зачем вносить xargsв это? find -type f -iname '*.txt' -exec mv {} /folder/to/destination/,
Тердон
xargs просто манипулирует результатами поиска здесь: -I присваивает все результаты переменной F, тогда как 0 и print0 просто гарантируют, что файлы с пробелами не экранированы. и -0 не позволяет xargs избежать результатов. не уверен, как -exec ведет себя с пробелами ..
Ноэль Алекс Макумули
2
-execотлично работает с произвольными именами файлов (включая те, которые содержат пробелы, переводы строки или любые другие странности). Я знаю, как xargsработает, спасибо :) Я просто не вижу смысла в вызове отдельной команды, когда она findуже может сделать это для вас. В этом нет ничего плохого, просто неэффективно.
Тердон
@terdony Вы правы .. -exec {} / path / destination работает гладко .. Я предпочитаю xargs из-за большего количества дополнительных вещей, которые я могу с ним сделать.
Ноэль Алекс Макумули
xargsтакже имеет недостаток в залипании стандартного ввода (которое mvнеобходимо для его подсказок). Также проблема с решением @ terdon. С GNU xargsи оболочками с заменой процесса можно смягчить с помощьюxargs -0IF -a <(find...) mv F /dest
Стефана
-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 / Баш

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

для ФАЙЛА в "$ {FILE [0]}"

do omxplayer "$ {FILE [0]}" готово

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

! / Bin / Баш

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

для ФАЙЛА в "$ {FILE [0]}"

проиграть "$ {FILE [0]}"

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

! / Bin / Баш

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

для DIR в "$ {DIR [0]}"

сделать CD "$ {DIR [0]}" && найти. -тип f -name ' .ogg' -o -name ' .mp3' | sort --version-sort | пока читаешь DIR; играть "$ DIR" готово

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

! / Bin / Баш

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

для DIR в "$ {DIR [0]}"

сделать CD "$ {DIR [0]}" && найти. -тип f -name ' .ogg' -o -name ' .mp3' | пока читаешь DIR; играть "$ DIR" готово

Альфред Гауф
источник
Пожалуйста, прочитайте об уценке и примените макияж к своему ответу. И, пожалуйста, объясните, почему это отвечает на вопрос. И не кричите !!!
У меня такое чувство, что этот ответ не относится к этому вопросу. Но если это так, то путь к такому простому решению идет очень далеко.
MrJingles87