Практически каждый разработчик программного обеспечения (ПО), программист или верстальщик сталкивается (и довольно часто) с необходимостью модификации некоторой части рабочего проекта или даже нескольких строк кода. Особенно это актуально, когда в разработке участвует несколько человек, которые могут вносить правки в разных частях проекта. Для удобства и автоматизации действий по составлению таких правок используются специализированные утилиты. Одной из таких является утилита patch и о ней более подробно будет рассказано в данной статье.
Что такое патч?
Говоря о патчах вкупе с утилитой patch, следует подразумевать, что это касается исключительно текстовых данных. Другими словами, происходит работа с исходными кодами проекта, будь то код C++, PHP, HTML и т. д. Вообще, все самые «суровые» программисты или разработчики в процессе своей совместной работы над проектом обмениваются исключительно отдельными правками, а не пересылают друг другу актуальные версии проектов целиком.
Сама правка, т. е. текстовые изменения в исходном коде проектов (для одного его файла или сразу для нескольких) и есть патч или «заплатка». Патч, помимо самих изменений кода содержит также и некоторую служебную информацию, необходимую для правильного «наложения заплатки», т. е. для установки патча. Таким образом, патч — это текстовый файл определённого формата, содержащий в себе данные и инструкции для приведения конечного файла (или проекта) к нужному или актуальному виду.
Утилита patch умеет быстро и эффективно распоряжаться данными из файла-патча, используя для этого хранящиеся в нём инструкции. И таким образом выполняет все рутинные действия по редактированию. Пользователю (разработчику) необходимо лишь правильно выполнить соответствующую команду, задав все необходимые аргументы и опции.
Синтаксис и основные опции команды patch
Нет ничего удивительного в том, что утилита patch относится к категории ПО, которое обязательно должно быть установлено на любой машине для разработки программ, да и вообще для ведения разработки. Практически любой дистрибутив Linux предоставляет утилиту patch предустановленной по-умолчанию.
Стоит также отметить, что по своей функциональности, patch довольно сложна и обладает, без преувеличения, просто огромным набором опций. По этой причине в данной статье будут приведены только самые распространённые приёмы при работе с этой утилитой и только сопутствующие им опции команд. Синтаксис команды patch следующий:
patch [options] [originalfile] [patchfile]
Здесь originalfile – это файл, который необходимо «пропатчить» до актуального состояния. А patchfile – файл-патч. Сразу возникает вопрос: а откуда берётся этот файл-патч? Ответ: он генерируется другой утилитой — diff, которая находит построчные различия между файлами. Либо же патч может быть составлен вручную, автором, если он знаком с соответствующим форматом. Но это бывает крайне редко, обычно прибегают к помощи diff или её аналогов.
В следующей таблице приведены опции команды patch, которые используются наиболее часто:
<td»>Помещает неудавшиеся (отклонённые) изменения в отдельный файл rejecfile вместо файла .rej по-умолчанию.
Опция | Значение |
-i patchfile | Читает информацию из патч-файла, указываемого параметром patchfile. |
-r rejectfile, —reject-file=rejectfile | |
-N, —forward | Когда патч не применяется, то утилита patch обычно пытается определить, выглядит ли ситуация так, как если бы патч уже был применён. Опция -N отключает такое поведение. |
-pnum, strip=num | Обрезает части пути к файлу, разделяемые символом косой черты до уровня, указанного в параметре num. Например: p0 оставит путь /u/john/src/blurfl/blurfl.cpp неизменным, а p4 обрежет тот же путь до blurfl/blurfl.cpp. |
-o outputfile, —output=outputfile | Отправляет вывод в указываемый в параметре outputfile файл. Не следует использовать эту опцию, если в качестве outputfile указывается файл, который должен быть пропатчен. Если в качестве outputfile указать символ дефиса «-», то вывод будет направляться в стандартный поток STD_OUT. |
-E, —remove-empty-file | Удаляет файлы, оказавшиеся пустыми после применения патча. Эта опция имеет смысл, когда используемые патчи имеют не контекстный формат. |
—dry-run | Печатает результаты применения патча без реальной модификации файлов. Полезно для быстрого и безопасного тестирования патчей. |
-R, —reverse | Откатывает все изменения (если они возможны), т. е. отменяет установку патча. |
-c, —context | Интерпретирует файл патча как обычный контекстный формат, генерируемый утилитой diff. |
-b, —backup | Создаёт резервную копию оригинального файла вместо его удаления. |
Применение патчей к отдельным файлам
Прежде, чем начать рассмотрение практических примеров, необходимо сказать несколько слов о той самой утилите, которая и создаёт патчи — diff. Она может генерировать патчи трёх типов — простой, контекстный и контекстный унифицированный. Простой гораздо более компактный по размеру, чем контекстные, но последние гораздо более удобочитаемы и понятны для восприятия пользователем. Для того, чтобы сгенерировать простой патч, для команды diff никаких специальных опций не требуется. А для генерации контекстного или унифицированного контекстного патчей предназначены опции -с и -u соответственно:
$ diff oldfile newfile > patch $ diff -c oldfile newfile > contextpatch $ diff -u oldfile newfile > upatch
Пусть имеется файл с кодом C++ ChildClass.cpp:
#include "../include/ChildClass.h" ChildClass::ChildClass() : BaseClass() { //ctor } ChildClass::ChildClass(char* inputBase[]) : BaseClass(inputBase) { } void ChildClass::valueSqr() { value *= value; } ChildClass::~ChildClass() { //dtor }
И пусть в этот файл было внесено следующее изменение: метод valueSqr() был переименован в calcSqr(). Тогда контекстный патч (файл contextpatch) будет выглядеть следующим образом:
*** ChildClass.cpp 2018-11-13 15:13:08.000000000 +0400 --- ChildClass_new.cpp 2019-06-04 19:34:41.176769204 +0400 *************** *** 10,16 **** } ! void ChildClass::valueSqr() { value *= value; } --- 10,16 ---- } ! void ChildClass::calcSqr() { value *= value; } *************** *** 18,21 **** ChildClass::~ChildClass() { //dtor ! } \ В конце файла нет новой строки --- 18,21 ---- ChildClass::~ChildClass() { //dtor ! }
Теперь, чтобы пропатчить старую версию ChildClass.cpp, нужно выполнить команду:
$ patch ChildClass.cpp -i contextpatch -o ChildClass_new.cpp
В результате будет получен файл ChildClass_new.cpp с актуальным содержимым.
Работа с проектами
С помощью утилиты patch можно также применять патчи для нескольких файлов, причём расположенных в разных каталогах. Это удобно, когда изменения проводятся в масштабах целого проекта. Но в этом случае и сам патч должен быть особым образом подготовлен утилитой diff.
Пусть имеется старый проект в каталоге base-project. Внутри него имеются подкаталоги include и src, в которых, в свою очередь находятся файлы с изменениями — ChildClass.h (в каталоге include) и ChildClass.cpp (в каталоге src). Сам изменённый (актуальный) проект был помещён в отдельный каталог new-project. Подготовка патча будет выглядеть следующим образом:
$ diff -r -c ./base-project ./new-project > project-patch
Сгенерированный файл-патч project-patch:
diff -r -c ./base-project/include/ChildClass.h ./new-project/include/ChildClass.h *** ./base-project/include/ChildClass.h 2019-06-04 17:55:15.081868602 +0400 --- ./new-project/include/ChildClass.h 2019-06-04 17:56:42.929902111 +0400 *************** *** 9,15 **** public: ChildClass(); ChildClass(char* inputBase[]); ! void valueSqr(); virtual ~ChildClass(); protected: --- 9,15 ---- public: ChildClass(); ChildClass(char* inputBase[]); ! void calcSqr(); virtual ~ChildClass(); protected: diff -r -c ./base-project/src/ChildClass.cpp ./new-project/src/ChildClass.cpp *** ./base-project/src/ChildClass.cpp 2019-06-04 16:52:34.884229162 +0400 --- ./new-project/src/ChildClass.cpp 2019-06-04 17:57:01.798768449 +0400 *************** *** 10,16 **** } ! void ChildClass::valueSqr() { value *= value; } --- 10,16 ---- } ! void ChildClass::calcSqr() { value *= value; } *************** *** 18,21 **** ChildClass::~ChildClass() { //dtor ! } \ В конце файла нет новой строки --- 18,21 ---- ChildClass::~ChildClass() { //dtor ! }
Следует обратить внимание, что в данных примерах указываются относительные пути. Файл-патч будет помещён в текущий активный каталог.
Чтобы применить патч нужно выполнить следующую команду:
$ patch -p0 < project-patch patching file ./base-project/include/ChildClass.h patching file ./base-project/src/ChildClass.cpp
Как видно, вместо ключа -i можно использовать символ «<» для перенаправления потока из файла на вход команды patch. Здесь также нужно обратить внимание и понимать, что при выполнении команды patch активным каталогом должен быть каталог уровнем выше, чем каталог проекта, к которому применяется патч, ведь используются относительные пути. Параметр -p0 (см. таблицу из главы «Синтаксис и основные опции команды patch») указывает, что применение патча должно затрагивать весь проект. Если бы этот параметр был бы равен -p1, то патч применялся не выше уровня каталогов include и src. Нередко бывают случаи, когда кроме изменений в содержимом файлов меняется также и содержимое каталогов проекта. Другими словами, добавляются новые или удаляются ранее существовавшие файлы и подкаталоги. Пусть, например, в проект из предыдущего примера в каталог include был добавлен файл Readme.txt с содержанием «This is Readme content.». В этом случае подготовка патча будет выглядеть следующим образом:
$ diff -r -c -N ./base-project ./new-project > project-patch
Сгенерированный файл-патч project-patch:
diff -r -c -N ./base-project/include/Readme.txt ./new-project/include/Readme.txt *** ./base-project/include/Readme.txt 1970-01-01 04:00:00.000000000 +0400 --- ./new-project/include/Readme.txt 2019-06-04 17:25:22.383487767 +0400 *************** *** 0 **** --- 1 ---- + This is Readme content.
Теперь можно пропатчить проект:
$ patch -p0 -E < project-patch patching file ./base-project/include/Readme.txt
Откат патчей
Если по каким-то причинам патч оказался бесполезен и необходимо вернуться к предыдущей версии файлов (проекта), то можно сделать откат изменений, используя опцию -R:
$ patch -p0 -R < project-patch patching file ./base-project/include/Readme.txt
В результате будет удалён файл Readme.txt, который был внесён в проект в примере из предыдущей главы, т. е. фактически откат изменений.
Рекомендуется перед применением патчей проверять, подходят ли они. Для этого используется опция —dry-run:
$ patch -p0 --dry-run < patch
При возникновении каких-либо ошибок во время применения патча, утилита patch создаёт файлы *.rej, по которым можно восстановить исходную версию файла. Однако, следует учитывать, что восстановление содержимого файлов таким способом — довольно долгое и нудное занятие. Практичнее создавать резервные копии файлов, указывая в команде patch опцию -b:
$ patch -b -p0 < project-patch
Заключение
В заключение необходимо отметить, что значение использования утилиты patch двоякое. С одной стороны гораздо меньше хлопот с редактированием файлов и проектов. Не нужно вручную переписывать много кода, когда изменений довольно много. С другой стороны, необходимо использовать данную утилиту с особой осторожностью, чтобы не испортить результаты собственных трудов. Но это уже фактор, зависящий в большей степени от самого пользователя.