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

Чому перший приклад працює, а другий - сир? Як я можу змусити його працювати?

Відповіді:


20

Ніколи, ніколи не використовуйтеfor foo in $(cat bar) . Це класична помилка, загальновідома як підводний камінь №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 так, справді. Відповідь відредаговано, спасибі
terdon

IFS=не допоможе з новими рядками у назвах файлів. Формат файлу тут просто не дозволяє назви файлів з новими рядками. IFS=потрібен для імен файлів, що починаються із пробілу чи вкладки (або будь-яких символів, крім попереднього рядка $ IFS, що містяться попередньо).
Стефан Хазелас

Цей пакетний підводний камінь більше стосується спроби розбору результатів lsабо findз заміною команд, коли є кращі, надійніші способи зробити це. $(cat file)було б добре, якщо зробити все правильно. Це приблизно так важко "зробити це правильно" з циклом читання, який знаходиться під час читання, як і з ціною $ + (...). Дивіться мою відповідь.
Стефан Шазелас

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циклом) для відміни порожніх рядків, збереження безперервної рядкової лінії та збереження mvstdin (необхідний у разі запитів, наприклад).

Він має і недолік, але повний вміст файлу повинен зберігатися в пам'яті (кілька разів з оболонками як 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. Гарна думка. Додано.
Стефан Шазелас

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 просто маніпулює результатами пошуку тут: -Я призначає всі результати змінній F, тоді як 0 і print0 просто гарантує, що файли з пробілами не уникають. і -0 не дозволяє xargs уникнути результатів. не впевнений, як поводить -exec з пробілами ..
Ноель Алекс Макумулі

2
-execчудово поводиться з довільними іменами (включаючи ті, що містять пробіли, нові рядки чи будь-які інші дивацтва). Я знаю, як це xargsпрацює, дякую :) Я просто не бачу сенсу викликати окрему команду, коли findце вже можна зробити для вас. Нічого поганого в цьому, просто неефективно.
тердон

@terdonyou правий .. -exec {} / шлях / місце призначення працює безперебійно .. Я віддаю перевагу xargs через більше зайвих речей, з якими я можу це зробити.
Ноель Алекс Макумулі

xargsтакож є недолік клобінгу stdin (який mvпотребує своїх підказок). Також проблема з рішенням @ terdon. З GNU xargsта оболонками із заміщенням процесу можна полегшитиxargs -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 ------- (

#! / бін / баш

ФАЙЛ =zenity --title "Pick a Movie" --file-selection

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

зробіть omxplayer "$ {FILE [0]}"

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

! / бін / баш

ФАЙЛ =zenity --title "Pick a Song" --file-selection

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

виконайте гру "$ {FILE [0]}"

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

! / бін / баш

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

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

зробіть cd "$ {DIR [0]}" && знайти. -тип f -name ' .ogg' -o -name ' .mp3' | сортування - перетворення-сортування | під час читання DIR; грати "$ DIR" зроблено зроблено

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

! / бін / баш

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

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

зробіть cd "$ {DIR [0]}" && знайти. -тип f -name ' .ogg' -o -name ' .mp3' | під час читання DIR; грати "$ DIR" зроблено зроблено


Будь ласка, прочитайте про розмітку та застосуйте до своєї відповіді деякий склад. І поясніть, будь ласка, чому це відповідає на питання. І НЕ ПОШУКУЙ !!!

У мене таке відчуття, що ця відповідь не належить до цього питання. Але якщо так, то це іде далеко, щоб встановити стільки програмного забезпечення для такої простої проблеми.
MrJingles87
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.