Данное сообщение не претендует на оригинальность, это просто очередной пример
того, как можно переименовать много файлов с помощью командной строки в Linux,
используя find
, sed
, xargs
и mv
.
Идея такая:
ls
, echo
или find
).sed
).xargs
).На языке bash эти 3 команды могут выглядеть так:
user ~ $ | ls | sed | xargs mv echo | sed | xargs mv find | sed | xargs mv |
Мы будем говорить о последнем варианте, поскольку find
является очень мощной и гибкой утилитой для поиска файлов.
Простейший случай, необходимо произвести такую замену с большим количеством файлов:
01-filename.txt → 01-new.txt
02-filename.txt → 02-new.txt
...
Для экспериментов создадим директорию ~/test/
.
user ~ $ | mkdir ~/test/ cd ~/test/ |
user test $ | touch {1..5}-filename.txt |
Теперь в директории ~/test/
должно появиться 5 пронумерованных файлов. Выведем эти файлы (точка после find
означает, что поиск надо проводить в текущей директории; ключ -type f
просит выводить только файлы).
user test $ | find . -type f |
./1-filename.txt
./2-filename.txt
./5-filename.txt
./4-filename.txt
./3-filename.txt
Потоковый редактор sed
в команде find | sed
получает поток названий файлов от find. И с каждым названием файла он может делать какую-то операцию. Нас интересует замена, общий вид которой в sed
выглядит так.
Общий вид команд в редакторе sed:
sed 's/old/new/g'
sed 's#old#new#g'
sed 's:old:new:g'
Если выполнить данные команды, то sed
будет ждать ввода аргументов с клавиатуры. После каждого нажатия Enter sed
выведет строку, заменив в ней ‘old’ на ‘new’, если ‘old’ будет присутствовать в введённой строке. Но обычно sed используется либо в связках типа find | sed
, либо принимает в качестве аргумента имена файлов, чтобы произвести замены внутри файлов.
Заметьте, что вы сами можете выбирать разделитель — например, /
, #
или
:
(хотя можно использовать почти любой символ), комбинировать их нельзя. Буква ‘s’ — заменить (от англ. substitute);
буква ‘g’ (от англ. global) в конце стоит для того, чтобы замена происходила
во всей строке не только для первого вхождения, а столько раз, сколько там
встретится «old». Когда в заменяемых выражениях встречаются слэши (/
или \
), то каждую из них приходится предварять дополнительным обратным
слэшем, тогда код слишком сильно напоминает частокол и удобнее использовать
двоеточие или что-то другое в качестве разделителя.
Итак, посмотрим, что мы можем сделать двумя первыми шагами find | sed
:
user test $ | find . -type f | sed 's:filename:new:g' |
./1-new.txt
./2-new.txt
./5-new.txt
./4-new.txt
./3-new.txt
Отлично, нужные нам имена выведены. Но в третьем шаге утилите xargs
нужны и старое, и новое имя файла, чтобы применить к ним команду mv
. Поэтому модифицируем команду sed
:
sed 'p;s:old:new:g'
Знак «p;» (p — print), стоящий перед «s», просит sed выдавать не только результат, но и исходный материал. Теперь вывод будет такой:
user test $ | find . -type f | sed 'p;s:filename:new:g' |
./1-filename.txt
./1-new.txt
./2-filename.txt
./2-new.txt
./5-filename.txt
./5-new.txt
./4-filename.txt
./4-new.txt
./3-filename.txt
./3-new.txt
То, что нужно. Переходим к третьему шагу.
Программа xargs
работает с потоками данных. Утилита принимает один поток и может «распараллелить» его на несколько. Чтобы было лучше понятно, посмотрите простые примеры:
user ~ $ | echo "1 2 3 4 5 6 7 8 9" |
1 2 3 4 5 6 7 8 9
user ~ $ | echo "1 2 3 4 5 6 7 8 9" | xargs -n1 |
1
2
3
4
5
6
7
8
9
user ~ $ | echo "1 2 3 4 5 6 7 8 9" | xargs -n2 |
1 2
3 4
5 6
7 8
9
user ~ $ | echo "1 2 3 4 5 6 7 8 9" | xargs -n3 |
1 2 3
4 5 6
7 8 9
user ~ $ | echo "1 2 3 4 5 6 7 8 9" | xargs -n4 |
1 2 3 4
5 6 7 8
9
Для переименования файлов, очевидно, нам нужен ключик -n2
, тогда xargs
разделит поток из старых и новых имён в две колонки.
user test $ | find . -type f | sed 'p;s:filename:new:' | xargs -n2 |
./1-filename.txt ./1-new.txt
./2-filename.txt ./2-new.txt
./5-filename.txt ./5-new.txt
./4-filename.txt ./4-new.txt
./3-filename.txt ./3-new.txt
Отлично. Осталось перед каждой парой вставить mv
, и переименование произойдёт. Выполняем:
user test $ | find . -type f | sed 'p;s:filename:new:' | xargs -n2 mv |
Вывода никакого не последует, но переименование произошло. Чтобы быть уверенным в результате, можно произвести проверку перед выполнением этой команды. Для этого надо добавить ключ -p
к xargs
. Тогда утилита будет показывать, что она сделала бы без ключа -p
. Можно нажимать Enter и просматривать, что собирается сделать команда. Изменения применены не будут.
user test $ | find . -type f | sed 'p;s:filename:new:' | xargs -n2 -p mv |
mv ./1-filename.txt ./1-new.txt ?...
Если результаты устраивают, то нужно убрать ключ -p
и повторить команду.
В директориях dir1/subdir1/
, dir1/subdir2/
,… dir5/subdir5/
лежат файлы filename.txt. Нужно вытащить их оттуда, но уже с разными именами.
dir1/subdir1/filename.txt → dir1-subdir1-filename.txt
dir1/subdir2/filename.txt → dir1-subdir2-filename.txt
...
dir5/subdir5/filename.txt → dir5-subdir5-filename.txt
При необходимости создадим и очистим нашу экспериментальную директорию ~/test/
и создадим там необходимую структуру файлов:
user ~ $ | mkdir -p ~/test/ rm ~/test/* cd ~/test/ |
user test $ | mkdir -p dir{1..5}/subdir{1..5} touch dir{1..5}/subdir{1..5}/filename.txt |
Итак, у нас получилось 25 файлов filename.txt в директориях dir1/subdir1/
, dir1/subdir2
,… dir5/subdir5/
.
Выведем эти файлы. В этом случае -type f
тоже сработал бы, но воспользуемся для разнообразия поиском по имени. Также используем звёздочку вместо точки, чтобы избавиться от ведущего ./
в выводе.
user test $ | find * -name 'filename.txt' |
dir1/subdir2/filename.txt
dir1/subdir4/filename.txt
dir1/subdir1/filename.txt
dir1/subdir5/filename.txt
dir1/subdir3/filename.txt
...
Попросим sed
заменить все слэши на дефисы.
user test $ | find * -name 'filename.txt' | sed 'p;s:/:-:g' |
Завершаем переименование:
user test $ | find * -name 'filename.txt' | sed 'p;s:/:-:g' | xargs -n2 -p mv |
Убедившись, что всё произойдёт правильно, убираем ключ -p
и повторяем команду.
В директории лежат файлы, пронумерованные от 0 до 1001. Если их выводить командой ls
, они будут отсортированы по имени не так, как этого хотелось бы[1]. Добавим необходимое количество нулей в названия файлов.
0-filename.txt → 0000-filename.txt
...
50-filename.txt → 0050-filename.txt
...
150-filename.txt → 0150-filename.txt
...
1001-filename.txt → 1001-filename.txt
При необходимости создаём тестовую директорию и/или чистим её и создаём наши 1001 файл:
user ~ $ | mkdir -p ~/test/ rm -r ~/test/* cd ~/test/ |
user test $ | touch {0..1001}-filename.txt |
Можно действовать так (не лучший вариант).
user test $ | ls | sed 'p;s:^\([0-9]\)-:000\1-:g' | xargs -n2 mv ls | sed 'p;s:^\([0-9][0-9]\)-:00\1-:g' | xargs -n2 mv ls | sed 'p;s:^\([0-9][0-9][0-9]\)-:0\1-:g' | xargs -n2 mv |
Первая команда всем файлам, начинающимся с одной цифры, добавит 3 нуля. Вторая команда всем файлам, начинающимся с двух цифр, добавит 2 нуля. Третья команда всем файлам, начинающимся с трёх цифр, добавит 1 ноль.
Здесь использованы регулярные выражения в sed
. Разберём конструкцию
^\([0-9][0-9][0-9]\)-
^
означает, что нужно искать с начала имени\(...\)
позволяет заключённое в неё использовать как \1
в шаблоне замены[0-9][0-9][0-9]
означает три любые цифры подряд.-
— просто дефис.В принципе, с этим можно справиться командой ls -v
(natural sorting: 10 будет выведено после 9, а не после единицы), но мы всё-таки для упражнения произведём переименование. ↩︎