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.